commit fd4b9b26b127fba1de4070fb7e3be65c82ac11b3 Author: 奈科斯(NexIoT) <16120872+nexiot@user.noreply.gitee.com> Date: Mon Sep 22 21:27:27 2025 +0800 合并最新版本代码 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cc3599 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +git###################################################################### + +classpath.txt + +# 1. 清除所有缓存(保留本地文件) +#git rm -r --cached . +# 或清除单个文件:git rm --cached <文件名> + +# 2. 重新添加所有文件(应用新规则) +#git add . + +# 3. 提交更改 +#git commit -m "更新 .gitignore 规则" + +########################################### +.DS_Store +.DS_Store/ +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +target/ +!.mvn/wrapper/maven-wrapper.jar + +###################################################################### +# IDE + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +uploadPath/ + +### NetBeans ### +nbproject/private/ +build/* +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ +###################################################################### +# Others +*.log +*.xml.versionsBackup + +!*/build/*.java +!*/build/*.html +!*/build/*.xml +/.vscode/settings.json +/logs/ +/.specstory/ +/cn-universal-web/logs/cn-universal/sys-error.*.gz +/cn-universal-web/logs/cn-universal/sys-info.*.gz +/cn-universal-web/logs/cn-universal/tcp.2025-*.gz +/cn-universal-web/logs/cn-universal/api.2025-*.gz +/.cursorindexingignore diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f57672 --- /dev/null +++ b/README.md @@ -0,0 +1,345 @@ +# 🚀 NexIoT物联网平台 + +
+ +![License](https://img.shields.io/badge/license-AGPL3.0-blue.svg) +![Java](https://img.shields.io/badge/Java-21-orange.svg) +![SpringBoot](https://img.shields.io/badge/SpringBoot-3.5-brightgreen.svg) +![MySQL](https://img.shields.io/badge/MySQL-5.7+-blue.svg) +![Redis](https://img.shields.io/badge/Redis-Latest-red.svg) + +## 一款基于Java技术栈构建的"零代码侵入"企业级物联网平台 + +[📖 文档地址](https://nexiotplatform.github.io/universal-iot-docs/) | [🌐 在线演示](http://iot.192886.xyz:81/) | [🔧 调试IDE](http://iot.192886.xyz:81/magic/debug/index.html) + +
+ +## 📋 目录 + +- [✨ 平台简介](#-平台简介) +- [🌟 平台亮点](#-平台亮点) +- [🚀 快速开始](#-快速开始) +- [📦 运行部署](#-运行部署) +- [🎯 核心功能](#-核心功能) +- [🏗️ 技术架构](#️-技术架构) +- [📈 正在推进](#-正在推进) +- [💡 最佳实践](#-最佳实践) +- [🎥 视频教程](#-视频教程) +- [📸 界面展示](#-界面展示) +- [🤝 贡献指南](#-贡献指南) +- [📄 开源协议](#-开源协议) + +## ✨ 平台简介 + +NexIoT IoT +是一款基于Java技术栈构建的“零代码侵入”企业级物联网平台,具备高内聚低耦合,零代码侵入。平台集成了云调试功能,支持TCP、MQTT、天翼物联、HTTP等多种协议的统一管理,提供完整的产品定义、物模型设计、设备接入、北向应用输出、规则引擎等核心功能,为物联网应用提供一站式解决方案。 + +## 🌟 平台亮点 + +- ✅ **低成本对接**:接入一款设备在`外置调试器`完成,不用`改`系统任何代码,无代码侵入,`大学生`就能完成。 +- ✅ **模块化**:TCP、MQTT、天翼物联等按需开启,对接一个新平台太容易了 +- ✅ **没有标准化应对各种标准**:国内行业的协议现状,不再搞标准,主打应对各种标准接入,看着很复杂无头绪,上手用起来很舒服 +- ✅ **强大的协议调试器**:基于浏览器的实时协议开发和调试,直连设备接入已经半年多没重启过。 +- ✅ **极简架构**:Java21、mysql5.7+、redis、MQTT(不限emqx还是其他),轻松应付百万级别的设备;支持clickhouse存储(配置就行) + +## 🚀 快速开始 + +### 📋 环境要求 + +- **JDK**: 21+ +- **MySQL**: 5.7+ +- **Redis**: 6.0+ +- **Maven**: 3.9+ + +### 🎯 演示地址 + +- **管理后台**: +- **调试IDE**: +- **演示账号**: test/Haha@2025 +- **文档地址**: + +## 📦 运行部署 + +### 🖥️ 本地部署 + +```bash +# 1. 修改配置文件 +vim cn-universal-web/src/main/resources/application-{dev}.properties + +# 2. 配置数据库、redis、emqx +spring: + datasource: + url: jdbc:mysql://localhost:3306/universal_iot?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: 123456 + +# 3. 启动应用 +mvn spring-boot:run +``` + +### ☁️ 生产环境部署 + +```bash +# 1. 打包应用 +mvn clean package -DskipTests -PProd + +# 2. 上传到服务器 +scp cn-universal-web/target/cn-universal-web.tar.gz root@your-server:/opt/nexiot/ + +# 3. 启动服务 +tar -zxvf cn-universal-web.tar.gz + +cd cn-universal-web & ./bin/restart.sh + +``` + +## 🎯 核心功能 + +### 🚀 设备接入能力 + +- ✅ **TCP设备接入**:16进制报文传输、协议解析、云调试、在线测试 +- ✅ **MQTT设备接入**:物模型/透传模式、云调试、在线测试、支持EMQX/HiveMQ等 +- ✅ **HTTP设备接入**:REST API、Java SDK、OAuth2认证、完整接口规范 +- ✅ **天翼物联接入**:设备注册、数据上报、指令下发、标准协议对接 +- ✅ **云云对接接入**:已支持,具备完整的SDK和API,支持多平台数据同步、协议转换 + +### 🎯 核心管理功能 + +- ✅ **产品管理**:多协议产品创建、物模型定义、产品标签、公共配置 +- ✅ **协议管理**:云调试环境、插件化架构、实时协议开发、统一管理平台 +- ✅ **设备管理**:设备详情监控、在线指令下发、设备影子、消息订阅、日志查看 +- ✅ **网络组件**:TCP/MQTT服务管理、启动/暂停/禁用操作、个性化参数配置 +- ✅ **应用管理**:北向应用输出、OAuth2 API SDK、设备绑定、推送配置 +- ✅ **通知管理**:多渠道支持(钉钉、阿里云、腾讯云、webhook、邮箱、飞书) +- ✅ **场景联动**:基于设备属性/事件的触发规则、多动作执行、实时自动化 +- 🔄 **RuleGO规则引擎**:可视化规则引擎执行、支持OPCUA、Moudbus工业场景 + +### 📊 协议接入现状 + +| 协议类型 | 状态 | 核心功能 | +|------------|-------|-----------------------------------| +| **TCP设备** | ✅ 已实现 | 16进制报文传输、协议解析、云调试、在线测试 | +| **MQTT设备** | ✅ 已实现 | 物模型/透传模式、云调试、在线测试、支持EMQX/HiveMQ等 | +| **HTTP设备** | ✅ 已实现 | REST API、Java SDK、OAuth2认证、完整接口规范 | +| **天翼物联** | ✅ 已实现 | 完整的产品创建设备注册、数据上报、指令下发、标准协议对接 | +| **云云对接** | ✅ 已支持 | Java SDK 自行扩展协议模块 | + +## 🏗️ 技术架构 + +### 🛠️ 技术栈 + +- **后端框架**:`Java 21` `SpringBoot 3.5` `Tk.Mybatis 5.0.1` +- **数据存储**:MySQL5.7+ / Redis / ClickHouse(可选) +- **架构模式**:单体架构设计,支持微服务化部署 +- **前端技术**:基于`RuoYi-Antdv`构建,感谢开源社区! + +### 🏛️ 系统架构 + +```text +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 设备接入层 │ │ 协议处理层 │ │ 业务服务层 │ +│ │ │ │ │ │ +│ • TCP Server │───▶│ • 协议解析 │───▶│ • 设备管理 │ +│ • MQTT Broker │ │ • 数据转换 │ │ • 产品管理 │ +│ • HTTP API │ │ • 消息路由 │ │ • 规则引擎 │ +│ • 天翼物联 │ │ • 云调试 │ │ • 通知服务 │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 数据存储层 │ │ 缓存层 │ │ 外部接口层 │ +│ │ │ │ │ │ +│ • MySQL │ │ • Redis │ │ • 北向API │ +│ • ClickHouse │ │ • 本地缓存 │ │ • Webhook │ +│ • 时序数据 │ │ • 会话管理 │ │ • 第三方集成 │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## 📈 正在推进 + +- **🔄 小程序开发**:正在定制UI,具备指令控制、属性、告警查看,轻量化 +- **🔄 扩展推送**:MQTT / RocketMQ / DB / Kafka +- **🔄 其他**:也可以提一些意见 + +## 💡 最佳实践 + +### 我在1688自费购入了两款设备(NB水浸、4G电表),通过电信AEP与TCP直连。将围绕典型接入方式,发布视频和教程。以过来人的经验,单看文档不如对照实物从0到1动手实践更高效,你在实际操作中会有更多收获 + +### 当然你也可以购买与作者同款设备进行学习,另外请注意用电安全 + +| | | | +|------------------------------|------------------------------|-----------------------------| +| ![电表设备](/__MACOSX/shot/dian.jpg) | ![水浸设备](/__MACOSX/shot/shui.jpg) | ![设备展示](/__MACOSX/shot/mai.jpg) | + +## 🎥 视频教程 + +我们提供了详细的视频教程,帮助您快速上手NexIoT平台。所有教程都是基于真实设备进行实战演示。 + +### 📡 天翼物联接入教程 + +#### 🌊 NB水浸传感器系列 + +1. **【手把手实战:水浸传感器开箱:初见与硬件解析】** + - 📺 [B站观看](https://www.bilibili.com/video/BV1kLY5z5EMH/?share_source=copy_web&vd_source=c9e1500efcc8aa0763f711fadaa68dff) + - 📝 内容:设备开箱、硬件介绍、基本功能测试 + +2. **【手把手实战:NexIoT接入Ctwing参数配置】** + - 📺 [B站观看](https://www.bilibili.com/video/BV1jVY7zVEjM/?share_source=copy_web&vd_source=c9e1500efcc8aa0763f711fadaa68dff) + - 📝 内容:天翼物联平台参数配置、设备注册流程 + +3. **【手把手实战:NB水浸传感器CTWing接入全流程】** + - 📺 [B站观看](https://www.bilibili.com/video/BV1sJaZzBEfr/?share_source=copy_web&vd_source=c9e1500efcc8aa0763f711fadaa68dff) + - 📝 内容:完整的设备接入流程,从配置到数据上报 + +### 🔌 TCP直连接入教程 + +#### ⚡ DL/T64电表系列 + +1. **【【开源】手把手实战:DL/T64电表TCP直连接入】** + - 📺 [B站观看](https://www.bilibili.com/video/BV1x4pBzZEVN/?share_source=copy_web&vd_source=c9e1500efcc8aa0763f711fadaa68dff) + - 📝 内容:TCP协议解析、设备直连配置、数据解析 + +2. **【【开源】手把手实战:产品创建与SNI解读】** + - 📺 [B站观看](https://www.bilibili.com/video/BV1zSWNzoETF/?share_source=copy_web&vd_source=c9e1500efcc8aa0763f711fadaa68dff) + - 📝 内容:产品创建流程、SNI设备标识解析、协议调试 + +3. **【【NexIoT开源】手把手实战:如何写TCP接入的DL/T645电表解析脚本】** + - 📺 [B站观看](https://www.bilibili.com/video/BV1SAWVzwEZE/?share_source=copy_web&vd_source=c9e1500efcc8aa0763f711fadaa68dff) + - 📝 内容:数据流介绍、调试IDE的编写和调试、645电表的编写 + + +### 🎯 更多教程 + +我们正在持续制作更多设备的接入教程,尽情期待 + +## 📸 界面展示 + +### 🏗️ 架构图 + +![架构图](/__MACOSX/shot/dataflow.png) + +### 🚀 部署图 + +![部署图](/__MACOSX/shot/deploy.jpg) + +### 🖥️ 功能截图 + +#### 🌐 直接使用天翼产品 + +![直接使用天翼产品](__MACOSX/shot/99.gif) + +#### 🔄 流程编排 + +![流程编排](__MACOSX/shot/53.png) + +#### 🛠️ 协议统一管理与IDE(推荐) + +![协议统一管理与IDE](__MACOSX/shot/54.png) + +| ![功能截图1](/__MACOSX/shot/01.png) | ![功能截图2](/__MACOSX/shot/02.png) | ![功能截图3](/__MACOSX/shot/03.png) | +| ![功能截图4](/__MACOSX/shot/04.png) | ![功能截图5](/__MACOSX/shot/05.png) | ![功能截图6](/__MACOSX/shot/06.png) | +| ![功能截图7](/__MACOSX/shot/07.png) | ![功能截图8](/__MACOSX/shot/08.png) | ![功能截图9](/__MACOSX/shot/09.png) | +| ![功能截图10](/__MACOSX/shot/10.png) | ![功能截图11](/__MACOSX/shot/11.png) | ![功能截图12](/__MACOSX/shot/12.png) | +| ![功能截图13](/__MACOSX/shot/13.png) | ![功能截图14](/__MACOSX/shot/14.png) | ![功能截图15](/__MACOSX/shot/15.png) | +| ![功能截图16](/__MACOSX/shot/16.png) | ![功能截图17](/__MACOSX/shot/17.png) | ![功能截图18](/__MACOSX/shot/18.png) | +| ![功能截图19](/__MACOSX/shot/19.png) | ![功能截图20](/__MACOSX/shot/20.png) | ![功能截图21](/__MACOSX/shot/21.png) | +| ![功能截图22](/__MACOSX/shot/22.png) | ![功能截图23](/__MACOSX/shot/23.png) | ![功能截图24](/__MACOSX/shot/24.png) | + +| ![功能截图52](/__MACOSX/shot/52.png) | ![功能截图51](/__MACOSX/shot/51.png) | ![功能截图50](/__MACOSX/shot/50.png) | + +### 🔧 强大调试器 + +| ![调试器登录](/__MACOSX/shot/debug/login.png) | ![调试器界面](/__MACOSX/shot/debug/debug.png) | ![调试器运行](/__MACOSX/shot/debug/run.png) | + +### 💬 社区交流 + +有任何问题加入QQ群 + +| ![QQ群](/__MACOSX/shot/qq.png) | ![微信群](/__MACOSX/shot/wx.png) | + +## 🤝 贡献指南 + +我们欢迎所有形式的贡献!无论是代码、文档、问题反馈还是功能建议。 + +### 🚀 如何贡献 + +1. **Fork 项目** + + ```bash + # 点击右上角 Fork 按钮 + ``` + +2. **创建分支** + + ```bash + git checkout -b feature/your-feature-name + ``` + +3. **提交代码** + + ```bash + git add . + git commit -m "feat: add your feature" + git push origin feature/your-feature-name + ``` + +4. **创建 Pull Request** + - 在 Gitee 上创建 Pull Request + - 详细描述你的修改内容 + - 等待代码审查 + +### 📝 代码规范 + +- 遵循 Java 编码规范 +- 添加必要的注释和文档 +- 确保代码通过所有测试 +- 提交信息使用 [Conventional Commits](https://www.conventionalcommits.org/) 格式 + +### 🐛 问题反馈 + +- 使用 [Issues](https://gitee.com/nexiotplatform/Universal-IoT-Java/issues) 报告 Bug +- 提供详细的问题描述和复现步骤 +- 包含系统环境信息 + +### 💡 功能建议 + +- 在 Issues 中提出新功能建议 +- 详细描述功能需求和预期效果 +- 欢迎讨论技术实现方案 + +## 📄 开源协议 + +### 📋 协议说明 + +开源版本采用 **AGPL3.0** 协议,可用于个人学习和使用,商业用途需赞助项目,获得授权。 + +二开项目同样遵守 AGPL3.0 协议进行开源,可以向原作者申请授权。 + +大学生毕设项目免费使用 + +### 📜 授权说明 + +1. **授权用户可无限期在其内部使用** + - 企业授权用户不含分公司、子公司及第三方公司 + - 个人授权用户仅可个人使用,不可用于在职公司或任何第三方 + +2. **授权用户不可向第三方发行或转让授权源码** + - 无论有偿或无偿 + - 不可申请包含本项目的授权源码的软件著作权 + - 用户可申请自主二次研发部分源码的软件著作权 + +3. **授权用户仅可通过项目形式进行源码的二次开发** + - 完成的定制化软件必须加密打包后才可交付客户 + - 如需交付项目源码,必须为客户购买对应的商业授权 + +### 🔒 开源和商业 + +开源版本暂不包含 **TCP**、**天翼物联ctaiot** 模块。可自行二次开发扩展,商用授权需获取授权。 + +### 🙏 致谢 + +感谢以下开源项目和技术平台: + +- **开源框架**:若依、Antdv、jetlink、ssssssss-team +- **云平台**:阿里云、华为云、腾讯云、AEP、OneNet 等物联网平台 +- **社区支持**:所有贡献者和用户的支持与反馈 diff --git a/__MACOSX/._images b/__MACOSX/._images new file mode 100644 index 0000000..6d715dc Binary files /dev/null and b/__MACOSX/._images differ diff --git a/__MACOSX/images/._image-20250618153147421.png b/__MACOSX/images/._image-20250618153147421.png new file mode 100644 index 0000000..6d715dc Binary files /dev/null and b/__MACOSX/images/._image-20250618153147421.png differ diff --git a/__MACOSX/images/._image-20250618153849308.png b/__MACOSX/images/._image-20250618153849308.png new file mode 100644 index 0000000..6d715dc Binary files /dev/null and b/__MACOSX/images/._image-20250618153849308.png differ diff --git a/__MACOSX/images/._image-20250618154116767.png b/__MACOSX/images/._image-20250618154116767.png new file mode 100644 index 0000000..6d715dc Binary files /dev/null and b/__MACOSX/images/._image-20250618154116767.png differ diff --git a/__MACOSX/images/._image-20250618154255040.png b/__MACOSX/images/._image-20250618154255040.png new file mode 100644 index 0000000..6d715dc Binary files /dev/null and b/__MACOSX/images/._image-20250618154255040.png differ diff --git a/__MACOSX/images/._image-20250618154523702.png b/__MACOSX/images/._image-20250618154523702.png new file mode 100644 index 0000000..6d715dc Binary files /dev/null and b/__MACOSX/images/._image-20250618154523702.png differ diff --git a/__MACOSX/images/._image-20250618154604068.png b/__MACOSX/images/._image-20250618154604068.png new file mode 100644 index 0000000..6d715dc Binary files /dev/null and b/__MACOSX/images/._image-20250618154604068.png differ diff --git a/__MACOSX/images/._image-20250618154629567.png b/__MACOSX/images/._image-20250618154629567.png new file mode 100644 index 0000000..6d715dc Binary files /dev/null and b/__MACOSX/images/._image-20250618154629567.png differ diff --git a/__MACOSX/images/._image-20250618154651955.png b/__MACOSX/images/._image-20250618154651955.png new file mode 100644 index 0000000..6d715dc Binary files /dev/null and b/__MACOSX/images/._image-20250618154651955.png differ diff --git a/__MACOSX/images/._image-20250618154731766.png b/__MACOSX/images/._image-20250618154731766.png new file mode 100644 index 0000000..6d715dc Binary files /dev/null and b/__MACOSX/images/._image-20250618154731766.png differ diff --git a/__MACOSX/images/._image-20250618154856527.png b/__MACOSX/images/._image-20250618154856527.png new file mode 100644 index 0000000..6d715dc Binary files /dev/null and b/__MACOSX/images/._image-20250618154856527.png differ diff --git a/__MACOSX/images/._image-20250618155121608.png b/__MACOSX/images/._image-20250618155121608.png new file mode 100644 index 0000000..6d715dc Binary files /dev/null and b/__MACOSX/images/._image-20250618155121608.png differ diff --git a/__MACOSX/shot/01.png b/__MACOSX/shot/01.png new file mode 100644 index 0000000..95aee5f Binary files /dev/null and b/__MACOSX/shot/01.png differ diff --git a/__MACOSX/shot/02.png b/__MACOSX/shot/02.png new file mode 100644 index 0000000..346ed04 Binary files /dev/null and b/__MACOSX/shot/02.png differ diff --git a/__MACOSX/shot/03.png b/__MACOSX/shot/03.png new file mode 100644 index 0000000..e8d0b3d Binary files /dev/null and b/__MACOSX/shot/03.png differ diff --git a/__MACOSX/shot/04.png b/__MACOSX/shot/04.png new file mode 100644 index 0000000..3bc36f4 Binary files /dev/null and b/__MACOSX/shot/04.png differ diff --git a/__MACOSX/shot/05.png b/__MACOSX/shot/05.png new file mode 100644 index 0000000..e90fca1 Binary files /dev/null and b/__MACOSX/shot/05.png differ diff --git a/__MACOSX/shot/06.png b/__MACOSX/shot/06.png new file mode 100644 index 0000000..509544d Binary files /dev/null and b/__MACOSX/shot/06.png differ diff --git a/__MACOSX/shot/07.png b/__MACOSX/shot/07.png new file mode 100644 index 0000000..f2a3bed Binary files /dev/null and b/__MACOSX/shot/07.png differ diff --git a/__MACOSX/shot/08.png b/__MACOSX/shot/08.png new file mode 100644 index 0000000..79dc0bf Binary files /dev/null and b/__MACOSX/shot/08.png differ diff --git a/__MACOSX/shot/09.png b/__MACOSX/shot/09.png new file mode 100644 index 0000000..a233290 Binary files /dev/null and b/__MACOSX/shot/09.png differ diff --git a/__MACOSX/shot/10.png b/__MACOSX/shot/10.png new file mode 100644 index 0000000..ed0a723 Binary files /dev/null and b/__MACOSX/shot/10.png differ diff --git a/__MACOSX/shot/11.png b/__MACOSX/shot/11.png new file mode 100644 index 0000000..db5de33 Binary files /dev/null and b/__MACOSX/shot/11.png differ diff --git a/__MACOSX/shot/12.png b/__MACOSX/shot/12.png new file mode 100644 index 0000000..c411db3 Binary files /dev/null and b/__MACOSX/shot/12.png differ diff --git a/__MACOSX/shot/13.png b/__MACOSX/shot/13.png new file mode 100644 index 0000000..4425a87 Binary files /dev/null and b/__MACOSX/shot/13.png differ diff --git a/__MACOSX/shot/14.png b/__MACOSX/shot/14.png new file mode 100644 index 0000000..1ccca2a Binary files /dev/null and b/__MACOSX/shot/14.png differ diff --git a/__MACOSX/shot/15.png b/__MACOSX/shot/15.png new file mode 100644 index 0000000..f95f1a2 Binary files /dev/null and b/__MACOSX/shot/15.png differ diff --git a/__MACOSX/shot/16.png b/__MACOSX/shot/16.png new file mode 100644 index 0000000..bc08551 Binary files /dev/null and b/__MACOSX/shot/16.png differ diff --git a/__MACOSX/shot/17.png b/__MACOSX/shot/17.png new file mode 100644 index 0000000..3f0dc2b Binary files /dev/null and b/__MACOSX/shot/17.png differ diff --git a/__MACOSX/shot/18.png b/__MACOSX/shot/18.png new file mode 100644 index 0000000..1178c02 Binary files /dev/null and b/__MACOSX/shot/18.png differ diff --git a/__MACOSX/shot/19.png b/__MACOSX/shot/19.png new file mode 100644 index 0000000..e78a199 Binary files /dev/null and b/__MACOSX/shot/19.png differ diff --git a/__MACOSX/shot/20.png b/__MACOSX/shot/20.png new file mode 100644 index 0000000..dd9849a Binary files /dev/null and b/__MACOSX/shot/20.png differ diff --git a/__MACOSX/shot/21.png b/__MACOSX/shot/21.png new file mode 100644 index 0000000..fcb38cc Binary files /dev/null and b/__MACOSX/shot/21.png differ diff --git a/__MACOSX/shot/22.png b/__MACOSX/shot/22.png new file mode 100644 index 0000000..3f229fd Binary files /dev/null and b/__MACOSX/shot/22.png differ diff --git a/__MACOSX/shot/23.png b/__MACOSX/shot/23.png new file mode 100644 index 0000000..0c7eac6 Binary files /dev/null and b/__MACOSX/shot/23.png differ diff --git a/__MACOSX/shot/50.png b/__MACOSX/shot/50.png new file mode 100644 index 0000000..b5b39e8 Binary files /dev/null and b/__MACOSX/shot/50.png differ diff --git a/__MACOSX/shot/51.png b/__MACOSX/shot/51.png new file mode 100644 index 0000000..dc796de Binary files /dev/null and b/__MACOSX/shot/51.png differ diff --git a/__MACOSX/shot/52.png b/__MACOSX/shot/52.png new file mode 100644 index 0000000..c52cdb0 Binary files /dev/null and b/__MACOSX/shot/52.png differ diff --git a/__MACOSX/shot/53.png b/__MACOSX/shot/53.png new file mode 100644 index 0000000..f5b91df Binary files /dev/null and b/__MACOSX/shot/53.png differ diff --git a/__MACOSX/shot/54.png b/__MACOSX/shot/54.png new file mode 100644 index 0000000..d44ef2b Binary files /dev/null and b/__MACOSX/shot/54.png differ diff --git a/__MACOSX/shot/99.gif b/__MACOSX/shot/99.gif new file mode 100644 index 0000000..bb0bf4f Binary files /dev/null and b/__MACOSX/shot/99.gif differ diff --git a/__MACOSX/shot/99.png b/__MACOSX/shot/99.png new file mode 100644 index 0000000..253c3c2 Binary files /dev/null and b/__MACOSX/shot/99.png differ diff --git a/__MACOSX/shot/ctaiot.mp4 b/__MACOSX/shot/ctaiot.mp4 new file mode 100644 index 0000000..2199de8 Binary files /dev/null and b/__MACOSX/shot/ctaiot.mp4 differ diff --git a/__MACOSX/shot/dataflow.png b/__MACOSX/shot/dataflow.png new file mode 100644 index 0000000..6ab8d2d Binary files /dev/null and b/__MACOSX/shot/dataflow.png differ diff --git a/__MACOSX/shot/debug/debug.png b/__MACOSX/shot/debug/debug.png new file mode 100644 index 0000000..a6028aa Binary files /dev/null and b/__MACOSX/shot/debug/debug.png differ diff --git a/__MACOSX/shot/debug/history.png b/__MACOSX/shot/debug/history.png new file mode 100644 index 0000000..898edee Binary files /dev/null and b/__MACOSX/shot/debug/history.png differ diff --git a/__MACOSX/shot/debug/login.png b/__MACOSX/shot/debug/login.png new file mode 100644 index 0000000..9f5c8a3 Binary files /dev/null and b/__MACOSX/shot/debug/login.png differ diff --git a/__MACOSX/shot/debug/run.png b/__MACOSX/shot/debug/run.png new file mode 100644 index 0000000..45bd747 Binary files /dev/null and b/__MACOSX/shot/debug/run.png differ diff --git a/__MACOSX/shot/deploy.jpg b/__MACOSX/shot/deploy.jpg new file mode 100644 index 0000000..1204e5c Binary files /dev/null and b/__MACOSX/shot/deploy.jpg differ diff --git a/__MACOSX/shot/dian.jpg b/__MACOSX/shot/dian.jpg new file mode 100644 index 0000000..c5e87ad Binary files /dev/null and b/__MACOSX/shot/dian.jpg differ diff --git a/__MACOSX/shot/douyin.jpg b/__MACOSX/shot/douyin.jpg new file mode 100644 index 0000000..34bd696 Binary files /dev/null and b/__MACOSX/shot/douyin.jpg differ diff --git a/__MACOSX/shot/mai.jpg b/__MACOSX/shot/mai.jpg new file mode 100644 index 0000000..2ed39d8 Binary files /dev/null and b/__MACOSX/shot/mai.jpg differ diff --git a/__MACOSX/shot/qq.png b/__MACOSX/shot/qq.png new file mode 100644 index 0000000..5d17b43 Binary files /dev/null and b/__MACOSX/shot/qq.png differ diff --git a/__MACOSX/shot/shui.jpg b/__MACOSX/shot/shui.jpg new file mode 100644 index 0000000..3c43f82 Binary files /dev/null and b/__MACOSX/shot/shui.jpg differ diff --git a/__MACOSX/shot/wx.png b/__MACOSX/shot/wx.png new file mode 100644 index 0000000..21f28d3 Binary files /dev/null and b/__MACOSX/shot/wx.png differ diff --git a/cn-universal-admin/pom.xml b/cn-universal-admin/pom.xml new file mode 100644 index 0000000..6bb577f --- /dev/null +++ b/cn-universal-admin/pom.xml @@ -0,0 +1,120 @@ + + + + cn-universal + cn.universal.iot + 1.5-SNAPSHOT + + 4.0.0 + + cn-universal-admin + + + + + org.apache.velocity + velocity-engine-core + 2.3 + + + commons-io + commons-io + 2.11.0 + + + cn.universal.iot + cn-universal-persistence + ${project.version} + + + cn.universal.iot + cn-universal-rule + ${project.version} + + + cn.universal.iot + cn-universal-dm + ${project.version} + + + cn.universal.iot + cn-universal-core + ${project.version} + + + + cn.universal.iot + cn-universal-oss + ${project.version} + + + + org.springframework.security + spring-security-core + + + spring-boot-starter-web + + + slf4j-log4j12 + org.slf4j + + + + org.springframework.boot + ${spring-boot.version} + provided + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework + spring-web + + + + + junit + junit + 4.12 + test + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + com.vaadin.external.google + android-json + + + + + cn.universal.iot + cn-universal-mqtt-protocol + ${project.version} + compile + + + + + \ No newline at end of file diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/annotation/DataScope.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/annotation/DataScope.java new file mode 100644 index 0000000..8b20e5c --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/annotation/DataScope.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** 数据权限过滤注解 */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataScope { + + String alias() default ""; + + String created() default ""; +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/annotation/Log.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/annotation/Log.java new file mode 100644 index 0000000..0f6aef9 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/annotation/Log.java @@ -0,0 +1,40 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.annotation; + +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.enums.OperatorType; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** 自定义操作日志记录注解 */ +@Target({ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Log { + + /** 模块 */ + public String title() default ""; + + /** 功能 */ + public BusinessType businessType() default BusinessType.OTHER; + + /** 操作人类别 */ + public OperatorType operatorType() default OperatorType.MANAGE; + + /** 是否保存请求的参数 */ + public boolean isSaveRequestData() default true; +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/aspectj/DataScopeAspect.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/aspectj/DataScopeAspect.java new file mode 100644 index 0000000..c16de5f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/aspectj/DataScopeAspect.java @@ -0,0 +1,118 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.aspectj; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import cn.universal.admin.common.annotation.DataScope; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.common.domain.BaseEntity; +import cn.universal.common.utils.ReflectUtils; +import cn.universal.persistence.entity.IoTUser; +import jakarta.annotation.Resource; +import java.lang.reflect.Method; +import java.util.Map; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +/** 数据过滤处理 */ +@Aspect +@Component +public class DataScopeAspect { + + /** 数据范围 */ + public static final String SCOPE = "scope"; + + @Resource private IIotUserService iIotUserService; + + // 配置织入点 + @Pointcut("@annotation(cn.universal.admin.common.annotation.DataScope)") + public void dataScopePointCut() {} + + @Before("dataScopePointCut()") + public void doBefore(JoinPoint point) throws Throwable { + clearDataScope(point); + handleDataScope(point); + } + + protected void handleDataScope(final JoinPoint joinPoint) { + // 获得注解 + DataScope controllerDataScope = getAnnotationLog(joinPoint); + if (controllerDataScope == null) { + return; + } + IoTUser iotUser = iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin()) { + dataScopeFilter(joinPoint, controllerDataScope, iotUser.getUnionId()); + } + } + + /** + * 数据范围过滤 + * + * @param joinPoint 切点 + * @param dataScope 数据范围 + * @param unionId 用户unionId + */ + public static void dataScopeFilter(JoinPoint joinPoint, DataScope dataScope, String unionId) { + + StringBuilder sqlString = new StringBuilder(); + String alias = StrUtil.isNotBlank(dataScope.alias()) ? dataScope.alias() + "." : ""; + sqlString.append(StrUtil.format(" {}union_id = '{}' ", alias, unionId)); + + if (StrUtil.isNotBlank(dataScope.created())) { + sqlString.append(StrUtil.format(" or {} = '{}' ", dataScope.created(), unionId)); + } + + putDataScope(joinPoint, sqlString.toString(), SCOPE); + } + + /** 是否存在注解,如果存在就获取 */ + private DataScope getAnnotationLog(JoinPoint joinPoint) { + Signature signature = joinPoint.getSignature(); + MethodSignature methodSignature = (MethodSignature) signature; + Method method = methodSignature.getMethod(); + + if (method != null) { + return method.getAnnotation(DataScope.class); + } + return null; + } + + /** 拼接权限sql前先清空params.dataScope参数防止注入 */ + private void clearDataScope(final JoinPoint joinPoint) { + Object params = joinPoint.getArgs()[0]; + if (Validator.isNotNull(params)) { + putDataScope(joinPoint, "", SCOPE); + } + } + + private static void putDataScope(JoinPoint joinPoint, String sql, String queryKey) { + Object params = joinPoint.getArgs()[0]; + if (Validator.isNotNull(params)) { + if (params instanceof BaseEntity) { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(queryKey, sql); + } else { + Map invoke = ReflectUtils.invokeGetter(params, "params"); + invoke.put(queryKey, sql); + } + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/aspectj/LogAspect.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/aspectj/LogAspect.java new file mode 100644 index 0000000..589d26b --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/aspectj/LogAspect.java @@ -0,0 +1,222 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.aspectj; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessStatus; +import cn.universal.admin.common.enums.HttpMethod; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.AsyncService; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.common.utils.ServletUtils; +import cn.universal.common.utils.SpringUtils; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.SysOperLog; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Map; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; + +/** 操作日志记录处理 */ +@Aspect +@Component +public class LogAspect { + + @Resource private IIotUserService iIotUserService; + private static final Logger log = LoggerFactory.getLogger(LogAspect.class); + + // 配置织入点 + @Pointcut("@annotation(cn.universal.admin.common.annotation.Log)") + public void logPointCut() {} + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) { + handleLog(joinPoint, null, jsonResult); + } + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointCut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + handleLog(joinPoint, e, null); + } + + protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) { + try { + // 获得注解 + Log controllerLog = getAnnotationLog(joinPoint); + if (controllerLog == null) { + return; + } + + // 获取当前的用户 + IoTUser iotUser = iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()); + + // *========数据库日志=========*// + SysOperLog operLog = new SysOperLog(); + operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); + // 请求的地址 + String ip = ServletUtils.getClientIP(); + operLog.setOperIp(ip); + // 返回参数 + operLog.setJsonResult(JSONUtil.toJsonStr(jsonResult)); + + operLog.setOperUrl(ServletUtils.getRequest().getRequestURI()); + if (iotUser != null) { + operLog.setOperName(iotUser.getUsername()); + } + + if (e != null) { + operLog.setStatus(BusinessStatus.FAIL.ordinal()); + operLog.setErrorMsg(StrUtil.sub(e.getMessage(), 0, 2000)); + } + // 设置方法名称 + String className = joinPoint.getTarget().getClass().getName(); + String methodName = joinPoint.getSignature().getName(); + operLog.setMethod(className + "." + methodName + "()"); + // 设置请求方式 + operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); + // 处理设置注解上的参数 + getControllerMethodDescription(joinPoint, controllerLog, operLog); + // 保存数据库 + SpringUtils.getBean(AsyncService.class).recordOper(operLog); + } catch (Exception exp) { + // 记录本地异常日志 + log.error("==前置通知异常=="); + log.error("异常信息", exp); + } + } + + /** + * 获取注解中对方法的描述信息 用于Controller层注解 + * + * @param log 日志 + * @param operLog 操作日志 + */ + public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog) + throws Exception { + // 设置action动作 + operLog.setBusinessType(log.businessType().ordinal()); + // 设置标题 + operLog.setTitle(log.title()); + // 设置操作人类别 + operLog.setOperatorType(log.operatorType().ordinal()); + // 是否需要保存request,参数和值 + if (log.isSaveRequestData()) { + // 获取参数的信息,传入到数据库中。 + setRequestValue(joinPoint, operLog); + } + } + + /** + * 获取请求的参数,放到log中 + * + * @param operLog 操作日志 + * @throws Exception 异常 + */ + private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception { + String requestMethod = operLog.getRequestMethod(); + if (HttpMethod.PUT.name().equals(requestMethod) + || HttpMethod.POST.name().equals(requestMethod)) { + String params = argsArrayToString(joinPoint.getArgs()); + operLog.setOperParam(StrUtil.sub(params, 0, 2000)); + } else { + Map paramsMap = + (Map) + ServletUtils.getRequest() + .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operLog.setOperParam(StrUtil.sub(paramsMap.toString(), 0, 2000)); + } + } + + /** 是否存在注解,如果存在就获取 */ + private Log getAnnotationLog(JoinPoint joinPoint) throws Exception { + Signature signature = joinPoint.getSignature(); + MethodSignature methodSignature = (MethodSignature) signature; + Method method = methodSignature.getMethod(); + + if (method != null) { + return method.getAnnotation(Log.class); + } + return null; + } + + /** 参数拼装 */ + private String argsArrayToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null && paramsArray.length > 0) { + for (Object o : paramsArray) { + if (Validator.isNotNull(o) && !isFilterObject(o)) { + params.append(JSONUtil.toJsonStr(o)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param o 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + @SuppressWarnings("rawtypes") + public boolean isFilterObject(final Object o) { + Class clazz = o.getClass(); + if (clazz.isArray()) { + return clazz.getComponentType().isAssignableFrom(MultipartFile.class); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection collection = (Collection) o; + for (Object value : collection) { + return value instanceof MultipartFile; + } + } else if (Map.class.isAssignableFrom(clazz)) { + Map map = (Map) o; + for (Object value : map.entrySet()) { + Map.Entry entry = (Map.Entry) value; + return entry.getValue() instanceof MultipartFile; + } + } + return o instanceof MultipartFile + || o instanceof HttpServletRequest + || o instanceof HttpServletResponse + || o instanceof BindingResult; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/enums/BusinessStatus.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/enums/BusinessStatus.java new file mode 100644 index 0000000..47aab01 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/enums/BusinessStatus.java @@ -0,0 +1,22 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.enums; + +/** 操作状态枚举 */ +public enum BusinessStatus { + /** 成功 */ + SUCCESS, + + /** 失败 */ + FAIL, +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/enums/BusinessType.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/enums/BusinessType.java new file mode 100644 index 0000000..b96c709 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/enums/BusinessType.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.enums; + +/** 业务操作类型枚举 */ +public enum BusinessType { + /** 其它 */ + OTHER, + + /** 新增 */ + INSERT, + + /** 修改 */ + UPDATE, + + /** 删除 */ + DELETE, + + /** 授权 */ + GRANT, + + /** 导出 */ + EXPORT, + + /** 导入 */ + IMPORT, + + /** 强退 */ + FORCE, + + /** 生成代码 */ + GENCODE, + + /** 清空数据 */ + CLEAN, +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/enums/HttpMethod.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/enums/HttpMethod.java new file mode 100644 index 0000000..63178f5 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/enums/HttpMethod.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.enums; + +import java.util.HashMap; +import java.util.Map; +import org.springframework.lang.Nullable; + +/** HTTP请求方式枚举 */ +public enum HttpMethod { + GET, + HEAD, + POST, + PUT, + PATCH, + DELETE, + OPTIONS, + TRACE; + + private static final Map mappings = new HashMap<>(16); + + static { + for (HttpMethod httpMethod : values()) { + mappings.put(httpMethod.name(), httpMethod); + } + } + + @Nullable + public static HttpMethod resolve(@Nullable String method) { + return (method != null ? mappings.get(method) : null); + } + + public boolean matches(String method) { + return (this == resolve(method)); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/enums/OperatorType.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/enums/OperatorType.java new file mode 100644 index 0000000..9b0adf6 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/enums/OperatorType.java @@ -0,0 +1,25 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.enums; + +/** 操作人类别枚举 */ +public enum OperatorType { + /** 其它 */ + OTHER, + + /** 后台用户 */ + MANAGE, + + /** 手机端用户 */ + MOBILE +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/service/BaseService.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/service/BaseService.java new file mode 100644 index 0000000..5b28f1f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/service/BaseService.java @@ -0,0 +1,20 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.service; + +import cn.universal.persistence.entity.IoTUser; + +public interface BaseService { + + IoTUser queryIotUser(String unionId); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/service/BaseServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/service/BaseServiceImpl.java new file mode 100644 index 0000000..c16ba72 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/service/BaseServiceImpl.java @@ -0,0 +1,34 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.service; + +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.persistence.entity.IoTUser; +import jakarta.annotation.Resource; +import org.springframework.cache.annotation.Cacheable; + +public class BaseServiceImpl implements BaseService { + + @Resource private IIotUserService iIotUserService; + + @Override + @Cacheable(cacheNames = "user_parent_entity", key = "''+#unionId", unless = "#result == null") + public IoTUser queryIotUser(String unionId) { + IoTUser iotUser = iIotUserService.selectUserByUnionId(unionId); + if (iotUser.getParentUnionId() != null) { + iotUser = iIotUserService.selectUserByUnionId(iotUser.getParentUnionId()); + return iotUser; + } + return iotUser; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/DictUtils.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/DictUtils.java new file mode 100644 index 0000000..e813f3b --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/DictUtils.java @@ -0,0 +1,161 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.universal.common.constant.Constants; +import cn.universal.persistence.entity.admin.SysDictData; +import java.util.List; + +/** 字典工具类 */ +public class DictUtils { + + /** 分隔符 */ + public static final String SEPARATOR = ","; + + /** + * 设置字典缓存 + * + * @param key 参数键 + * @param dictDatas 字典数据列表 + */ + public static void setDictCache(String key, List dictDatas) { + // SpringUtils.getBean(RedisCache.class).setCacheObject(getCacheKey(key), dictDatas); + } + + /** + * 获取字典缓存 + * + * @param key 参数键 + * @return dictDatas 字典数据列表 + */ + public static List getDictCache(String key) { + // Object cacheObj = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key)); + // if (Validator.isNotNull(cacheObj)) { + // List dictDatas = (List) cacheObj; + // return dictDatas; + // } + return null; + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue) { + return getDictLabel(dictType, dictValue, SEPARATOR); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel) { + return getDictValue(dictType, dictLabel, SEPARATOR); + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue, String separator) { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + + if (StrUtil.containsAny(dictValue, separator) && CollUtil.isNotEmpty(datas)) { + for (SysDictData dict : datas) { + for (String value : dictValue.split(separator)) { + if (value.equals(dict.getDictValue())) { + propertyString.append(dict.getDictLabel() + separator); + break; + } + } + } + } else { + for (SysDictData dict : datas) { + if (dictValue.equals(dict.getDictValue())) { + return dict.getDictLabel(); + } + } + } + return StrUtil.strip(propertyString.toString(), null, separator); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @param separator 分隔符 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel, String separator) { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + + if (StrUtil.containsAny(dictLabel, separator) && CollUtil.isNotEmpty(datas)) { + for (SysDictData dict : datas) { + for (String label : dictLabel.split(separator)) { + if (label.equals(dict.getDictLabel())) { + propertyString.append(dict.getDictValue() + separator); + break; + } + } + } + } else { + for (SysDictData dict : datas) { + if (dictLabel.equals(dict.getDictLabel())) { + return dict.getDictValue(); + } + } + } + return StrUtil.strip(propertyString.toString(), null, separator); + } + + /** + * 删除指定字典缓存 + * + * @param key 字典键 + */ + public static void removeDictCache(String key) { + // SpringUtils.getBean(RedisCache.class).deleteObject(getCacheKey(key)); + } + + /** 清空字典缓存 */ + public static void clearDictCache() { + // Collection keys = SpringUtils.getBean(RedisCache.class) + // .keys(Constants.SYS_DICT_KEY + "*"); + // SpringUtils.getBean(RedisCache.class).deleteObject(keys); + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + public static String getCacheKey(String configKey) { + return Constants.SYS_DICT_KEY + configKey; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/ExcelUtil.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/ExcelUtil.java new file mode 100644 index 0000000..f09151f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/ExcelUtil.java @@ -0,0 +1,1201 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.utils; + +import cn.hutool.core.convert.Convert; +import cn.universal.common.annotation.Excel; +import cn.universal.common.annotation.Excel.ColumnType; +import cn.universal.common.annotation.Excel.Type; +import cn.universal.common.annotation.Excels; +import cn.universal.common.config.UnivUPloadConfig; +import cn.universal.common.exception.IoTException; +import cn.universal.common.poi.ExcelHandlerAdapter; +import cn.universal.common.utils.DateUtils; +import cn.universal.common.utils.FileTypeUtils; +import cn.universal.common.utils.FileUtils; +import cn.universal.common.utils.ReflectUtils; +import cn.universal.common.utils.StringUtils; +import cn.universal.persistence.query.AjaxResult; +import jakarta.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import org.apache.poi.hssf.usermodel.HSSFClientAnchor; +import org.apache.poi.hssf.usermodel.HSSFPicture; +import org.apache.poi.hssf.usermodel.HSSFPictureData; +import org.apache.poi.hssf.usermodel.HSSFShape; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ooxml.POIXMLDocumentPart; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.PictureData; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFClientAnchor; +import org.apache.poi.xssf.usermodel.XSSFDataValidation; +import org.apache.poi.xssf.usermodel.XSSFDrawing; +import org.apache.poi.xssf.usermodel.XSSFPicture; +import org.apache.poi.xssf.usermodel.XSSFShape; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Excel相关处理工具类 */ +public class ExcelUtil { + + private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class); + + /** Excel sheet最大行数,默认65536 */ + public static final int sheetSize = 65536; + + /** 工作表名称 */ + private String sheetName; + + /** 导出类型(EXPORT:导出数据;IMPORT:导入模板) */ + private Type type; + + /** 工作薄对象 */ + private Workbook wb; + + /** 工作表对象 */ + private Sheet sheet; + + /** 样式列表 */ + private Map styles; + + /** 导入导出数据列表 */ + private List list; + + /** 注解列表 */ + private List fields; + + /** 当前行号 */ + private int rownum; + + /** 标题 */ + private String title; + + /** 最大高度 */ + private short maxHeight; + + /** 统计列表 */ + private Map statistics = new HashMap(); + + /** 数字格式 */ + private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00"); + + /** 实体对象 */ + public Class clazz; + + public ExcelUtil(Class clazz) { + this.clazz = clazz; + } + + public void init(List list, String sheetName, String title, Type type) { + if (list == null) { + list = new ArrayList(); + } + this.list = list; + this.sheetName = sheetName; + this.type = type; + this.title = title; + createExcelField(); + createWorkbook(); + createTitle(); + } + + /** 创建excel第一行标题 */ + public void createTitle() { + if (StringUtils.isNotEmpty(title)) { + Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0); + titleRow.setHeightInPoints(30); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellStyle(styles.get("title")); + titleCell.setCellValue(title); + sheet.addMergedRegion( + new CellRangeAddress( + titleRow.getRowNum(), + titleRow.getRowNum(), + titleRow.getRowNum(), + this.fields.size() - 1)); + } + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(InputStream is) throws Exception { + return importExcel(is, 0); + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @param titleNum 标题占用行数 + * @return 转换后集合 + */ + public List importExcel(InputStream is, int titleNum) throws Exception { + return importExcel(StringUtils.EMPTY, is, titleNum); + } + + /** + * 对excel表单指定表格索引名转换成list + * + * @param sheetName 表格索引名 + * @param titleNum 标题占用行数 + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(String sheetName, InputStream is, int titleNum) throws Exception { + this.type = Type.IMPORT; + this.wb = WorkbookFactory.create(is); + List list = new ArrayList(); + // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet + Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0); + if (sheet == null) { + throw new IOException("文件sheet不存在"); + } + boolean isXSSFWorkbook = !(wb instanceof HSSFWorkbook); + Map pictures; + if (isXSSFWorkbook) { + pictures = getSheetPictures07((XSSFSheet) sheet, (XSSFWorkbook) wb); + } else { + pictures = getSheetPictures03((HSSFSheet) sheet, (HSSFWorkbook) wb); + } + // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1 + int rows = sheet.getLastRowNum(); + + if (rows > 0) { + // 定义一个map用于存放excel列的序号和field.value列号跟 key列名 + Map cellMap = new HashMap(); + // 获取表头 + Row heard = sheet.getRow(titleNum); + + // otherParams处理 + Row firstRow = sheet.getRow(0); + Map keyMap = new HashMap<>(); + + if (firstRow != null) { + for (int i = 0; i < firstRow.getPhysicalNumberOfCells(); i++) { + if (firstRow.getCell(i) != null) { + String cellValue = firstRow.getCell(i).getStringCellValue(); + if (cellValue.contains(".")) { + keyMap.put(i, cellValue); + } + } + } + } + + for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++) { + Cell cell = heard.getCell(i); + if (StringUtils.isNotNull(cell)) { + String value = this.getCellValue(heard, i).toString(); + cellMap.put(value, i); + } else { + cellMap.put(null, i); + } + } + // 有数据时才处理 得到类的所有field. + List fields = this.getFields(); + Map fieldsMap = new HashMap(); + // Map otherParams = new HashMap<>(); + for (Object[] objects : fields) { + + // 在fieldMap中,key是第几列,value是Object,其中Object[0]是实体类的属性字段,Object[1]是@Excel注解的name属性 + Excel attr = (Excel) objects[1]; + // 第column列 + Integer column = cellMap.get(attr.name()); + if (column != null) { + fieldsMap.put(column, objects); + } + } + for (int i = titleNum + 1; i <= rows; i++) { + // 从第2行开始取数据,默认第一行是表头. + Row row = sheet.getRow(i); + // 判断当前行是否是空行 + if (isRowEmpty(row)) { + continue; + } + T entity = null; + for (Map.Entry entry : fieldsMap.entrySet()) { + // 表格中第row行第entry.getKey()列填入的数据,根据行列获取单元格值 + Object val = this.getCellValue(row, entry.getKey()); + + // 如果不存在实例则新建. + entity = (entity == null ? clazz.newInstance() : entity); + // 从map中得到对应列的field. [0] -> 实体类字段,[1] -> @Excel注解 + Field field = (Field) entry.getValue()[0]; + Excel attr = (Excel) entry.getValue()[1]; + // 取得类型,并根据对象类型设置值. 实体类字段的类型 (String、Integer、Boolean) + Class fieldType = field.getType(); + // 将表格中的数据转换成对象中属性的类型 + if (String.class == fieldType) { + // 如果字段是String类型,则把表格中数据转换成String类型 + String s = Convert.toStr(val); + if (StringUtils.endsWith(s, ".0")) { + val = StringUtils.substringBefore(s, ".0"); + } else { + // 获取@Excel注解的dateFormat属性 + String dateFormat = field.getAnnotation(Excel.class).dateFormat(); + if (StringUtils.isNotEmpty(dateFormat)) { + val = DateUtils.parseDateToStr(dateFormat, (Date) val); + } else { + val = Convert.toStr(val); + } + } + // 实体类字段的类型是Integer,将表格数据转换成int + } else if ((Integer.TYPE == fieldType || Integer.class == fieldType) + && StringUtils.isNumeric(Convert.toStr(val))) { + val = Convert.toInt(val); + // 实体类字段的类型是Long,将表格数据转换成Long + } else if (Long.TYPE == fieldType || Long.class == fieldType) { + val = Convert.toLong(val); + } else if (Double.TYPE == fieldType || Double.class == fieldType) { + val = Convert.toDouble(val); + } else if (Float.TYPE == fieldType || Float.class == fieldType) { + val = Convert.toFloat(val); + } else if (BigDecimal.class == fieldType) { + val = Convert.toBigDecimal(val); + } else if (Date.class == fieldType) { + if (val instanceof String) { + val = DateUtils.parseDate(val); + } else if (val instanceof Double) { + val = DateUtil.getJavaDate((Double) val); + } + } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) { + val = Convert.toBool(val, false); + } + if (StringUtils.isNotNull(fieldType)) { + // propertyName -> deviceId + String propertyName = field.getName(); + // attr.targetAttr():另一个类中的属性名称,支持多级获取,以小数点隔开 + if (StringUtils.isNotEmpty(attr.targetAttr())) { + propertyName = field.getName() + "." + attr.targetAttr(); + } else if (StringUtils.isNotEmpty(attr.readConverterExp())) { + val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator()); + } else if (StringUtils.isNotEmpty(attr.dictType())) { + val = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator()); + } else if (!attr.handler().equals(ExcelHandlerAdapter.class)) { + val = dataFormatHandlerAdapter(val, attr); + } else if (ColumnType.IMAGE == attr.cellType() && StringUtils.isNotEmpty(pictures)) { + PictureData image = pictures.get(row.getRowNum() + "_" + entry.getKey()); + if (image == null) { + val = ""; + } else { + byte[] data = image.getData(); + val = FileUtils.writeImportBytes(data); + } + } + // entity,对象实体。propertyName:字段名,val:值 + ReflectUtils.invokeSetter(entity, propertyName, val); + } + } + // otherParams处理 + Map otherParamsValue = new HashMap<>(); + String propertyName = null; + for (Map.Entry entry : keyMap.entrySet()) { + + String[] value = entry.getValue().split("\\."); + if (propertyName == null) { + propertyName = value[0]; + } + Object val = this.getCellValue(row, entry.getKey()); + otherParamsValue.put(value[1], (String) val); + } + ReflectUtils.invokeSetter(entity, propertyName, otherParamsValue); + // 验证 + list.add(entity); + } + } + return list; + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName) { + return exportExcel(list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName, String title) { + this.init(list, sheetName, title, Type.EXPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + * @throws IOException + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName) { + exportExcel(response, list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + * @throws IOException + */ + public void exportExcel( + HttpServletResponse response, List list, String sheetName, String title) { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(list, sheetName, title, Type.EXPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName) { + return importTemplateExcel(sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName, String title) { + this.init(null, sheetName, title, Type.IMPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName) { + importTemplateExcel(response, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName, String title) { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(null, sheetName, title, Type.IMPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public void exportExcel(HttpServletResponse response) { + try { + writeSheet(); + wb.write(response.getOutputStream()); + } catch (Exception e) { + log.error("导出Excel异常", e); + + } finally { + IOUtils.closeQuietly(wb); + } + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public AjaxResult exportExcel() { + OutputStream out = null; + try { + writeSheet(); + String filename = encodingFilename(sheetName); + out = new FileOutputStream(getAbsoluteFile(filename)); + wb.write(out); + return AjaxResult.success(filename); + } catch (Exception e) { + log.error("导出Excel异常", e); + throw new IoTException("导出Excel失败,请联系网站管理员!"); + } finally { + IOUtils.closeQuietly(wb); + IOUtils.closeQuietly(out); + } + } + + /** 创建写入数据到Sheet */ + public void writeSheet() { + // 取出一共有多少个sheet. + int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize)); + for (int index = 0; index < sheetNo; index++) { + createSheet(sheetNo, index); + + // 产生一行 + Row row = sheet.createRow(rownum); + int column = 0; + // 写入各个字段的列头名称 + for (Object[] os : fields) { + Excel excel = (Excel) os[1]; + this.createCell(excel, row, column++); + } + if (Type.EXPORT.equals(type)) { + fillExcelData(index, row); + addStatisticsRow(); + } + } + } + + /** + * 填充excel数据 + * + * @param index 序号 + * @param row 单元格行 + */ + public void fillExcelData(int index, Row row) { + int startNo = index * sheetSize; + int endNo = Math.min(startNo + sheetSize, list.size()); + for (int i = startNo; i < endNo; i++) { + row = sheet.createRow(i + 1 + rownum - startNo); + // 得到导出对象. + T vo = (T) list.get(i); + int column = 0; + for (Object[] os : fields) { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + this.addCell(excel, row, vo, field, column++); + } + } + } + + /** + * 创建表格样式 + * + * @param wb 工作薄对象 + * @return 样式列表 + */ + private Map createStyles(Workbook wb) { + // 写入各条记录,每条记录对应excel表中的一行 + Map styles = new HashMap(); + CellStyle style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font titleFont = wb.createFont(); + titleFont.setFontName("Arial"); + titleFont.setFontHeightInPoints((short) 16); + titleFont.setBold(true); + style.setFont(titleFont); + styles.put("title", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + style.setFont(dataFont); + styles.put("data", style); + + style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font headerFont = wb.createFont(); + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short) 10); + headerFont.setBold(true); + headerFont.setColor(IndexedColors.WHITE.getIndex()); + style.setFont(headerFont); + styles.put("header", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font totalFont = wb.createFont(); + totalFont.setFontName("Arial"); + totalFont.setFontHeightInPoints((short) 10); + style.setFont(totalFont); + styles.put("total", style); + + style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.LEFT); + styles.put("data1", style); + + style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.CENTER); + styles.put("data2", style); + + style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.RIGHT); + styles.put("data3", style); + + return styles; + } + + /** 创建单元格 */ + public Cell createCell(Excel attr, Row row, int column) { + // 创建列 + Cell cell = row.createCell(column); + // 写入列信息 + cell.setCellValue(attr.name()); + setDataValidation(attr, row, column); + cell.setCellStyle(styles.get("header")); + return cell; + } + + /** + * 设置单元格信息 + * + * @param value 单元格值 + * @param attr 注解相关 + * @param cell 单元格信息 + */ + public void setCellVo(Object value, Excel attr, Cell cell) { + if (ColumnType.STRING == attr.cellType()) { + cell.setCellValue(StringUtils.isNull(value) ? attr.defaultValue() : value + attr.suffix()); + } else if (ColumnType.NUMERIC == attr.cellType()) { + if (StringUtils.isNotNull(value)) { + cell.setCellValue( + StringUtils.contains(Convert.toStr(value), ".") + ? Convert.toDouble(value) + : Convert.toInt(value)); + } + } else if (ColumnType.IMAGE == attr.cellType()) { + ClientAnchor anchor = + new XSSFClientAnchor( + 0, + 0, + 0, + 0, + (short) cell.getColumnIndex(), + cell.getRow().getRowNum(), + (short) (cell.getColumnIndex() + 1), + cell.getRow().getRowNum() + 1); + String imagePath = Convert.toStr(value); + if (StringUtils.isNotEmpty(imagePath)) { + byte[] data = ImageUtils.getImage(imagePath); + getDrawingPatriarch(cell.getSheet()) + .createPicture( + anchor, cell.getSheet().getWorkbook().addPicture(data, getImageType(data))); + } + } + } + + /** 获取画布 */ + public static Drawing getDrawingPatriarch(Sheet sheet) { + if (sheet.getDrawingPatriarch() == null) { + sheet.createDrawingPatriarch(); + } + return sheet.getDrawingPatriarch(); + } + + /** 获取图片类型,设置图片插入类型 */ + public int getImageType(byte[] value) { + String type = FileTypeUtils.getFileExtendName(value); + if ("JPG".equalsIgnoreCase(type)) { + return Workbook.PICTURE_TYPE_JPEG; + } else if ("PNG".equalsIgnoreCase(type)) { + return Workbook.PICTURE_TYPE_PNG; + } + return Workbook.PICTURE_TYPE_JPEG; + } + + /** 创建表格样式 */ + public void setDataValidation(Excel attr, Row row, int column) { + if (attr.name().indexOf("注:") >= 0) { + sheet.setColumnWidth(column, 6000); + } else { + // 设置列宽 + sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256)); + } + // 如果设置了提示信息则鼠标放上去提示. + if (StringUtils.isNotEmpty(attr.prompt())) { + // 这里默认设了2-101列提示. + setXSSFPrompt(sheet, "", attr.prompt(), 1, 100, column, column); + } + // 如果设置了combo属性则本列只能选择不能输入 + if (attr.combo().length > 0) { + // 这里默认设了2-101列只能选择不能输入. + setXSSFValidation(sheet, attr.combo(), 1, 100, column, column); + } + } + + /** 添加单元格 */ + public Cell addCell(Excel attr, Row row, T vo, Field field, int column) { + Cell cell = null; + try { + // 设置行高 + row.setHeight(maxHeight); + // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列. + if (attr.isExport()) { + // 创建cell + cell = row.createCell(column); + int align = attr.align().value(); + cell.setCellStyle(styles.get("data" + (align >= 1 && align <= 3 ? align : ""))); + + // 用于读取对象中的属性 + Object value = getTargetValue(vo, field, attr); + String dateFormat = attr.dateFormat(); + String readConverterExp = attr.readConverterExp(); + String separator = attr.separator(); + String dictType = attr.dictType(); + if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value)) { + cell.setCellValue(DateUtils.parseDateToStr(dateFormat, (Date) value)); + } else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value)) { + cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator)); + } else if (StringUtils.isNotEmpty(dictType) && StringUtils.isNotNull(value)) { + cell.setCellValue(convertDictByExp(Convert.toStr(value), dictType, separator)); + } else if (value instanceof BigDecimal && -1 != attr.scale()) { + cell.setCellValue( + (((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).toString()); + } else if (!attr.handler().equals(ExcelHandlerAdapter.class)) { + cell.setCellValue(dataFormatHandlerAdapter(value, attr)); + } else { + // 设置列类型 + setCellVo(value, attr, cell); + } + addStatisticsData(column, Convert.toStr(value), attr); + } + } catch (Exception e) { + log.error("导出Excel失败", e); + } + return cell; + } + + /** + * 设置 POI XSSFSheet 单元格提示 + * + * @param sheet 表单 + * @param promptTitle 提示标题 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setXSSFPrompt( + Sheet sheet, + String promptTitle, + String promptContent, + int firstRow, + int endRow, + int firstCol, + int endCol) { + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = helper.createCustomConstraint("DD1"); + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + DataValidation dataValidation = helper.createValidation(constraint, regions); + dataValidation.createPromptBox(promptTitle, promptContent); + dataValidation.setShowPromptBox(true); + sheet.addValidationData(dataValidation); + } + + /** + * 设置某些列的值只能输入预制的数据,显示下拉框. + * + * @param sheet 要设置的sheet. + * @param textlist 下拉框显示的内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + * @return 设置好的sheet. + */ + public void setXSSFValidation( + Sheet sheet, String[] textlist, int firstRow, int endRow, int firstCol, int endCol) { + DataValidationHelper helper = sheet.getDataValidationHelper(); + // 加载下拉列表内容 + DataValidationConstraint constraint = helper.createExplicitListConstraint(textlist); + // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + // 数据有效性对象 + DataValidation dataValidation = helper.createValidation(constraint, regions); + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } else { + dataValidation.setSuppressDropDownArrow(false); + } + + sheet.addValidationData(dataValidation); + } + + /** + * 解析导出值 0=男,1=女,2=未知 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String convertByExp(String propertyValue, String converterExp, String separator) { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(","); + for (String item : convertSource) { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(separator, propertyValue)) { + for (String value : propertyValue.split(separator)) { + if (itemArray[0].equals(value)) { + propertyString.append(itemArray[1] + separator); + break; + } + } + } else { + if (itemArray[0].equals(propertyValue)) { + return itemArray[1]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 反向解析值 男=0,女=1,未知=2 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String reverseByExp(String propertyValue, String converterExp, String separator) { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(","); + for (String item : convertSource) { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(separator, propertyValue)) { + for (String value : propertyValue.split(separator)) { + if (itemArray[1].equals(value)) { + propertyString.append(itemArray[0] + separator); + break; + } + } + } else { + if (itemArray[1].equals(propertyValue)) { + return itemArray[0]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 解析字典值 + * + * @param dictValue 字典值 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String convertDictByExp(String dictValue, String dictType, String separator) { + return DictUtils.getDictLabel(dictType, dictValue, separator); + } + + /** + * 反向解析值字典值 + * + * @param dictLabel 字典标签 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典值 + */ + public static String reverseDictByExp(String dictLabel, String dictType, String separator) { + return DictUtils.getDictValue(dictType, dictLabel, separator); + } + + /** + * 数据处理器 + * + * @param value 数据值 + * @param excel 数据注解 + * @return + */ + public String dataFormatHandlerAdapter(Object value, Excel excel) { + try { + Object instance = excel.handler().newInstance(); + Method formatMethod = + excel.handler().getMethod("format", new Class[] {Object.class, String[].class}); + value = formatMethod.invoke(instance, value, excel.args()); + } catch (Exception e) { + log.error("不能格式化数据 " + excel.handler(), e.getMessage()); + } + return Convert.toStr(value); + } + + /** 合计统计信息 */ + private void addStatisticsData(Integer index, String text, Excel entity) { + if (entity != null && entity.isStatistics()) { + Double temp = 0D; + if (!statistics.containsKey(index)) { + statistics.put(index, temp); + } + try { + temp = Double.valueOf(text); + } catch (NumberFormatException e) { + } + statistics.put(index, statistics.get(index) + temp); + } + } + + /** 创建统计行 */ + public void addStatisticsRow() { + if (statistics.size() > 0) { + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + Set keys = statistics.keySet(); + Cell cell = row.createCell(0); + cell.setCellStyle(styles.get("total")); + cell.setCellValue("合计"); + + for (Integer key : keys) { + cell = row.createCell(key); + cell.setCellStyle(styles.get("total")); + cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key))); + } + statistics.clear(); + } + } + + /** 编码文件名 */ + public String encodingFilename(String filename) { + filename = UUID.randomUUID().toString() + "_" + filename + ".xlsx"; + return filename; + } + + /** + * 获取下载路径 + * + * @param filename 文件名称 + */ + public String getAbsoluteFile(String filename) { + String downloadPath = UnivUPloadConfig.getDownloadPath() + filename; + File desc = new File(downloadPath); + if (!desc.getParentFile().exists()) { + desc.getParentFile().mkdirs(); + } + return downloadPath; + } + + /** + * 获取bean中的属性值 + * + * @param vo 实体对象 + * @param field 字段 + * @param excel 注解 + * @return 最终的属性值 + * @throws Exception + */ + private Object getTargetValue(T vo, Field field, Excel excel) throws Exception { + Object o = field.get(vo); + if (StringUtils.isNotEmpty(excel.targetAttr())) { + String target = excel.targetAttr(); + if (target.indexOf(".") > -1) { + String[] targets = target.split("[.]"); + for (String name : targets) { + o = getValue(o, name); + } + } else { + o = getValue(o, target); + } + } + return o; + } + + /** + * 以类的属性的get方法方法形式获取值 + * + * @param o + * @param name + * @return value + * @throws Exception + */ + private Object getValue(Object o, String name) throws Exception { + if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name)) { + Class clazz = o.getClass(); + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + o = field.get(o); + } + return o; + } + + /** 得到所有定义字段 */ + private void createExcelField() { + this.fields = getFields(); + this.fields = + this.fields.stream() + .sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())) + .collect(Collectors.toList()); + this.maxHeight = getRowHeight(); + } + + /** 获取字段注解信息 */ + public List getFields() { + List fields = new ArrayList(); + List tempFields = new ArrayList<>(); + tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields())); + tempFields.addAll(Arrays.asList(clazz.getDeclaredFields())); + for (Field field : tempFields) { + // 单注解 + if (field.isAnnotationPresent(Excel.class)) { + Excel attr = field.getAnnotation(Excel.class); + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) { + field.setAccessible(true); + fields.add(new Object[] {field, attr}); + } + } + + // 多注解 + if (field.isAnnotationPresent(Excels.class)) { + Excels attrs = field.getAnnotation(Excels.class); + Excel[] excels = attrs.value(); + for (Excel attr : excels) { + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) { + field.setAccessible(true); + fields.add(new Object[] {field, attr}); + } + } + } + } + return fields; + } + + /** 根据注解获取最大行高 */ + public short getRowHeight() { + double maxHeight = 0; + for (Object[] os : this.fields) { + Excel excel = (Excel) os[1]; + maxHeight = maxHeight > excel.height() ? maxHeight : excel.height(); + } + return (short) (maxHeight * 20); + } + + /** 创建一个工作簿 */ + public void createWorkbook() { + this.wb = new SXSSFWorkbook(500); + this.sheet = wb.createSheet(); + wb.setSheetName(0, sheetName); + this.styles = createStyles(wb); + } + + /** + * 创建工作表 + * + * @param sheetNo sheet数量 + * @param index 序号 + */ + public void createSheet(int sheetNo, int index) { + // 设置工作表的名称. + if (sheetNo > 1 && index > 0) { + this.sheet = wb.createSheet(); + this.createTitle(); + wb.setSheetName(index, sheetName + index); + } + } + + /** + * 获取单元格值 + * + * @param row 获取的行 + * @param column 获取单元格列号 + * @return 单元格值 + */ + public Object getCellValue(Row row, int column) { + if (row == null) { + return row; + } + Object val = ""; + try { + Cell cell = row.getCell(column); + if (StringUtils.isNotNull(cell)) { + if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) { + val = cell.getNumericCellValue(); + if (DateUtil.isCellDateFormatted(cell)) { + val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换 + } else { + if ((Double) val % 1 != 0) { + val = new BigDecimal(val.toString()); + } else { + val = new DecimalFormat("0").format(val); + } + } + } else if (cell.getCellType() == CellType.STRING) { + val = cell.getStringCellValue(); + } else if (cell.getCellType() == CellType.BOOLEAN) { + val = cell.getBooleanCellValue(); + } else if (cell.getCellType() == CellType.ERROR) { + val = cell.getErrorCellValue(); + } + } + } catch (Exception e) { + return val; + } + return val; + } + + /** + * 判断是否是空行 + * + * @param row 判断的行 + * @return + */ + private boolean isRowEmpty(Row row) { + if (row == null) { + return true; + } + for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) { + Cell cell = row.getCell(i); + if (cell != null && cell.getCellType() != CellType.BLANK) { + return false; + } + } + return true; + } + + /** + * 获取Excel2003图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map getSheetPictures03( + HSSFSheet sheet, HSSFWorkbook workbook) { + Map sheetIndexPicMap = new HashMap(); + List pictures = workbook.getAllPictures(); + if (!pictures.isEmpty()) { + for (HSSFShape shape : sheet.getDrawingPatriarch().getChildren()) { + HSSFClientAnchor anchor = (HSSFClientAnchor) shape.getAnchor(); + if (shape instanceof HSSFPicture) { + HSSFPicture pic = (HSSFPicture) shape; + int pictureIndex = pic.getPictureIndex() - 1; + HSSFPictureData picData = pictures.get(pictureIndex); + String picIndex = + String.valueOf(anchor.getRow1()) + "_" + String.valueOf(anchor.getCol1()); + sheetIndexPicMap.put(picIndex, picData); + } + } + return sheetIndexPicMap; + } else { + return sheetIndexPicMap; + } + } + + /** + * 获取Excel2007图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map getSheetPictures07( + XSSFSheet sheet, XSSFWorkbook workbook) { + Map sheetIndexPicMap = new HashMap(); + for (POIXMLDocumentPart dr : sheet.getRelations()) { + if (dr instanceof XSSFDrawing) { + XSSFDrawing drawing = (XSSFDrawing) dr; + List shapes = drawing.getShapes(); + for (XSSFShape shape : shapes) { + if (shape instanceof XSSFPicture) { + XSSFPicture pic = (XSSFPicture) shape; + XSSFClientAnchor anchor = pic.getPreferredSize(); + CTMarker ctMarker = anchor.getFrom(); + String picIndex = ctMarker.getRow() + "_" + ctMarker.getCol(); + sheetIndexPicMap.put(picIndex, pic.getPictureData()); + } + } + } + } + return sheetIndexPicMap; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/ImageUtils.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/ImageUtils.java new file mode 100644 index 0000000..6368c7f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/ImageUtils.java @@ -0,0 +1,91 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.utils; + +import cn.universal.common.config.UnivUPloadConfig; +import cn.universal.common.constant.Constants; +import cn.universal.common.utils.StringUtils; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import org.apache.poi.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** 图片工具类 */ +public class ImageUtils { + + private static final Logger log = LoggerFactory.getLogger(ImageUtils.class); + + public static byte[] getImage(String imagePath) { + InputStream is = getFile(imagePath); + try { + return IOUtils.toByteArray(is); + } catch (Exception e) { + log.error("加载异常", e); + return null; + } finally { + IOUtils.closeQuietly(is); + } + } + + public static InputStream getFile(String imagePath) { + try { + byte[] result = readFile(imagePath); + result = Arrays.copyOf(result, result.length); + return new ByteArrayInputStream(result); + } catch (Exception e) { + log.error("图片异常", e); + } + return null; + } + + /** + * 读取文件为字节数据 + * + * @param url 地址 + * @return 字节数据 + */ + public static byte[] readFile(String url) { + InputStream in = null; + ByteArrayOutputStream baos = null; + try { + if (url.startsWith("http")) { + // 网络地址 + URL urlObj = new URL(url); + URLConnection urlConnection = urlObj.openConnection(); + urlConnection.setConnectTimeout(30 * 1000); + urlConnection.setReadTimeout(60 * 1000); + urlConnection.setDoInput(true); + in = urlConnection.getInputStream(); + } else { + // 本机地址 + String localPath = UnivUPloadConfig.getProfile(); + String downloadPath = + localPath + StringUtils.substringAfter(url, Constants.RESOURCE_PREFIX); + in = new FileInputStream(downloadPath); + } + return IOUtils.toByteArray(in); + } catch (Exception e) { + log.error("获取文件路径异常", e); + return null; + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(baos); + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/PageUtils.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/PageUtils.java new file mode 100644 index 0000000..9f84c8f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/PageUtils.java @@ -0,0 +1,50 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.utils; + +import cn.universal.common.utils.StringUtils; +import cn.universal.persistence.page.PageDomain; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.page.TableSupport; +import com.github.pagehelper.Page; +import com.github.pagehelper.PageHelper; +import java.util.List; + +/** 分页工具类 */ +public class PageUtils extends PageHelper { + + /** 设置请求分页数据 */ + public static void startPage() { + PageDomain pageDomain = TableSupport.buildPageRequest(); + Integer pageNum = pageDomain.getPageNum(); + Integer pageSize = pageDomain.getPageSize(); + if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize)) { + String orderBy = pageDomain.getOrderBy(); + startPage(pageNum, pageSize, orderBy); + } + } + + /** 封装分页数据 */ + public static TableDataInfo getDataTable(List list) { + TableDataInfo rspData = new TableDataInfo(); + rspData.setCode(0); + rspData.setRows(list); + rspData.setMsg("查询成功"); + if (list instanceof Page) { + rspData.setTotal((int) ((Page) list).getTotal()); + } else { + rspData.setTotal(list.size()); + } + return rspData; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/SecurityUtils.java b/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/SecurityUtils.java new file mode 100644 index 0000000..0754ea1 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/common/utils/SecurityUtils.java @@ -0,0 +1,143 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.common.utils; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpStatus; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.common.exception.BaseException; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.IoTUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +/** 安全服务工具类 */ +@Component +public class SecurityUtils { + + private static ApplicationContext applicationContext; + private static final Logger log = LoggerFactory.getLogger(SecurityUtils.class); + + @Autowired + public void setApplicationContext(ApplicationContext applicationContext) { + SecurityUtils.applicationContext = applicationContext; + } + + /** 获取unionId用户 */ + public static String getUnionId() { + try { + Authentication authentication = getAuthentication(); + Object principal = authentication.getPrincipal(); + // 如果 principal 是 JWT,从 JWT 中提取用户信息 + if (principal instanceof Jwt) { + Jwt jwt = (Jwt) principal; + // 优先使用 sub 字段 + String subject = jwt.getSubject(); + String currentJti = jwt.getId(); + log.debug("oauth2 subject={}, currentJti={}", subject, currentJti); + if (StringUtils.hasText(subject)) { + return subject; // 如果 sub 就是 unionId,直接返回 + } + } + // 如果无法获取用户信息,抛出异常 + throw new BaseException("无法获取用户信息", HttpStatus.HTTP_UNAUTHORIZED + ""); + } catch (Exception e) { + throw new IoTException("获取用户信息异常", HttpStatus.HTTP_UNAUTHORIZED); + } + } + + /** 获取当前认证用户信息(从 JWT 中获取) */ + public static IoTUser getIoTUnionUser() { + try { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.getPrincipal() instanceof Jwt jwt) { + // 优先使用 sub 字段 + String subject = jwt.getSubject(); + if (StringUtils.hasText(subject)) { + // 从数据库加载用户信息 + return getIotUserByUnionId(subject); + } + // 如果 sub 为空,尝试从 unionId claim 获取 + String unionId = jwt.getClaimAsString("unionId"); + if (StringUtils.hasText(unionId)) { + return getIotUserByUnionId(unionId); + } + } + } catch (Exception e) { + log.error("获取当前用户信息失败", e); + } + return null; + } + + /** 根据用户名获取 IoTUser */ + private static IoTUser getIotUserByUsername(String username) { + if (StrUtil.isBlank(username)) { + throw new BaseException("获取用户信息异常", HttpStatus.HTTP_UNAUTHORIZED + ""); + } + try { + IIotUserService iIotUserService = applicationContext.getBean(IIotUserService.class); + return iIotUserService.selectUserByUserName(username); + } catch (Exception e) { + throw new BaseException("获取用户信息异常", HttpStatus.HTTP_UNAUTHORIZED + ""); + } + } + + /** 根据 unionId 获取 IoTUser */ + private static IoTUser getIotUserByUnionId(String unionId) { + if (StrUtil.isBlank(unionId)) { + throw new BaseException("获取用户信息异常", HttpStatus.HTTP_UNAUTHORIZED + ""); + } + try { + IIotUserService iIotUserService = applicationContext.getBean(IIotUserService.class); + return iIotUserService.selectUserByUnionId(unionId); + } catch (Exception e) { + throw new BaseException("获取用户信息异常", HttpStatus.HTTP_UNAUTHORIZED + ""); + } + } + + /** 获取Authentication */ + public static Authentication getAuthentication() { + return SecurityContextHolder.getContext().getAuthentication(); + } + + /** + * 生成BCryptPasswordEncoder密码 + * + * @param password 密码 + * @return 加密字符串 + */ + public static String encryptPassword(String password) { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.encode(password); + } + + /** + * 判断密码是否相同 + * + * @param rawPassword 真实密码 + * @param encodedPassword 加密后字符 + * @return 结果 + */ + public static boolean matchesPassword(String rawPassword, String encodedPassword) { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.matches(rawPassword, encodedPassword); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/config/GenConfig.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/config/GenConfig.java new file mode 100644 index 0000000..f9f770f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/config/GenConfig.java @@ -0,0 +1,75 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +/** 读取代码生成相关配置 @Author ruoyi */ +@Component +@ConfigurationProperties(prefix = "gen") +@PropertySource(value = {"classpath:generator.yml"}) +@Profile({"beta"}) +public class GenConfig { + + /** 作者 */ + public static String Author; + + /** 生成包路径 */ + public static String packageName; + + /** 自动去除表前缀,默认是false */ + public static boolean autoRemovePre; + + /** 表前缀(类名不会包含表前缀) */ + public static String tablePrefix; + + public static String getAuthor() { + return Author; + } + + @Value("${Author}") + public void setAuthor(String Author) { + GenConfig.Author = Author; + } + + public static String getPackageName() { + return packageName; + } + + @Value("${packageName}") + public void setPackageName(String packageName) { + GenConfig.packageName = packageName; + } + + public static boolean getAutoRemovePre() { + return autoRemovePre; + } + + @Value("${autoRemovePre}") + public void setAutoRemovePre(boolean autoRemovePre) { + GenConfig.autoRemovePre = autoRemovePre; + } + + public static String getTablePrefix() { + return tablePrefix; + } + + @Value("${tablePrefix}") + public void setTablePrefix(String tablePrefix) { + GenConfig.tablePrefix = tablePrefix; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/constant/GenConstants.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/constant/GenConstants.java new file mode 100644 index 0000000..fa5e025 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/constant/GenConstants.java @@ -0,0 +1,139 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.constant; + +/** 代码生成通用常量 @Author ruoyi */ +public class GenConstants { + + /** 单表(增删改查) */ + public static final String TPL_CRUD = "crud"; + + /** 树表(增删改查) */ + public static final String TPL_TREE = "tree"; + + /** 主子表(增删改查) */ + public static final String TPL_SUB = "sub"; + + /** 树编码字段 */ + public static final String TREE_CODE = "treeCode"; + + /** 树父编码字段 */ + public static final String TREE_PARENT_CODE = "treeParentCode"; + + /** 树名称字段 */ + public static final String TREE_NAME = "treeName"; + + /** 上级菜单ID字段 */ + public static final String PARENT_MENU_ID = "parentMenuId"; + + /** 上级菜单名称字段 */ + public static final String PARENT_MENU_NAME = "parentMenuName"; + + /** 数据库字符串类型 */ + public static final String[] COLUMNTYPE_STR = {"char", "varchar", "nvarchar", "varchar2"}; + + /** 数据库文本类型 */ + public static final String[] COLUMNTYPE_TEXT = {"tinytext", "text", "mediumtext", "longtext"}; + + /** 数据库时间类型 */ + public static final String[] COLUMNTYPE_TIME = {"datetime", "time", "date", "timestamp"}; + + /** 数据库数字类型 */ + public static final String[] COLUMNTYPE_NUMBER = { + "tinyint", + "smallint", + "mediumint", + "int", + "number", + "integer", + "bit", + "bigint", + "float", + "double", + "decimal" + }; + + /** 页面不需要编辑字段 */ + public static final String[] COLUMNNAME_NOT_EDIT = {"id", "create_by", "create_time", "del_flag"}; + + /** 页面不需要显示的列表字段 */ + public static final String[] COLUMNNAME_NOT_LIST = { + "id", "create_by", "create_time", "del_flag", "update_by", "update_time" + }; + + /** 页面不需要查询字段 */ + public static final String[] COLUMNNAME_NOT_QUERY = { + "id", "create_by", "create_time", "del_flag", "update_by", "update_time", "remark" + }; + + /** Entity基类字段 */ + public static final String[] BASE_ENTITY = { + "createBy", "createTime", "updateBy", "updateTime", "remark" + }; + + /** Tree基类字段 */ + public static final String[] TREE_ENTITY = { + "parentName", "parentId", "orderNum", "ancestors", "children" + }; + + /** 文本框 */ + public static final String HTML_INPUT = "input"; + + /** 文本域 */ + public static final String HTML_TEXTAREA = "textarea"; + + /** 下拉框 */ + public static final String HTML_SELECT = "select"; + + /** 单选框 */ + public static final String HTML_RADIO = "radio"; + + /** 复选框 */ + public static final String HTML_CHECKBOX = "checkbox"; + + /** 日期控件 */ + public static final String HTML_DATETIME = "datetime"; + + /** 图片上传控件 */ + public static final String HTML_IMAGE_UPLOAD = "imageUpload"; + + /** 文件上传控件 */ + public static final String HTML_FILE_UPLOAD = "fileUpload"; + + /** 富文本控件 */ + public static final String HTML_EDITOR = "editor"; + + /** 字符串类型 */ + public static final String TYPE_STRING = "String"; + + /** 整型 */ + public static final String TYPE_INTEGER = "Integer"; + + /** 长整型 */ + public static final String TYPE_LONG = "Long"; + + /** 浮点型 */ + public static final String TYPE_DOUBLE = "Double"; + + /** 高精度计算类型 */ + public static final String TYPE_BIGDECIMAL = "BigDecimal"; + + /** 时间类型 */ + public static final String TYPE_DATE = "Date"; + + /** 模糊查询 */ + public static final String QUERY_LIKE = "LIKE"; + + /** 需要 */ + public static final String REQUIRE = "1"; +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/domain/GenTable.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/domain/GenTable.java new file mode 100644 index 0000000..23a6393 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/domain/GenTable.java @@ -0,0 +1,144 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.domain; + +import cn.universal.admin.generator.constant.GenConstants; +import cn.universal.common.domain.BaseEntity; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import java.util.List; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +/** 业务表 gen_table @Author ruoyi */ +@EqualsAndHashCode(callSuper = true) +@Data +public class GenTable extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long tableId; + + /** 表名称 */ + @NotBlank(message = "表名称不能为空") + private String tableName; + + /** 表描述 */ + @NotBlank(message = "表描述不能为空") + private String tableComment; + + /** 关联父表的表名 */ + private String subTableName; + + /** 本表关联父表的外键名 */ + private String subTableFkName; + + /** 实体类名称(首字母大写) */ + @NotBlank(message = "实体类名称不能为空") + private String className; + + /** 使用的模板(crud单表操作 tree树表操作 sub主子表操作) */ + private String tplCategory; + + /** 生成包路径 */ + @NotBlank(message = "生成包路径不能为空") + private String packageName; + + /** 生成模块名 */ + @NotBlank(message = "生成模块名不能为空") + private String moduleName; + + /** 生成业务名 */ + @NotBlank(message = "生成业务名不能为空") + private String businessName; + + /** 生成功能名 */ + @NotBlank(message = "生成功能名不能为空") + private String functionName; + + /** 生成作者 */ + @NotBlank(message = "作者不能为空") + private String functionAuthor; + + /** 生成代码方式(0zip压缩包 1自定义路径) */ + private String genType; + + /** 生成路径(不填默认项目路径) */ + private String genPath; + + /** 主键信息 */ + private GenTableColumn pkColumn; + + /** 子表信息 */ + private GenTable subTable; + + /** 表列信息 */ + @Valid private List columns; + + /** 其它生成选项 */ + private String options; + + /** 树编码字段 */ + private String treeCode; + + /** 树父编码字段 */ + private String treeParentCode; + + /** 树名称字段 */ + private String treeName; + + /** 上级菜单ID字段 */ + private String parentMenuId; + + /** 上级菜单名称字段 */ + private String parentMenuName; + + public boolean isSub() { + return isSub(this.tplCategory); + } + + public static boolean isSub(String tplCategory) { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_SUB, tplCategory); + } + + public boolean isTree() { + return isTree(this.tplCategory); + } + + public static boolean isTree(String tplCategory) { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_TREE, tplCategory); + } + + public boolean isCrud() { + return isCrud(this.tplCategory); + } + + public static boolean isCrud(String tplCategory) { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_CRUD, tplCategory); + } + + public boolean isSuperColumn(String javaField) { + return isSuperColumn(this.tplCategory, javaField); + } + + public static boolean isSuperColumn(String tplCategory, String javaField) { + if (isTree(tplCategory)) { + return StringUtils.equalsAnyIgnoreCase( + javaField, ArrayUtils.addAll(GenConstants.TREE_ENTITY, GenConstants.BASE_ENTITY)); + } + return StringUtils.equalsAnyIgnoreCase(javaField, GenConstants.BASE_ENTITY); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/domain/GenTableColumn.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/domain/GenTableColumn.java new file mode 100644 index 0000000..690ab50 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/domain/GenTableColumn.java @@ -0,0 +1,330 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.domain; + +import cn.universal.common.domain.BaseEntity; +import jakarta.validation.constraints.NotBlank; +import org.apache.commons.lang3.StringUtils; + +/** 代码生成业务字段表 gen_table_column @Author ruoyi */ +public class GenTableColumn extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long columnId; + + /** 归属表编号 */ + private Long tableId; + + /** 列名称 */ + private String columnName; + + /** 列描述 */ + private String columnComment; + + /** 列类型 */ + private String columnType; + + /** JAVA类型 */ + private String javaType; + + /** JAVA字段名 */ + @NotBlank(message = "Java属性不能为空") + private String javaField; + + /** 是否主键(1是) */ + private String isPk; + + /** 是否自增(1是) */ + private String isIncrement; + + /** 是否必填(1是) */ + private String isRequired; + + /** 是否为插入字段(1是) */ + private String isInsert; + + /** 是否编辑字段(1是) */ + private String isEdit; + + /** 是否列表字段(1是) */ + private String isList; + + /** 是否查询字段(1是) */ + private String isQuery; + + /** 查询方式(EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围) */ + private String queryType; + + /** + * 显示类型(input文本框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、image图片上传控件、upload文件上传控件、editor富文本控件) + */ + private String htmlType; + + /** 字典类型 */ + private String dictType; + + /** 排序 */ + private Integer sort; + + public void setColumnId(Long columnId) { + this.columnId = columnId; + } + + public Long getColumnId() { + return columnId; + } + + public void setTableId(Long tableId) { + this.tableId = tableId; + } + + public Long getTableId() { + return tableId; + } + + public void setColumnName(String columnName) { + this.columnName = columnName; + } + + public String getColumnName() { + return columnName; + } + + public void setColumnComment(String columnComment) { + this.columnComment = columnComment; + } + + public String getColumnComment() { + return columnComment; + } + + public void setColumnType(String columnType) { + this.columnType = columnType; + } + + public String getColumnType() { + return columnType; + } + + public void setJavaType(String javaType) { + this.javaType = javaType; + } + + public String getJavaType() { + return javaType; + } + + public void setJavaField(String javaField) { + this.javaField = javaField; + } + + public String getJavaField() { + return javaField; + } + + public String getCapJavaField() { + return StringUtils.capitalize(javaField); + } + + public void setIsPk(String isPk) { + this.isPk = isPk; + } + + public String getIsPk() { + return isPk; + } + + public boolean isPk() { + return isPk(this.isPk); + } + + public boolean isPk(String isPk) { + return isPk != null && StringUtils.equals("1", isPk); + } + + public String getIsIncrement() { + return isIncrement; + } + + public void setIsIncrement(String isIncrement) { + this.isIncrement = isIncrement; + } + + public boolean isIncrement() { + return isIncrement(this.isIncrement); + } + + public boolean isIncrement(String isIncrement) { + return isIncrement != null && StringUtils.equals("1", isIncrement); + } + + public void setIsRequired(String isRequired) { + this.isRequired = isRequired; + } + + public String getIsRequired() { + return isRequired; + } + + public boolean isRequired() { + return isRequired(this.isRequired); + } + + public boolean isRequired(String isRequired) { + return isRequired != null && StringUtils.equals("1", isRequired); + } + + public void setIsInsert(String isInsert) { + this.isInsert = isInsert; + } + + public String getIsInsert() { + return isInsert; + } + + public boolean isInsert() { + return isInsert(this.isInsert); + } + + public boolean isInsert(String isInsert) { + return isInsert != null && StringUtils.equals("1", isInsert); + } + + public void setIsEdit(String isEdit) { + this.isEdit = isEdit; + } + + public String getIsEdit() { + return isEdit; + } + + public boolean isEdit() { + return isInsert(this.isEdit); + } + + public boolean isEdit(String isEdit) { + return isEdit != null && StringUtils.equals("1", isEdit); + } + + public void setIsList(String isList) { + this.isList = isList; + } + + public String getIsList() { + return isList; + } + + public boolean isList() { + return isList(this.isList); + } + + public boolean isList(String isList) { + return isList != null && StringUtils.equals("1", isList); + } + + public void setIsQuery(String isQuery) { + this.isQuery = isQuery; + } + + public String getIsQuery() { + return isQuery; + } + + public boolean isQuery() { + return isQuery(this.isQuery); + } + + public boolean isQuery(String isQuery) { + return isQuery != null && StringUtils.equals("1", isQuery); + } + + public void setQueryType(String queryType) { + this.queryType = queryType; + } + + public String getQueryType() { + return queryType; + } + + public String getHtmlType() { + return htmlType; + } + + public void setHtmlType(String htmlType) { + this.htmlType = htmlType; + } + + public void setDictType(String dictType) { + this.dictType = dictType; + } + + public String getDictType() { + return dictType; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + public Integer getSort() { + return sort; + } + + public boolean isSuperColumn() { + return isSuperColumn(this.javaField); + } + + public static boolean isSuperColumn(String javaField) { + return StringUtils.equalsAnyIgnoreCase( + javaField, + // BaseEntity + "createBy", + "createTime", + "updateBy", + "updateTime", + "remark", + // TreeEntity + "parentName", + "parentId", + "orderNum", + "ancestors"); + } + + public boolean isUsableColumn() { + return isUsableColumn(javaField); + } + + public static boolean isUsableColumn(String javaField) { + // isSuperColumn()中的名单用于避免生成多余Domain属性,若某些属性在生成页面时需要用到不能忽略,则放在此处白名单 + return StringUtils.equalsAnyIgnoreCase(javaField, "parentId", "orderNum", "remark"); + } + + public String readConverterExp() { + String remarks = StringUtils.substringBetween(this.columnComment, "(", ")"); + StringBuffer sb = new StringBuffer(); + if (StringUtils.isNotEmpty(remarks)) { + for (String value : remarks.split(" ")) { + if (StringUtils.isNotEmpty(value)) { + Object startStr = value.subSequence(0, 1); + String endStr = value.substring(1); + sb.append("").append(startStr).append("=").append(endStr).append(","); + } + } + return sb.deleteCharAt(sb.length() - 1).toString(); + } else { + return this.columnComment; + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/mapper/GenTableColumnMapper.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/mapper/GenTableColumnMapper.java new file mode 100644 index 0000000..f5d62c8 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/mapper/GenTableColumnMapper.java @@ -0,0 +1,68 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.mapper; + +import cn.universal.admin.generator.domain.GenTableColumn; +import java.util.List; + +/** 业务字段 数据层 @Author ruoyi */ +public interface GenTableColumnMapper { + + /** + * 根据表名称查询列信息 + * + * @param tableName 表名称 + * @return 列信息 + */ + List selectDbTableColumnsByName(String tableName); + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段 + * + * @param genTableColumns 列数据 + * @return 结果 + */ + int deleteGenTableColumns(List genTableColumns); + + /** + * 批量删除业务字段 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + int deleteGenTableColumnByIds(Long[] ids); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/mapper/GenTableMapper.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/mapper/GenTableMapper.java new file mode 100644 index 0000000..fd39064 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/mapper/GenTableMapper.java @@ -0,0 +1,91 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.mapper; + +import cn.universal.admin.generator.domain.GenTable; +import java.util.List; + +/** 业务 数据层 @Author ruoyi */ +public interface GenTableMapper { + + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + List selectGenTableAll(); + + /** + * 查询表ID业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + GenTable selectGenTableById(Long id); + + /** + * 查询表名称业务信息 + * + * @param tableName 表名称 + * @return 业务信息 + */ + GenTable selectGenTableByName(String tableName); + + /** + * 新增业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + int insertGenTable(GenTable genTable); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + int updateGenTable(GenTable genTable); + + /** + * 批量删除业务 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + int deleteGenTableByIds(Long[] ids); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/service/GenTableColumnServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/service/GenTableColumnServiceImpl.java new file mode 100644 index 0000000..21fea90 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/service/GenTableColumnServiceImpl.java @@ -0,0 +1,71 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.service; + +import cn.hutool.core.convert.Convert; +import cn.universal.admin.generator.domain.GenTableColumn; +import cn.universal.admin.generator.mapper.GenTableColumnMapper; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** 业务字段 服务层实现 @Author ruoyi */ +@Service +public class GenTableColumnServiceImpl implements IGenTableColumnService { + + @Autowired private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + @Override + public List selectGenTableColumnListByTableId(Long tableId) { + return genTableColumnMapper.selectGenTableColumnListByTableId(tableId); + } + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int insertGenTableColumn(GenTableColumn genTableColumn) { + return genTableColumnMapper.insertGenTableColumn(genTableColumn); + } + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int updateGenTableColumn(GenTableColumn genTableColumn) { + return genTableColumnMapper.updateGenTableColumn(genTableColumn); + } + + /** + * 删除业务字段对象 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteGenTableColumnByIds(String ids) { + return genTableColumnMapper.deleteGenTableColumnByIds(Convert.toLongArray(ids)); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/service/GenTableServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/service/GenTableServiceImpl.java new file mode 100644 index 0000000..2e6a4f2 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/service/GenTableServiceImpl.java @@ -0,0 +1,454 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.service; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.generator.constant.GenConstants; +import cn.universal.admin.generator.domain.GenTable; +import cn.universal.admin.generator.domain.GenTableColumn; +import cn.universal.admin.generator.mapper.GenTableColumnMapper; +import cn.universal.admin.generator.mapper.GenTableMapper; +import cn.universal.admin.generator.util.GenUtils; +import cn.universal.admin.generator.util.VelocityInitializer; +import cn.universal.admin.generator.util.VelocityUtils; +import cn.universal.common.exception.IoTException; +import jakarta.annotation.Resource; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +/** 业务 服务层实现 @Author ruoyi */ +@Service +public class GenTableServiceImpl implements IGenTableService { + + private static final Logger log = LoggerFactory.getLogger(GenTableServiceImpl.class); + + @Resource private GenTableMapper genTableMapper; + + @Resource private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + @Override + public GenTable selectGenTableById(Long id) { + GenTable genTable = genTableMapper.selectGenTableById(id); + setTableFromOptions(genTable); + return genTable; + } + + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + @Override + public List selectGenTableList(GenTable genTable) { + return genTableMapper.selectGenTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + @Override + public List selectDbTableList(GenTable genTable) { + return genTableMapper.selectDbTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + @Override + public List selectDbTableListByNames(String[] tableNames) { + return genTableMapper.selectDbTableListByNames(tableNames); + } + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + @Override + public List selectGenTableAll() { + return genTableMapper.selectGenTableAll(); + } + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + @Override + @Transactional + public void updateGenTable(GenTable genTable) { + String options = JSONUtil.toJsonStr(genTable.getParams()); + genTable.setOptions(options); + int row = genTableMapper.updateGenTable(genTable); + if (row > 0) { + for (GenTableColumn cenTableColumn : genTable.getColumns()) { + genTableColumnMapper.updateGenTableColumn(cenTableColumn); + } + } + } + + /** + * 删除业务对象 + * + * @param tableIds 需要删除的数据ID + * @return 结果 + */ + @Override + @Transactional + public void deleteGenTableByIds(Long[] tableIds) { + genTableMapper.deleteGenTableByIds(tableIds); + genTableColumnMapper.deleteGenTableColumnByIds(tableIds); + } + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + @Override + @Transactional + public void importGenTable(List tableList) { + String operName = SecurityUtils.getUnionId(); + try { + for (GenTable table : tableList) { + String tableName = table.getTableName(); + GenUtils.initTable(table, operName); + int row = genTableMapper.insertGenTable(table); + if (row > 0) { + // 保存列信息 + List genTableColumns = + genTableColumnMapper.selectDbTableColumnsByName(tableName); + for (GenTableColumn column : genTableColumns) { + GenUtils.initColumnField(column, table); + genTableColumnMapper.insertGenTableColumn(column); + } + } + } + } catch (Exception e) { + throw new IoTException("导入失败:" + e.getMessage()); + } + } + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + @Override + public Map previewCode(Long tableId) { + Map dataMap = new LinkedHashMap<>(); + // 查询表信息 + GenTable table = genTableMapper.selectGenTableById(tableId); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory()); + for (String template : templates) { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, "utf-8"); + tpl.merge(context, sw); + dataMap.put(template, sw.toString()); + } + return dataMap; + } + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + @Override + public byte[] downloadCode(String tableName) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + generatorCode(tableName, zip); + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + */ + @Override + public void generatorCode(String tableName) { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory()); + for (String template : templates) { + if (!StringUtils.containsAny( + template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm")) { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, StandardCharsets.UTF_8.toString()); + tpl.merge(context, sw); + try { + String path = getGenPath(table, template); + FileUtils.writeStringToFile(new File(path), sw.toString(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IoTException("渲染模板失败,表名:" + table.getTableName()); + } + } + } + } + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + @Override + @Transactional + public void synchDb(String tableName) { + GenTable table = genTableMapper.selectGenTableByName(tableName); + List tableColumns = table.getColumns(); + List tableColumnNames = + tableColumns.stream().map(GenTableColumn::getColumnName).collect(Collectors.toList()); + + List dbTableColumns = + genTableColumnMapper.selectDbTableColumnsByName(tableName); + if (CollectionUtils.isEmpty(dbTableColumns)) { + throw new IoTException("同步数据失败,原表结构不存在"); + } + List dbTableColumnNames = + dbTableColumns.stream().map(GenTableColumn::getColumnName).collect(Collectors.toList()); + + dbTableColumns.forEach( + column -> { + if (!tableColumnNames.contains(column.getColumnName())) { + GenUtils.initColumnField(column, table); + genTableColumnMapper.insertGenTableColumn(column); + } + }); + + List delColumns = + tableColumns.stream() + .filter(column -> !dbTableColumnNames.contains(column.getColumnName())) + .collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(delColumns)) { + genTableColumnMapper.deleteGenTableColumns(delColumns); + } + } + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + for (String tableName : tableNames) { + generatorCode(tableName, zip); + } + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** 查询表信息并生成代码 */ + private void generatorCode(String tableName, ZipOutputStream zip) { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory()); + for (String template : templates) { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, StandardCharsets.UTF_8.toString()); + tpl.merge(context, sw); + try { + // 添加到zip + zip.putNextEntry(new ZipEntry(VelocityUtils.getFileName(template, table))); + IOUtils.write(sw.toString(), zip, StandardCharsets.UTF_8.toString()); + IOUtils.closeQuietly(sw); + zip.flush(); + zip.closeEntry(); + } catch (IOException e) { + log.error("渲染模板失败,表名:" + table.getTableName(), e); + } + } + } + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + @Override + public void validateEdit(GenTable genTable) { + if (GenConstants.TPL_TREE.equals(genTable.getTplCategory())) { + String options = JSONUtil.toJsonStr(genTable.getParams()); + JSONObject paramsObj = JSONUtil.parseObj(options); + if (StringUtils.isEmpty(paramsObj.getStr(GenConstants.TREE_CODE))) { + throw new IoTException("树编码字段不能为空"); + } else if (StringUtils.isEmpty(paramsObj.getStr(GenConstants.TREE_PARENT_CODE))) { + throw new IoTException("树父编码字段不能为空"); + } else if (StringUtils.isEmpty(paramsObj.getStr(GenConstants.TREE_NAME))) { + throw new IoTException("树名称字段不能为空"); + } else if (GenConstants.TPL_SUB.equals(genTable.getTplCategory())) { + if (StringUtils.isEmpty(genTable.getSubTableName())) { + throw new IoTException("关联子表的表名不能为空"); + } else if (StringUtils.isEmpty(genTable.getSubTableFkName())) { + throw new IoTException("子表关联的外键名不能为空"); + } + } + } + } + + /** + * 设置主键列信息 + * + * @param table 业务表信息 + */ + public void setPkColumn(GenTable table) { + for (GenTableColumn column : table.getColumns()) { + if (column.isPk()) { + table.setPkColumn(column); + break; + } + } + if (Objects.isNull(table.getPkColumn())) { + table.setPkColumn(table.getColumns().get(0)); + } + if (GenConstants.TPL_SUB.equals(table.getTplCategory())) { + for (GenTableColumn column : table.getSubTable().getColumns()) { + if (column.isPk()) { + table.getSubTable().setPkColumn(column); + break; + } + } + if (Objects.isNull(table.getSubTable().getPkColumn())) { + table.getSubTable().setPkColumn(table.getSubTable().getColumns().get(0)); + } + } + } + + /** + * 设置主子表信息 + * + * @param table 业务表信息 + */ + public void setSubTable(GenTable table) { + String subTableName = table.getSubTableName(); + if (StringUtils.isNotEmpty(subTableName)) { + table.setSubTable(genTableMapper.selectGenTableByName(subTableName)); + } + } + + /** + * 设置代码生成其他选项值 + * + * @param genTable 设置后的生成对象 + */ + public void setTableFromOptions(GenTable genTable) { + JSONObject paramsObj = JSONUtil.parseObj(genTable.getOptions()); + String treeCode = paramsObj.getStr(GenConstants.TREE_CODE); + String treeParentCode = paramsObj.getStr(GenConstants.TREE_PARENT_CODE); + String treeName = paramsObj.getStr(GenConstants.TREE_NAME); + String parentMenuId = paramsObj.getStr(GenConstants.PARENT_MENU_ID); + String parentMenuName = paramsObj.getStr(GenConstants.PARENT_MENU_NAME); + + genTable.setTreeCode(treeCode); + genTable.setTreeParentCode(treeParentCode); + genTable.setTreeName(treeName); + genTable.setParentMenuId(parentMenuId); + genTable.setParentMenuName(parentMenuName); + } + + /** + * 获取代码生成地址 + * + * @param table 业务表信息 + * @param template 模板文件路径 + * @return 生成地址 + */ + public static String getGenPath(GenTable table, String template) { + String genPath = table.getGenPath(); + if (StringUtils.equals(genPath, "/")) { + return System.getProperty("user.dir") + + File.separator + + "src" + + File.separator + + VelocityUtils.getFileName(template, table); + } + return genPath + File.separator + VelocityUtils.getFileName(template, table); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/service/IGenTableColumnService.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/service/IGenTableColumnService.java new file mode 100644 index 0000000..af56206 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/service/IGenTableColumnService.java @@ -0,0 +1,52 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.service; + +import cn.universal.admin.generator.domain.GenTableColumn; +import java.util.List; + +/** 业务字段 服务层 @Author ruoyi */ +public interface IGenTableColumnService { + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(String ids); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/service/IGenTableService.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/service/IGenTableService.java new file mode 100644 index 0000000..e3d1e32 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/service/IGenTableService.java @@ -0,0 +1,129 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.service; + +import cn.universal.admin.generator.domain.GenTable; +import java.util.List; +import java.util.Map; + +/** 业务 服务层 @Author ruoyi */ +public interface IGenTableService { + + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + List selectGenTableAll(); + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + GenTable selectGenTableById(Long id); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + void updateGenTable(GenTable genTable); + + /** + * 删除业务信息 + * + * @param tableIds 需要删除的表数据ID + * @return 结果 + */ + void deleteGenTableByIds(Long[] tableIds); + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + void importGenTable(List tableList); + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + Map previewCode(Long tableId); + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + byte[] downloadCode(String tableName); + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + * @return 数据 + */ + void generatorCode(String tableName); + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + void synchDb(String tableName); + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + byte[] downloadCode(String[] tableNames); + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + void validateEdit(GenTable genTable); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/util/GenUtils.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/util/GenUtils.java new file mode 100644 index 0000000..8df674f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/util/GenUtils.java @@ -0,0 +1,233 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.util; + +import cn.hutool.core.util.StrUtil; +import cn.universal.admin.generator.config.GenConfig; +import cn.universal.admin.generator.constant.GenConstants; +import cn.universal.admin.generator.domain.GenTable; +import cn.universal.admin.generator.domain.GenTableColumn; +import java.util.Arrays; +import org.apache.commons.lang3.RegExUtils; +import org.apache.commons.lang3.StringUtils; + +/** 代码生成器 工具类 @Author ruoyi */ +public class GenUtils { + + /** 初始化表信息 */ + public static void initTable(GenTable genTable, String operName) { + genTable.setClassName(convertClassName(genTable.getTableName())); + genTable.setPackageName(GenConfig.getPackageName()); + genTable.setModuleName(getModuleName(GenConfig.getPackageName())); + genTable.setBusinessName(getBusinessName(genTable.getTableName())); + genTable.setFunctionName(replaceText(genTable.getTableComment())); + genTable.setFunctionAuthor(GenConfig.getAuthor()); + genTable.setCreateBy(operName); + } + + /** 初始化列属性字段 */ + public static void initColumnField(GenTableColumn column, GenTable table) { + String dataType = getDbType(column.getColumnType()); + String columnName = column.getColumnName(); + column.setTableId(table.getTableId()); + column.setCreateBy(table.getCreateBy()); + // 设置java字段名 + column.setJavaField(StrUtil.toCamelCase(columnName)); + // 设置默认类型 + column.setJavaType(GenConstants.TYPE_STRING); + + if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) + || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) { + // 字符串长度超过500设置为文本域 + Integer columnLength = getColumnLength(column.getColumnType()); + String htmlType = + columnLength >= 500 || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType) + ? GenConstants.HTML_TEXTAREA + : GenConstants.HTML_INPUT; + column.setHtmlType(htmlType); + } else if (arraysContains(GenConstants.COLUMNTYPE_TIME, dataType)) { + column.setJavaType(GenConstants.TYPE_DATE); + column.setHtmlType(GenConstants.HTML_DATETIME); + } else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER, dataType)) { + column.setHtmlType(GenConstants.HTML_INPUT); + + // 如果是浮点型 统一用BigDecimal + String[] str = + StringUtils.split(StringUtils.substringBetween(column.getColumnType(), "(", ")"), ","); + if (str != null && str.length == 2 && Integer.parseInt(str[1]) > 0) { + column.setJavaType(GenConstants.TYPE_BIGDECIMAL); + } + // 如果是整形 + else if (str != null && str.length == 1 && Integer.parseInt(str[0]) <= 10) { + column.setJavaType(GenConstants.TYPE_INTEGER); + } + // 长整形 + else { + column.setJavaType(GenConstants.TYPE_LONG); + } + } + + // 插入字段(默认所有字段都需要插入) + column.setIsInsert(GenConstants.REQUIRE); + + // 编辑字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_EDIT, columnName) && !column.isPk()) { + column.setIsEdit(GenConstants.REQUIRE); + } + // 列表字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_LIST, columnName) && !column.isPk()) { + column.setIsList(GenConstants.REQUIRE); + } + // 查询字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_QUERY, columnName) && !column.isPk()) { + column.setIsQuery(GenConstants.REQUIRE); + } + + // 查询字段类型 + if (StringUtils.endsWithIgnoreCase(columnName, "name")) { + column.setQueryType(GenConstants.QUERY_LIKE); + } + // 状态字段设置单选框 + if (StringUtils.endsWithIgnoreCase(columnName, "status")) { + column.setHtmlType(GenConstants.HTML_RADIO); + } + // 类型&性别字段设置下拉框 + else if (StringUtils.endsWithIgnoreCase(columnName, "type") + || StringUtils.endsWithIgnoreCase(columnName, "sex")) { + column.setHtmlType(GenConstants.HTML_SELECT); + } + // 图片字段设置图片上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "image")) { + column.setHtmlType(GenConstants.HTML_IMAGE_UPLOAD); + } + // 文件字段设置文件上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "file")) { + column.setHtmlType(GenConstants.HTML_FILE_UPLOAD); + } + // 内容字段设置富文本控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "content")) { + column.setHtmlType(GenConstants.HTML_EDITOR); + } + } + + /** + * 校验数组是否包含指定值 + * + * @param arr 数组 + * @param targetValue 值 + * @return 是否包含 + */ + public static boolean arraysContains(String[] arr, String targetValue) { + return Arrays.asList(arr).contains(targetValue); + } + + /** + * 获取模块名 + * + * @param packageName 包名 + * @return 模块名 + */ + public static String getModuleName(String packageName) { + int lastIndex = packageName.lastIndexOf("."); + int nameLength = packageName.length(); + String moduleName = StringUtils.substring(packageName, lastIndex + 1, nameLength); + return moduleName; + } + + /** + * 获取业务名 + * + * @param tableName 表名 + * @return 业务名 + */ + public static String getBusinessName(String tableName) { + int lastIndex = tableName.lastIndexOf("_"); + int nameLength = tableName.length(); + String businessName = StringUtils.substring(tableName, lastIndex + 1, nameLength); + return businessName; + } + + /** + * 表名转换成Java类名 + * + * @param tableName 表名称 + * @return 类名 + */ + public static String convertClassName(String tableName) { + boolean autoRemovePre = GenConfig.getAutoRemovePre(); + String tablePrefix = GenConfig.getTablePrefix(); + if (autoRemovePre && StringUtils.isNotEmpty(tablePrefix)) { + String[] searchList = StringUtils.split(tablePrefix, ","); + tableName = replaceFirst(tableName, searchList); + } + String name = StrUtil.toCamelCase(tableName); + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + + /** + * 批量替换前缀 + * + * @param replacementm 替换值 + * @param searchList 替换列表 + * @return + */ + public static String replaceFirst(String replacementm, String[] searchList) { + String text = replacementm; + for (String searchString : searchList) { + if (replacementm.startsWith(searchString)) { + text = replacementm.replaceFirst(searchString, ""); + break; + } + } + return text; + } + + /** + * 关键字替换 + * + * @param text 需要被替换的名字 + * @return 替换后的名字 + */ + public static String replaceText(String text) { + return RegExUtils.replaceAll(text, "(?:表|若依)", ""); + } + + /** + * 获取数据库类型字段 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static String getDbType(String columnType) { + if (StringUtils.indexOf(columnType, "(") > 0) { + return StringUtils.substringBefore(columnType, "("); + } else { + return columnType; + } + } + + /** + * 获取字段长度 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static Integer getColumnLength(String columnType) { + if (StringUtils.indexOf(columnType, "(") > 0) { + String length = StringUtils.substringBetween(columnType, "(", ")"); + return Integer.valueOf(length); + } else { + return 0; + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/util/VelocityInitializer.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/util/VelocityInitializer.java new file mode 100644 index 0000000..d9a692c --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/util/VelocityInitializer.java @@ -0,0 +1,37 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.util; + +import java.util.Properties; +import org.apache.velocity.app.Velocity; + +/** VelocityEngine工厂 @Author ruoyi */ +public class VelocityInitializer { + + /** 初始化vm方法 */ + public static void initVelocity() { + Properties p = new Properties(); + try { + // 加载classpath目录下的vm文件 + p.setProperty( + "resource.loader.file.class", + "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + // 定义字符集 + p.setProperty(Velocity.INPUT_ENCODING, "utf-8"); + // 初始化Velocity引擎,指定配置Properties + Velocity.init(p); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/util/VelocityUtils.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/util/VelocityUtils.java new file mode 100644 index 0000000..ac8530d --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/util/VelocityUtils.java @@ -0,0 +1,348 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.util; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.generator.constant.GenConstants; +import cn.universal.admin.generator.domain.GenTable; +import cn.universal.admin.generator.domain.GenTableColumn; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import org.apache.commons.lang3.StringUtils; +import org.apache.velocity.VelocityContext; + +/** 模板处理工具类 @Author ruoyi */ +public class VelocityUtils { + + /** 项目空间路径 */ + private static final String PROJECT_PATH = "main/java"; + + /** mybatis空间路径 */ + private static final String MYBATIS_PATH = "main/resources/mapper"; + + /** 默认上级菜单,系统工具 */ + private static final String DEFAULT_PARENT_MENU_ID = "3"; + + /** + * 设置模板变量信息 + * + * @return 模板列表 + */ + public static VelocityContext prepareContext(GenTable genTable) { + String moduleName = genTable.getModuleName(); + String businessName = genTable.getBusinessName(); + String packageName = genTable.getPackageName(); + String tplCategory = genTable.getTplCategory(); + String functionName = genTable.getFunctionName(); + + VelocityContext velocityContext = new VelocityContext(); + velocityContext.put("tplCategory", genTable.getTplCategory()); + velocityContext.put("tableName", genTable.getTableName()); + velocityContext.put( + "functionName", StrUtil.isNotEmpty(functionName) ? functionName : "【请填写功能名称】"); + velocityContext.put("ClassName", genTable.getClassName()); + velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName())); + velocityContext.put("moduleName", genTable.getModuleName()); + velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName())); + velocityContext.put("businessName", genTable.getBusinessName()); + velocityContext.put("basePackage", getPackagePrefix(packageName)); + velocityContext.put("packageName", packageName); + velocityContext.put("Author", genTable.getFunctionAuthor()); + velocityContext.put("datetime", DateUtil.formatDate(new Date())); + velocityContext.put("pkColumn", genTable.getPkColumn()); + velocityContext.put("importList", getImportList(genTable)); + velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName)); + velocityContext.put("columns", genTable.getColumns()); + velocityContext.put("table", genTable); + velocityContext.put("dicts", getDicts(genTable)); + setMenuVelocityContext(velocityContext, genTable); + if (GenConstants.TPL_TREE.equals(tplCategory)) { + setTreeVelocityContext(velocityContext, genTable); + } + if (GenConstants.TPL_SUB.equals(tplCategory)) { + setSubVelocityContext(velocityContext, genTable); + } + return velocityContext; + } + + public static void setMenuVelocityContext(VelocityContext context, GenTable genTable) { + String options = genTable.getOptions(); + JSONObject paramsObj = JSONUtil.parseObj(options); + String parentMenuId = getParentMenuId(paramsObj); + context.put("parentMenuId", parentMenuId); + } + + public static void setTreeVelocityContext(VelocityContext context, GenTable genTable) { + String options = genTable.getOptions(); + JSONObject paramsObj = JSONUtil.parseObj(options); + String treeCode = getTreecode(paramsObj); + String treeParentCode = getTreeParentCode(paramsObj); + String treeName = getTreeName(paramsObj); + + context.put("treeCode", treeCode); + context.put("treeParentCode", treeParentCode); + context.put("treeName", treeName); + context.put("expandColumn", getExpandColumn(genTable)); + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) { + context.put("tree_parent_code", paramsObj.getStr(GenConstants.TREE_PARENT_CODE)); + } + if (paramsObj.containsKey(GenConstants.TREE_NAME)) { + context.put("tree_name", paramsObj.getStr(GenConstants.TREE_NAME)); + } + } + + public static void setSubVelocityContext(VelocityContext context, GenTable genTable) { + GenTable subTable = genTable.getSubTable(); + String subTableName = genTable.getSubTableName(); + String subTableFkName = genTable.getSubTableFkName(); + String subClassName = genTable.getSubTable().getClassName(); + String subTableFkClassName = StrUtil.toCamelCase(subTableFkName); + subTableFkClassName = + subTableFkClassName.substring(0, 1).toUpperCase() + subTableFkClassName.substring(1); + + context.put("subTable", subTable); + context.put("subTableName", subTableName); + context.put("subTableFkName", subTableFkName); + context.put("subTableFkClassName", subTableFkClassName); + context.put("subTableFkclassName", StringUtils.uncapitalize(subTableFkClassName)); + context.put("subClassName", subClassName); + context.put("subclassName", StringUtils.uncapitalize(subClassName)); + context.put("subImportList", getImportList(genTable.getSubTable())); + } + + /** + * 获取模板信息 + * + * @return 模板列表 + */ + public static List getTemplateList(String tplCategory) { + List templates = new ArrayList(); + templates.add("vm/java/domain.java.vm"); + templates.add("vm/java/mapper.java.vm"); + templates.add("vm/java/service.java.vm"); + templates.add("vm/java/serviceImpl.java.vm"); + templates.add("vm/java/controller.java.vm"); + templates.add("vm/xml/mapper.xml.vm"); + templates.add("vm/sql/sql.vm"); + templates.add("vm/js/api.js.vm"); + if (GenConstants.TPL_CRUD.equals(tplCategory)) { + templates.add("vm/vue/index.vue.vm"); + } else if (GenConstants.TPL_TREE.equals(tplCategory)) { + templates.add("vm/vue/index-tree.vue.vm"); + } else if (GenConstants.TPL_SUB.equals(tplCategory)) { + templates.add("vm/vue/index.vue.vm"); + templates.add("vm/java/sub-domain.java.vm"); + } + return templates; + } + + /** 获取文件名 */ + public static String getFileName(String template, GenTable genTable) { + // 文件名称 + String fileName = ""; + // 包路径 + String packageName = genTable.getPackageName(); + // 模块名 + String moduleName = genTable.getModuleName(); + // 大写类名 + String className = genTable.getClassName(); + // 业务名称 + String businessName = genTable.getBusinessName(); + + String javaPath = PROJECT_PATH + "/" + StrUtil.replace(packageName, ".", "/"); + String mybatisPath = MYBATIS_PATH + "/" + moduleName; + String vuePath = "vue"; + + if (template.contains("domain.java.vm")) { + fileName = StrUtil.format("{}/domain/{}.java", javaPath, className); + } + if (template.contains("sub-domain.java.vm") + && StrUtil.equals(GenConstants.TPL_SUB, genTable.getTplCategory())) { + fileName = + StrUtil.format("{}/domain/{}.java", javaPath, genTable.getSubTable().getClassName()); + } else if (template.contains("mapper.java.vm")) { + fileName = StrUtil.format("{}/mapper/{}Mapper.java", javaPath, className); + } else if (template.contains("third.java.vm")) { + fileName = StrUtil.format("{}/third/I{}Service.java", javaPath, className); + } else if (template.contains("serviceImpl.java.vm")) { + fileName = StrUtil.format("{}/third/third/{}ServiceImpl.java", javaPath, className); + } else if (template.contains("controller.java.vm")) { + fileName = StrUtil.format("{}/controller/{}Controller.java", javaPath, className); + } else if (template.contains("mapper.xml.vm")) { + fileName = StrUtil.format("{}/{}Mapper.xml", mybatisPath, className); + } else if (template.contains("sql.vm")) { + fileName = businessName + "Menu.sql"; + } else if (template.contains("api.js.vm")) { + fileName = StrUtil.format("{}/api/{}/{}.js", vuePath, moduleName, businessName); + } else if (template.contains("index.vue.vm")) { + fileName = StrUtil.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } else if (template.contains("index-tree.vue.vm")) { + fileName = StrUtil.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + return fileName; + } + + /** + * 获取包前缀 + * + * @param packageName 包名称 + * @return 包前缀名称 + */ + public static String getPackagePrefix(String packageName) { + int lastIndex = packageName.lastIndexOf("."); + String basePackage = StringUtils.substring(packageName, 0, lastIndex); + return basePackage; + } + + /** + * 根据列类型获取导入包 + * + * @param genTable 业务表对象 + * @return 返回需要导入的包列表 + */ + public static HashSet getImportList(GenTable genTable) { + List columns = genTable.getColumns(); + GenTable subGenTable = genTable.getSubTable(); + HashSet importList = new HashSet(); + if (Objects.nonNull(subGenTable)) { + importList.add("java.util.List"); + } + for (GenTableColumn column : columns) { + if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) { + importList.add("java.util.Date"); + importList.add("com.fasterxml.jackson.annotation.JsonFormat"); + } else if (!column.isSuperColumn() + && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) { + importList.add("java.math.BigDecimal"); + } + } + return importList; + } + + /** + * 根据列类型获取字典组 + * + * @param genTable 业务表对象 + * @return 返回字典组 + */ + public static String getDicts(GenTable genTable) { + List columns = genTable.getColumns(); + List dicts = new ArrayList(); + for (GenTableColumn column : columns) { + if (!column.isSuperColumn() + && StrUtil.isNotEmpty(column.getDictType()) + && StrUtil.equalsAny( + column.getHtmlType(), + new String[] { + GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX + })) { + dicts.add("'" + column.getDictType() + "'"); + } + } + return StringUtils.join(dicts, ", "); + } + + /** + * 获取权限前缀 + * + * @param moduleName 模块名称 + * @param businessName 业务名称 + * @return 返回权限前缀 + */ + public static String getPermissionPrefix(String moduleName, String businessName) { + return StrUtil.format("{}:{}", moduleName, businessName); + } + + /** + * 获取上级菜单ID字段 + * + * @param paramsObj 生成其他选项 + * @return 上级菜单ID字段 + */ + public static String getParentMenuId(JSONObject paramsObj) { + if (Objects.nonNull(paramsObj) + && paramsObj.containsKey(GenConstants.PARENT_MENU_ID) + && StrUtil.isNotEmpty(paramsObj.getStr(GenConstants.PARENT_MENU_ID))) { + return paramsObj.getStr(GenConstants.PARENT_MENU_ID); + } + return DEFAULT_PARENT_MENU_ID; + } + + /** + * 获取树编码 + * + * @param paramsObj 生成其他选项 + * @return 树编码 + */ + public static String getTreecode(JSONObject paramsObj) { + if (paramsObj.containsKey(GenConstants.TREE_CODE)) { + return StrUtil.toCamelCase(paramsObj.getStr(GenConstants.TREE_CODE)); + } + return StrUtil.EMPTY; + } + + /** + * 获取树父编码 + * + * @param paramsObj 生成其他选项 + * @return 树父编码 + */ + public static String getTreeParentCode(JSONObject paramsObj) { + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) { + return StrUtil.toCamelCase(paramsObj.getStr(GenConstants.TREE_PARENT_CODE)); + } + return StrUtil.EMPTY; + } + + /** + * 获取树名称 + * + * @param paramsObj 生成其他选项 + * @return 树名称 + */ + public static String getTreeName(JSONObject paramsObj) { + if (paramsObj.containsKey(GenConstants.TREE_NAME)) { + return StrUtil.toCamelCase(paramsObj.getStr(GenConstants.TREE_NAME)); + } + return StrUtil.EMPTY; + } + + /** + * 获取需要在哪一列上面显示展开按钮 + * + * @param genTable 业务表对象 + * @return 展开按钮列序号 + */ + public static int getExpandColumn(GenTable genTable) { + String options = genTable.getOptions(); + JSONObject paramsObj = JSONUtil.parseObj(options); + String treeName = paramsObj.getStr(GenConstants.TREE_NAME); + int num = 0; + for (GenTableColumn column : genTable.getColumns()) { + if (column.isList()) { + num++; + String columnName = column.getColumnName(); + if (columnName.equals(treeName)) { + break; + } + } + } + return num; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/generator/web/GenController.java b/cn-universal-admin/src/main/java/cn/universal/admin/generator/web/GenController.java new file mode 100644 index 0000000..99a5c5f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/generator/web/GenController.java @@ -0,0 +1,159 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.generator.web; + +import cn.hutool.core.convert.Convert; +import cn.universal.admin.generator.domain.GenTable; +import cn.universal.admin.generator.domain.GenTableColumn; +import cn.universal.admin.generator.service.IGenTableColumnService; +import cn.universal.admin.generator.service.IGenTableService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 代码生成 操作处理 @Author ruoyi */ +@RestController +@RequestMapping("/admin/tool/gen") +public class GenController extends BaseController { + + @Autowired private IGenTableService genTableService; + + @Autowired private IGenTableColumnService genTableColumnService; + + @GetMapping("/list") + public TableDataInfo genList(GenTable genTable) { + startPage(); + List list = genTableService.selectGenTableList(genTable); + return getDataTable(list); + } + + /** 修改代码生成业务 */ + @GetMapping(value = "/{talbleId}") + public AjaxResult getInfo(@PathVariable Long talbleId) { + GenTable table = genTableService.selectGenTableById(talbleId); + List tables = genTableService.selectGenTableAll(); + List list = genTableColumnService.selectGenTableColumnListByTableId(talbleId); + Map map = new HashMap(); + map.put("info", table); + map.put("rows", list); + map.put("tables", tables); + return AjaxResult.success(map); + } + + /** 查询数据库列表 */ + @GetMapping("/db/list") + public TableDataInfo dataList(GenTable genTable) { + startPage(); + List list = genTableService.selectDbTableList(genTable); + return getDataTable(list); + } + + /** 查询数据表字段列表 */ + @GetMapping(value = "/column/{talbleId}") + public TableDataInfo columnList(Long tableId) { + TableDataInfo dataInfo = new TableDataInfo(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + dataInfo.setRows(list); + dataInfo.setTotal(list.size()); + return dataInfo; + } + + /** 导入表结构(保存) */ + @PostMapping("/importTable") + public AjaxResult importTableSave(String tables) { + String[] tableNames = Convert.toStrArray(tables); + // 查询表信息 + List tableList = genTableService.selectDbTableListByNames(tableNames); + genTableService.importGenTable(tableList); + return AjaxResult.success(); + } + + /** 修改保存代码生成业务 */ + @PutMapping + public AjaxResult editSave(@Validated @RequestBody GenTable genTable) { + genTableService.validateEdit(genTable); + genTableService.updateGenTable(genTable); + return AjaxResult.success(); + } + + /** 删除代码生成 */ + @DeleteMapping("/{tableIds}") + public AjaxResult remove(@PathVariable Long[] tableIds) { + genTableService.deleteGenTableByIds(tableIds); + return AjaxResult.success(); + } + + /** 预览代码 */ + @GetMapping("/preview/{tableId}") + public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException { + Map dataMap = genTableService.previewCode(tableId); + return AjaxResult.success(dataMap); + } + + /** 生成代码(下载方式) */ + @GetMapping("/download/{tableName}") + public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) + throws IOException { + byte[] data = genTableService.downloadCode(tableName); + genCode(response, data); + } + + /** 生成代码(自定义路径) */ + @GetMapping("/genCode/{tableName}") + public AjaxResult genCode(@PathVariable("tableName") String tableName) { + genTableService.generatorCode(tableName); + return AjaxResult.success(); + } + + /** 同步数据库 */ + @GetMapping("/synchDb/{tableName}") + public AjaxResult synchDb(@PathVariable("tableName") String tableName) { + genTableService.synchDb(tableName); + return AjaxResult.success(); + } + + /** 批量生成代码 */ + @GetMapping("/batchGenCode") + public void batchGenCode(HttpServletResponse response, String tables) throws IOException { + String[] tableNames = Convert.toStrArray(tables); + byte[] data = genTableService.downloadCode(tableNames); + genCode(response, data); + } + + /** 生成zip文件 */ + private void genCode(HttpServletResponse response, byte[] data) throws IOException { + response.reset(); + response.addHeader("Access-Control-Allow-Origin", "*"); + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition"); + response.setHeader("Content-Disposition", "attachment; filename=\"ruoyi.zip\""); + response.addHeader("Content-Length", "" + data.length); + response.setContentType("application/octet-stream; charset=UTF-8"); + IOUtils.write(data, response.getOutputStream()); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/config/NetworkConfig.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/config/NetworkConfig.java new file mode 100644 index 0000000..60100db --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/config/NetworkConfig.java @@ -0,0 +1,64 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.network.config; + +import java.util.Map; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +/** + * 网络组件配置 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "network") +public class NetworkConfig { + + /** TCP配置模板 */ + private Map tcpTemplate; + + /** MQTT配置模板 */ + private Map mqttTemplate; + + /** 默认配置 */ + private DefaultConfig defaults = new DefaultConfig(); + + /** 配置 RestTemplate Bean */ + @Bean + public RestTemplate restTemplate() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); // 连接超时5秒 + factory.setReadTimeout(10000); // 读取超时10秒 + return new RestTemplate(factory); + } + + @Data + public static class DefaultConfig { + + /** 默认端口 */ + private int defaultPort = 8080; + + /** 默认超时时间 */ + private int defaultTimeout = 30; + + /** 默认心跳间隔 */ + private int defaultKeepAlive = 60; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/doc/NetworkApiDoc.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/doc/NetworkApiDoc.java new file mode 100644 index 0000000..8066702 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/doc/NetworkApiDoc.java @@ -0,0 +1,82 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.network.doc; + +/** + * 网络组件API文档 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +public class NetworkApiDoc { + + /** + * 网络组件管理API文档 + * + *

基础路径: /admin/network + * + *

1. 查询网络组件列表 GET /admin/network/list 参数: - page: 页码 (可选,默认1) - size: 每页大小 (可选,默认10) - type: + * 网络类型 (可选,单个类型,如 TCP_CLIENT) - types: 网络类型列表 (可选,多个类型,如 ["MQTT_CLIENT", "MQTT_SERVER"]) - name: + * 网络组件名称 (可选,模糊查询) - productKey: 产品Key (可选) - state: 状态 (可选,true/false) - unionId: 唯一标识 (可选) + * + *

2. 查询网络组件列表(支持多个类型) GET /admin/network/list/multi-type 参数: - page: 页码 (可选,默认1) - size: 每页大小 + * (可选,默认10) - type: 网络类型 (可选,支持逗号分隔多个类型,如 "MQTT_CLIENT,MQTT_SERVER") - name: 网络组件名称 (可选,模糊查询) - + * productKey: 产品Key (可选) - state: 状态 (可选,true/false) - unionId: 唯一标识 (可选) + * + *

3. 根据ID查询网络组件 GET /admin/network/{id} 参数: - id: 网络组件ID (路径参数) + * + *

4. 新增网络组件 POST /admin/network 请求体: { "type": "TCP_SERVER", "unionId": "unique_id", + * "productKey": "product_key", "name": "网络组件名称", "description": "详细描述", "state": false, + * "configuration": "{\"host\":\"0.0.0.0\",\"port\":6372,\"ssl\":false}", "createUser": "admin" } + * + *

5. 修改网络组件 PUT /admin/network 请求体: { "id": 1, "type": "TCP_SERVER", "unionId": "unique_id", + * "productKey": "product_key", "name": "网络组件名称", "description": "详细描述", "state": false, + * "configuration": "{\"host\":\"0.0.0.0\",\"port\":6372,\"ssl\":false}" } + * + *

6. 删除网络组件 DELETE /admin/network/{id} 参数: - id: 网络组件ID (路径参数) + * + *

7. 批量删除网络组件 DELETE /admin/network/batch/{ids} 参数: - ids: 网络组件ID数组 (路径参数,逗号分隔) + * + *

8. 启动网络组件 POST /admin/network/start/{id} 参数: - id: 网络组件ID (路径参数) + * + *

9. 停止网络组件 POST /admin/network/stop/{id} 参数: - id: 网络组件ID (路径参数) + * + *

10. 重启网络组件 POST /admin/network/restart/{id} 参数: - id: 网络组件ID (路径参数) + * + *

11. 获取网络类型列表 GET /admin/network/types + * + *

12. 验证网络组件配置 POST /admin/network/validate 请求体: { "type": "TCP_SERVER", "unionId": + * "unique_id", "name": "网络组件名称", "configuration": + * "{\"host\":\"0.0.0.0\",\"port\":6372,\"ssl\":false}" } + * + *

响应格式: { "code": 200, "msg": "操作成功", "data": { // 具体数据 } } + * + *

网络类型说明: - TCP_CLIENT: TCP客户端 - TCP_SERVER: TCP服务端 - MQTT_CLIENT: MQTT客户端 - MQTT_SERVER: + * MQTT服务端 + * + *

配置示例: + * + *

TCP_SERVER配置: { "allIdleTime": 0, "allowInsert": false, "alwaysPreDecode": false, + * "decoderType": "STRING", "host": "0.0.0.0", "idleInterval": 0, "onlyCache": false, + * "parserConfiguration": { "byteOrderLittle": true, "delimited": "]", "delimitedMaxlength": 1024, + * "failFast": true }, "parserType": "DELIMITED", "port": 6372, "productKey": + * "630477f1cd68445d90b04653", "readerIdleTime": 360, "readTimeout": 0, "sendTimeout": 0, "ssl": + * false, "writerIdleTime": 0 } + * + *

MQTT_SERVER配置: { "autoReconnect": true, "cleanSession": true, "clientIdPrefix": "univ_cli_", + * "defaultQos": 1, "enabled": true, "host": "tcp://125.210.48.96:1883", "id": + * "6863896d75386b5c1e7e908c", "keepAliveInterval": 60, "password": "univiot@2025!", "productKey": + * "local", "ssl": false, "subscribeTopics": "$univ_cli/up/property/+/+", "username": "univ_cli", + * "connectTimeout": 30 } + */ +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/enums/NetworkTypeEnum.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/enums/NetworkTypeEnum.java new file mode 100644 index 0000000..72ba20e --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/enums/NetworkTypeEnum.java @@ -0,0 +1,85 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.network.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 网络类型枚举 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Getter +@AllArgsConstructor +public enum NetworkTypeEnum { + + /** TCP客户端 */ + TCP_CLIENT("TCP_CLIENT", "TCP客户端", "tcp_parser_type"), + + /** TCP服务端 */ + TCP_SERVER("TCP_SERVER", "TCP服务端", "tcp_parser_type"), + + /** MQTT客户端 */ + MQTT_CLIENT("MQTT_CLIENT", "MQTT客户端", "mqtt_parser_type"), + + /** MQTT服务端 */ + MQTT_SERVER("MQTT_SERVER", "MQTT服务端", "mqtt_parser_type"); + + /** 类型代码 */ + private final String code; + + /** 类型名称 */ + private final String name; + + /** 字典类型 */ + private final String dictType; + + /** + * 根据代码获取枚举 + * + * @param code 类型代码 + * @return 枚举 + */ + public static NetworkTypeEnum getByCode(String code) { + for (NetworkTypeEnum type : values()) { + if (type.getCode().equals(code)) { + return type; + } + } + return null; + } + + /** + * 根据代码获取名称 + * + * @param code 类型代码 + * @return 类型名称 + */ + public static String getNameByCode(String code) { + NetworkTypeEnum type = getByCode(code); + return type != null ? type.getName() : code; + } + + /** + * 根据代码获取字典类型 + * + * @param code 类型代码 + * @return 字典类型 + */ + public static String getDictTypeByCode(String code) { + NetworkTypeEnum type = getByCode(code); + return type != null ? type.getDictType() : null; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/service/INetworkService.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/service/INetworkService.java new file mode 100644 index 0000000..08dbe55 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/service/INetworkService.java @@ -0,0 +1,146 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.network.service; + +import cn.universal.common.domain.R; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.Network; +import cn.universal.persistence.entity.bo.IoTDeviceBO; +import cn.universal.persistence.entity.bo.NetworkBO; +import cn.universal.persistence.entity.vo.NetworkVO; +import cn.universal.persistence.query.NetworkQuery; +import java.util.List; + +/** + * 网络组件Service接口 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +public interface INetworkService { + + boolean start(String productKey); + + boolean stop(String productKey); + + Object get(String productKey); + + boolean del(String productKey); + + List selectNetworkList(NetworkBO bo); + + NetworkVO getDetail(Long id); + + List queryMileSightList(NetworkBO bo); + + R insertDevInstance(IoTDeviceBO devInstancebo); + + R deleteDevInstanceByIds(String[] ids); + + void reloadTcpClient(String applicationId); + + /** + * 查询网络组件列表 + * + * @param query 查询条件 + * @return 分页结果 + */ + List selectNetworkList(NetworkQuery query); + + /** + * 根据ID查询网络组件 + * + * @param id 网络组件ID + * @return 网络组件 + */ + NetworkVO selectNetworkById(Integer id); + + /** + * 新增网络组件 + * + * @param network 网络组件 + * @return 结果 + */ + int insertNetwork(Network network); + + /** + * 修改网络组件 + * + * @param network 网络组件 + * @return 结果 + */ + int updateNetwork(Network network); + + /** + * 删除网络组件 + * + * @param id 网络组件ID + * @return 结果 + */ + int deleteNetworkById(Integer id); + + /** + * 批量删除网络组件 + * + * @param ids 网络组件ID数组 + * @return 结果 + */ + int deleteNetworkByIds(Integer[] ids); + + /** + * 启动网络组件 + * + * @param id 网络组件ID + * @return 结果 + */ + int startNetwork(Integer id); + + /** + * 停止网络组件 + * + * @param id 网络组件ID + * @return 结果 + */ + int stopNetwork(Integer id); + + /** + * 重启网络组件 + * + * @param id 网络组件ID + * @return 结果 + */ + int restartNetwork(Integer id); + + /** + * 获取网络类型列表 + * + * @return 网络类型列表 + */ + List getNetworkTypes(); + + /** + * 验证网络组件配置 + * + * @param network 网络组件 + * @return 验证结果 + */ + boolean validateNetworkConfig(Network network); + + /** + * 根据ID查询设备 + * + * @param id 设备ID + * @return 设备 + */ + IoTDevice getDeviceById(String id); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/service/impl/NetworkServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/service/impl/NetworkServiceImpl.java new file mode 100644 index 0000000..5f4038c --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/service/impl/NetworkServiceImpl.java @@ -0,0 +1,884 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.network.service.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.network.service.INetworkService; +import cn.universal.admin.network.utils.NetworkTypeUtil; +import cn.universal.common.constant.IoTConstant.DownCmd; +import cn.universal.common.constant.NetworkTypeConstants; +import cn.universal.common.domain.R; +import cn.universal.common.enums.NetworkType; +import cn.universal.common.exception.IoTException; +import cn.universal.core.service.IoTDownlFactory; +import cn.universal.mqtt.protocol.third.ThirdMQTTServerManager; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.Network; +import cn.universal.persistence.entity.bo.IoTDeviceBO; +import cn.universal.persistence.entity.bo.NetworkBO; +import cn.universal.persistence.entity.vo.IoTProductVO; +import cn.universal.persistence.entity.vo.NetworkVO; +import cn.universal.persistence.mapper.IoTDeviceFenceRelMapper; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.IoTProductMapper; +import cn.universal.persistence.mapper.NetworkMapper; +import cn.universal.persistence.query.NetworkQuery; +import com.github.pagehelper.Page; +import com.github.pagehelper.PageHelper; +import jakarta.annotation.Resource; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import tk.mybatis.mapper.entity.Example; + +/** + * 网络组件Service实现类 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +@Service +public class NetworkServiceImpl implements INetworkService { + + @Autowired + private NetworkMapper networkMapper; + + @Resource + private NetworkMapper netWorkMapper; + @Resource + private IoTDeviceMapper ioTDeviceMapper; + @Resource + private IoTProductMapper ioTProductMapper; + @Resource + private IoTDeviceFenceRelMapper ioTDeviceFenceRelMapper; + + + @Autowired(required = false) + private ThirdMQTTServerManager mqttServerManager; + + @Override + public boolean start(String productKey) { + + return true; + } + + @Override + public boolean stop(String productKey) { + + return true; + } + + @Override + public Object get(String productKey) { + return null; + } + + @Override + public boolean del(String productKey) { + Example ex = new Example(Network.class); + ex.createCriteria().andEqualTo("productKey", productKey); + return netWorkMapper.deleteByExample(ex) > 0; + } + + @Override + public List selectNetworkList(NetworkBO bo) { + return netWorkMapper.selectNetworkListV1(bo); + } + + @Override + public NetworkVO getDetail(Long id) { + return netWorkMapper.selectById(id); + } + + @Override + public List queryMileSightList(NetworkBO bo) { + return ioTDeviceMapper.queryMileSightList(bo); + } + + @Override + public R insertDevInstance(IoTDeviceBO devInstancebo) { + JSONObject downObj = new JSONObject(); + // 预添加归属为空 + downObj.set("appUnionId", ""); + // 产品编号 + downObj.set("productKey", devInstancebo.getProductKey()); + // 设备序列号 + downObj.set("deviceId", devInstancebo.getDeviceId()); + // Lora密匙 + downObj.set("deviceKey", devInstancebo.getSecretKey()); + // 添加 + downObj.set("cmd", DownCmd.DEV_ADD.name()); + // 说明 + downObj.set("detail", devInstancebo.getDetail()); + JSONObject ob = new JSONObject(); + // 设备实例名称 + ob.set("deviceName", devInstancebo.getDeviceName()); + // 设备序列号 + ob.set("imei", devInstancebo.getDeviceId()); + // 维度 + ob.set("latitude", devInstancebo.getLatitude()); + // 经度 + ob.set("longitude", devInstancebo.getLongitude()); + // 添加额外参数 + for (Map.Entry entry : devInstancebo.getOtherParams().entrySet()) { + ob.set(entry.getKey(), entry.getValue()); + } + downObj.set("data", ob); + IoTProduct ioTProduct = ioTProductMapper.getProductByProductKey(devInstancebo.getProductKey()); + return IoTDownlFactory.getIDown(ioTProduct.getThirdPlatform()).doAction(downObj); + } + + @Override + public R deleteDevInstanceByIds(String[] ids) { + R result = new R(); + // 获取网关eui + String id1 = ids[0]; + IoTDevice ioTDevice1 = ioTDeviceMapper.selectDevInstanceById(id1); + String gatewayEUI = JSONUtil.parseObj(ioTDevice1.getConfiguration()).getStr("gatewayEUI"); + if (StrUtil.isBlank(gatewayEUI)) { + return R.error("删除失败,缺少网关EUI"); + } + for (String id : ids) { + IoTDevice ioTDevice = ioTDeviceMapper.selectDevInstanceById(id); + JSONObject downRequest = new JSONObject(); + downRequest.set("appUnionId", ""); + downRequest.set("productKey", ioTDevice.getProductKey()); + downRequest.set("deviceId", ioTDevice.getDeviceId()); + downRequest.set("cmd", DownCmd.DEV_DEL.name()); + JSONObject ob = new JSONObject(); + ob.set("gatewayEUI", gatewayEUI); + downRequest.set("data", ob); + IoTProduct ioTProduct = ioTProductMapper.getProductByProductKey(ioTDevice.getProductKey()); + result = IoTDownlFactory.getIDown(ioTProduct.getThirdPlatform()).doAction(downRequest); + if (!result.getCode().equals(0)) { + return R.error( + "设备名称: " + ioTDevice.getDeviceName() + " 删除失败,原因: " + result.getMsg()); + } + ioTDeviceFenceRelMapper.deleteFenceInstance(ioTDevice.getIotId()); + } + return R.ok("删除成功"); + } + + @Override + public void reloadTcpClient(String applicationId) { + } + + @Override + public List selectNetworkList(NetworkQuery query) { + // 验证类型参数 + // 如果type参数不为空,将其转换为types列表 + if (StrUtil.isNotBlank(query.getType())) { + List types = NetworkTypeUtil.parseTypes(query.getType()); + + // 验证所有类型是否有效 + if (!NetworkTypeUtil.isValidTypes(types)) { + List invalidTypes = NetworkTypeUtil.getInvalidTypes(types); + throw new IoTException("无效的网络类型: " + String.join(", ", invalidTypes)); + } + // 设置types列表,清空type字段 + query.setTypes(types); + query.setType(null); + } + // 计算分页参数 + Page page = PageHelper.startPage(query.getPageNum(), query.getPageSize()); + List list = networkMapper.selectNetworkList(query); + for (NetworkBO bo : list) { + // 启用/停用状态 + bo.setEnableName(Boolean.TRUE.equals(bo.getState()) ? "已启用" : "已停用"); + // 实际运行状态 + String type = bo.getType(); + String productKey = bo.getProductKey(); + String unionId = bo.getUnionId(); + if (NetworkType.TCP_CLIENT.getId().equals(type) || NetworkType.TCP_SERVER.getId() + .equals(type)) { + + } else if (NetworkType.MQTT_CLIENT.getId().equals(type) || NetworkType.MQTT_SERVER.getId() + .equals(type)) { + if (mqttServerManager != null && mqttServerManager.isConnected(unionId)) { + bo.setStateName("已启动"); + bo.setRunning(Boolean.TRUE); + } else { + bo.setStateName("未启动"); + bo.setRunning(Boolean.FALSE); + } + } else { + bo.setStateName("未知"); + bo.setRunning(Boolean.FALSE); + } + } + return list; + } + + @Override + public NetworkVO selectNetworkById(Integer id) { + Network network = networkMapper.selectNetworkById(id); + if (network == null) { + return null; + } + NetworkVO vo = new NetworkVO(); + // 复制基本属性 + vo.setId(network.getId()); + vo.setType(network.getType()); + vo.setUnionId(network.getUnionId()); + vo.setProductKey(network.getProductKey()); + vo.setName(network.getName()); + vo.setDescription(network.getDescription()); + vo.setCreateDate(network.getCreateDate()); + vo.setCreateUser(network.getCreateUser()); + vo.setState(network.getState()); + vo.setConfiguration(network.getConfiguration()); + // 类型名称 + switch (network.getType()) { + case NetworkTypeConstants.TCP_CLIENT: + vo.setTypeName(NetworkType.TCP_CLIENT.getDescription()); + break; + case NetworkTypeConstants.TCP_SERVER: + vo.setTypeName(NetworkType.TCP_SERVER.getDescription()); + break; + case NetworkTypeConstants.MQTT_CLIENT: + vo.setTypeName(NetworkType.MQTT_CLIENT.getDescription()); + break; + case NetworkTypeConstants.MQTT_SERVER: + vo.setTypeName(NetworkType.MQTT_SERVER.getDescription()); + break; + default: + vo.setTypeName(network.getType()); + } + // 启用/停用状态 + vo.setEnableName(Boolean.TRUE.equals(network.getState()) ? "已启用" : "已停用"); + // 实际运行状态 + String type = network.getType(); + String productKey = network.getProductKey(); + String unionId = network.getUnionId(); + if (NetworkType.TCP_CLIENT.getId().equals(type) || NetworkType.TCP_SERVER.getId() + .equals(type)) { + //TODO 开源版本无 + + } else if (NetworkType.MQTT_CLIENT.getId().equals(type) || NetworkType.MQTT_SERVER.getId() + .equals(type)) { + if (mqttServerManager != null && mqttServerManager.isConnected(unionId)) { + vo.setStateName("已启动"); + vo.setRunning(Boolean.TRUE); + } else { + vo.setStateName("未启动"); + vo.setRunning(Boolean.FALSE); + } + } else { + vo.setStateName("未知"); + vo.setRunning(Boolean.FALSE); + } + return vo; + } + + /** + * 检查端口是否被占用或为常用端口 + */ + private void checkPortAvailable(Network network, boolean isUpdate) { + if (StrUtil.isBlank(network.getConfiguration())) { + return; + } + JSONObject config = JSONUtil.parseObj(network.getConfiguration()); + Integer port = config.getInt("port"); + if (port == null) { + return; + } + // 常用端口 + Set commonPorts = new HashSet<>(); + int[] arr = {80, 443, 3306, 6379, 8080, 22, 21, 25, 53, 123, 1521, 5432, 8888, 9000, 5000, 7001, + 27017}; + for (int p : arr) { + commonPorts.add(p); + } + if (commonPorts.contains(port)) { + throw new RuntimeException("端口 " + port + " 为常用端口,禁止使用"); + } + // 检查数据库是否被占用 + Integer excludeId = isUpdate ? network.getId() : null; + int count = networkMapper.selectTcpNetworkByPortCount(port, excludeId); + if (count > 0) { + throw new RuntimeException("端口 " + port + " 已被其他网络组件占用"); + } + } + + @Override + @Transactional + public int insertNetwork(Network network) { + // 验证配置 + if (!validateNetworkConfig(network)) { + throw new RuntimeException("网络组件配置验证失败"); + } + + // 检查配置唯一性 + checkConfigurationUniqueness(network, null); + + // 设置默认值 + if (network.getState() == null) { + network.setState(false); + } + + // 设置创建用户(如果未设置) + if (StrUtil.isBlank(network.getCreateUser())) { + // 从当前登录用户获取 unionId + network.setCreateUser(SecurityUtils.getUnionId()); + } + network.setUnionId(IdUtil.objectId()); + network.setState(false); + + // 新增TCP_SERVER时,必须指定productKey,并且不能重复绑定 + if (NetworkType.TCP_SERVER.getId().equals(network.getType()) || NetworkType.TCP_CLIENT.getId() + .equals(network.getType())) { + if (StrUtil.isBlank(network.getProductKey())) { + throw new RuntimeException("TCP服务组件必须选择产品"); + } + network.setUnionId(network.getProductKey()); + // 检查是否已被绑定 + int refCount = ioTProductMapper.countByNetworkUnionId(network.getProductKey()); + if (refCount > 0) { + throw new RuntimeException("该产品已绑定其他TCP服务组件,不能重复绑定"); + } + } + checkPortAvailable(network, false); + int result = networkMapper.insertNetwork(network); + // 新增成功后,若为TCP_SERVER,更新iot_product表 + if (result > 0 && NetworkType.TCP_SERVER.getId().equals(network.getType())) { + ioTProductMapper.updateNetworkUnionIdByProductKey(network.getProductKey(), + network.getProductKey()); + } + return result; + } + + @Override + @Transactional + public int updateNetwork(Network network) { + if (network == null || network.getId() == null) { + throw new RuntimeException("网络组件配置验证失败"); + } + // 验证配置 + if (!validateNetworkConfig(network)) { + throw new RuntimeException("网络组件配置验证失败"); + } + // 检查权限(只能操作自己创建的网络组件,除非是管理员) + Network existNetwork = networkMapper.selectNetworkById(network.getId()); + if (existNetwork == null) { + throw new RuntimeException("网络组件不存在"); + } + IoTUser currentUser = SecurityUtils.getIoTUnionUser(); + if (!existNetwork.getCreateUser().equals(currentUser.getUnionId()) && !currentUser.isAdmin()) { + throw new RuntimeException("不能操作不是自己的网络组件"); + } + + String type = existNetwork.getType(); + if ((NetworkType.TCP_SERVER.getId().equals(type) || NetworkType.TCP_CLIENT.getId() + .equals(type))) { + // 只有当unionId发生变化时,才做"已被产品关联且产品下有设备时禁止修改"的校验 + String oldProductKey = existNetwork.getProductKey(); + String networkProductKey = network.getProductKey(); + String current = ioTProductMapper.findProductKeyByNetworkUnionId(oldProductKey); + if (current != null && !oldProductKey.equals(networkProductKey)) { + int deviceCount = ioTDeviceMapper.countByProductKey(oldProductKey); + if (deviceCount > 0) { + throw new RuntimeException("原关联产品有设备,无法变更"); + } + } + } + // 检查配置唯一性(排除自己) + checkConfigurationUniqueness(network, network.getId()); + checkPortAvailable(network, true); + int count = networkMapper.updateNetwork(network); + if (count > 0 && NetworkType.TCP_SERVER.getId().equals(network.getType())) { + ioTProductMapper.updateNetworkUnionIdByProductKey(network.getProductKey(), + network.getProductKey()); + } + return count; + } + + @Override + @Transactional + public int deleteNetworkById(Integer id) { + Network network = networkMapper.selectNetworkById(id); + if (network == null) { + throw new RuntimeException("网络组件不存在"); + } + String type = network.getType(); + // 只对TCP_SERVER和TCP_CLIENT做判断 + if (NetworkType.TCP_SERVER.getId().equals(type) || NetworkType.TCP_CLIENT.getId() + .equals(type)) { + // 查找是否有关联产品 + String productKey = ioTProductMapper.findProductKeyByNetworkUnionId(network.getUnionId()); + if (productKey != null) { + // 查找该产品下是否有设备 + int deviceCount = ioTDeviceMapper.countByProductKey(productKey); + if (deviceCount > 0) { + throw new RuntimeException("该网络组件已被产品关联且产品下存在设备,无法删除"); + } + } + } + return networkMapper.deleteNetworkById(id); + } + + @Override + @Transactional + public int deleteNetworkByIds(Integer[] ids) { + return networkMapper.deleteNetworkByIds(ids); + } + + @Override + @Transactional + public int startNetwork(Integer id) { + Network network = networkMapper.selectNetworkById(id); + if (network == null) { + throw new RuntimeException("网络组件不存在"); + } + String type = network.getType(); + JSONObject config = JSONUtil.parseObj(network.getConfiguration()); + if (NetworkType.TCP_CLIENT.getId().equals(type) || NetworkType.TCP_SERVER.getId() + .equals(type)) { + Integer port = config.getInt("port"); + if (port == null) { + throw new RuntimeException("TCP网络组件启动/重启时端口不能为空"); + } + } else if (NetworkType.MQTT_CLIENT.getId().equals(type) || NetworkType.MQTT_SERVER.getId() + .equals(type)) { + String host = config.getStr("host"); + if (StrUtil.isBlank(host)) { + throw new RuntimeException("MQTT网络组件启动/重启时host不能为空"); + } + } + try { + // 根据网络类型调用相应的启动逻辑 + boolean success = startNetworkByType(network); + + if (success) { + log.info("启动网络组件成功: {}", network.getName()); + return 1; + } else { + throw new RuntimeException("启动失败: " + network.getName()); + } + } catch (Exception e) { + log.error("启动网络组件失败: {}", network.getName(), e); + throw new RuntimeException("启动网络组件失败: " + e.getMessage()); + } + } + + @Override + @Transactional + public int stopNetwork(Integer id) { + Network network = networkMapper.selectNetworkById(id); + if (network == null) { + throw new RuntimeException("网络组件不存在"); + } + + try { + // 根据网络类型调用相应的停止逻辑 + boolean success = stopNetworkByType(network); + if (success) { + log.info("停止网络组件成功: {}", network.getName()); + return 1; + } else { + throw new RuntimeException("停止网络组件失败: " + network.getName()); + } + } catch (Exception e) { + log.error("停止网络组件失败: {}", network.getName(), e); + throw new RuntimeException("停止网络组件失败: " + e.getMessage()); + } + } + + @Override + @Transactional + public int restartNetwork(Integer id) { + Network network = networkMapper.selectNetworkById(id); + if (network == null) { + throw new RuntimeException("网络组件不存在"); + } + String type = network.getType(); + JSONObject config = JSONUtil.parseObj(network.getConfiguration()); + if (NetworkType.TCP_CLIENT.getId().equals(type) || NetworkType.TCP_SERVER.getId() + .equals(type)) { + Integer port = config.getInt("port"); + if (port == null) { + throw new RuntimeException("TCP网络组件启动/重启时端口不能为空"); + } + } else if (NetworkType.MQTT_CLIENT.getId().equals(type) || NetworkType.MQTT_SERVER.getId() + .equals(type)) { + String host = config.getStr("host"); + if (StrUtil.isBlank(host)) { + throw new RuntimeException("MQTT网络组件启动/重启时host不能为空"); + } + } + try { + // 根据网络类型调用相应的重启逻辑 + boolean success = restartNetworkByType(network); + + if (success) { + log.info("重启网络组件成功: {}", network.getName()); + // 重启后状态应该是启动的 + return 1; + } else { + throw new RuntimeException("重启网络组件失败: " + network.getName()); + } + } catch (Exception e) { + log.error("重启网络组件失败: {}", network.getName(), e); + throw new RuntimeException("重启网络组件失败: " + e.getMessage()); + } + } + + @Override + public List getNetworkTypes() { + return Arrays.asList(NetworkType.TCP_CLIENT.getId(), NetworkType.TCP_SERVER.getId(), + NetworkType.MQTT_CLIENT.getId(), NetworkType.MQTT_SERVER.getId()); + } + + /** + * 验证网络类型是否有效 + * + * @param type 网络类型 + * @return 是否有效 + */ + private boolean isValidNetworkType(String type) { + return getNetworkTypes().contains(type); + } + + /** + * 验证多个网络类型是否都有效 + * + * @param types 网络类型列表 + * @return 是否都有效 + */ + private boolean isValidNetworkTypes(List types) { + return types.stream().allMatch(this::isValidNetworkType); + } + + @Override + public boolean validateNetworkConfig(Network network) { + if (network == null) { + return false; + } + + // 验证必填字段 + if (StrUtil.isBlank(network.getType()) || StrUtil.isBlank(network.getName())) { + return false; + } + + // 验证配置JSON格式 + if (StrUtil.isNotBlank(network.getConfiguration())) { + try { + JSONUtil.parseObj(network.getConfiguration()); + } catch (Exception e) { + log.warn("网络组件配置JSON格式错误: {}", e.getMessage()); + return false; + } + } + return true; + } + + /** + * 检查网络组件配置的唯一性 + * + * @param network 网络组件 + * @param excludeId 排除的网络组件ID(用于更新时排除自己) + */ + private void checkConfigurationUniqueness(Network network, Integer excludeId) { + if (StrUtil.isBlank(network.getConfiguration())) { + return; + } + + try { + JSONObject config = JSONUtil.parseObj(network.getConfiguration()); + String type = network.getType(); + // TCP类型检查端口唯一性 + if (NetworkType.TCP_CLIENT.getId().equals(type) || NetworkType.TCP_SERVER.getId() + .equals(type)) { + Integer port = config.getInt("port"); + if (port != null) { + List existingNetworks = networkMapper.selectTcpNetworkByPort(port, excludeId); + if (!existingNetworks.isEmpty()) { + Network existingNetwork = existingNetworks.get(0); + throw new RuntimeException( + "端口 " + port + " 已被 " + existingNetwork.getName() + " 网络组件使用"); + } + } + } + + // MQTT类型检查主机和用户名组合唯一性 + if (NetworkType.MQTT_CLIENT.getId().equals(type) || NetworkType.MQTT_SERVER.getId() + .equals(type)) { + String host = config.getStr("host"); + String username = config.getStr("username"); + + if (StrUtil.isNotBlank(host) && StrUtil.isNotBlank(username)) { + List existingNetworks = networkMapper.selectMqttNetworkByHostAndUsername(host, + username, excludeId); + if (!existingNetworks.isEmpty()) { + Network existingNetwork = existingNetworks.get(0); + throw new RuntimeException("主机 " + host + " 和用户名 " + username + " 的组合已被 " + + existingNetwork.getName() + " 网络组件使用"); + } + } + } + } catch (Exception e) { + if (e instanceof RuntimeException) { + throw e; + } + log.warn("检查网络组件配置唯一性时发生错误: {}", e.getMessage()); + } + } + + /** + * 根据网络类型启动网络组件 + * + * @param network 网络组件 + * @return 是否成功 + */ + private boolean startNetworkByType(Network network) { + String type = network.getType(); + + try { + switch (type) { + case NetworkTypeConstants.TCP_CLIENT: + case NetworkTypeConstants.TCP_SERVER: + return startTcpNetwork(network); + case NetworkTypeConstants.MQTT_CLIENT: + case NetworkTypeConstants.MQTT_SERVER: + return startMqttNetwork(network); + default: + log.warn("不支持的网络类型: {}", type); + return false; + } + } catch (Exception e) { + log.error("启动网络组件失败: type={}, name={}", type, network.getName(), e); + return false; + } + } + + /** + * 根据网络类型停止网络组件 + * + * @param network 网络组件 + * @return 是否成功 + */ + private boolean stopNetworkByType(Network network) { + String type = network.getType(); + + try { + switch (type) { + case NetworkTypeConstants.TCP_CLIENT: + case NetworkTypeConstants.TCP_SERVER: + return stopTcpNetwork(network); + case NetworkTypeConstants.MQTT_CLIENT: + case NetworkTypeConstants.MQTT_SERVER: + return stopMqttNetwork(network); + default: + log.warn("不支持的网络类型: {}", type); + return false; + } + } catch (Exception e) { + log.error("停止网络组件失败: type={}, name={}", type, network.getName(), e); + return false; + } + } + + /** + * 根据网络类型重启网络组件 + * + * @param network 网络组件 + * @return 是否成功 + */ + private boolean restartNetworkByType(Network network) { + String type = network.getType(); + + try { + switch (type) { + case NetworkTypeConstants.TCP_CLIENT: + case NetworkTypeConstants.TCP_SERVER: + return restartTcpNetwork(network); + case NetworkTypeConstants.MQTT_CLIENT: + case NetworkTypeConstants.MQTT_SERVER: + return restartMqttNetwork(network); + default: + log.warn("不支持的网络类型: {}", type); + return false; + } + } catch (Exception e) { + log.error("重启网络组件失败: type={}, name={}", type, network.getName(), e); + return false; + } + } + + /** + * 启动TCP网络组件 + * + * @param network 网络组件 + * @return 是否成功 + */ + private boolean startTcpNetwork(Network network) { + try { + //TODO 开源版本无 + return true; + } catch (Exception e) { + log.error("启动TCP网络组件失败: productKey={}", "", e); + return false; + } + } + + /** + * 停止TCP网络组件 + * + * @param network 网络组件 + * @return 是否成功 + */ + private boolean stopTcpNetwork(Network network) { + + String productKey = network.getProductKey(); + if (StrUtil.isBlank(productKey)) { + log.error("TCP网络组件缺少productKey: {}", network.getName()); + return false; + } + try { + + return true; + } catch (Exception e) { + log.error("停止TCP网络组件失败: productKey={}", "", e); + return false; + } + } + + /** + * 重启TCP网络组件 + * + * @param network 网络组件 + * @return 是否成功 + */ + private boolean restartTcpNetwork(Network network) { + + try { + return true; + } catch (Exception e) { + log.error("重启TCP网络组件失败: productKey={}", "", e); + return false; + } + } + + /** + * 启动MQTT网络组件 + * + * @param network 网络组件 + * @return 是否成功 + */ + private boolean startMqttNetwork(Network network) { + if (mqttServerManager == null) { + log.error("MQTT服务管理器未注入,无法启动MQTT网络组件: {}", network.getName()); + return false; + } + + String unionId = network.getUnionId(); + if (StrUtil.isBlank(unionId)) { + log.error("MQTT网络组件缺少unionId: {}", network.getName()); + return false; + } + + try { + boolean success = mqttServerManager.startMqttClient(unionId); + if (success) { + log.info("MQTT网络组件启动成功: unionId={}", unionId); + } else { + log.error("MQTT网络组件启动失败: unionId={}", unionId); + } + return success; + } catch (Exception e) { + log.error("启动MQTT网络组件失败: unionId={}", unionId, e); + return false; + } + } + + /** + * 停止MQTT网络组件 + * + * @param network 网络组件 + * @return 是否成功 + */ + private boolean stopMqttNetwork(Network network) { + if (mqttServerManager == null) { + log.error("MQTT服务管理器未注入,无法停止MQTT网络组件: {}", network.getName()); + return false; + } + + String unionId = network.getUnionId(); + if (StrUtil.isBlank(unionId)) { + log.error("MQTT网络组件缺少unionId: {}", network.getName()); + return false; + } + + try { + boolean success = mqttServerManager.stopMqttClient(unionId); + if (success) { + log.info("MQTT网络组件停止成功: unionId={}", unionId); + } else { + log.error("MQTT网络组件停止失败: unionId={}", unionId); + } + return success; + } catch (Exception e) { + log.error("停止MQTT网络组件失败: unionId={}", unionId, e); + return false; + } + } + + /** + * 重启MQTT网络组件 + * + * @param network 网络组件 + * @return 是否成功 + */ + private boolean restartMqttNetwork(Network network) { + if (mqttServerManager == null) { + log.error("MQTT服务管理器未注入,无法重启MQTT网络组件: {}", network.getName()); + return false; + } + + String unionId = network.getUnionId(); + if (StrUtil.isBlank(unionId)) { + log.error("MQTT网络组件缺少unionId: {}", network.getName()); + return false; + } + + try { + boolean success = mqttServerManager.restartMqttClient(unionId); + if (success) { + log.info("MQTT网络组件重启成功: unionId={}", unionId); + } else { + log.error("MQTT网络组件重启失败: unionId={}", unionId); + } + return success; + } catch (Exception e) { + log.error("重启MQTT网络组件失败: unionId={}", unionId, e); + return false; + } + } + + @Override + public IoTDevice getDeviceById(String id) { + return ioTDeviceMapper.selectDevInstanceById(id); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/NetworkConfigUtil.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/NetworkConfigUtil.java new file mode 100644 index 0000000..d1b5fb4 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/NetworkConfigUtil.java @@ -0,0 +1,243 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.network.utils; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.persistence.entity.Network; +import lombok.extern.slf4j.Slf4j; + +/** + * 网络组件配置工具类 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +public class NetworkConfigUtil { + + /** + * 验证网络组件配置 + * + * @param network 网络组件 + * @return 验证结果 + */ + public static boolean validateConfig(Network network) { + if (network == null) { + return false; + } + + // 验证必填字段 + if (StrUtil.isBlank(network.getType()) + || StrUtil.isBlank(network.getUnionId()) + || StrUtil.isBlank(network.getName())) { + return false; + } + + // 验证配置JSON格式 + if (StrUtil.isNotBlank(network.getConfiguration())) { + try { + JSONUtil.parseObj(network.getConfiguration()); + } catch (Exception e) { + log.warn("网络组件配置JSON格式错误: {}", e.getMessage()); + return false; + } + } + + return true; + } + + /** + * 验证TCP配置 + * + * @param config 配置JSON字符串 + * @return 验证结果 + */ + public static boolean validateTcpConfig(String config) { + if (StrUtil.isBlank(config)) { + return false; + } + + try { + JSONObject jsonConfig = JSONUtil.parseObj(config); + + // 验证必填字段 + if (!jsonConfig.containsKey("host") || !jsonConfig.containsKey("port")) { + return false; + } + + // 验证端口范围 + int port = jsonConfig.getInt("port", 0); + if (port <= 0 || port > 65535) { + return false; + } + + return true; + } catch (Exception e) { + log.warn("TCP配置验证失败: {}", e.getMessage()); + return false; + } + } + + /** + * 验证MQTT配置 + * + * @param config 配置JSON字符串 + * @return 验证结果 + */ + public static boolean validateMqttConfig(String config) { + if (StrUtil.isBlank(config)) { + return false; + } + + try { + JSONObject jsonConfig = JSONUtil.parseObj(config); + + // 验证必填字段 + if (!jsonConfig.containsKey("host")) { + return false; + } + + // 验证QoS范围 + if (jsonConfig.containsKey("defaultQos")) { + int qos = jsonConfig.getInt("defaultQos", -1); + if (qos < 0 || qos > 2) { + return false; + } + } + + // 验证心跳间隔 + if (jsonConfig.containsKey("keepAliveInterval")) { + int keepAlive = jsonConfig.getInt("keepAliveInterval", 0); + if (keepAlive < 0) { + return false; + } + } + + return true; + } catch (Exception e) { + log.warn("MQTT配置验证失败: {}", e.getMessage()); + return false; + } + } + + /** + * 根据网络类型验证配置 + * + * @param network 网络组件 + * @return 验证结果 + */ + public static boolean validateConfigByType(Network network) { + if (network == null + || StrUtil.isBlank(network.getType()) + || StrUtil.isBlank(network.getConfiguration())) { + return false; + } + + switch (network.getType()) { + case "TCP_CLIENT": + case "TCP_SERVER": + return validateTcpConfig(network.getConfiguration()); + case "MQTT_CLIENT": + case "MQTT_SERVER": + return validateMqttConfig(network.getConfiguration()); + default: + return false; + } + } + + /** + * 获取默认TCP配置 + * + * @return 默认TCP配置 + */ + public static String getDefaultTcpConfig() { + JSONObject config = new JSONObject(); + config.set("host", "0.0.0.0"); + config.set("port", 8080); + config.set("ssl", false); + config.set("allIdleTime", 0); + config.set("readerIdleTime", 360); + config.set("writerIdleTime", 0); + config.set("decoderType", "STRING"); + config.set("parserType", "DELIMITED"); + config.set( + "parserConfiguration", + new JSONObject() + .set("byteOrderLittle", true) + .set("delimited", "]") + .set("delimitedMaxlength", 1024) + .set("failFast", true)); + return config.toString(); + } + + /** + * 获取默认MQTT配置 + * + * @return 默认MQTT配置 + */ + public static String getDefaultMqttConfig() { + JSONObject config = new JSONObject(); + config.set("host", "tcp://localhost:1883"); + config.set("username", ""); + config.set("password", ""); + config.set("clientIdPrefix", "univ_cli_"); + config.set("defaultQos", 1); + config.set("keepAliveInterval", 60); + config.set("connectTimeout", 30); + config.set("autoReconnect", true); + config.set("cleanSession", true); + config.set("ssl", false); + config.set("enabled", true); + return config.toString(); + } + + /** + * 根据网络类型获取默认配置 + * + * @param type 网络类型 + * @return 默认配置 + */ + public static String getDefaultConfigByType(String type) { + switch (type) { + case "TCP_CLIENT": + case "TCP_SERVER": + return getDefaultTcpConfig(); + case "MQTT_CLIENT": + case "MQTT_SERVER": + return getDefaultMqttConfig(); + default: + return "{}"; + } + } + + /** + * 格式化配置显示 + * + * @param config 配置JSON字符串 + * @return 格式化后的配置 + */ + public static String formatConfig(String config) { + if (StrUtil.isBlank(config)) { + return "{}"; + } + + try { + return JSONUtil.parseObj(config).toStringPretty(); + } catch (Exception e) { + log.warn("格式化配置失败: {}", e.getMessage()); + return config; + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/NetworkTypeUtil.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/NetworkTypeUtil.java new file mode 100644 index 0000000..dbeb0d0 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/NetworkTypeUtil.java @@ -0,0 +1,130 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.network.utils; + +import cn.hutool.core.util.StrUtil; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; + +/** + * 网络类型工具类 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +public class NetworkTypeUtil { + + /** 支持的网络类型列表 */ + public static final List SUPPORTED_TYPES = + Arrays.asList("TCP_CLIENT", "TCP_SERVER", "MQTT_CLIENT", "MQTT_SERVER"); + + /** + * 解析网络类型字符串,支持逗号分隔的多个类型 + * + * @param typeStr 类型字符串,如 "MQTT_CLIENT,MQTT_SERVER" + * @return 类型列表 + */ + public static List parseTypes(String typeStr) { + if (StrUtil.isBlank(typeStr)) { + return List.of(); + } + + // 分割并清理空白字符 + return Arrays.stream(typeStr.split(",")) + .map(String::trim) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + } + + /** + * 验证网络类型是否有效 + * + * @param type 网络类型 + * @return 是否有效 + */ + public static boolean isValidType(String type) { + return SUPPORTED_TYPES.contains(type); + } + + /** + * 验证多个网络类型是否都有效 + * + * @param types 网络类型列表 + * @return 是否都有效 + */ + public static boolean isValidTypes(List types) { + return types.stream().allMatch(NetworkTypeUtil::isValidType); + } + + /** + * 获取无效的类型列表 + * + * @param types 网络类型列表 + * @return 无效的类型列表 + */ + public static List getInvalidTypes(List types) { + return types.stream().filter(type -> !isValidType(type)).collect(Collectors.toList()); + } + + /** + * 格式化类型字符串(去重、排序) + * + * @param typeStr 原始类型字符串 + * @return 格式化后的类型字符串 + */ + public static String formatTypes(String typeStr) { + List types = parseTypes(typeStr); + return types.stream().distinct().sorted().collect(Collectors.joining(",")); + } + + /** + * 检查是否包含指定类型 + * + * @param typeStr 类型字符串 + * @param targetType 目标类型 + * @return 是否包含 + */ + public static boolean containsType(String typeStr, String targetType) { + List types = parseTypes(typeStr); + return types.contains(targetType); + } + + /** + * 获取类型的中文名称 + * + * @param type 类型代码 + * @return 中文名称 + */ + public static String getTypeName(String type) { + return switch (type) { + case "TCP_CLIENT" -> "TCP客户端"; + case "TCP_SERVER" -> "TCP服务端"; + case "MQTT_CLIENT" -> "MQTT客户端"; + case "MQTT_SERVER" -> "MQTT服务端"; + default -> type; + }; + } + + /** + * 获取多个类型的中文名称 + * + * @param types 类型列表 + * @return 中文名称列表 + */ + public static List getTypeNames(List types) { + return types.stream().map(NetworkTypeUtil::getTypeName).collect(Collectors.toList()); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/UserContextUtil.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/UserContextUtil.java new file mode 100644 index 0000000..cf03da6 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/UserContextUtil.java @@ -0,0 +1,55 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.network.utils; + +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.persistence.entity.IoTUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** 用户上下文工具类 用于获取当前登录用户信息 @Author Aleo */ +public class UserContextUtil { + + private static final Logger log = LoggerFactory.getLogger(UserContextUtil.class); + + /** + * 获取当前登录用户的 unionId 如果无法获取用户信息,返回默认值 "system" + * + * @return 当前用户的 unionId,如果获取失败则返回 "system" + */ + public static String getCurrentUserUnionId() { + try { + IoTUser currentUser = SecurityUtils.getIoTUnionUser(); + if (currentUser != null && currentUser.getUnionId() != null) { + return currentUser.getUnionId(); + } + } catch (Exception e) { + log.warn("获取当前用户信息失败,使用默认用户: {}", e.getMessage()); + } + return "system"; + } + + /** + * 获取当前登录用户信息 如果无法获取用户信息,返回 null + * + * @return 当前用户信息,如果获取失败则返回 null + */ + public static IoTUser getCurrentUser() { + try { + return SecurityUtils.getIoTUnionUser(); + } catch (Exception e) { + log.warn("获取当前用户信息失败: {}", e.getMessage()); + return null; + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/web/NetworkController.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/web/NetworkController.java new file mode 100644 index 0000000..c6e2bab --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/web/NetworkController.java @@ -0,0 +1,461 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.network.web; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.network.service.INetworkService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.common.domain.R; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.Network; +import cn.universal.persistence.entity.bo.IoTDeviceBO; +import cn.universal.persistence.entity.bo.NetworkBO; +import cn.universal.persistence.entity.vo.NetworkVO; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import cn.universal.persistence.query.NetworkQuery; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Controller + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +@RestController +@Tag(name = "网络管理", description = "网络管理") +@RequestMapping("/admin/network") +public class NetworkController extends BaseController { + + @Autowired private INetworkService networkService; + + /** 查询列表 */ + @GetMapping("/list") + public TableDataInfo list(NetworkQuery query) { + try { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (iotUser != null && !iotUser.isAdmin()) { + query.setCreateUser(iotUser.getUnionId()); + } + List list = networkService.selectNetworkList(query); + return getDataTable(list); + } catch (Exception e) { + log.error("查询列表失败", e); + throw new IoTException("查询列表失败: " + e.getMessage()); + } + } + + /** 根据ID查询 */ + @GetMapping("/{id}") + public AjaxResult getInfo(@PathVariable Integer id) { + try { + String createUser = SecurityUtils.getUnionId(); + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + NetworkVO network = networkService.selectNetworkById(id); + if (network == null) { + return AjaxResult.error("不存在"); + } + // 校验用户权限 + if (!createUser.equals(network.getCreateUser()) && !iotUser.isAdmin()) { + return AjaxResult.error("无权限访问该"); + } + return AjaxResult.success(network); + } catch (Exception e) { + log.error("查询失败", e); + return AjaxResult.error("查询失败: " + e.getMessage()); + } + } + + /** 新增 */ + @PostMapping + public AjaxResult add(@RequestBody Network network) { + try { + // 设置创建用户 + network.setCreateUser(SecurityUtils.getUnionId()); + int result = networkService.insertNetwork(network); + if (result > 0) { + return AjaxResult.success("新增成功"); + } else { + return AjaxResult.error("新增失败"); + } + } catch (Exception e) { + log.error("新增失败", e); + return AjaxResult.error("新增失败: " + e.getMessage()); + } + } + + /** 修改 */ + @PutMapping + public AjaxResult edit(@RequestBody Network network) { + try { + // 校验用户权限 + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + + NetworkVO existingNetwork = networkService.selectNetworkById(network.getId()); + if (existingNetwork == null) { + return AjaxResult.error("不存在"); + } + String createUser = SecurityUtils.getUnionId(); + if (!createUser.equals(existingNetwork.getCreateUser()) && !iotUser.isAdmin()) { + return AjaxResult.error("无权限修改该"); + } + + int result = networkService.updateNetwork(network); + if (result > 0) { + return AjaxResult.success("修改成功"); + } else { + return AjaxResult.error("修改失败"); + } + } catch (Exception e) { + log.error("修改失败", e); + return AjaxResult.error("操作失败" + e.getMessage()); + } + } + + /** 删除 */ + @DeleteMapping("/{id}") + public AjaxResult remove(@PathVariable Integer id) { + try { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + + // 校验用户权限 + NetworkVO network = networkService.selectNetworkById(id); + if (network == null) { + return AjaxResult.error("不存在"); + } + String createUser = SecurityUtils.getUnionId(); + if (!createUser.equals(network.getCreateUser()) && !iotUser.isAdmin()) { + return AjaxResult.error("无权限删除该"); + } + + int result = networkService.deleteNetworkById(id); + if (result > 0) { + return AjaxResult.success("删除成功"); + } else { + return AjaxResult.error("删除失败"); + } + } catch (Exception e) { + log.error("删除失败", e); + return AjaxResult.error("删除失败: " + e.getMessage()); + } + } + + /** 批量删除 */ + @DeleteMapping("/batch/{ids}") + public AjaxResult removeBatch(@PathVariable Integer[] ids) { + try { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + + // 校验用户权限 + String createUser = SecurityUtils.getUnionId(); + for (Integer id : ids) { + NetworkVO network = networkService.selectNetworkById(id); + if (network == null) { + return AjaxResult.error("不存在: " + id); + } + if (!createUser.equals(network.getCreateUser()) && !iotUser.isAdmin()) { + return AjaxResult.error("无权限删除: " + id); + } + } + + int result = networkService.deleteNetworkByIds(ids); + if (result > 0) { + return AjaxResult.success("批量删除成功"); + } else { + return AjaxResult.error("批量删除失败"); + } + } catch (Exception e) { + log.error("批量删除失败", e); + return AjaxResult.error("批量删除失败: " + e.getMessage()); + } + } + + /** 启动 */ + @PostMapping("/start/{id}") + public AjaxResult start(@PathVariable Integer id) { + try { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + + // 校验用户权限 + NetworkVO network = networkService.selectNetworkById(id); + if (network == null) { + return AjaxResult.error("不存在"); + } + String createUser = SecurityUtils.getUnionId(); + if (!createUser.equals(network.getCreateUser()) && !iotUser.isAdmin()) { + return AjaxResult.error("无权限操作该"); + } + + int result = networkService.startNetwork(id); + if (result > 0) { + return AjaxResult.success("启动成功"); + } else { + return AjaxResult.error("启动失败"); + } + } catch (Exception e) { + log.error("启动失败", e); + return AjaxResult.error("启动失败: " + e.getMessage()); + } + } + + /** 停止 */ + @PostMapping("/stop/{id}") + public AjaxResult stop(@PathVariable Integer id) { + try { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + + // 校验用户权限 + NetworkVO network = networkService.selectNetworkById(id); + if (network == null) { + return AjaxResult.error("不存在"); + } + String createUser = SecurityUtils.getUnionId(); + if (!createUser.equals(network.getCreateUser()) && !iotUser.isAdmin()) { + return AjaxResult.error("无权限操作该"); + } + + int result = networkService.stopNetwork(id); + if (result > 0) { + return AjaxResult.success("操作成功"); + } else { + return AjaxResult.error("操作失败"); + } + } catch (Exception e) { + log.error("操作失败", e); + return AjaxResult.error("操作失败: " + e.getMessage()); + } + } + + /** 重启 */ + @PostMapping("/restart/{id}") + public AjaxResult restart(@PathVariable Integer id) { + try { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + + // 校验用户权限 + NetworkVO network = networkService.selectNetworkById(id); + if (network == null) { + return AjaxResult.error("不存在"); + } + String createUser = SecurityUtils.getUnionId(); + if (!createUser.equals(network.getCreateUser()) && !iotUser.isAdmin()) { + return AjaxResult.error("无权限操作该"); + } + + int result = networkService.restartNetwork(id); + if (result > 0) { + return AjaxResult.success("重启成功"); + } else { + return AjaxResult.error("重启失败"); + } + } catch (Exception e) { + log.error("重启失败", e); + return AjaxResult.error("重启失败: " + e.getMessage()); + } + } + + /** 获取网络类型列表 */ + @GetMapping("/types") + public AjaxResult getNetworkTypes() { + try { + List types = networkService.getNetworkTypes(); + return AjaxResult.success(types); + } catch (Exception e) { + log.error("获取网络类型列表失败", e); + return AjaxResult.error("获取网络类型列表失败: " + e.getMessage()); + } + } + + /** 验证配置 */ + @PostMapping("/validate") + public AjaxResult validateConfig(@RequestBody Network network) { + try { + // 设置创建用户 + network.setCreateUser(SecurityUtils.getUnionId()); + boolean isValid = networkService.validateNetworkConfig(network); + if (isValid) { + return AjaxResult.success("配置验证通过"); + } else { + return AjaxResult.error("配置验证失败"); + } + } catch (Exception e) { + log.error("验证配置失败", e); + return AjaxResult.error("验证配置失败: " + e.getMessage()); + } + } + + @Resource private INetworkService iNetworkService; + + /** 查询网络列表 */ + @GetMapping("/v1/list") + public TableDataInfo list(NetworkBO bo) { + startPage(); + // 设置用户权限过滤 + bo.setCreateUser(SecurityUtils.getUnionId()); + List list = iNetworkService.selectNetworkList(bo); + return getDataTable(list); + } + + /** 查询绑定指定星纵网关的设备 */ + @GetMapping("/milesight/list") + public TableDataInfo queryMileSightList(NetworkBO bo) { + startPage(); + // 设置用户权限过滤 + bo.setCreateUser(SecurityUtils.getUnionId()); + List list = iNetworkService.queryMileSightList(bo); + return getDataTable(list); + } + + /** 新增设备 */ + @PostMapping("/milesight/add") + @Log(title = "新增设备", businessType = BusinessType.INSERT) + public R add(@RequestBody IoTDeviceBO devInstancebo) { + // 设置创建用户 + devInstancebo.setCreatorId(SecurityUtils.getUnionId()); + R result = iNetworkService.insertDevInstance(devInstancebo); + JSONObject jsonObject = JSONUtil.parseObj(result); + String code = jsonObject.getStr("code"); + String msg = jsonObject.getStr("msg"); + if ("0".equals(code)) { + return R.ok(); + } else { + return R.error(msg); + } + } + + /** 删除设备 */ + @DeleteMapping("/milesight/{ids}") + @Log(title = "删除设备", businessType = BusinessType.DELETE) + public R remove(@PathVariable String[] ids) { + // 校验用户权限 + String currentUser = SecurityUtils.getUnionId(); + for (String id : ids) { + IoTDevice device = iNetworkService.getDeviceById(id); + if (device == null) { + return R.error("设备不存在: " + id); + } + if (!currentUser.equals(device.getCreatorId())) { + return R.error("无权限删除设备: " + id); + } + } + R result = iNetworkService.deleteDevInstanceByIds(ids); + JSONObject jsonObject = JSONUtil.parseObj(result); + String code = jsonObject.getStr("code"); + String msg = jsonObject.getStr("msg"); + if ("0".equals(code)) { + return R.ok(); + } else { + return R.error(msg); + } + } + + @GetMapping("/reload/tcpclient/{applicationId}") + public R reloadTcpClient(@PathVariable("applicationId") String applicationId) { + // 校验用户权限 - 需要根据applicationId获取相关的信息进行权限校验 + // 这里可能需要额外的业务逻辑来实现权限校验 + iNetworkService.reloadTcpClient(applicationId); + return R.ok(); + } + + /** 获取状态 */ + @GetMapping("/status/{id}") + public AjaxResult getStatus(@PathVariable Integer id) { + try { + NetworkVO network = networkService.selectNetworkById(id); + if (network == null) { + return AjaxResult.error("不存在"); + } + + // 校验用户权限 + String createUser = SecurityUtils.getUnionId(); + if (!createUser.equals(network.getCreateUser())) { + return AjaxResult.error("无权限查看该状态"); + } + + // 构建状态信息 + Map status = new HashMap<>(); + status.put("id", network.getId()); + status.put("name", network.getName()); + status.put("type", network.getType()); + status.put("typeName", network.getTypeName()); + status.put("state", network.getState()); + status.put("stateName", network.getStateName()); + status.put("productKey", network.getProductKey()); + status.put("unionId", network.getUnionId()); + status.put("createDate", network.getCreateDate()); + status.put("createUser", network.getCreateUser()); + + return AjaxResult.success(status); + } catch (Exception e) { + log.error("获取状态失败", e); + return AjaxResult.error("获取状态失败: " + e.getMessage()); + } + } + + /** 获取所有状态 */ + @GetMapping("/status") + public AjaxResult getAllStatus() { + try { + NetworkQuery query = new NetworkQuery(); + query.setPageNum(1); + query.setPageSize(1000); // 获取所有 + // 设置用户权限过滤 + query.setCreateUser(SecurityUtils.getUnionId()); + + List networks = networkService.selectNetworkList(query); + + List> statusList = new ArrayList<>(); + for (NetworkBO network : networks) { + Map status = new HashMap<>(); + status.put("id", network.getId()); + status.put("name", network.getName()); + status.put("type", network.getType()); + status.put("state", network.getState()); + status.put("productKey", network.getProductKey()); + status.put("unionId", network.getUnionId()); + statusList.add(status); + } + + Map result = new HashMap<>(); + result.put("total", statusList.size()); + result.put("networks", statusList); + + return AjaxResult.success(result); + } catch (Exception e) { + log.error("获取所有状态失败", e); + return AjaxResult.error("获取所有状态失败: " + e.getMessage()); + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/BatchFunctionTask.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/BatchFunctionTask.java new file mode 100644 index 0000000..b91f1dd --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/BatchFunctionTask.java @@ -0,0 +1,182 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.ConcurrentHashSet; +import cn.hutool.core.thread.NamedThreadFactory; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant.DownCmd; +import cn.universal.common.domain.R; +import cn.universal.core.service.IoTDownlFactory; +import cn.universal.persistence.dto.IoTDeviceFunctionHistoryDTO; +import cn.universal.persistence.entity.IoTDeviceFunctionHistory; +import cn.universal.persistence.entity.IoTDeviceFunctionTask; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.mapper.IoTDeviceFunctionHistoryMapper; +import cn.universal.persistence.mapper.IoTDeviceFunctionTaskMapper; +import cn.universal.persistence.mapper.IoTProductMapper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tk.mybatis.mapper.entity.Example; + +/** + * @version 1.0 @Author Aleo + * @since 2025/3/21 + */ +@Component +@Slf4j +public class BatchFunctionTask { + + @Resource private IoTProductMapper ioTProductMapper; + @Resource private IoTDeviceFunctionTaskMapper ioTDeviceFunctionTaskMapper; + @Resource private IoTDeviceFunctionHistoryMapper ioTDeviceFunctionHistoryMapper; + + private AtomicBoolean scheduled = new AtomicBoolean(); + + private static ScheduledExecutorService executor = + Executors.newScheduledThreadPool(1, new NamedThreadFactory("function-batch-task-log", true)); + + private Set functionSet = new ConcurrentHashSet<>(); + + /** 定时 */ + @Profile({"prod", "dev", "out"}) + @Scheduled(cron = "0 0/10 * * * ? ") + public void doTask() { + if (functionSet.size() > 0) { + return; + } else { + IoTDeviceFunctionTask bo = new IoTDeviceFunctionTask(); + bo.setStatus(0); + IoTDeviceFunctionTask ioTDeviceFunctionTask = ioTDeviceFunctionTaskMapper.selectOneTask(bo); + if (ObjectUtil.isEmpty(ioTDeviceFunctionTask)) { + return; + } + log.info( + "开始批量功能下发,taskId={},任务名称={},执行指令={}", + ioTDeviceFunctionTask.getId(), + ioTDeviceFunctionTask.getTaskName(), + ioTDeviceFunctionTask.getCommand()); + Example ex2 = new Example(IoTDeviceFunctionHistory.class); + // 重新下发时不更改任务时间 + ioTDeviceFunctionTask.setBeginTime( + ioTDeviceFunctionTask.getBeginTime() != null + ? ioTDeviceFunctionTask.getBeginTime() + : new Date()); + ioTDeviceFunctionTask.setStatus(2); + ex2.createCriteria() + .andEqualTo("taskId", ioTDeviceFunctionTask.getId()) + .andEqualTo("downState", 0); + List histories = + ioTDeviceFunctionHistoryMapper.selectByExample(ex2); + List dtos = new ArrayList<>(); + if (CollectionUtil.isEmpty(histories)) { + ioTDeviceFunctionTask.setEndTime( + ioTDeviceFunctionTask.getEndTime() != null + ? ioTDeviceFunctionTask.getEndTime() + : new Date()); + ioTDeviceFunctionTask.setStatus(1); + } + ioTDeviceFunctionTaskMapper.updateByPrimaryKey(ioTDeviceFunctionTask); + IoTProduct productByProductKey = + ioTProductMapper.getProductByProductKey(ioTDeviceFunctionTask.getProductKey()); + String thirdPlatform = productByProductKey.getThirdPlatform(); + histories.forEach( + ioTDeviceFunctionHistory -> { + IoTDeviceFunctionHistoryDTO dto = + BeanUtil.copyProperties( + ioTDeviceFunctionHistory, IoTDeviceFunctionHistoryDTO.class); + JSONObject commandData = JSONUtil.parseObj(ioTDeviceFunctionTask.getCommandData()); + if (ObjectUtil.isNotEmpty(ioTDeviceFunctionHistory.getExtParam())) { + JSONObject data = commandData.getJSONObject("data"); + data.putAll(JSONUtil.parseObj(ioTDeviceFunctionHistory.getExtParam())); + commandData.set("data", data); + } + dto.setCommand(ioTDeviceFunctionTask.getCommand()); + dto.setCommandData(JSONUtil.toJsonStr(commandData)); + dto.setThirdPlatform(thirdPlatform); + dtos.add(dto); + }); + functionSet.addAll(dtos); + consumer(); + } + if (scheduled.compareAndSet(false, true)) { + executor.scheduleWithFixedDelay(this::consumer, 5, 5, TimeUnit.SECONDS); + } + } + + public void consumer() { + try { + if (!functionSet.isEmpty()) { + Iterator iterator = functionSet.iterator(); + if (iterator.hasNext()) { + IoTDeviceFunctionHistoryDTO history = iterator.next(); + iterator.remove(); + down(history); + // 最后一个设备指令时 修改任务完成状态 + if (!iterator.hasNext()) { + IoTDeviceFunctionTask task = new IoTDeviceFunctionTask(); + task.setId(history.getTaskId()); + IoTDeviceFunctionTask ioTDeviceFunctionTask = + ioTDeviceFunctionTaskMapper.selectOneTask(task); + if (ioTDeviceFunctionTask.getEndTime() == null) { + ioTDeviceFunctionTask.setEndTime(new Date()); + } + ioTDeviceFunctionTask.setStatus(1); + ioTDeviceFunctionTaskMapper.updateByPrimaryKey(ioTDeviceFunctionTask); + log.info( + "结束批量功能下发,taskId={},任务名称={},执行指令={}", + ioTDeviceFunctionTask.getId(), + ioTDeviceFunctionTask.getTaskName(), + ioTDeviceFunctionTask.getCommand()); + } + } + } + } catch (Exception e) { + log.error("批量功能下发异常", e); + } + } + + private void down(IoTDeviceFunctionHistoryDTO h) { + JSONObject param = new JSONObject(); + param.set("appUnionId", "innerBatchTask"); + param.set("productKey", h.getProductKey()); + param.set("deviceId", h.getDeviceId()); + param.set("cmd", DownCmd.DEV_FUNCTION.getValue()); + param.set("function", JSONUtil.parseObj(h.getCommandData())); + param.set("applicationId", "innerBatchTask"); + R r = IoTDownlFactory.getIDown(h.getThirdPlatform()).doAction(param); + IoTDeviceFunctionHistory history = BeanUtil.copyProperties(h, IoTDeviceFunctionHistory.class); + history.setDownState(2); + history.setDownResult(r.getCode() == 0 ? 1 : 0); + history.setDownError(r.getMsg()); + history.setRetry(history.getRetry() + 1); + history.setUpdateTime(new Date()); + ioTDeviceFunctionHistoryMapper.updateByPrimaryKeySelective(history); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/ICertificateService.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/ICertificateService.java new file mode 100644 index 0000000..e4f6255 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/ICertificateService.java @@ -0,0 +1,27 @@ +package cn.universal.admin.platform.service; + +import cn.universal.persistence.entity.IoTCertificate; +import java.util.List; + +public interface ICertificateService { + + IoTCertificate getBySslKey(String sslKey); + + List listAll(); + + void addCertificate(IoTCertificate cert); + + void updateCertificate(IoTCertificate cert); + + void deleteCertificate(Long id); + + List listByUser(String userId); + + List searchCertificates( + String name, + String userId, + String sslKey, + String expireStart, + String expireEnd, + boolean isAdmin); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDevLogService.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDevLogService.java new file mode 100644 index 0000000..2be4457 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDevLogService.java @@ -0,0 +1,41 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service; + +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.bo.IoTDeviceLogBO; +import cn.universal.persistence.entity.vo.IoTDeviceLogVO; +import cn.universal.persistence.query.LogQuery; +import java.util.List; + +/** + * 设备日志Service接口 + * + * @since 2025-12-30 + */ +public interface IIoTDevLogService { + + /** 分页查询设备日志列表 */ + List queryPageList(IoTDeviceLogBO bo); + + /** 根据日志id查询日志详情 */ + IoTDeviceLogVO queryById(LogQuery logQuery); + + /** + * 获取设备事件的统计信息 + * + * @param productId 产品Id + * @param iotId 设备id + */ + List queryEventTotal(String productId, String iotId); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceFunctionTaskService.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceFunctionTaskService.java new file mode 100644 index 0000000..dd4fdb4 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceFunctionTaskService.java @@ -0,0 +1,37 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service; + +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.bo.IoTDeviceFunctionHistoryBO; +import cn.universal.persistence.entity.bo.IoTDeviceFunctionTaskBO; +import cn.universal.persistence.entity.vo.IoTDeviceFunctionHistoryVO; +import cn.universal.persistence.entity.vo.IoTDeviceFunctionTaskVO; +import java.util.List; + +/** + * 设备Service接口 + * + * @since 2025-12-24 + */ +public interface IIoTDeviceFunctionTaskService { + + List selectTaskList(IoTDeviceFunctionTaskBO bo, IoTUser iotUser); + + List queryFunctionListByTaskId( + IoTDeviceFunctionHistoryBO bo, IoTUser iotUser); + + Boolean addFunctionTask(IoTDeviceFunctionTaskBO ioTDeviceFunctionTaskBO, IoTUser iotUser); + + Boolean retryFunctionTask(IoTDeviceFunctionTaskBO ioTDeviceFunctionTaskBO); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceGroupService.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceGroupService.java new file mode 100644 index 0000000..b978896 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceGroupService.java @@ -0,0 +1,72 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service; + +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.bo.IoTDeviceGroupBO; +import cn.universal.persistence.entity.vo.IoTDeviceGroupVO; +import cn.universal.persistence.query.AjaxResult; +import java.util.List; + +/** + * 设备分组Service接口 + * + * @since 2025-12-30 + */ +public interface IIoTDeviceGroupService { + + /** + * 查询设备分组 + * + * @param id 设备分组ID + * @return 设备分组 + */ + IoTDeviceGroupVO selectDevGroupById(Long id); + + /** + * 查询设备分组列表 + * + * @return 设备分组集合 + */ + List selectDevGroupList(); + + /** + * 新增设备分组 + * + * @param devGroup 设备分组 + * @return 结果 + */ + AjaxResult insertDevGroup(IoTDeviceGroupBO devGroup); + + /** + * 修改设备分组 + * + * @param devGroup 设备分组 + * @return 结果 + */ + int updateDevGroup(IoTDeviceGroupBO devGroup); + + /** + * 删除设备分组信息 + * + * @param id 设备分组ID + * @return 结果 + */ + AjaxResult deleteDevGroupById(Long id); + + AjaxResult bindDevToGroup(IoTDeviceGroupBO devGroup); + + List selectDevInstanceListByGroupId(String groupId); + + AjaxResult unBindDevByGroupId(String groupId, String[] devId); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceService.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceService.java new file mode 100644 index 0000000..0eeb243 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceService.java @@ -0,0 +1,155 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service; + +import cn.universal.common.domain.R; +import cn.universal.common.poi.ExcelTemplate; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTDeviceShadow; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.bo.IoTDeviceBO; +import cn.universal.persistence.entity.bo.IoTGwDeviceBO; +import cn.universal.persistence.entity.vo.GatewayDeviceVo; +import cn.universal.persistence.entity.vo.IoTDeviceCompanyVO; +import cn.universal.persistence.entity.vo.IoTDeviceCountVO; +import cn.universal.persistence.entity.vo.IoTDeviceModelVO; +import cn.universal.persistence.entity.vo.IoTDeviceVO; +import com.github.pagehelper.Page; +import java.util.List; + +/** + * 设备Service接口 + * + * @since 2025-12-24 + */ +public interface IIoTDeviceService { + + /** + * 查询设备分组 + * + * @param iotId iotId + */ + List selectDevInstanceGroupName(String iotId); + + /** + * 查询设备 + * + * @param id 设备主键 + * @return 设备 + */ + public IoTDevice selectDevInstanceById(String id); + + List countDevByApplication(String appUniqueId); + + /** + * 查询设备列表 + * + * @param ioTDevice 设备 + * @return 设备集合 + */ + public List selectDevInstanceList(IoTDevice ioTDevice, IoTUser iotUser); + + public List selectDevInstanceUnList(IoTDevice ioTDevice, IoTUser iotUser); + + /** 导出 */ + List selectDevInstanceForExport(IoTDevice ioTDevice, IoTUser iotUser); + + /** 设备批量导入 */ + public int importDevice(List devicesImport, IoTProduct ioTProduct, String deviceId); + + // 设备应用绑定 + int bindApp(String ids, String appUniqueId); + + /** + * 新增设备 + * + * @param devInstancebo 设备 + * @return 结果 + */ + public R insertDevInstance(IoTDeviceBO devInstancebo); + + /** + * 修改设备 + * + * @param devInstancebo 设备 + * @return 结果 + */ + public R updateDevInstance(IoTDeviceBO devInstancebo); + + /** + * 批量删除设备 + * + * @param ids 需要删除的设备主键集合 + * @return 结果 + */ + public R deleteDevInstanceByIds(String[] ids); + + /** + * 删除设备信息 + * + * @param id 设备主键 + * @return 结果 + */ + public R deleteDevInstanceById(String id); + + /** 根据应用id查找设备 */ + public int selectDevByAppUniqueId(String[] ids); + + /** + * 查询所以设备型号 + * + * @param supportChild 等于true 查询支持子设备的型号 + * @return 设备型号 + */ + List queryAllBySupportChild(Boolean supportChild, String apps); + + /** + * 查询所以设备型号 + * + * @return 设备影子 + */ + IoTDeviceShadow getDeviceShadow(String id); + + /** + * 设备下发 + * + * @param productKey 产品key + * @param downRequest 下发内容 + * @return 结果 + */ + public R iotFunctionsDown(String productKey, String downRequest); + + /** + * 根据产品key查型号配置 + * + * @param productKey 产品key + * @return 结果 + */ + public IoTDeviceModelVO getModelByProductKey(String productKey); + + List selectNotBindDevInstanceList( + String groupId, IoTDevice ioTDevice, IoTUser iotUser); + + Page selectFenceDevice(IoTDeviceBO ioTDeviceBO, int page, int size); + + ExcelTemplate importTemplateExcel(String productKey); + + List selectGwIotDevices(IoTGwDeviceBO bo); + + IoTDevice selectDeviceByDeviceId(String deviceId); + + List selectDevInstanceListWithTags(IoTDeviceBO ioTDeviceBO, IoTUser iotUser); + + List selectProductListInBatchFunction(Long applicationId, IoTUser iotUser); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTProductService.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTProductService.java new file mode 100644 index 0000000..aa2317f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTProductService.java @@ -0,0 +1,200 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service; + +import cn.universal.common.constant.IoTConstant.TcpFlushType; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.IoTDeviceFunction; +import cn.universal.persistence.entity.IoTDeviceProperties; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.bo.IoTProductBO; +import cn.universal.persistence.entity.bo.IoTProductImportBO; +import cn.universal.persistence.entity.vo.IoTDeviceModelVO; +import cn.universal.persistence.entity.vo.IoTProductExportVO; +import cn.universal.persistence.entity.vo.IoTProductVO; +import cn.universal.persistence.query.AjaxResult; +import cn.universal.persistence.query.IoTProductQuery; +import java.util.List; +import java.util.Map; + +/** + * 设备产品Service接口 + * + * @since 2025-12-24 + */ +public interface IIoTProductService { + + /** + * 查询设备产品 + * + * @param id 设备产品主键 + * @return 设备产品 + */ + IoTProductVO selectDevProductById(String id); + + /** + * 查询设备产品列表 + * + * @param ioTProduct 设备产品 + * @return 设备产品集合 + */ + List selectDevProductList(IoTProduct ioTProduct); + + /** + * 查询设备产品列表 + * + * @param ioTProductQuery 设备产品QU + * @return 设备产品集合 + */ + List selectDevProductV2List(IoTProductQuery ioTProductQuery); + + /** + * 查询设备产品列表V3,单表 + * + * @param ioTProductQuery 设备产品QU + * @return 设备产品集合 + */ + List selectDevProductV3List(IoTProductQuery ioTProductQuery); + + /** + * 查询设备产品列表V4,单表 + * + * @param ioTProductQuery 设备产品QU + * @return 设备产品集合 + */ + List selectDevProductV4List(IoTProductQuery ioTProductQuery); + + /** + * 搜索当前条件下产品的所有字段 + * + * @param ioTProductQuery 设备产品QU + * @return 设备产品集合 + */ + List selectAllDevProductV2List(IoTProductQuery ioTProductQuery); + + /** + * 新增设备产品 + * + * @param ioTProduct 设备产品 + * @return 结果 + */ + int insertDevProduct(IoTProduct ioTProduct); + + /** + * 新增设备产品 + * + * @param ioTProductList 设备产品列表 + * @return 结果 + */ + int insertList(List ioTProductList); + + /** 批量导入 */ + String importProduct(List ioTProductImportBOS, String unionId); + + /** + * 修改设备产品 + * + * @param ioTProduct 设备产品 + * @return 结果 + */ + int updateDevProduct(IoTProduct ioTProduct); + + /** + * 修改网络信息 + * + * @param id + * @param networkUnionId + * @return + */ + int updateNetworkUnionId(Long id, String networkUnionId); + + /** + * 批量删除设备产品 + * + * @param ids 需要删除的设备产品主键集合 + * @return 结果 + */ + int deleteDevProductByIds(String[] ids); + + /** + * 删除设备产品信息 + * + * @param id 设备产品主键 + * @return 结果 + */ + int deleteDevProductById(String id); + + /** 修改产品配置信息 */ + int updateDevProductConfig(IoTProductVO devProduct); + + /** + * 查询设备产品物模型属性列表 + * + * @return 设备产品物模型属性集合 + */ + List selectDevProperties(String Id); + + /** + * 查询设备产品物模型事件列表 + * + * @return 设备产品物模型事件集合 + */ + List selectDevEvents(String Id); + + /** + * 查询设备产品物模型方法列表 + * + * @return 设备产品物模型方法集合 + */ + List selectDevFunctions(String Id); + + /** + * 根据产品key查model配置 + * + * @param productKey 产品key + * @return 结果 + */ + IoTDeviceModelVO getModelByProductKey(String productKey); + + int insertMetadata(IoTProductBO ioTProductBO); + + int deleteMetadata(IoTProductBO ioTProductBO); + + int updateMetadata(IoTProductBO ioTProductBO); + + IoTProductBO getMetadata(IoTProductBO ioTProductBO); + + String selectMetadataByDevId(String devId); + + Map countDevNumberByProductKey(String unionId); + + AjaxResult selectDevProductByKey(String key); + + int updateDevProductOtherConfig(String otherConfig); + + int updateDevProductStoreConfig(IoTProductVO storeConfig); + + void flushNettyServer(String config, String productKey, TcpFlushType type); + + List selectDevProductAllList(); + + List getGatewayList(); + + List getGatewaySubDeviceList(String productKey); + + /** 绑定证书到产品(将sslKey和ssl=true写入configuration) */ + int bindCertificate(String productKey, String sslKey); + + /** 解绑证书(将ssl=false, sslKey设空) */ + int unbindCertificate(String productKey); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTProductSortService.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTProductSortService.java new file mode 100644 index 0000000..4b5e9c5 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTProductSortService.java @@ -0,0 +1,115 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service; + +import cn.universal.persistence.entity.IoTProductSort; +import cn.universal.persistence.entity.bo.RuleModelBO.IoTProductSortBO; +import java.util.List; + +/** + * 产品分类Service接口 @Author Aleo + * + * @since 2025-12-29 + */ +public interface IIoTProductSortService { + + /** + * 查询产品分类 + * + * @param id 产品分类主键 + * @return 产品分类 + */ + public IoTProductSort selectDevProductSortById(String id); + + /** + * 查询产品分类列表 + * + * @param ioTProductSort 产品分类 + * @return 产品分类集合 + */ + public List selectDevProductSortList(IoTProductSort ioTProductSort); + + /** + * 新增产品分类 + * + * @param ioTProductSort 产品分类 + * @return 结果 + */ + public int insertDevProductSort(IoTProductSort ioTProductSort); + + /** + * 修改产品分类 + * + * @param ioTProductSort 产品分类 + * @return 结果 + */ + public int updateDevProductSort(IoTProductSort ioTProductSort); + + /** + * 批量删除产品分类 + * + * @param ids 需要删除的产品分类主键集合 + * @return 结果 + */ + public int deleteDevProductSortByIds(String[] ids); + + /** + * 删除产品分类信息 + * + * @param id 产品分类主键 + * @return 结果 + */ + public int deleteDevProductSortById(String id); + + /** + * 构建前端所需要树结构 + * + * @param ioTProductSortList 菜单列表 + * @return 树结构列表 + */ + public List buildProductSortTree(List ioTProductSortList); + + // ==================== 新增的BO方法 ==================== + + /** + * 新增产品分类(使用BO) + * + * @param bo 产品分类业务对象 + * @return 结果 + */ + public int insertDevProductSort(IoTProductSortBO bo); + + /** + * 修改产品分类(使用BO) + * + * @param bo 产品分类业务对象 + * @return 结果 + */ + public int updateDevProductSort(IoTProductSortBO bo); + + /** + * 获取产品分类树结构 + * + * @return 树结构列表 + */ + public List getProductSortTree(); + + /** + * 构建前端所需要下拉树结构 + * + * @param devProductSortList 菜单列表 + * @return 下拉树结构列表 + */ + // public List buildProductSortTreeSelect(List devProductSortList); + +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/CertificateServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/CertificateServiceImpl.java new file mode 100644 index 0000000..e8b156a --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/CertificateServiceImpl.java @@ -0,0 +1,106 @@ +package cn.universal.admin.platform.service.impl; + +import cn.hutool.json.JSONObject; +import cn.universal.admin.platform.service.ICertificateService; +import cn.universal.common.utils.CertInfoUtil; +import cn.universal.persistence.entity.IoTCertificate; +import cn.universal.persistence.mapper.IoTCertificateMapper; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import tk.mybatis.mapper.entity.Example; + +@Service +public class CertificateServiceImpl implements ICertificateService { + + private static final Logger log = LoggerFactory.getLogger(CertificateServiceImpl.class); + + @Autowired private IoTCertificateMapper certificateMapper; + + @Override + public IoTCertificate getBySslKey(String sslKey) { + Example example = new Example(IoTCertificate.class); + example.createCriteria().andEqualTo("sslKey", sslKey); + return certificateMapper.selectOneByExample(example); + } + + @Override + public List listAll() { + return certificateMapper.selectAll(); + } + + @Override + public void addCertificate(IoTCertificate cert) { + fillCertInfo(cert); + certificateMapper.insertSelective(cert); + } + + @Override + public void updateCertificate(IoTCertificate cert) { + fillCertInfo(cert); + certificateMapper.updateByPrimaryKeySelective(cert); + } + + @Override + public void deleteCertificate(Long id) { + certificateMapper.deleteByPrimaryKey(id); + } + + @Override + public List listByUser(String userId) { + Example example = new Example(IoTCertificate.class); + example.createCriteria().andEqualTo("createUser", userId); + return certificateMapper.selectByExample(example); + } + + @Override + public List searchCertificates( + String name, + String userId, + String sslKey, + String expireStart, + String expireEnd, + boolean isAdmin) { + Example example = new Example(IoTCertificate.class); + Example.Criteria c = example.createCriteria(); + if (!isAdmin && userId != null) { + c.andEqualTo("createUser", userId); + } + if (name != null && !name.isEmpty()) { + c.andLike("name", "%" + name + "%"); + } + if (sslKey != null && !sslKey.isEmpty()) { + c.andEqualTo("sslKey", sslKey); + } + if (expireStart != null && !expireStart.isEmpty()) { + c.andGreaterThanOrEqualTo("expireTime", expireStart); + } + if (expireEnd != null && !expireEnd.isEmpty()) { + c.andLessThanOrEqualTo("expireTime", expireEnd); + } + example.orderBy("createTime").desc(); + return certificateMapper.selectByExample(example); + } + + /** 自动提取证书详细信息并填充到certInfo字段 */ + private void fillCertInfo(IoTCertificate cert) { + if (cert == null || cert.getCertContent() == null) { + return; + } + try { + JSONObject jsonObject = CertInfoUtil.extractCertInfo(cert.getCertContent()); + if (jsonObject != null) { + cert.setCertInfo(jsonObject.toString()); + if (jsonObject.getDate("notAfter") != null) { + cert.setExpireTime(jsonObject.getDate("notAfter")); + } + } else { + log.warn("证书详细信息提取失败: sslKey={}", cert.getSslKey()); + } + } catch (Exception e) { + log.warn("证书详细信息提取异常: sslKey={}, err={}", cert.getSslKey(), e.getMessage()); + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDevLogServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDevLogServiceImpl.java new file mode 100644 index 0000000..635a194 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDevLogServiceImpl.java @@ -0,0 +1,71 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service.impl; + +import cn.universal.admin.platform.service.IIoTDevLogService; +import cn.universal.admin.platform.service.IIoTProductService; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.bo.IoTDeviceLogBO; +import cn.universal.persistence.entity.vo.IoTDeviceLogVO; +import cn.universal.persistence.mapper.IoTDeviceLogMapper; +import cn.universal.persistence.query.LogQuery; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 设备日志Service业务层处理 + * + * @since 2025-12-30 + */ +@Service +public class IoTDevLogServiceImpl implements IIoTDevLogService { + + @Autowired private IoTDeviceLogMapper ioTDeviceLogMapper; + + @Autowired private IIoTProductService devProductService; + + /** 分页查询设备日志列表 */ + @Override + public List queryPageList(IoTDeviceLogBO bo) { + return ioTDeviceLogMapper.queryLogPageList(bo); + } + + /** 根据日志id查询日志详情 */ + @Override + public IoTDeviceLogVO queryById(LogQuery logQuery) { + IoTDeviceLogVO ioTDeviceLogVO = ioTDeviceLogMapper.queryLogById(logQuery); + // IoTDeviceLogVO ioTDeviceLogVO = BeanUtil.toBean(devLog, IoTDeviceLogVO.class); + return ioTDeviceLogVO; + } + + /** + * 获取设备事件的统计信息 + * + * @param productId 产品Id + * @param iotId 设备id + */ + @Override + public List queryEventTotal(String productId, String iotId) { + List list = devProductService.selectDevEvents(productId); + for (IoTDeviceEvents devEvent : list) { + List events = ioTDeviceLogMapper.queryEventTotalByEventAndId(devEvent.getId(), iotId); + int size = events.size(); + if (size > 0) { + devEvent.setTime(events.get(0)); + devEvent.setQty(size >= 100 ? "99+" : String.valueOf(size)); + } + } + return list; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceFunctionTaskServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceFunctionTaskServiceImpl.java new file mode 100644 index 0000000..c4d9229 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceFunctionTaskServiceImpl.java @@ -0,0 +1,164 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.service.BaseServiceImpl; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.platform.service.BatchFunctionTask; +import cn.universal.admin.platform.service.IIoTDeviceFunctionTaskService; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTDeviceFunctionHistory; +import cn.universal.persistence.entity.IoTDeviceFunctionTask; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.bo.IoTDeviceFunctionHistoryBO; +import cn.universal.persistence.entity.bo.IoTDeviceFunctionTaskBO; +import cn.universal.persistence.entity.vo.IoTDeviceFunctionHistoryVO; +import cn.universal.persistence.entity.vo.IoTDeviceFunctionTaskVO; +import cn.universal.persistence.mapper.IoTDeviceFunctionHistoryMapper; +import cn.universal.persistence.mapper.IoTDeviceFunctionTaskMapper; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 设备Service业务层处理 + * + * @since 2025-12-24 + */ +@Service +@Slf4j +public class IoTDeviceFunctionTaskServiceImpl extends BaseServiceImpl + implements IIoTDeviceFunctionTaskService { + + @Resource private IoTDeviceFunctionTaskMapper ioTDeviceFunctionTaskMapper; + @Resource private IoTDeviceMapper ioTDeviceMapper; + + @Resource private IoTDeviceFunctionHistoryMapper ioTDeviceFunctionHistoryMapper; + @Resource private BatchFunctionTask batchFunctionTask; + + @Override + public List selectTaskList(IoTDeviceFunctionTaskBO bo, IoTUser iotUser) { + return ioTDeviceFunctionTaskMapper.selectTaskList( + bo, iotUser.isAdmin() ? null : iotUser.getUnionId()); + } + + @Override + public List queryFunctionListByTaskId( + IoTDeviceFunctionHistoryBO bo, IoTUser iotUser) { + return ioTDeviceFunctionTaskMapper.queryFunctionListByTaskId( + bo, iotUser.isAdmin() ? null : iotUser.getUnionId()); + } + + @Override + public Boolean addFunctionTask(IoTDeviceFunctionTaskBO ioTDeviceFunctionTaskBO, IoTUser iotUser) { + List ioTDevices; + // 全选 + if (ioTDeviceFunctionTaskBO.getChooseAll()) { + IoTDevice param = new IoTDevice(); + param.setApplication(ioTDeviceFunctionTaskBO.getApplicationId()); + param.setProductKey(ioTDeviceFunctionTaskBO.getProductKey()); + ioTDevices = + ioTDeviceMapper.selectDevInstanceList( + param, iotUser.isAdmin() ? null : iotUser.getUnionId()); + if (ioTDeviceFunctionTaskBO.getRemoveIds() != null + && ioTDeviceFunctionTaskBO.getRemoveIds().length > 0) { + Map collect = + Arrays.stream(ioTDeviceFunctionTaskBO.getRemoveIds()) + .collect(Collectors.toMap(String::valueOf, Function.identity())); + ioTDevices = + ioTDevices.stream() + .filter( + devInstance -> { + if (collect.containsKey(devInstance.getId().toString())) { + return false; + } + return true; + }) + .collect(Collectors.toList()); + } + } else { + ioTDevices = ioTDeviceMapper.listByIds(ioTDeviceFunctionTaskBO.getIds()); + } + if (CollectionUtil.isEmpty(ioTDevices)) { + throw new IoTException("无匹配设备"); + } + // 保存任务 + IoTDeviceFunctionTask ioTDeviceFunctionTask = new IoTDeviceFunctionTask(); + ioTDeviceFunctionTask.setTaskName(ioTDeviceFunctionTaskBO.getTaskName()); + ioTDeviceFunctionTask.setCreator(SecurityUtils.getUnionId()); + ioTDeviceFunctionTask.setCreatorId(SecurityUtils.getUnionId()); + ioTDeviceFunctionTask.setProductKey(ioTDeviceFunctionTaskBO.getProductKey()); + ioTDeviceFunctionTask.setCommand(ioTDeviceFunctionTaskBO.getCommand()); + ioTDeviceFunctionTask.setCommandData(ioTDeviceFunctionTaskBO.getCommandData()); + ioTDeviceFunctionTask.setStatus(0); + ioTDeviceFunctionTaskMapper.insert(ioTDeviceFunctionTask); + // 保存初始指令记录 + List histories = new ArrayList<>(); + ioTDevices.forEach( + devInstance -> { + IoTDeviceFunctionHistory h = new IoTDeviceFunctionHistory(); + h.setId(IdUtil.getSnowflake().nextId()); + h.setDeviceId(devInstance.getDeviceId()); + h.setDeviceName(devInstance.getDeviceName()); + h.setTaskId(ioTDeviceFunctionTask.getId()); + h.setProductKey(devInstance.getProductKey()); + h.setIotId(devInstance.getIotId()); + h.setDownState(0); + h.setRetry(0); + // 额外字段 如表号等 + if (CollectionUtil.isNotEmpty(ioTDeviceFunctionTaskBO.getExtParam())) { + JSONObject config = JSONUtil.parseObj(devInstance.getConfiguration()); + JSONObject extParam = new JSONObject(); + ioTDeviceFunctionTaskBO + .getExtParam() + .forEach( + s -> { + if (config.containsKey(s)) { + extParam.set(s, config.getStr(s)); + } + }); + h.setExtParam(JSONUtil.toJsonStr(extParam)); + } + histories.add(h); + }); + ioTDeviceFunctionHistoryMapper.batchInsert(histories); + batchFunctionTask.doTask(); + return true; + } + + @Override + public Boolean retryFunctionTask(IoTDeviceFunctionTaskBO ioTDeviceFunctionTaskBO) { + int row = ioTDeviceFunctionTaskMapper.retryTask(ioTDeviceFunctionTaskBO.getTaskId()); + if (row == 0) { + return false; + } + int row2 = ioTDeviceFunctionHistoryMapper.retryHistory(ioTDeviceFunctionTaskBO); + if (row2 == 0) { + return false; + } + batchFunctionTask.doTask(); + return true; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceGroupServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceGroupServiceImpl.java new file mode 100644 index 0000000..c922b2a --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceGroupServiceImpl.java @@ -0,0 +1,323 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.universal.admin.common.service.BaseServiceImpl; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.platform.service.IIoTDeviceGroupService; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTDeviceGroup; +import cn.universal.persistence.entity.IoTDeviceTags; +import cn.universal.persistence.entity.bo.IoTDeviceGroupBO; +import cn.universal.persistence.entity.vo.IoTDeviceGroupVO; +import cn.universal.persistence.mapper.IoTDeviceGroupMapper; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.IoTDeviceTagsMapper; +import cn.universal.persistence.query.AjaxResult; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** 设备分组 */ +@Service +@Slf4j +public class IoTDeviceGroupServiceImpl extends BaseServiceImpl implements IIoTDeviceGroupService { + + @Resource private IoTDeviceGroupMapper ioTDeviceGroupMapper; + + @Resource private IoTDeviceTagsMapper ioTDeviceTagsMapper; + + @Resource private IoTDeviceMapper ioTDeviceMapper; + + /** + * 查询设备分组 + * + * @param id 设备分组ID + * @return 设备分组 + */ + @Override + public IoTDeviceGroupVO selectDevGroupById(Long id) { + IoTDeviceGroup ioTDeviceGroup = ioTDeviceGroupMapper.selectDevGroupById(id); + IoTDeviceGroupVO groupVo = BeanUtil.toBean(ioTDeviceGroup, IoTDeviceGroupVO.class); + // to do 获取当前分组下的设备列表 + return groupVo; + } + + /** + * 查询设备分组列表 + * + * @return 设备分组 + */ + @Override + public List selectDevGroupList() { + // 查询所有设备分组 + List ioTDeviceGroupVOS = + ioTDeviceGroupMapper.selectDevGroupList( + queryIotUser(SecurityUtils.getUnionId()).getUnionId()); + // 按照父id分组 + Map> collect = + ioTDeviceGroupVOS.stream().collect(Collectors.groupingBy(IoTDeviceGroupVO::getParentId)); + // 创建根节点 + ArrayList groupVos = new ArrayList<>(); + IoTDeviceGroupVO ioTDeviceGroupVO = + IoTDeviceGroupVO.builder().id(0L).groupName("所有群组").groupLevel(0).build(); + groupVos.add(ioTDeviceGroupVO); + // 生成设备分组树 + List roots = collect.get(0L); + ioTDeviceGroupVO.setChildren(roots); + buildGroupTree(roots, collect); + return groupVos; + } + + /** + * 新增设备分组 + * + * @param devGroup 设备分组 + * @return 结果 + */ + @Override + @Transactional + public AjaxResult insertDevGroup(IoTDeviceGroupBO devGroup) { + // 父节点为0 + if (devGroup.getParentId() == 0) { + IoTDeviceGroup group = BeanUtil.toBean(devGroup, IoTDeviceGroup.class); + // 是否有子节点 + group.setHasChild(0); + // 层级 + group.setGroupLevel(1); + // 创建者 + group.setCreatorId(queryIotUser(SecurityUtils.getUnionId()).getUnionId()); + // 添加 + int result = ioTDeviceGroupMapper.insert(group); + if (result >= 1) { + return AjaxResult.success(); + } + return AjaxResult.error("添加失败"); + } + // 获取父亲id + IoTDeviceGroup parentGroup = ioTDeviceGroupMapper.selectDevGroupById(devGroup.getParentId()); + if (parentGroup == null) { + return AjaxResult.error("未获取到父群组"); + } + if (parentGroup.getGroupLevel() >= 5) { + return AjaxResult.error("群组层级已到达上限"); + } + IoTDeviceGroup group = BeanUtil.toBean(devGroup, IoTDeviceGroup.class); + // 是否有子节点 + group.setHasChild(0); + // 层级 + group.setGroupLevel(parentGroup.getGroupLevel() + 1); + // 创建者 + group.setCreatorId(queryIotUser(SecurityUtils.getUnionId()).getUnionId()); + // 添加 + int result = ioTDeviceGroupMapper.insert(group); + if (result >= 1) { + // 更新父节点 + if (parentGroup.getHasChild() == 0) { + parentGroup.setHasChild(1); + ioTDeviceGroupMapper.updateDevGroup(parentGroup); + } + return AjaxResult.success(); + } + return AjaxResult.error("添加失败"); + } + + /** + * 修改设备分组 + * + * @param devGroup 设备分组 + * @return 结果 + */ + @Override + public int updateDevGroup(IoTDeviceGroupBO devGroup) { + IoTDeviceGroup group = BeanUtil.toBean(devGroup, IoTDeviceGroup.class); + return ioTDeviceGroupMapper.updateDevGroup(group); + } + + /** + * 删除设备分组信息 + * + * @param id 设备分组ID + * @return 结果 + */ + @Override + @Transactional + public AjaxResult deleteDevGroupById(Long id) { + // 查询分组的子集 + int total = ioTDeviceGroupMapper.queryChildrenCountById(id); + if (total >= 1) { + return AjaxResult.error("存在子分组,无法删除"); + } + // 分组当前有设备无法被删除 + int devCount = ioTDeviceTagsMapper.selectDevIds(String.valueOf(id)); + if (devCount > 0) { + return AjaxResult.error("分组下绑定有设备,无法删除"); + } + // 获取当前分组实例 + IoTDeviceGroup ioTDeviceGroup = ioTDeviceGroupMapper.selectDevGroupById(id); + // 删除当前分组 + int result = ioTDeviceGroupMapper.deleteDevGroupById(id); + if (result >= 1) { + // 产出当前分组 + int count = ioTDeviceGroupMapper.queryChildrenCountById(ioTDeviceGroup.getParentId()); + // 清除分组中已删除的设备 + ioTDeviceTagsMapper.deleteByValueId(String.valueOf(id)); + // 如果父子集为0,更改状态 + if (count == 0) { + ioTDeviceGroupMapper.updateDevGroup( + IoTDeviceGroup.builder().id(ioTDeviceGroup.getParentId()).hasChild(0).build()); + } + return AjaxResult.success(); + } + return AjaxResult.error("添加失败"); + } + + /** + * 绑定设备到分组 + * + * @param devGroup + * @return + */ + @Override + public AjaxResult bindDevToGroup(IoTDeviceGroupBO devGroup) { + // 判断分组是否存在 + Long groupId = devGroup.getId(); + if (groupId == null) { + return AjaxResult.error("分组序号不能为空"); + } + IoTDeviceGroup group = ioTDeviceGroupMapper.selectDevGroupById(groupId); + if (group == null) { + return AjaxResult.error("要绑定的分组不存在"); + } + // 获取设备id集合; 清除已经添加到此分组的设备id + ArrayList devIdsNewOne = new ArrayList<>(); + ArrayList ioTDeviceTags = new ArrayList<>(); + StringBuffer successMsg = new StringBuffer(); + StringBuffer errorMsg = new StringBuffer(); + errorMsg.append("设备绑定分组数超过10个,"); + successMsg.append("绑定成功; 设备绑定分组数超过10个,"); + List oldIotIds = Arrays.asList(devGroup.getDevIds()); + for (String iotId : oldIotIds) { + IoTDeviceTags one = + ioTDeviceTagsMapper.selectOne( + IoTDeviceTags.builder().iotId(iotId).value(String.valueOf(groupId)).build()); + if (one == null) { + devIdsNewOne.add(iotId); + } + } + if (devIdsNewOne.size() == 0) { + return AjaxResult.success("设备已绑定在该分组下"); + } + // 清除超过或等于10个分组的设备 + for (String iotId : devIdsNewOne) { + int count = ioTDeviceGroupMapper.queryDevCountBindGroup(iotId, SecurityUtils.getUnionId()); + HashMap map = new HashMap<>(); + map.put("iot_id", iotId); + IoTDeviceDTO deviceDTO = ioTDeviceMapper.selectIoTDeviceBO(map); + if (deviceDTO == null) { + log.warn("设备不存在,跳过编组"); + continue; + } + if (count < 10) { + ioTDeviceTags.add( + IoTDeviceTags.builder() + .iotId(iotId) + .name(group.getGroupName()) + .productKey(deviceDTO.getProductKey()) + .deviceId(deviceDTO.getDeviceId()) + .type("devGroup") + .key("devGroup") + .value(String.valueOf(groupId)) + .build()); + } else { + successMsg.append(deviceDTO.getDeviceName() + "、"); + errorMsg.append(deviceDTO.getDeviceName() + "、"); + } + } + if (ioTDeviceTags.size() == 0) { + errorMsg.append("设备无法被绑定"); + return AjaxResult.error(errorMsg.toString()); + } + // 添加 + int i = ioTDeviceTagsMapper.insertList(ioTDeviceTags); + if (i >= 1) { + if (devIdsNewOne.size() == ioTDeviceTags.size()) { + return AjaxResult.success("绑定成功"); + } else { + successMsg.append("设备无法被绑定"); + return AjaxResult.success(successMsg.toString()); + } + } + + return AjaxResult.error("绑定失败"); + } + + /** + * 根据分组id查询分组下的所有设备 + * + * @param groupId + * @return + */ + @Override + public List selectDevInstanceListByGroupId(String groupId) { + List ioTDevices = ioTDeviceMapper.selectDevListByIds(groupId); + return ioTDevices; + } + + /** + * 设备解绑 + * + * @param groupId 分组id + * @param devId 设备id + * @return + */ + @Override + @Transactional + public AjaxResult unBindDevByGroupId(String groupId, String[] devId) { + for (int i = 0; i < devId.length; i++) { + ioTDeviceTagsMapper.delete(IoTDeviceTags.builder().iotId(devId[i]).value(groupId).build()); + } + return AjaxResult.success(); + } + + /** + * 生成设备分组树 + * + * @param ioTDeviceGroupVO + * @param groups + * @return + */ + private void buildGroupTree( + List ioTDeviceGroupVO, Map> groups) { + if (ioTDeviceGroupVO == null) { + return; + } + for (int i = 0; i < ioTDeviceGroupVO.size(); i++) { + if (ioTDeviceGroupVO.get(i).getHasChild() == 1) { + List devGroups = groups.get(ioTDeviceGroupVO.get(i).getId()); + ioTDeviceGroupVO.get(i).setChildren(devGroups); + buildGroupTree(devGroups, groups); + } + } + return; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceServiceImpl.java new file mode 100644 index 0000000..b907e9d --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceServiceImpl.java @@ -0,0 +1,543 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.service.BaseServiceImpl; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.platform.service.IIoTDeviceService; +import cn.universal.common.constant.IoTConstant.DeviceNode; +import cn.universal.common.constant.IoTConstant.DownCmd; +import cn.universal.common.domain.R; +import cn.universal.common.exception.BaseException; +import cn.universal.common.exception.IoTException; +import cn.universal.common.poi.ExcelTemplate; +import cn.universal.core.service.IoTDownlFactory; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTDeviceShadow; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.bo.IoTDeviceBO; +import cn.universal.persistence.entity.bo.IoTGwDeviceBO; +import cn.universal.persistence.entity.vo.GatewayDeviceVo; +import cn.universal.persistence.entity.vo.IoTDeviceCompanyVO; +import cn.universal.persistence.entity.vo.IoTDeviceCountVO; +import cn.universal.persistence.entity.vo.IoTDeviceModelVO; +import cn.universal.persistence.entity.vo.IoTDeviceVO; +import cn.universal.persistence.mapper.IoTDeviceFenceRelMapper; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.IoTDeviceProtocolMapper; +import cn.universal.persistence.mapper.IoTDeviceShadowMapper; +import cn.universal.persistence.mapper.IoTDeviceTagsMapper; +import cn.universal.persistence.mapper.IoTProductMapper; +import cn.universal.persistence.query.IoTDeviceQuery; +import com.github.pagehelper.Page; +import com.github.pagehelper.PageHelper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.map.LinkedMap; +import org.apache.commons.lang.StringUtils; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.BeanUtils; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +/** + * 设备Service业务层处理 + * + * @since 2025-12-24 + */ +@Service +@Slf4j +public class IoTDeviceServiceImpl extends BaseServiceImpl implements IIoTDeviceService { + + @Resource private IoTDeviceMapper ioTDeviceMapper; + @Resource private IoTProductMapper ioTProductMapper; + + @Resource private IoTDeviceProtocolMapper ioTDeviceProtocolMapper; + + @Resource private IoTDeviceShadowMapper ioTDeviceShadowMapper; + + @Resource private IoTDeviceTagsMapper ioTDeviceTagsMapper; + + @Resource private IoTDeviceFenceRelMapper ioTDeviceFenceRelMapper; + + @Override + @Cacheable(cacheNames = "iot_dev_tag_group_name", key = "#iotId") + public List selectDevInstanceGroupName(String iotId) { + return ioTDeviceTagsMapper.selectDevGroupName(iotId); + } + + /** + * 查询设备 + * + * @param id 设备主键 + * @return 设备 + */ + @Override + public IoTDevice selectDevInstanceById(String id) { + return checkSelf(id); + } + + public IoTDevice checkSelf(String id) { + IoTUser iotUser = queryIotUser(SecurityUtils.getUnionId()); + IoTDevice ioTDevice = ioTDeviceMapper.selectDevInstanceById(id); + if (Objects.isNull(ioTDevice)) { + throw new IoTException("该设备不存在", 400); + } + if (!iotUser.isAdmin() && !ioTDevice.getCreatorId().equals(iotUser.getUnionId())) { + throw new IoTException("你无权操作"); + } + + if (ioTDevice.getCoordinate() != null) { + ioTDevice.getParams().put("locationStr", ioTDevice.getCoordinate()); + } + + return ioTDevice; + } + + /** 设备归属判断 */ + public String ascription(String creatorId) { + IoTUser iotUser = queryIotUser(SecurityUtils.getUnionId()); + if (!creatorId.equals(iotUser.getUnionId()) && !iotUser.isAdmin()) { + throw new IoTException("您没有权限操作此设备!"); + } + return iotUser.getUnionId(); + } + + /** + * 查询每个应用的设备数量 + * + * @param appUniqueId 应用唯一标识 + */ + @Override + public List countDevByApplication(String appUniqueId) { + return ioTDeviceMapper.countDevByApplication(appUniqueId); + } + + /** + * 查询设备列表 + * + * @param ioTDevice 设备 + * @return 设备 + */ + @Override + // @DataScope(alias = "u", created = "d.creator_id") + public List selectDevInstanceList(IoTDevice ioTDevice, IoTUser iotUser) { + return ioTDeviceMapper.selectDevInstanceList( + ioTDevice, iotUser.isAdmin() ? null : iotUser.getUnionId()); + } + + /** + * 查询设备列表 + * + * @param ioTDevice 设备 + * @return 设备 + */ + @Override + // @DataScope(alias = "u", created = "d.creator_id") + public List selectDevInstanceUnList(IoTDevice ioTDevice, IoTUser iotUser) { + return ioTDeviceMapper.selectDevInstanceUnList(ioTDevice, iotUser.getUnionId()); + } + + @Override + public List selectDevInstanceForExport(IoTDevice ioTDevice, IoTUser iotUser) { + return ioTDeviceMapper.selectDevInstanceForExport( + ioTDevice, iotUser.isAdmin() ? null : iotUser.getUnionId()); + } + + @Override + public ExcelTemplate importTemplateExcel(String productKey) { + // 根据produceKey获取产品 + IoTProduct ioTProduct = ioTProductMapper.getProductByProductKey(productKey); + if (Objects.isNull(ioTProduct)) { + throw new BaseException("没有找到产品"); + } + JSONObject obj = JSONUtil.parseObj(ioTProduct.getThirdConfiguration()); + + Map headers = new LinkedMap<>(); + List demoRow = new ArrayList<>(); + + headers.put("deviceId", "设备序列号"); + demoRow.add("310000000000000"); + + headers.put("deviceName", "设备名称"); + demoRow.add("模拟设备"); + + headers.put("longitude", "经度"); + demoRow.add("120.291842"); + + headers.put("latitude", "纬度"); + demoRow.add("30.448014"); + + // headers.put("coordinate", "设备坐标"); + // demoRow.add("120.291842,30.448014"); + + headers.put("detail", "备注"); + demoRow.add("请把此行删除"); + + if (obj.containsKey("customField")) { + JSONArray customFieldArray = obj.getJSONArray("customField"); + if (CollectionUtil.isNotEmpty(customFieldArray)) { + // 获取其他属性 + for (int i = 1; i <= customFieldArray.size(); i++) { + String customFieldValueOfId = getCustomFieldBykey(customFieldArray, "id", i - 1); + headers.put( + "otherParams." + customFieldValueOfId, + obj.get("ext" + i + "Label").toString().replace("\"", "")); + } + } + } + if (ioTProduct.getDeviceNode().equals(DeviceNode.GATEWAY_SUB_DEVICE.name())) { + headers.put("gwProductKey", "网关产品ProductKey"); + demoRow.add("a900c81a4d544849a2b92a02b3548f16"); + } + return new ExcelTemplate(headers, demoRow, "设备导入"); + } + + @Override + public List selectGwIotDevices(IoTGwDeviceBO ioTGwDeviceBO) { + IoTProduct subDeviceProduct = + ioTProductMapper.getProductByProductKey(ioTGwDeviceBO.getProductKey()); + if (ObjectUtil.isNull(subDeviceProduct) + || StrUtil.isBlank(subDeviceProduct.getGwProductKey())) { + throw new IoTException("请先完成网关绑定"); + } + ioTGwDeviceBO.setGwProductKey(subDeviceProduct.getGwProductKey()); + return ioTDeviceMapper.getGatewayDeviceList(ioTGwDeviceBO); + } + + @Override + public IoTDevice selectDeviceByDeviceId(String deviceId) { + return ioTDeviceMapper.getOneByDeviceId(IoTDeviceQuery.builder().deviceId(deviceId).build()); + } + + @Override + public List selectDevInstanceListWithTags(IoTDeviceBO ioTDeviceBO, IoTUser iotUser) { + return ioTDeviceMapper.selectDevInstanceListWithTags( + ioTDeviceBO, iotUser.isAdmin() ? null : iotUser.getUnionId()); + } + + @Override + public List selectProductListInBatchFunction(Long applicationId, IoTUser iotUser) { + return ioTDeviceMapper.selectProductListInBatchFunction( + applicationId, iotUser.isAdmin() ? null : iotUser.getUnionId()); + } + + /** + * @param jsonArray -> customField数组 + * @param index key: id 、 name 、description + */ + public String getCustomFieldBykey(JSONArray jsonArray, String key, int index) { + return JSONUtil.parseObj(jsonArray.get(index)).get(key).toString().replace("\"", ""); + } + + // 导入设备 + @Override + public int importDevice(List devicesImport, IoTProduct ioTProduct, String deviceId) { + validImportDevice(devicesImport); + int successImportNum = 0; + List errorMsgs = new ArrayList<>(); + + for (int i = 0; i < devicesImport.size(); i++) { + try { + IoTDevice instance = devicesImport.get(i); + validImportDevice(instance, ioTProduct); + IoTDeviceBO ioTDeviceBO = new IoTDeviceBO(); + BeanUtils.copyProperties(instance, ioTDeviceBO); + ioTDeviceBO.setProductKey(ioTProduct.getProductKey()); + if (ioTDeviceBO.getOtherParams().containsKey("deviceKey")) { + ioTDeviceBO.setSecretKey(ioTDeviceBO.getOtherParams().get("deviceKey").toString()); + } + if (cn.universal.common.utils.StringUtils.isEmpty(ioTDeviceBO.getLatitude())) { + ioTDeviceBO.setLatitude("30.4484801"); + } + if (cn.universal.common.utils.StringUtils.isEmpty(ioTDeviceBO.getLongitude())) { + ioTDeviceBO.setLongitude("120.2922654"); + } + if (StrUtil.isNotBlank(deviceId)) { + ioTDeviceBO.setGwProductKey(deviceId); + } + ioTDeviceBO.setCoordinate(ioTDeviceBO.getLongitude() + "," + ioTDeviceBO.getLatitude()); + R r = ((IIoTDeviceService) AopContext.currentProxy()).insertDevInstance(ioTDeviceBO); + if (r.getMsg().equals("success") && r.getCode().equals(0)) { + successImportNum++; + } else { + throw new BaseException(r.getMsg()); + } + } catch (Exception e) { + errorMsgs.add(String.format("第%s行导入报错,原因【%s】", i + 1, e.getMessage())); + } + } + + if (CollectionUtil.isNotEmpty(errorMsgs)) { + throw new BaseException(StrUtil.join(",\n", errorMsgs)); + } + return successImportNum; + } + + private void validImportDevice(List devicesImport) { + if (CollectionUtils.isEmpty(devicesImport)) { + throw new BaseException("表格数据为空"); + } + if (devicesImport.size() > 500) { + throw new BaseException("超过一次最大导入数量500"); + } + } + + private void validImportDevice(IoTDevice device, IoTProduct ioTProduct) { + + JSONObject jsonObject = JSONUtil.parseObj(ioTProduct.getThirdConfiguration()); + + if (StringUtils.isEmpty(device.getDeviceName())) { + throw new BaseException("设备名称不能为空"); + } + if (StringUtils.isEmpty(device.getDeviceId())) { + throw new BaseException("设备序列号不能为空"); + } + if (jsonObject.containsKey("customField")) { + // 获取其他属性 + JSONArray customFieldArray = jsonObject.getJSONArray("customField"); + if (CollectionUtil.isNotEmpty(customFieldArray)) { + for (int j = 0; j < customFieldArray.size(); j++) { + String customFieldValueOfId = getCustomFieldBykey(customFieldArray, "id", j); + String customFieldValueOfName = getCustomFieldBykey(customFieldArray, "name", j); + Object obj = device.getOtherParams().get(customFieldValueOfId); + if (Objects.isNull(obj) || Objects.toString(obj).trim().isEmpty()) { + throw new BaseException(customFieldValueOfName + "不能为空"); + } + } + } + } + } + + @Override + public int bindApp(String ids, String appUniqueId) { + String[] a = ids.split(","); + return ioTDeviceMapper.bindApp(a, appUniqueId); + } + + /** + * 新增设备 + * + * @param devInstancebo 设备 + * @return 结果 + */ + @Override + public R insertDevInstance(IoTDeviceBO devInstancebo) { + JSONObject downRequest = new JSONObject(); + + // parentUnionId不为空时 认为该账号是子账号,所有操作的创建者均设为父账号 + downRequest.set("appUnionId", queryIotUser(SecurityUtils.getUnionId()).getUnionId()); + // 产品编号 + downRequest.set("productKey", devInstancebo.getProductKey()); + // 设备序列号 + downRequest.set("deviceId", devInstancebo.getDeviceId()); + // Lora密匙 + downRequest.set("deviceKey", devInstancebo.getSecretKey()); + // 添加 + downRequest.set("cmd", DownCmd.DEV_ADD.name()); + // 说明 + downRequest.set("detail", devInstancebo.getDetail()); + // 网关产品ProductKey + downRequest.set("gwProductKey", devInstancebo.getGwProductKey()); + downRequest.set("gwDeviceId", devInstancebo.getExtDeviceId()); + downRequest.set("extDeviceId", devInstancebo.getExtDeviceId()); + JSONObject ob = new JSONObject(); + // 设备实例名称 + ob.set("deviceName", devInstancebo.getDeviceName()); + // 设备序列号 + ob.set("imei", devInstancebo.getDeviceId()); + // 维度 + ob.set("latitude", devInstancebo.getLatitude()); + // 经度 + ob.set("longitude", devInstancebo.getLongitude()); + // 添加额外参数 + for (Map.Entry entry : devInstancebo.getOtherParams().entrySet()) { + ob.set(entry.getKey(), entry.getValue()); + } + downRequest.set("data", ob); + IoTProduct ioTProduct = ioTProductMapper.getProductByProductKey(devInstancebo.getProductKey()); + return IoTDownlFactory.getIDown(ioTProduct.getThirdPlatform()).doAction(downRequest); + } + + /** + * 修改设备 + * + * @param devInstancebo 设备 + * @return 结果 + */ + @Override + public R updateDevInstance(IoTDeviceBO devInstancebo) { + IoTDevice ioTDevice = + IoTDevice.builder() + .productKey(devInstancebo.getProductKey()) + .deviceId(devInstancebo.getDeviceId()) + .build(); + String creatorId = ioTDeviceMapper.selectOne(ioTDevice).getCreatorId(); + String appUnionId = ascription(creatorId); + JSONObject downRequest = new JSONObject(); + downRequest.set("appUnionId", appUnionId); + downRequest.set("productKey", devInstancebo.getProductKey()); + downRequest.set("deviceId", devInstancebo.getDeviceId()); + downRequest.set("detail", devInstancebo.getDetail()); + downRequest.set("cmd", DownCmd.DEV_UPDATE.name()); + JSONObject ob = new JSONObject(); + ob.set("deviceName", devInstancebo.getDeviceName()); + ob.set("latitude", devInstancebo.getLatitude()); + ob.set("longitude", devInstancebo.getLongitude()); + downRequest.set("data", ob); + IoTProduct ioTProduct = ioTProductMapper.getProductByProductKey(devInstancebo.getProductKey()); + return IoTDownlFactory.getIDown(ioTProduct.getThirdPlatform()).doAction(downRequest); + } + + /** + * 批量删除设备 + * + * @param ids 需要删除的设备主键 + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list"}, + allEntries = true) + public R deleteDevInstanceByIds(String[] ids) { + R result = new R(); + for (String id : ids) { + IoTDevice ioTDevice = ioTDeviceMapper.selectDevInstanceById(id); + String appUnionId = ascription(ioTDevice.getCreatorId()); + JSONObject downRequest = new JSONObject(); + downRequest.set("appUnionId", appUnionId); + downRequest.set("productKey", ioTDevice.getProductKey()); + downRequest.set("deviceId", ioTDevice.getDeviceId()); + downRequest.set("gwProductKey", ioTDevice.getGwProductKey()); + downRequest.set("cmd", DownCmd.DEV_DEL.name()); + JSONObject ob = new JSONObject(); + downRequest.set("data", ob); + IoTProduct ioTProduct = ioTProductMapper.getProductByProductKey(ioTDevice.getProductKey()); + result = IoTDownlFactory.getIDown(ioTProduct.getThirdPlatform()).doAction(downRequest); + if (!result.getCode().equals(0)) { + return R.error("设备名称: " + ioTDevice.getDeviceName() + " 删除失败,原因: " + result.getMsg()); + } + ioTDeviceFenceRelMapper.deleteFenceInstance(ioTDevice.getIotId()); + } + return R.ok("删除成功"); + } + + /** + * 删除设备信息 + * + * @param id 设备主键 + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list"}, + allEntries = true) + public R deleteDevInstanceById(String id) { + IoTDevice ioTDevice = ioTDeviceMapper.selectDevInstanceById(id); + String appUnionId = ascription(ioTDevice.getCreatorId()); + JSONObject downRequest = new JSONObject(); + downRequest.set("appUnionId", appUnionId); + downRequest.set("productKey", ioTDevice.getProductKey()); + downRequest.set("deviceId", ioTDevice.getDeviceId()); + downRequest.set("cmd", DownCmd.DEV_DEL.name()); + JSONObject ob = new JSONObject(); + downRequest.set("data", ob); + IoTProduct ioTProduct = ioTProductMapper.getProductByProductKey(ioTDevice.getProductKey()); + return IoTDownlFactory.getIDown(ioTProduct.getThirdPlatform()).doAction(downRequest); + } + + /** + * 根据应用id查找设备 + * + * @param ids 设备主键 + * @return 结果 + */ + @Override + public int selectDevByAppUniqueId(String[] ids) { + return ioTDeviceMapper.selectDevByAppUniqueId(ids); + } + + /** + * 查询所以设备型号 + * + * @param supportChild 等于true 查询支持子设备的型号 + * @return 设备型号 + */ + @Override + public List queryAllBySupportChild(Boolean supportChild, String apps) { + return ioTDeviceMapper.selectAllBySupportChild(supportChild, apps); + } + + /** + * 查询所以设备型号 + * + * @return 设备影子 + */ + @Override + public IoTDeviceShadow getDeviceShadow(String iotId) { + return ioTDeviceShadowMapper.getDeviceShadow(iotId); + } + + /** 设备下发 */ + @Override + public R iotFunctionsDown(String productKey, String downRequest) { + JSONObject jsonObject = JSONUtil.parseObj(downRequest); + String deviceId = jsonObject.getStr("deviceId"); + IoTDevice ioTDeviceBo = + ioTDeviceMapper.getOneByDeviceId( + IoTDeviceQuery.builder().deviceId(deviceId).productKey(productKey).build()); + if (ioTDeviceBo == null) { + return R.error("设备不存在"); + } + if (ObjectUtil.isNotNull(ioTDeviceBo)) { + ascription(ioTDeviceBo.getCreatorId()); + } + + IoTProduct ioTProduct = ioTProductMapper.getProductByProductKey(productKey); + R result = IoTDownlFactory.getIDown(ioTProduct.getThirdPlatform()).doAction(downRequest); + return result; + } + + /** 根据产品key查型号配置 */ + @Override + public IoTDeviceModelVO getModelByProductKey(String productKey) { + return ioTDeviceMapper.getModelByProductKey(productKey); + } + + @Override + public List selectNotBindDevInstanceList( + String groupId, IoTDevice ioTDevice, IoTUser iotUser) { + return ioTDeviceMapper.selectNotBindDevInstanceList( + groupId, ioTDevice, iotUser.isAdmin() ? null : iotUser.getUnionId()); + } + + @Override + public Page selectFenceDevice(IoTDeviceBO ioTDeviceBO, int page, int size) { + Page p = PageHelper.startPage(page, size); + List ioTDeviceVOList = ioTDeviceMapper.selectFenceDevice(ioTDeviceBO); + return p; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTProductServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTProductServiceImpl.java new file mode 100644 index 0000000..d471513 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTProductServiceImpl.java @@ -0,0 +1,1135 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.http.HttpStatus; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.service.BaseServiceImpl; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.platform.service.IIoTProductService; +import cn.universal.admin.system.service.IoTDeviceProtocolService; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.constant.IoTConstant.DeviceNode; +import cn.universal.common.constant.IoTConstant.ProductFlushType; +import cn.universal.common.constant.IoTConstant.ProtocolModule; +import cn.universal.common.constant.IoTConstant.TcpFlushType; +import cn.universal.common.domain.R; +import cn.universal.common.event.EventTopics; +import cn.universal.common.event.processer.EventPublisher; +import cn.universal.common.exception.IoTException; +import cn.universal.core.service.IoTDownlFactory; +import cn.universal.persistence.base.IoTProductAction; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.IoTDeviceFunction; +import cn.universal.persistence.entity.IoTDeviceProperties; +import cn.universal.persistence.entity.IoTDeviceProtocol; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.IoTProductSort; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.Network; +import cn.universal.persistence.entity.bo.IoTProductBO; +import cn.universal.persistence.entity.bo.IoTProductImportBO; +import cn.universal.persistence.entity.vo.IoTDeviceModelVO; +import cn.universal.persistence.entity.vo.IoTProductExportVO; +import cn.universal.persistence.entity.vo.IoTProductVO; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.IoTProductMapper; +import cn.universal.persistence.mapper.IoTProductSortMapper; +import cn.universal.persistence.mapper.NetworkMapper; +import cn.universal.persistence.query.AjaxResult; +import cn.universal.persistence.query.IoTProductQuery; +import com.github.pagehelper.Page; +import com.github.pagehelper.PageHelper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.stereotype.Service; +import tk.mybatis.mapper.entity.Example; + +/** + * 设备产品Service业务层处理 + * + * @since 2025-12-24 + */ +@Slf4j +@Service +public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProductService { + + @Resource private IoTProductMapper ioTProductMapper; + + @Resource private IoTDeviceMapper ioTDeviceMapper; + + @Resource private NetworkMapper networkMapper; + @Resource private IoTDeviceProtocolService devProtocolService; + + @Resource private IoTProductSortMapper ioTProductSortMapper; + + @Resource(name = "ioTProductActionService") + private IoTProductAction ioTProductAction; + + @Resource private EventPublisher eventPublisher; + + private String defaultMetadata = IoTConstant.defaultMetadata; + private String thirdConfig = "{\"customField\":[]}"; + + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "supportMQTTNetwork", + "iot_dev_product_list" + }, + allEntries = true) + public int updateNetworkUnionId(Long id, String networkUnionId) { + IoTProduct ioTProduct = new IoTProduct(); + ioTProduct.setNetworkUnionId(networkUnionId); + Example example = new Example(IoTProduct.class); + example.createCriteria().andEqualTo("id", id); + return ioTProductMapper.updateByExampleSelective(ioTProduct, example); + } + + /** + * 查询设备产品 + * + * @param id 设备产品主键 + * @return 设备产品 + */ + @Override + public IoTProductVO selectDevProductById(String id) { + IoTProduct product = ioTProductMapper.selectDevProductById(id); + IoTProductVO productVO = BeanUtil.toBean(product, IoTProductVO.class); + if ("tcp".equals(product.getThirdPlatform())) { + Network network = + networkMapper.selectOne(Network.builder().productKey(product.getProductKey()).build()); + if (Objects.nonNull(network)) { + JSONObject obj = JSONUtil.parseObj(network.getConfiguration()); + JSONObject parserConfiguration = obj.getJSONObject("parserConfiguration"); + if (MapUtil.isNotEmpty(parserConfiguration)) { + for (Map.Entry q : parserConfiguration.entrySet()) { + obj.set(q.getKey(), q.getValue()); + } + } + JSONObject configuration = JSONUtil.parseObj(product.getConfiguration()); + for (Map.Entry q : obj.entrySet()) { + configuration.set(q.getKey(), q.getValue()); + } + configuration.set("enabled", network.getState()); + productVO.setConfiguration(configuration.toString()); + } + } + // 网关子产品处理 + if (DeviceNode.GATEWAY_SUB_DEVICE.name().equals(product.getDeviceNode())) { + productVO.setGwProductKey(product.getGwProductKey()); + IoTProduct gwProduct = ioTProductMapper.getProductByProductKey(product.getGwProductKey()); + if (Objects.nonNull(gwProduct)) { + productVO.setGwName(gwProduct.getName()); + } + } + return productVO; + } + + /** + * 查询设备产品列表 + * + * @param ioTProduct 设备产品 + * @return 设备产品 + */ + @Override + // @Cacheable(cacheNames = "iot_dev_product_list", unless = "#result == null", keyGenerator = + // "redisKeyGenerate") + public List selectDevProductList(IoTProduct ioTProduct) { + return ioTProductMapper.selectDevProductList(ioTProduct); + } + + @Override + // @Cacheable(cacheNames = "iot_dev_product_list", unless = "#result == null", keyGenerator = + // "redisKeyGenerate") + public List selectDevProductV2List(IoTProductQuery ioTProductQuery) { + return ioTProductMapper.selectDevProductV2List(ioTProductQuery); + } + + @Override + // @Cacheable(cacheNames = "iot_dev_product_list", unless = "#result == null", keyGenerator = + // "redisKeyGenerate") + public List selectDevProductV3List(IoTProductQuery ioTProductQuery) { + Page page = + PageHelper.startPage(ioTProductQuery.getPageNum(), ioTProductQuery.getPageSize()); + ioTProductMapper.selectDevProductV3List(ioTProductQuery); + return page; + } + + @Override + // @Cacheable(cacheNames = "iot_dev_product_list", unless = "#result == null", keyGenerator = + // "redisKeyGenerate") + public List selectDevProductV4List(IoTProductQuery ioTProductQuery) { + Page page = + PageHelper.startPage(ioTProductQuery.getPageNum(), ioTProductQuery.getPageSize()); + List devProductVOS = ioTProductMapper.selectDevProductV3List(ioTProductQuery); + List results = + ioTProductMapper.countDevNumberByProductKey(ioTProductQuery.getCreatorId()); + if (CollUtil.isNotEmpty(results)) { + final Map collect = + results.stream() + .collect(Collectors.toMap(IoTProductVO::getProductKey, IoTProductVO::getDevNum)); + for (IoTProductVO devProduct : devProductVOS) { + devProduct.setDevNum(collect.getOrDefault(devProduct.getProductKey(), 0)); + if (JSONUtil.isTypeJSON(devProduct.getPhotoUrl())) { + JSONObject photo = JSONUtil.parseObj(devProduct.getPhotoUrl()); + devProduct.setImage(photo.getStr("img", "")); + } + } + } + // 按设备数量倒序排序 + // if (CollUtil.isNotEmpty(devProductVOS)) { + // devProductVOS.sort(Comparator.comparingInt(IoTProductVO::getDevNum).reversed()); + // } + return page; + } + + @Override + // @Cacheable(cacheNames = "iot_all_dev_product_list", unless = "#result == null", keyGenerator = + // "redisKeyGenerate") + public List selectAllDevProductV2List(IoTProductQuery ioTProductQuery) { + return ioTProductMapper.selectAllDevProductV2List(ioTProductQuery); + } + + /** + * 新增设备产品 + * + * @param ioTProduct 设备产品 + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = { + "iot_all_dev_product_list", + "iot_dev_product_list", + "selectAllEnableNetworkProductKey" + }, + allEntries = true) + public int insertDevProduct(IoTProduct ioTProduct) { + Date date = new Date(); + Long time = date.getTime(); + ioTProduct.setCreateTime(time); + // productKey + String productKey = RandomUtil.randomString(12); + if (Objects.nonNull(ioTProduct.getClassifiedId())) { + IoTProductSort ioTProductSort = + ioTProductSortMapper.selectDevProductSortById(ioTProduct.getClassifiedId()); + ioTProduct.setClassifiedName(ioTProductSort.getClassifiedName()); + } + if (StrUtil.isBlank(ioTProduct.getProductId())) { + ioTProduct.setProductId(productKey); + } + if (StrUtil.isBlank(ioTProduct.getProductSecret())) { + ioTProduct.setProductSecret(IdUtil.fastSimpleUUID()); + } + ioTProduct.setProductKey(productKey); + ioTProduct.setMetadata(defaultMetadata); + ioTProduct.setThirdConfiguration(thirdConfig); + ioTProduct.setStorePolicy("mysql"); + int i = ioTProductMapper.insertDevProduct(ioTProduct); + if (i > 0) { + ioTProductAction.create(productKey, ioTProduct.getCreatorId()); + } + return i; + } + + @Override + @CacheEvict( + cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list"}, + allEntries = true) + public int insertList(List ioTProductList) { + int i = ioTProductMapper.insertList(ioTProductList); + return i; + } + + /** 产品协议导入 */ + @Override + @CacheEvict( + cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list", "supportMQTTNetwork"}, + allEntries = true) + public String importProduct(List productImportBos, String unionId) { + Long timeMillis = System.currentTimeMillis(); + List exitProducts = selectDevProductList(new IoTProduct()); + List exitProductKeys = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(exitProducts)) { + exitProductKeys = + exitProducts.stream().map(IoTProduct::getProductKey).collect(Collectors.toList()); + } + List protocolList = new ArrayList<>(); + // 普通产品导入 + List customProductList = new ArrayList<>(); + // tcp配置表 + List networkList = new ArrayList<>(); + // 电信产品导入 + List ctwingProductList = new ArrayList<>(); + // 失败列表 + List failedList = new ArrayList<>(); + List finalExitProductKeys = exitProductKeys; + AtomicInteger duplicateNum = new AtomicInteger(); + productImportBos.forEach( + ioTProductImportBO -> { + if (StrUtil.isNotBlank(ioTProductImportBO.getProtocolType())) { + IoTDeviceProtocol ioTDeviceProtocol = new IoTDeviceProtocol(); + ioTDeviceProtocol.setId(ioTProductImportBO.getProductKey()); + ioTDeviceProtocol.setType(ioTProductImportBO.getProtocolType()); + ioTDeviceProtocol.setName(ioTProductImportBO.getProductKey()); // 使用productKey作为名称 + ioTDeviceProtocol.setState(ioTProductImportBO.getProtocolState()); + ioTDeviceProtocol.setConfiguration(ioTProductImportBO.getProtocolConfiguration()); + ioTDeviceProtocol.setExample(ioTProductImportBO.getProtocolExample()); + protocolList.add(ioTDeviceProtocol); + } + + IoTProduct ioTProduct = BeanUtil.toBean(ioTProductImportBO, IoTProduct.class); + if (ObjectUtil.isEmpty(ioTProduct)) { + return; + } + if (StrUtil.isBlank(ioTProduct.getProductSecret())) { + ioTProduct.setProductSecret(IdUtil.fastSimpleUUID()); + } + if (finalExitProductKeys.contains(ioTProduct.getProductKey())) { + duplicateNum.addAndGet(1); + failedList.add(ioTProduct); + return; + } + ioTProduct.setCreatorId(unionId); + ioTProduct.setCreateTime(timeMillis); + if (StrUtil.isBlank(ioTProduct.getConfiguration())) { + ioTProduct.setConfiguration(null); + } + if (ProtocolModule.ctaiot.name().equals(ioTProduct.getThirdPlatform())) { + JSONObject downRequest = JSONUtil.parseObj(ioTProduct.getThirdDownRequest()); + if (ObjectUtil.isNull(downRequest)) { + failedList.add(ioTProduct); + return; + } + downRequest.set("creatorId", unionId); + ioTProduct.setThirdDownRequest(JSONUtil.toJsonStr(downRequest)); + ctwingProductList.add(ioTProduct); + } else if (ProtocolModule.tcp.name().equals(ioTProduct.getThirdPlatform())) { + customProductList.add(ioTProduct); + } else { + customProductList.add(ioTProduct); + } + }); + + // 产品导入 + int customSuccess = 0; + if (CollectionUtil.isNotEmpty(customProductList)) { + customSuccess = insertList(customProductList); + } + AtomicInteger ctwingSuccess = new AtomicInteger(); + ctwingProductList.forEach( + devProduct -> { + R r = + IoTDownlFactory.safeInvokeDown( + ProtocolModule.ctaiot.name(), "downPro", devProduct.getThirdDownRequest()); + if (!R.SUCCESS.equals(r.getCode())) { + failedList.add(devProduct); + } else { + ctwingSuccess.addAndGet(1); + } + }); + + // 获取所有导入失败的产品key + List productKeyList = + failedList.stream().map(IoTProduct::getProductKey).collect(Collectors.toList()); + // 只导入已导入产品的协议 + List addProtocolList = + protocolList.stream() + .filter( + devProtocol -> { + return !productKeyList.contains(devProtocol.getId()); + }) + .collect(Collectors.toList()); + int protocolSuccess = 0; + if (CollectionUtil.isNotEmpty(addProtocolList)) { + protocolSuccess = devProtocolService.insertList(addProtocolList); + } + // 导入network表 并启动监听 + if (CollectionUtil.isNotEmpty(networkList)) { + networkMapper.insertList(networkList); + networkList.forEach( + network -> { + flushNettyServer( + JSONUtil.toJsonStr(network.getConfiguration()), + network.getProductKey(), + TcpFlushType.start); + }); + } + + ioTProductAction.create(null, unionId); + String result = + String.format( + "导入普通产品总数:%s 个,成功:%s个;导入电信产品总数:%s 个,成功:%s 个;导入协议总数:%s 个,成功:%s 个。", + customProductList.size() + duplicateNum.get(), + customSuccess, + ctwingProductList.size(), + ctwingSuccess.get(), + protocolList.size(), + protocolSuccess); + return result; + } + + /** + * 修改设备产品 + * + * @param ioTProduct 设备产品 + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list", "supportMQTTNetwork"}, + allEntries = true) + public int updateDevProduct(IoTProduct ioTProduct) { + IoTUser iotUser = queryIotUser(SecurityUtils.getUnionId()); + if (iotUser == null) { + throw new IoTException("获取用户信息异常", HttpStatus.HTTP_UNAUTHORIZED); + } + String appUnionId = iotUser.getUnionId(); + IoTProduct pro = ioTProductMapper.selectByPrimaryKey(ioTProduct.getId()); + if (!appUnionId.equals(pro.getCreatorId()) && !iotUser.isAdmin()) { + throw new IoTException("您没有权限操作此产品!"); + } + if (Objects.nonNull(ioTProduct.getClassifiedId())) { + IoTProductSort ioTProductSort = + ioTProductSortMapper.selectDevProductSortById(ioTProduct.getClassifiedId()); + ioTProduct.setClassifiedName(ioTProductSort.getClassifiedName()); + } + + int count = ioTProductMapper.updateDevProduct(ioTProduct); + log.info("修改产品记录数={},修改人={}", count, appUnionId); + ioTProductAction.update(ioTProduct); + return count; + } + + /** + * 批量删除设备产品 + * + * @param ids 需要删除的设备产品主键 + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list", "supportMQTTNetwork"}, + allEntries = true) + public int deleteDevProductByIds(String[] ids) { + String appUnionId = queryIotUser(SecurityUtils.getUnionId()).getUnionId(); + int count = 0; + for (String id : ids) { + IoTProduct ioTProduct = ioTProductMapper.selectDevProductById(id); + if (!appUnionId.equals(ioTProduct.getCreatorId())) { + throw new IoTException("您没有权限操作此产品!"); + } + count = +deleteDevProductById(id); + } + return count; + } + + /** + * 删除设备产品信息 + * + * @param id 设备产品主键 + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list", "supportMQTTNetwork"}, + allEntries = true) + public int deleteDevProductById(String id) { + String appUnionId = queryIotUser(SecurityUtils.getUnionId()).getUnionId(); + IoTProduct ioTProduct = selectDevProductById(id); + if (ioTProduct == null) { + throw new IoTException("产品不存在"); + } + if (!appUnionId.equals(ioTProduct.getCreatorId())) { + throw new IoTException("您没有权限操作此产品!"); + } + IoTDevice ioTDevice = IoTDevice.builder().productKey(ioTProduct.getProductKey()).build(); + int count = ioTDeviceMapper.selectCount(ioTDevice); + if (count > 0) { + throw new IoTException("" + ioTProduct.getName() + "存在设备,不允许删除"); + } + int delCount = ioTProductMapper.deleteDevProductById(id); + if (delCount > 0) { + ioTProductAction.delete(ioTProduct.getProductKey()); + log.info( + "删除产品={},删除状态={},删除人={}", + ioTProduct.getProductKey() + ioTProduct.getName(), + delCount, + appUnionId); + } + return delCount; + } + + /** 修改产品配置信息 */ + @Override + // @Transactional + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "supportMQTTNetwork", + "iot_dev_product_list" + }, + allEntries = true) + public int updateDevProductConfig(IoTProductVO devProduct) { + String appUnionId = queryIotUser(SecurityUtils.getUnionId()).getUnionId(); + // 根据id获取产品信息 + IoTProduct product = ioTProductMapper.selectDevProductById(String.valueOf(devProduct.getId())); + if (devProduct == null) { + throw new IoTException("产品不存在"); + } + if (!appUnionId.equals(product.getCreatorId())) { + throw new IoTException("您没有权限操作此产品!"); + } + // 获取新的产品配置信息 + JSONObject newConfig = JSONUtil.parseObj(devProduct.getConfiguration()); + // 判断tcp端口号是否冲突 + if ("tcp".equals(product.getThirdPlatform()) + && "privateConfiguration".equals(devProduct.getType())) { + Integer port = null; + try { + port = newConfig.getInt("port"); + } catch (Exception e) { + throw new IoTException("端口号存在字符"); + } + // 判断端口号是否冲突 + if (Objects.nonNull(port)) { + // 判断是否和数据库中其他tcp端口配置冲突 + int count = networkMapper.selectNetWorkByPort(port, product.getProductKey()); + if (count > 0) { + throw new IoTException(port + "端口" + "已被占用"); + } + } + Network network = + networkMapper.selectOne(Network.builder().productKey(product.getProductKey()).build()); + int updateCount; + try { + // 获取配置 + JSONObject tcpConfig = new JSONObject(); + JSONObject parserConfiguration = new JSONObject(); + parserConfiguration.set("cert", newConfig.getStr("cert")); + parserConfiguration.set("key", newConfig.getStr("key")); + parserConfiguration.set("rootCa", newConfig.getStr("rootCa")); + parserConfiguration.set("delimited", newConfig.getStr("delimited")); + parserConfiguration.set("lineMaxLength", newConfig.getInt("lineMaxLength")); + parserConfiguration.set("delimitedMaxlength", newConfig.getInt("delimitedMaxlength")); + parserConfiguration.set("fixedLength", newConfig.getInt("fixedLength")); + parserConfiguration.set("byteOrderLittle", newConfig.getBool("byteOrderLittle", false)); + parserConfiguration.set("maxFrameLength", newConfig.getInt("maxFrameLength")); + parserConfiguration.set("lengthFieldOffset", newConfig.getInt("lengthFieldOffset")); + parserConfiguration.set("lengthFieldLength", newConfig.getInt("lengthFieldLength")); + parserConfiguration.set("lengthAdjustment", newConfig.getInt("lengthAdjustment")); + parserConfiguration.set("initialBytesToStrip", newConfig.getInt("initialBytesToStrip")); + parserConfiguration.set("failFast", newConfig.getBool("failFast", false)); + tcpConfig.set("parserConfiguration", parserConfiguration); + tcpConfig.set("ssl", newConfig.getBool("ssl", false)); + tcpConfig.set("port", port); + tcpConfig.set("onlyCache", newConfig.getBool("onlyCache", false)); + tcpConfig.set("productKey", product.getProductKey()); + tcpConfig.set("host", newConfig.getStr("host")); + tcpConfig.set(IoTConstant.ALLOW_INSERT, newConfig.getBool(IoTConstant.ALLOW_INSERT, false)); + tcpConfig.set("preStore", newConfig.getBool("preStore", false)); + tcpConfig.set("alwaysPreDecode", newConfig.getBool("alwaysPreDecode", false)); + tcpConfig.set("decoderType", newConfig.getStr("decoderType")); + tcpConfig.set("parserType", newConfig.getStr("parserType")); + tcpConfig.set("allIdleTime", newConfig.getInt("allIdleTime")); + tcpConfig.set("readerIdleTime", newConfig.getInt("readerIdleTime")); + tcpConfig.set("writerIdleTime", newConfig.getInt("writerIdleTime")); + tcpConfig.set("readTimeout", newConfig.getInt("readTimeout")); + tcpConfig.set("sendTimeout", newConfig.getInt("sendTimeout")); + tcpConfig.set("idleInterval", newConfig.getInt("idleInterval")); + // if (Objects.isNull(network)) { + // // 新增 + // updateCount = networkMapper.insert( + // Network.builder().type("TCP_SERVER").productKey(product.getProductKey()) + // .description("tcp_server").createDate(new Date()).createUser(appUnionId) + // + // .state(newConfig.getBool("enabled")).configuration(tcpConfig.toString()).build()); + // } else { + // // 更新 + // network.setConfiguration(tcpConfig.toString()); + // network.setState(newConfig.getBool("enabled")); + // updateCount = networkMapper.updateByPrimaryKey(network); + // } + // 更改配置后重启tcp监听 + // if (updateCount > 0) { + // 暂时不重启 + // flushNettyServer(JSONUtil.toJsonStr(tcpConfig), product.getProductKey(), + // TcpFlushType.reload); + // } + } catch (Exception e) { + log.error("tcp网络组件配置失败", e); + throw new IoTException("添加失败"); + } + } + // 获取旧的产品配置信息 + JSONObject oldConfig = JSONUtil.parseObj(product.getConfiguration()); + // 循环遍历:新值替换旧值 + for (Map.Entry entry : newConfig.entrySet()) { + oldConfig.set(entry.getKey(), entry.getValue()); + } + devProduct.setConfiguration(oldConfig.toString()); + int updateCount = ioTProductMapper.updateDevProduct(devProduct); + if (updateCount > 0) { + ioTProductAction.update(devProduct); + log.info( + "更新产品={},更新状态={},更新人={}", + devProduct.getProductKey() + devProduct.getName(), + updateCount, + appUnionId); + } + + return updateCount; + } + + /** + * 查询设备产品物模型属性列表 + * + * @return 设备产品物模型属性集合 + */ + @Override + public List selectDevProperties(String Id) { + IoTProduct ioTProduct = ioTProductMapper.selectDevProductById(Id); + JSONObject metadata = JSONUtil.parseObj(ioTProduct.getMetadata()); + List ioTDevicePropertiesList = new ArrayList<>(); + JSONArray properties = metadata.getJSONArray("properties"); + if (properties != null) { + for (Object object : properties) { + JSONObject jsonObject = JSONUtil.parseObj(object); + JSONObject valueType = JSONUtil.parseObj(jsonObject.getStr("valueType")); + JSONObject expands = JSONUtil.parseObj(jsonObject.getStr("expands")); + IoTDeviceProperties ioTDeviceProperties = + IoTDeviceProperties.builder() + .id(jsonObject.getStr("id")) + .name(jsonObject.getStr("name")) + .type(valueType.getStr("type")) + .unit(valueType.getStr("unit")) + .elements(valueType.getStr("elements")) + .mode(jsonObject.getStr("mode")) + .source(jsonObject.getStr("source")) + .description(jsonObject.getStr("description")) + .build(); + ioTDevicePropertiesList.add(ioTDeviceProperties); + } + } + return ioTDevicePropertiesList; + } + + /** + * 查询设备产品物模型事件列表 + * + * @return 设备产品物模型事件集合 + */ + @Override + public List selectDevEvents(String Id) { + IoTProduct ioTProduct = ioTProductMapper.selectDevProductById(Id); + JSONObject metadata = JSONUtil.parseObj(ioTProduct.getMetadata()); + List ioTDeviceEventsList = new ArrayList<>(); + JSONArray properties = metadata.getJSONArray("events"); + if (properties != null) { + for (Object object : properties) { + JSONObject jsonObject = JSONUtil.parseObj(object); + JSONObject expands = JSONUtil.parseObj(jsonObject.getStr("expands")); + IoTDeviceEvents ioTDeviceEvents = + IoTDeviceEvents.builder() + .id(jsonObject.getStr("id")) + .name(jsonObject.getStr("name")) + .description(jsonObject.getStr("description")) + .level(expands.getStr("level")) + .build(); + ioTDeviceEventsList.add(ioTDeviceEvents); + } + } + return ioTDeviceEventsList; + } + + /** + * 查询设备产品物模型方法列表 + * + * @return 设备产品物模型方法集合 + */ + @Override + public List selectDevFunctions(String Id) { + IoTProduct ioTProduct = ioTProductMapper.selectDevProductById(Id); + JSONObject metadata = JSONUtil.parseObj(ioTProduct.getMetadata()); + List ioTDeviceFunctionList = new ArrayList<>(); + JSONArray properties = metadata.getJSONArray("functions"); + if (properties != null) { + for (Object object : properties) { + JSONObject jsonObject = JSONUtil.parseObj(object); + IoTDeviceFunction ioTDeviceFunction = + IoTDeviceFunction.builder() + .id(jsonObject.getStr("id")) + .name(jsonObject.getStr("name")) + .config(jsonObject.getBool("config") == null ? false : jsonObject.getBool("config")) + .inputs(jsonObject.getStr("inputs")) + .source(jsonObject.getStr("source")) + .description(jsonObject.getStr("description")) + .build(); + ioTDeviceFunctionList.add(ioTDeviceFunction); + } + } + return ioTDeviceFunctionList; + } + + // @PostConstruct + // public void s2s(){ + // JSONObject object=new JSONObject(); + // object.set("id","testt222"); + // object.set("name","测试df"); + //// object.set("expands","ff"); + // JSONObject k=new JSONObject(); + // object.set("valueType",k); + // k.set("type","string"); + // k.set("expands",null); + // int a= insertMetadata("test","properties", JSONUtil.toJsonStr(object)); + // System.out.println(a); + // } + // @PostConstruct + // public void ss(){ + // JSONObject object=new JSONObject(); + // object.set("id","testt222"); + // object.set("name","测试df"); + //// object.set("expands","ff"); + // JSONObject k=new JSONObject(); + // object.set("valueType",k); + // k.set("type","string"); + // k.set("expands",null); + // int a= deleteMetadata("test","properties", "testt222"); + // System.out.println(a); + // } + + /** 物模型新增字段 */ + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "selectAllEnableNetworkProductKey", + "iot_dev_product_list" + }, + allEntries = true) + public int insertMetadata(IoTProductBO ioTProductBO) { + IoTProduct ioTProduct = + ioTProductMapper.selectOne( + IoTProduct.builder().productKey(ioTProductBO.getProductKey()).build()); + if (ioTProduct == null) { + throw new IoTException("产品不存在"); + } + ioTProductBO.beanToJson(); + if (getMetadata(ioTProductBO) != null) { + throw new IoTException("该id已存在"); + } + switch (ioTProductBO.getType()) { + case "properties": + ioTProductBO.setPath("$.properties"); + break; + case "events": + ioTProductBO.setPath("$.events"); + break; + case "functions": + ioTProductBO.setPath("$.functions"); + break; + default: + throw new IoTException("物模型类型不存在!"); + } + int updateCount = ioTProductMapper.insertMetadata(ioTProductBO); + if (updateCount > 0) { + ioTProductAction.metadataCreate(ioTProductBO.getProductKey()); + log.info( + "产品新增物模型={},新增物模型状态={},修改人={}", + ioTProduct.getProductKey() + ioTProduct.getName(), + updateCount, + ioTProduct.getCreatorId()); + } + return updateCount; + } + + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "iot_dev_product_list" + }, + allEntries = true) + public int deleteMetadata(IoTProductBO ioTProductBO) { + String appUnionId = queryIotUser(SecurityUtils.getUnionId()).getUnionId(); + IoTProduct product = ioTProductMapper.getProductByProductKey(ioTProductBO.getProductKey()); + if (!appUnionId.equals(product.getCreatorId())) { + throw new IoTException("您没有权限操作此产品!"); + } + ioTProductBO.setPath(getPath(ioTProductBO)); + int delCount = ioTProductMapper.deleteMetadata(ioTProductBO); + if (delCount > 0) { + ioTProductAction.metadataDelete(ioTProductBO.getProductKey()); + log.info( + "删除物模型={},删除物模型状态={},修改人={}", + ioTProductBO.getProductKey() + ioTProductBO.getName(), + delCount, + appUnionId); + } + return delCount; + } + + @Override + public IoTProductBO getMetadata(IoTProductBO ioTProductBO) { + ioTProductBO.setPath(getPath(ioTProductBO)); + IoTProductBO result = ioTProductMapper.getMetadata(ioTProductBO); + // result.jsonToBean(); + return result; + } + + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "iot_dev_product_list" + }, + allEntries = true) + public int updateMetadata(IoTProductBO ioTProductBO) { + String appUnionId = queryIotUser(SecurityUtils.getUnionId()).getUnionId(); + IoTProduct product = ioTProductMapper.getProductByProductKey(ioTProductBO.getProductKey()); + if (!appUnionId.equals(product.getCreatorId())) { + throw new IoTException("您没有权限操作此产品!"); + } + ioTProductBO.beanToJson(); + // if(getMetadata(ioTProductBO)!=null){ + // throw new IoTException("该id已存在"); + // } + ioTProductBO.setPath(getPath(ioTProductBO)); + + int updateCount = ioTProductMapper.updateMetadata(ioTProductBO); + if (updateCount > 0) { + ioTProductAction.metadataUpdate(ioTProductBO.getProductKey()); + log.info( + "产品新增物模型={},新增物模型状态={},修改人={}", + ioTProductBO.getProductKey() + ioTProductBO.getName(), + updateCount, + appUnionId); + } + return updateCount; + } + + public String getPath(IoTProductBO ioTProductBO) { + switch (ioTProductBO.getType()) { + case "properties": + return "$.properties**.id"; + case "events": + return "$.events**.id"; + case "functions": + return "$.functions**.id"; + default: + throw new IoTException("物模型键不存在!"); + } + } + + @Override + public IoTDeviceModelVO getModelByProductKey(String productKey) { + return ioTProductMapper.getModelByProductKey(productKey); + } + + @Override + public String selectMetadataByDevId(String devId) { + return ioTProductMapper.selectMetadataByDevId(devId); + } + + @Override + public Map countDevNumberByProductKey(String unionId) { + List results = ioTProductMapper.countDevNumberByProductKey(unionId); + return results.stream() + .collect(Collectors.toMap(IoTProductVO::getProductKey, IoTProductVO::getDevNum)); + } + + @Override + public AjaxResult selectDevProductByKey(String key) { + IoTProduct ioTProduct = new IoTProduct(); + ioTProduct.setProductKey(key); + return AjaxResult.success(ioTProductMapper.selectOne(ioTProduct)); + } + + /** 修改产品其他配置信息 */ + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "selectAllEnableNetworkProductKey", + "iot_dev_product_list" + }, + allEntries = true) + public int updateDevProductOtherConfig(String otherConfig) { + // 解析新的配置 + JSONObject newConfig = JSONUtil.parseObj(otherConfig); + // 获取产品信息 + IoTProduct product = ioTProductMapper.selectDevProductById(newConfig.getStr("productId")); + IoTUser iotUser = queryIotUser(SecurityUtils.getUnionId()); + if (!product.getCreatorId().equals(iotUser.getUnionId())) { + throw new IoTException("您没有权限操作此产品!"); + } + // 获取第三方配置 + String thirdConfiguration = product.getThirdConfiguration(); + JSONObject oldConfig = null; + // 如果配置等于空,设置默认值 + if (StrUtil.isBlank(thirdConfiguration)) { + oldConfig = new JSONObject(); + } else { + oldConfig = JSONUtil.parseObj(thirdConfiguration); + } + + // 获取新的配置信息 + String ext1Id = newConfig.getStr("ext1Id"); + String ext2Id = newConfig.getStr("ext2Id"); + String ext3Id = newConfig.getStr("ext3Id"); + String ext4Id = newConfig.getStr("ext4Id"); + // 构造后端验证 + JSONArray customField = new JSONArray(); + if (StrUtil.isNotEmpty(ext1Id)) { + JSONObject customeDemo = new JSONObject(); + customeDemo.set("id", ext1Id); + customeDemo.set("name", newConfig.getStr("ext1Name")); + customeDemo.set("description", newConfig.getStr("ext1Des")); + customField.add(customeDemo); + oldConfig.set("ext1Label", newConfig.getStr("ext1Name")); + oldConfig.set("ext1LabelDes", newConfig.getStr("ext1Des")); + } + if (StrUtil.isNotEmpty(ext2Id)) { + JSONObject customeDemo = new JSONObject(); + customeDemo.set("id", ext2Id); + customeDemo.set("name", newConfig.getStr("ext2Name")); + customeDemo.set("description", newConfig.getStr("ext2Des")); + customField.add(customeDemo); + oldConfig.set("ext2Label", newConfig.getStr("ext2Name")); + oldConfig.set("ext2LabelDes", newConfig.getStr("ext2Des")); + } + if (StrUtil.isNotEmpty(ext3Id)) { + JSONObject customeDemo = new JSONObject(); + customeDemo.set("id", ext3Id); + customeDemo.set("name", newConfig.getStr("ext3Name")); + customeDemo.set("description", newConfig.getStr("ext3Des")); + customField.add(customeDemo); + oldConfig.set("ext3Label", newConfig.getStr("ext3Name")); + oldConfig.set("ext3LabelDes", newConfig.getStr("ext3Des")); + } + if (StrUtil.isNotEmpty(ext4Id)) { + JSONObject customeDemo = new JSONObject(); + customeDemo.set("id", ext4Id); + customeDemo.set("name", newConfig.getStr("ext4Name")); + customeDemo.set("description", newConfig.getStr("ext4Des")); + customField.add(customeDemo); + oldConfig.set("ext4Label", newConfig.getStr("ext4Name")); + oldConfig.set("ext4LabelDes", newConfig.getStr("ext4Des")); + } + if (StrUtil.isEmpty(ext1Id)) { + String ext1Label = oldConfig.getStr("ext1Label"); + if (StrUtil.isNotEmpty(ext1Label)) { + oldConfig.remove("ext1Label"); + } + String ext1LabelDes = oldConfig.getStr("ext1LabelDes"); + if (StrUtil.isNotEmpty(ext1LabelDes)) { + oldConfig.remove("ext1LabelDes"); + } + } + if (StrUtil.isEmpty(ext2Id)) { + String ext2Label = oldConfig.getStr("ext2Label"); + if (StrUtil.isNotEmpty(ext2Label)) { + oldConfig.remove("ext2Label"); + } + String ext2LabelDes = oldConfig.getStr("ext2LabelDes"); + if (StrUtil.isNotEmpty(ext2LabelDes)) { + oldConfig.remove("ext2LabelDes"); + } + } + if (StrUtil.isEmpty(ext3Id)) { + String ext3Label = oldConfig.getStr("ext3Label"); + if (StrUtil.isNotEmpty(ext3Label)) { + oldConfig.remove("ext3Label"); + } + String ext3LabelDes = oldConfig.getStr("ext3LabelDes"); + if (StrUtil.isNotEmpty(ext3LabelDes)) { + oldConfig.remove("ext3LabelDes"); + } + } + if (StrUtil.isEmpty(ext4Id)) { + String ext4Label = oldConfig.getStr("ext4Label"); + if (StrUtil.isNotEmpty(ext4Label)) { + oldConfig.remove("ext4Label"); + } + String ext4LabelDes = oldConfig.getStr("ext4LabelDes"); + if (StrUtil.isNotEmpty(ext4LabelDes)) { + oldConfig.remove("ext4LabelDes"); + } + } + + oldConfig.set("customField", customField); + product.setThirdConfiguration(oldConfig.toString()); + + int updateCount = ioTProductMapper.updateDevProduct(product); + if (updateCount > 0) { + ioTProductAction.update(product); + log.info( + "产品其他配置更改={},产品其他配置更改状态={},修改人={}", + product.getProductKey() + product.getName(), + updateCount, + product.getCreatorId()); + } + return updateCount; + } + + @Override + public int updateDevProductStoreConfig(IoTProductVO storeConfig) { + // 获取产品信息 + IoTProduct product = ioTProductMapper.selectDevProductById(String.valueOf(storeConfig.getId())); + if (product == null) { + throw new IoTException("产品不存在!"); + } + product.setStorePolicyConfiguration(storeConfig.getStorePolicyConfiguration()); + int updateCount = ioTProductMapper.updateDevProduct(product); + return updateCount; + } + + @Override + public void flushNettyServer(String config, String productKey, TcpFlushType type) { + + } + + @Override + public List selectDevProductAllList() { + return ioTProductMapper.selectDevProductAllList(); + } + + @Override + public List getGatewayList() { + List devProductVOS = ioTProductMapper.selectDevProductAllList(); + devProductVOS = + devProductVOS.stream() + .filter(devProductVO -> DeviceNode.GATEWAY.name().equals(devProductVO.getDeviceNode())) + .map( + devProductVO -> { + IoTProductVO vo = new IoTProductVO(); + vo.setName(devProductVO.getName()); + vo.setProductKey(devProductVO.getProductKey()); + return vo; + }) + .collect(Collectors.toList()); + return devProductVOS; + } + + @Override + public List getGatewaySubDeviceList(String productKey) { + List devProductVOS = ioTProductMapper.getGatewaySubDeviceList(productKey); + return devProductVOS; + } + + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "supportMQTTNetwork", + "iot_dev_product_list" + }, + allEntries = true) + public int bindCertificate(String productKey, String sslKey) { + IoTProduct product = + ioTProductMapper.selectOne(IoTProduct.builder().productKey(productKey).build()); + if (product == null) { + throw new IoTException("产品不存在"); + } + JSONObject config = null; + try { + config = JSONUtil.parseObj(product.getConfiguration()); + } catch (Exception e) { + config = new JSONObject(); + } + config.set("ssl", true); + config.set("sslKey", sslKey); + product.setConfiguration(JSONUtil.toJsonStr(config)); + return ioTProductMapper.updateByPrimaryKeySelective(product); + } + + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "supportMQTTNetwork", + "iot_dev_product_list" + }, + allEntries = true) + public int unbindCertificate(String productKey) { + IoTProduct product = + ioTProductMapper.selectOne(IoTProduct.builder().productKey(productKey).build()); + if (product == null) { + throw new IoTException("产品不存在"); + } + JSONObject config = null; + try { + config = JSONUtil.parseObj(product.getConfiguration()); + } catch (Exception e) { + config = new JSONObject(); + } + config.set("ssl", false); + config.set("sslKey", ""); + product.setConfiguration(JSONUtil.toJsonStr(config)); + return ioTProductMapper.updateByPrimaryKeySelective(product); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTProductSortServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTProductSortServiceImpl.java new file mode 100644 index 0000000..58381ad --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTProductSortServiceImpl.java @@ -0,0 +1,403 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.service.impl; + +import cn.hutool.core.util.StrUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.platform.service.IIoTProductSortService; +import cn.universal.persistence.entity.IoTProductSort; +import cn.universal.persistence.entity.bo.RuleModelBO.IoTProductSortBO; +import cn.universal.persistence.mapper.IoTProductSortMapper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 产品分类Service业务层处理 @Author Aleo + * + * @since 2025-12-29 + */ +@Slf4j +@Service +public class IoTProductSortServiceImpl implements IIoTProductSortService { + + @Resource private IoTProductSortMapper ioTProductSortMapper; + + /** + * 查询产品分类 + * + * @param id 产品分类主键 + * @return 产品分类 + */ + @Override + public IoTProductSort selectDevProductSortById(String id) { + if (StrUtil.isBlank(id)) { + log.warn("查询产品分类时ID为空"); + return null; + } + return ioTProductSortMapper.selectDevProductSortById(id); + } + + /** + * 查询产品分类列表 + * + * @param ioTProductSort 产品分类查询条件 + * @return 产品分类列表 + */ + @Override + public List selectDevProductSortList(IoTProductSort ioTProductSort) { + return ioTProductSortMapper.selectDevProductSortList(ioTProductSort); + } + + /** + * 新增产品分类 + * + * @param ioTProductSort 产品分类 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int insertDevProductSort(IoTProductSort ioTProductSort) { + // 参数验证 + if (ioTProductSort == null) { + throw new IllegalArgumentException("产品分类信息不能为空"); + } + if (StrUtil.isBlank(ioTProductSort.getClassifiedName())) { + throw new IllegalArgumentException("分类名称不能为空"); + } + if (StrUtil.isBlank(ioTProductSort.getParentId())) { + throw new IllegalArgumentException("父分类ID不能为空"); + } + + // 检查分类名称是否重复 + if (isClassNameExists(ioTProductSort.getClassifiedName(), ioTProductSort.getParentId(), null)) { + throw new IllegalArgumentException("同级分类下已存在相同名称的分类"); + } + + // 生成分类ID + String newId = generateProductSortId(ioTProductSort.getParentId()); + ioTProductSort.setId(newId); + + // 设置默认值 + ioTProductSort.setHasChild(0); + ioTProductSort.setCreateTime(new Date()); + ioTProductSort.setCreateBy(SecurityUtils.getUnionId()); + + // 插入新分类 + int result = ioTProductSortMapper.insertDevProductSort(ioTProductSort); + + // 更新父分类的hasChild状态 + updateParentHasChildStatus(ioTProductSort.getParentId()); + + log.info( + "新增产品分类成功: id={}, name={}, parentId={}", + newId, + ioTProductSort.getClassifiedName(), + ioTProductSort.getParentId()); + + return result; + } + + /** + * 修改产品分类 + * + * @param ioTProductSort 产品分类 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int updateDevProductSort(IoTProductSort ioTProductSort) { + // 参数验证 + if (ioTProductSort == null || StrUtil.isBlank(ioTProductSort.getId())) { + throw new IllegalArgumentException("产品分类ID不能为空"); + } + if (StrUtil.isBlank(ioTProductSort.getClassifiedName())) { + throw new IllegalArgumentException("分类名称不能为空"); + } + + // 检查分类是否存在 + IoTProductSort existingSort = + ioTProductSortMapper.selectDevProductSortById(ioTProductSort.getId()); + if (existingSort == null) { + throw new IllegalArgumentException("要修改的产品分类不存在"); + } + + // 检查分类名称是否重复(排除自己) + if (isClassNameExists( + ioTProductSort.getClassifiedName(), existingSort.getParentId(), ioTProductSort.getId())) { + throw new IllegalArgumentException("同级分类下已存在相同名称的分类"); + } + + // 不允许修改父分类(避免复杂的树结构变更) + if (!existingSort.getParentId().equals(ioTProductSort.getParentId())) { + throw new IllegalArgumentException("不允许修改分类的父级关系"); + } + + // 更新分类信息 + int result = ioTProductSortMapper.updateDevProductSort(ioTProductSort); + + log.info( + "修改产品分类成功: id={}, name={}", ioTProductSort.getId(), ioTProductSort.getClassifiedName()); + + return result; + } + + /** + * 批量删除产品分类 + * + * @param ids 需要删除的产品分类主键数组 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + @CacheEvict( + cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list"}, + allEntries = true) + public int deleteDevProductSortByIds(String[] ids) { + if (ids == null || ids.length == 0) { + throw new IllegalArgumentException("删除的产品分类ID不能为空"); + } + + int totalDeleted = 0; + for (String id : ids) { + if (StrUtil.isNotBlank(id)) { + totalDeleted += deleteDevProductSortById(id); + } + } + + log.info("批量删除产品分类完成: 删除数量={}", totalDeleted); + return totalDeleted; + } + + /** + * 删除产品分类信息 + * + * @param id 产品分类主键 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteDevProductSortById(String id) { + if (StrUtil.isBlank(id)) { + throw new IllegalArgumentException("删除的产品分类ID不能为空"); + } + + // 检查分类是否存在 + IoTProductSort ioTProductSort = ioTProductSortMapper.selectDevProductSortById(id); + if (ioTProductSort == null) { + throw new IllegalArgumentException("要删除的产品分类不存在"); + } + + // 检查是否有子分类 + List children = ioTProductSortMapper.getListByParentId(id); + if (!children.isEmpty()) { + throw new IllegalArgumentException("该分类下还有子分类,请先删除子分类"); + } + + // 删除分类 + int result = ioTProductSortMapper.deleteDevProductSortById(id); + + // 更新父分类的hasChild状态 + updateParentHasChildStatus(ioTProductSort.getParentId()); + + log.info("删除产品分类成功: id={}, name={}", id, ioTProductSort.getClassifiedName()); + + return result; + } + + /** + * 构建前端所需要树结构 + * + * @param ioTProductSortList 菜单列表 + * @return 树结构列表 + */ + @Override + public List buildProductSortTree(List ioTProductSortList) { + if (ioTProductSortList == null || ioTProductSortList.isEmpty()) { + return new ArrayList<>(); + } + + // 构建ID映射 + List allIds = + ioTProductSortList.stream().map(IoTProductSort::getId).collect(Collectors.toList()); + + // 找出根节点 + List rootNodes = + ioTProductSortList.stream() + .filter(sort -> !allIds.contains(sort.getParentId()) || "0".equals(sort.getParentId())) + .collect(Collectors.toList()); + + // 递归构建树结构 + for (IoTProductSort rootNode : rootNodes) { + buildTreeRecursively(ioTProductSortList, rootNode); + } + + return rootNodes.isEmpty() ? ioTProductSortList : rootNodes; + } + + /** + * 生成产品分类ID + * + * @param parentId 父分类ID + * @return 新的分类ID + */ + private String generateProductSortId(String parentId) { + List siblings = ioTProductSortMapper.getListByParentId(parentId); + if ("0".equals(parentId)) { + parentId = StrUtil.EMPTY; + } + if (siblings.isEmpty()) { + // 如果是第一个子分类,使用父ID + 100 + return parentId + "100"; + } else { + // 找到最大的ID并加1 + long maxId = + siblings.stream() + .mapToLong( + sort -> { + try { + return Long.parseLong(sort.getId()); + } catch (NumberFormatException e) { + return 0L; + } + }) + .max() + .orElse(0L); + + return String.valueOf(maxId + 1); + } + } + + /** + * 检查同级分类下是否存在相同名称 + * + * @param className 分类名称 + * @param parentId 父分类ID + * @param excludeId 排除的分类ID(用于更新时排除自己) + * @return 是否存在 + */ + private boolean isClassNameExists(String className, String parentId, String excludeId) { + List siblings = ioTProductSortMapper.getListByParentId(parentId); + + return siblings.stream() + .anyMatch( + sort -> className.equals(sort.getClassifiedName()) && !sort.getId().equals(excludeId)); + } + + /** + * 更新父分类的hasChild状态 + * + * @param parentId 父分类ID + */ + private void updateParentHasChildStatus(String parentId) { + if ("0".equals(parentId)) { + return; // 根分类不需要更新 + } + + IoTProductSort parent = ioTProductSortMapper.selectDevProductSortById(parentId); + if (parent != null) { + List children = ioTProductSortMapper.getListByParentId(parentId); + int hasChild = children.isEmpty() ? 0 : 1; + + if (parent.getHasChild() != hasChild) { + parent.setHasChild(hasChild); + ioTProductSortMapper.updateDevProductSort(parent); + } + } + } + + /** + * 递归构建树结构 + * + * @param allNodes 所有节点 + * @param currentNode 当前节点 + */ + private void buildTreeRecursively(List allNodes, IoTProductSort currentNode) { + List children = + allNodes.stream() + .filter(node -> currentNode.getId().equals(node.getParentId())) + .collect(Collectors.toList()); + + if (!children.isEmpty()) { + currentNode.setChildren(children); + // 递归处理子节点 + for (IoTProductSort child : children) { + buildTreeRecursively(allNodes, child); + } + } + } + + // ==================== 新增的BO方法实现 ==================== + + /** + * 新增产品分类(使用BO) + * + * @param bo 产品分类业务对象 + * @return 结果 + */ + @Override + public int insertDevProductSort(IoTProductSortBO bo) { + if (bo == null) { + throw new IllegalArgumentException("产品分类信息不能为空"); + } + + // 处理parentId,前端可能传入数字0,需要转换为字符串"0" + if ("0".equals(String.valueOf(bo.getParentId()))) { + bo.setParentId("0"); + } + + IoTProductSort entity = bo.toEntity(); + return insertDevProductSort(entity); + } + + /** + * 修改产品分类(使用BO) + * + * @param bo 产品分类业务对象 + * @return 结果 + */ + @Override + public int updateDevProductSort(IoTProductSortBO bo) { + if (bo == null) { + throw new IllegalArgumentException("产品分类信息不能为空"); + } + + // 处理parentId,前端可能传入数字0,需要转换为字符串"0" + if ("0".equals(String.valueOf(bo.getParentId()))) { + bo.setParentId("0"); + } + + IoTProductSort entity = bo.toEntity(); + return updateDevProductSort(entity); + } + + /** + * 获取产品分类树结构 + * + * @return 树结构列表 + */ + @Override + public List getProductSortTree() { + // 查询所有分类 + List allSorts = + ioTProductSortMapper.selectDevProductSortList(new IoTProductSort()); + // 构建树结构 + return buildProductSortTree(allSorts); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTBatchFunsController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTBatchFunsController.java new file mode 100644 index 0000000..35d3725 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTBatchFunsController.java @@ -0,0 +1,141 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.web; + +import cn.hutool.core.util.StrUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.platform.service.IIoTDeviceFunctionTaskService; +import cn.universal.admin.platform.service.IIoTDeviceService; +import cn.universal.admin.system.service.IIoTUserApplicationService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.common.domain.R; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.IoTUserApplication; +import cn.universal.persistence.entity.bo.IoTDeviceBO; +import cn.universal.persistence.entity.bo.IoTDeviceFunctionHistoryBO; +import cn.universal.persistence.entity.bo.IoTDeviceFunctionTaskBO; +import cn.universal.persistence.entity.vo.IoTDeviceFunctionHistoryVO; +import cn.universal.persistence.entity.vo.IoTDeviceFunctionTaskVO; +import cn.universal.persistence.entity.vo.IoTUserApplicationVO; +import cn.universal.persistence.page.TableDataInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 批量指令 @Author ruoyi + * + * @since 2024-12-24 + */ +@RestController +@Tag(name = "批量指令") +@RequestMapping("/admin/v1/function/batch") +public class IoTBatchFunsController extends BaseController { + + @Autowired private IIoTDeviceService iIotDeviceService; + + @Resource private IIoTDeviceFunctionTaskService iIoTDeviceFunctionTaskService; + @Resource private IIoTUserApplicationService iotUserApplicationService; + + /** 查询设备列表 */ + @Operation(summary = "查询设备列表") + @GetMapping("/dev/list") + public TableDataInfo list(IoTDeviceBO ioTDeviceBO) { + if (StrUtil.isBlank(ioTDeviceBO.getProductKey()) + && StrUtil.isBlank(ioTDeviceBO.getDeviceId())) { + throw new IoTException("设备序列号或产品不能为空"); + } + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + startPage(); + List list = iIotDeviceService.selectDevInstanceListWithTags(ioTDeviceBO, iotUser); + return getDataTable(list); + } + + /** 查询任务列表 */ + @Operation(summary = "查询任务列表") + @GetMapping("/task/list") + public TableDataInfo taskList(IoTDeviceFunctionTaskBO bo) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + startPage(); + List list = iIoTDeviceFunctionTaskService.selectTaskList(bo, iotUser); + return getDataTable(list); + } + + /** 根据任务id查询下发历史列表 */ + @Operation(summary = "根据任务id查询下发历史列表") + @GetMapping("/task/history") + public TableDataInfo taskHistory(IoTDeviceFunctionHistoryBO bo) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + startPage(); + List list = + iIoTDeviceFunctionTaskService.queryFunctionListByTaskId(bo, iotUser); + return getDataTable(list); + } + + /** 新建批量下发任务 */ + @Operation(summary = "新建批量下发任务") + @PostMapping("/task") + public R addFunctionTask(@RequestBody IoTDeviceFunctionTaskBO ioTDeviceFunctionTaskBO) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + return R.toAjax( + iIoTDeviceFunctionTaskService.addFunctionTask(ioTDeviceFunctionTaskBO, iotUser) ? 1 : 0); + } + + /** 重新下发任务 */ + @Operation(summary = "重新下发任务") + @PostMapping("/task/retry") + public R retryFunctionTask(@RequestBody IoTDeviceFunctionTaskBO ioTDeviceFunctionTaskBO) { + return R.toAjax( + iIoTDeviceFunctionTaskService.retryFunctionTask(ioTDeviceFunctionTaskBO) ? 1 : 0); + } + + /** 设备型号类型列表 */ + @Operation(summary = "设备型号类型列表") + @GetMapping("/product/list/{id}") + public R> productList(@PathVariable("id") Long applicationId) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + List list = + iIotDeviceService.selectProductListInBatchFunction(applicationId, iotUser); + return R.ok(list); + } + + /** 查询用户应用信息列表 */ + @Operation(summary = "查询用户应用列表") + @GetMapping("/application/list") + public R> selectApplicationList(IoTUserApplication application) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + List list = + iotUserApplicationService.selectApplicationList(application, iotUser); + List result = new ArrayList<>(); + list.forEach( + iotUserApplicationVo -> { + IoTUserApplicationVO vo = new IoTUserApplicationVO(); + vo.setAppUniqueId(iotUserApplicationVo.getAppUniqueId()); + vo.setAppName(iotUserApplicationVo.getAppName()); + result.add(vo); + }); + return R.ok(result); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTCertificateController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTCertificateController.java new file mode 100644 index 0000000..6832fb8 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTCertificateController.java @@ -0,0 +1,197 @@ +package cn.universal.admin.platform.web; + +import static cn.universal.common.constant.IoTConstant.CERT_DEFAULT_KEY; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.platform.service.ICertificateService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.persistence.dto.IoTCertificateQueryDTO; +import cn.universal.persistence.entity.IoTCertificate; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/admin/v1/certificate") +public class IoTCertificateController extends BaseController { + + @Autowired private ICertificateService certificateService; + + /** 证书多条件查询,参数用DTO封装 */ + @GetMapping("/list") + public TableDataInfo list(IoTCertificateQueryDTO query) { + String userId = SecurityUtils.getUnionId(); + boolean isAdmin = + SecurityUtils.getIoTUnionUser() != null && SecurityUtils.getIoTUnionUser().isAdmin(); + startPage(); + List list = + certificateService.searchCertificates( + query.getName(), + userId, + query.getSslKey(), + query.getExpireStart(), + query.getExpireEnd(), + isAdmin); + if (CollUtil.isNotEmpty(list)) { + list.forEach( + item -> { + if (StrUtil.isNotBlank(item.getCertPassword())) { + item.setCertPassword(null); + } + if (StrUtil.isNotBlank(item.getKeyPassword())) { + item.setKeyPassword(null); + } + }); + } + return getDataTable(list); + } + + @GetMapping("/get") + public IoTCertificate get(@RequestParam String sslKey) { + IoTCertificate cert = certificateService.getBySslKey(sslKey); + String userId = SecurityUtils.getUnionId(); + boolean isAdmin = + SecurityUtils.getIoTUnionUser() != null && SecurityUtils.getIoTUnionUser().isAdmin(); + if (cert == null || (!isAdmin && !userId.equals(cert.getCreateUser()))) { + return null; // 或抛出无权限异常 + } + return cert; + } + + @PostMapping("/add") + public AjaxResult add(@RequestBody IoTCertificate cert) { + // 仅管理员可添加默认证书 + if (CERT_DEFAULT_KEY.equals(cert.getSslKey()) + && (SecurityUtils.getIoTUnionUser() == null + || !SecurityUtils.getIoTUnionUser().isAdmin())) { + return AjaxResult.error("无权限:只有管理员可操作默认证书"); + } + if (cert != null && StrUtil.isBlank(cert.getSslKey())) { + cert.setSslKey(IdUtil.objectId()); + } + cert.setCreateUser(SecurityUtils.getUnionId()); + certificateService.addCertificate(cert); + return AjaxResult.success("添加成功"); + } + + @PostMapping("/update") + public AjaxResult update(@RequestBody IoTCertificate cert) { + // 仅管理员可更新默认证书 + if (CERT_DEFAULT_KEY.equals(cert.getSslKey()) + && (SecurityUtils.getIoTUnionUser() == null + || !SecurityUtils.getIoTUnionUser().isAdmin())) { + return AjaxResult.error("无权限:只有管理员可操作默认证书"); + } + String userId = SecurityUtils.getUnionId(); + IoTCertificate old = certificateService.getBySslKey(cert.getSslKey()); + if (old == null + || (SecurityUtils.getIoTUnionUser() != null + && !SecurityUtils.getIoTUnionUser().isAdmin() + && !userId.equals(old.getCreateUser()))) { + return AjaxResult.error("无权限"); + } + certificateService.updateCertificate(cert); + return AjaxResult.success("更新成功"); + } + + @PostMapping("/delete") + public AjaxResult delete(@RequestParam Long id) { + IoTCertificate cert = + certificateService.listAll().stream() + .filter(c -> c.getId().equals(id)) + .findFirst() + .orElse(null); + // 仅管理员可删除默认证书 + if (cert != null && CERT_DEFAULT_KEY.equals(cert.getSslKey())) { + return AjaxResult.error("默认证书,只能编辑不能删除"); + } + String userId = SecurityUtils.getUnionId(); + if (cert == null + || (SecurityUtils.getIoTUnionUser() != null + && !SecurityUtils.getIoTUnionUser().isAdmin() + && !userId.equals(cert.getCreateUser()))) { + return AjaxResult.error("无权限"); + } + certificateService.deleteCertificate(id); + return AjaxResult.success("删除成功"); + } + + /** 证书内容直接上传接口 */ + @PostMapping("/uploadContent") + public AjaxResult uploadContent( + @RequestParam("sslKey") String sslKey, + @RequestParam("certContent") String certContent, + @RequestParam("keyContent") String keyContent, + @RequestParam(value = "name", required = false) String name) { + // 仅管理员可上传默认证书 + if (CERT_DEFAULT_KEY.equals(sslKey) + && (SecurityUtils.getIoTUnionUser() == null + || !SecurityUtils.getIoTUnionUser().isAdmin())) { + return AjaxResult.error("无权限:只有管理员可操作默认证书"); + } + IoTCertificate cert = + IoTCertificate.builder() + .sslKey(sslKey) + .certContent(certContent) + .keyContent(keyContent) + .name(name) + .createUser(SecurityUtils.getUnionId()) + .build(); + certificateService.addCertificate(cert); + return AjaxResult.success("上传成功"); + } + + /** 证书文件上传接口,支持pem/key文件,自动保存内容到数据库 */ + @PostMapping("/uploadFile") + public AjaxResult uploadFile( + @RequestParam("sslKey") String sslKey, + @RequestParam("file") List files, + @RequestParam(value = "name", required = false) String name) { + // 仅管理员可上传默认证书 + if (CERT_DEFAULT_KEY.equals(sslKey) + && (SecurityUtils.getIoTUnionUser() == null + || !SecurityUtils.getIoTUnionUser().isAdmin())) { + return AjaxResult.error("无权限:只有管理员可操作默认证书"); + } + String certContent = null; + String keyContent = null; + try { + for (MultipartFile file : files) { + String filename = file.getOriginalFilename(); + String content = new String(file.getBytes()); + if (filename != null && filename.endsWith(".pem")) { + certContent = content; + } else if (filename != null + && (filename.endsWith(".key") || filename.contains("private"))) { + keyContent = content; + } + } + if (certContent == null || keyContent == null) { + return AjaxResult.error("请同时上传pem和key文件"); + } + IoTCertificate cert = + IoTCertificate.builder() + .sslKey(sslKey) + .certContent(certContent) + .keyContent(keyContent) + .name(name) + .createUser(SecurityUtils.getUnionId()) + .build(); + certificateService.addCertificate(cert); + return AjaxResult.success("上传并保存成功", cert); + } catch (Exception e) { + return AjaxResult.error("上传失败: " + e.getMessage()); + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceController.java new file mode 100644 index 0000000..84efb18 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceController.java @@ -0,0 +1,317 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.web; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.ExcelUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.platform.service.IIoTDeviceService; +import cn.universal.admin.system.service.IIoTUserApplicationService; +import cn.universal.admin.system.service.IoTDeviceProtocolService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.common.domain.R; +import cn.universal.common.exception.IoTException; +import cn.universal.common.poi.ExcelTemplate; +import cn.universal.core.service.IUP; +import cn.universal.core.service.IoTDownlFactory; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTDeviceShadow; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.IoTUserApplication; +import cn.universal.persistence.entity.bo.IoTDeviceBO; +import cn.universal.persistence.entity.bo.IoTGwDeviceBO; +import cn.universal.persistence.entity.vo.GatewayDeviceVo; +import cn.universal.persistence.entity.vo.IoTDeviceCompanyVO; +import cn.universal.persistence.entity.vo.IoTDeviceModelVO; +import cn.universal.persistence.mapper.IoTProductMapper; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Objects; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +/** + * 设备Controller @Author ruoyi + * + * @since 2025-12-24 + */ +@RestController +@RequestMapping("/admin/v1/device") +public class IoTDeviceController extends BaseController { + + @Autowired private IIoTDeviceService iIotDeviceService; + + @Resource private IoTProductDeviceService iotProductDeviceService; + + @Resource private IoTDeviceProtocolService ioTDeviceProtocolService; + + @Resource private IIoTUserApplicationService iIotUserApplicationService; + + @Resource private IoTProductMapper ioTProductMapper; + + /** 查询设备列表 */ + @GetMapping("/list") + public TableDataInfo list(IoTDevice ioTDevice) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + startPage(); + List list = iIotDeviceService.selectDevInstanceList(ioTDevice, iotUser); + return getDataTable(list); + } + + /** 查询分组未绑定设备列表 */ + @GetMapping("/list/{groupId}") + public TableDataInfo list(@PathVariable("groupId") String groupId, IoTDevice ioTDevice) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + startPage(); + List list = + iIotDeviceService.selectNotBindDevInstanceList(groupId, ioTDevice, iotUser); + return getDataTable(list); + } + + /** 查询未绑定的设备列表 */ + @GetMapping("/unlist") + public TableDataInfo unlist(IoTDevice ioTDevice) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + startPage(); + List list = iIotDeviceService.selectDevInstanceUnList(ioTDevice, iotUser); + return getDataTable(list); + } + + /** 导出设备列表 */ + @PostMapping("/export") + @Log(title = "导出设备列表", businessType = BusinessType.EXPORT) + public void export(HttpServletResponse response, IoTDevice ioTDevice) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + List list = iIotDeviceService.selectDevInstanceForExport(ioTDevice, iotUser); + ExcelUtil util = new ExcelUtil(IoTDevice.class); + util.exportExcel(response, list, "设备数据"); + } + + @Operation(summary = "设备模板下载") + @PostMapping("/import/template") + public void importTemplate( + @RequestParam() @Parameter(description = "产品编号", required = true) String productKey, + HttpServletResponse response) { + ExcelTemplate excelTemplate = iIotDeviceService.importTemplateExcel(productKey); + excelTemplate.exportExcel(response); + } + + /** 导入设备列表 */ + @PostMapping("/import") + @Log(title = "设备批量导入", businessType = BusinessType.IMPORT) + public AjaxResult importDevice( + MultipartFile file, + @RequestParam() @Parameter(description = "产品编号", required = true) String productKey, + @RequestParam(required = false) @Parameter(description = "父设备deviceId", required = false) + String deviceId) + throws Exception { + + ExcelUtil util = new ExcelUtil<>(IoTDevice.class); + IoTProduct ioTProduct = ioTProductMapper.getProductByProductKey(productKey); + if (Objects.isNull(ioTProduct)) { + return AjaxResult.error("没有找到产品"); + } + // 每一列的标题,list集合,标题所在行数 - 1 + List devicesImport = util.importExcel(file.getInputStream(), 1); + + int num = iIotDeviceService.importDevice(devicesImport, ioTProduct, deviceId); + return AjaxResult.success(String.format("成功导入%s条数据", num)); + } + + /** 获取设备详细信息 */ + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") String id) { + + IoTDevice ioTDevice = iIotDeviceService.selectDevInstanceById(id); + // if (ObjectUtil.isEmpty(ioTDevice)) { + // throw new IoTException("设备不存在"); + // } + // IoTUser iotUser = checkParent(SecurityUtils.getUnionId()); + // if (!iotUser.isAdmin() && !ioTDevice.getCreatorId() + // .equals(iotUser.getUnionId())) { + // throw new IoTException("你无权操作"); + // } + // 是否有编解码插件 + int count = ioTDeviceProtocolService.countProtocol(ioTDevice.getProductKey()); + IoTDeviceBO ioTDeviceBO = new IoTDeviceBO(); + BeanUtil.copyProperties(ioTDevice, ioTDeviceBO); + // 查询网关产品信息 + if (StrUtil.isNotBlank(ioTDeviceBO.getGwProductKey())) { + IoTDevice parentDevice = + iIotDeviceService.selectDeviceByDeviceId(ioTDevice.getGwProductKey()); + if (ObjectUtil.isNotNull(parentDevice)) { + ioTDeviceBO.setParentName( + parentDevice.getDeviceId() + "(" + parentDevice.getDeviceName() + ")"); + } + } + if (StrUtil.isNotBlank(ioTDevice.getApplication())) { + ioTDeviceBO.setApplication( + iIotUserApplicationService.selectApplicationName(ioTDevice.getApplication())); + } + ioTDeviceBO.setDevGroupName(iIotDeviceService.selectDevInstanceGroupName(ioTDevice.getIotId())); + ioTDeviceBO.setHasProtocol(count > 0); + return AjaxResult.success(ioTDeviceBO); + } + + /** 新增设备 */ + @PostMapping + @Log(title = "新增设备", businessType = BusinessType.INSERT) + public R add(@RequestBody IoTDeviceBO devInstancebo) { + R result = iIotDeviceService.insertDevInstance(devInstancebo); + JSONObject jsonObject = JSONUtil.parseObj(result); + String code = jsonObject.getStr("code"); + String msg = jsonObject.getStr("msg"); + if ("0".equals(code)) { + return R.ok(); + } else { + return R.error(msg); + } + } + + /** 修改设备 */ + @PutMapping + @Log(title = "修改设备", businessType = BusinessType.UPDATE) + public R edit(@RequestBody IoTDeviceBO devInstancebo) { + IoTDevice ioTDevice = + iIotDeviceService.selectDevInstanceById(String.valueOf(devInstancebo.getId())); + R result = iIotDeviceService.updateDevInstance(devInstancebo); + JSONObject jsonObject = JSONUtil.parseObj(result); + String code = jsonObject.getStr("code"); + String msg = jsonObject.getStr("msg"); + if ("0".equals(code)) { + return R.ok(); + } else { + return R.error(msg); + } + } + + /** 绑定设备平台 */ + @PutMapping(value = "/app") + @Log(title = "绑定设备平台", businessType = BusinessType.OTHER) + public AjaxResult bindApp(@RequestBody JSONObject object) { + String ids = object.getStr("ids"); + String appUniqueId = object.getStr("appUniqueId"); + if (StrUtil.isBlank(appUniqueId)) { + String id = ids.split(",")[0]; + IoTDevice ioTDevice = iIotDeviceService.selectDevInstanceById(id); + appUniqueId = ioTDevice.getApplication(); + } + IoTUserApplication application = + iIotUserApplicationService.selectIotUserApplicationById(appUniqueId); + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (ObjectUtil.isEmpty(application) + || (!iotUser.isAdmin() && !application.getUnionId().equals(iotUser.getUnionId()))) { + throw new IoTException("你无权操作"); + } + return toAjax(iIotDeviceService.bindApp(object.getStr("ids"), object.getStr("appUniqueId"))); + } + + /** 删除设备 */ + @DeleteMapping("/{ids}") + @Log(title = "删除设备", businessType = BusinessType.DELETE) + public R remove(@PathVariable String[] ids) { + R result = iIotDeviceService.deleteDevInstanceByIds(ids); + JSONObject jsonObject = JSONUtil.parseObj(result); + String code = jsonObject.getStr("code"); + String msg = jsonObject.getStr("msg"); + if ("0".equals(code)) { + return R.ok(); + } else { + return R.error(msg); + } + } + + /** 获取所有设备型号 */ + @Operation(summary = "获取所有设备型号") + @GetMapping("/models") + public AjaxResult> getModels( + @RequestParam(required = false, defaultValue = "false") + @Parameter(description = "只查询子设备", required = false) + Boolean child, + @Parameter(description = "支持应用", required = false) String apps) { + List ioTDeviceCompanyVOS = + iIotDeviceService.queryAllBySupportChild(child, apps); + return AjaxResult.success(ioTDeviceCompanyVOS); + } + + /** 获取设备影子 */ + @Operation(summary = "获取设备影子") + @GetMapping("/shadow/{id}") + public AjaxResult getDeviceShadow(@PathVariable("id") String id) { + IoTDeviceShadow deviceShadow = iIotDeviceService.getDeviceShadow(id); + return AjaxResult.success(deviceShadow); + } + + /** 下行加保存日志 */ + @RequestMapping("/down/{productKey}") + @Operation(summary = "下行加保存日志") + @Log(title = "下行加保存日志", businessType = BusinessType.OTHER) + public R iotFunctionsDown( + @PathVariable("productKey") String productKey, @RequestBody String downRequest) { + return iIotDeviceService.iotFunctionsDown(productKey, downRequest); + } + + /** 上行 */ + @RequestMapping("/up/{productKey}") + @Log(title = "上行", businessType = BusinessType.OTHER) + public R iotUp(@PathVariable("productKey") String productKey, @RequestBody String upMsg) { + IoTProduct ioTProduct = iotProductDeviceService.getProduct(productKey); + IUP doUp = IoTDownlFactory.getIUP(ioTProduct.getThirdPlatform()); + doUp.debugAsyncUP(upMsg); + return R.ok(); + } + + /** 根据产品key查型号 */ + @Operation(summary = "获取产品型号") + @GetMapping("/model/{productKey}") + public AjaxResult model(@PathVariable("productKey") String productKey) { + IoTDeviceModelVO ioTDeviceModelVO = iIotDeviceService.getModelByProductKey(productKey); + return AjaxResult.success(ioTDeviceModelVO); + } + + /** 根据产品key查询匹配的网关设备 */ + @Operation(summary = "根据产品key查询匹配的网关设备") + @GetMapping("/gateway/{productKey}") + public AjaxResult> getGatewayDeviceList( + @PathVariable("productKey") String productKey, IoTUser iotUser) { + IoTGwDeviceBO bo = new IoTGwDeviceBO(); + bo.setProductKey(productKey); + if (!iotUser.isAdmin()) { + bo.setCreatorId(iotUser.getUnionId()); + } + List gatewayDeviceVo = iIotDeviceService.selectGwIotDevices(bo); + return AjaxResult.success(gatewayDeviceVo); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceGroupController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceGroupController.java new file mode 100644 index 0000000..5de5f66 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceGroupController.java @@ -0,0 +1,113 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.web; + +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.platform.service.IIoTDeviceGroupService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.bo.IoTDeviceGroupBO; +import cn.universal.persistence.entity.vo.IoTDeviceGroupVO; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 设备分组Controller + * + * @since 2025-12-30 + */ +@RestController +@Tag(name = "设备分组管理", description = "设备分组管理") +@RequestMapping("/admin/v1/instance/group") +public class IoTDeviceGroupController extends BaseController { + + @Autowired private IIoTDeviceGroupService devGroupService; + + /** 查询设备分组列表 */ + @Operation(summary = "查询设备分组树") + @GetMapping("/list") + public AjaxResult> list() { + List ioTDeviceGroupVO = devGroupService.selectDevGroupList(); + return AjaxResult.success(ioTDeviceGroupVO); + } + + /** 获取设备分组详细信息 */ + @Operation(summary = "获取设备分组详细信息") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return AjaxResult.success(devGroupService.selectDevGroupById(id)); + } + + /** 新增设备分组 */ + @Operation(summary = "新增设备分组") + @PostMapping + @Log(title = "新增设备分组", businessType = BusinessType.INSERT) + public AjaxResult add(@RequestBody IoTDeviceGroupBO devGroup) { + return devGroupService.insertDevGroup(devGroup); + } + + /** 修改设备分组 */ + @Operation(summary = "修改设备分组") + @PutMapping + @Log(title = "修改设备分组", businessType = BusinessType.UPDATE) + public AjaxResult edit(@RequestBody IoTDeviceGroupBO devGroup) { + devGroupService.updateDevGroup(devGroup); + return AjaxResult.success(); + } + + /** 删除设备分组 */ + @Operation(summary = "删除设备分组") + @DeleteMapping("/{id}") + @Log(title = "删除设备分组", businessType = BusinessType.DELETE) + public AjaxResult remove(@PathVariable("id") Long id) { + return devGroupService.deleteDevGroupById(id); + } + + /** 批量绑定设备到分组 */ + @Operation(summary = "批量绑定设备到分组") + @PostMapping("/binds") + @Log(title = "批量绑定设备到分组", businessType = BusinessType.INSERT) + public AjaxResult binds(@RequestBody IoTDeviceGroupBO devGroup) { + return devGroupService.bindDevToGroup(devGroup); + } + + /** 查询分组下的设备列表 */ + @GetMapping("/device/list") + public TableDataInfo getDeviceList(String groupId) { + startPage(); + List list = devGroupService.selectDevInstanceListByGroupId(groupId); + return getDataTable(list); + } + + /** 解除绑定 */ + @Operation(summary = "解除绑定") + @Log(title = "解除绑定", businessType = BusinessType.OTHER) + @DeleteMapping("/{groupId}/{devId}") + public AjaxResult unBind( + @PathVariable("groupId") String groupId, @PathVariable("devId") String[] devId) { + return devGroupService.unBindDevByGroupId(groupId, devId); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceLogController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceLogController.java new file mode 100644 index 0000000..1614a64 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceLogController.java @@ -0,0 +1,74 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.web; + +import cn.universal.admin.system.web.BaseController; +import cn.universal.dm.device.service.log.IIoTDeviceDataService; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.vo.IoTDeviceLogMetadataVO; +import cn.universal.persistence.entity.vo.IoTDeviceLogVO; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.LogQuery; +import cn.universal.persistence.query.PageBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** */ +@RestController +@RequestMapping("/admin/v2/device/log") +public class IoTDeviceLogController extends BaseController { + + @Autowired private IIoTDeviceDataService iIoTDeviceDataService; + + /** 查询设备日志列表 */ + @GetMapping("/list/inner") + public PageBean listInner(@Validated LogQuery logQuery) { + PageBean devLogVoPageBean = iIoTDeviceDataService.pageList(logQuery); + return devLogVoPageBean; + } + + /** 属性消息 */ + @GetMapping("/meta/list") + public PageBean logMeta(LogQuery logQuery) { + PageBean devLogMetaVoPageBean = + iIoTDeviceDataService.queryLogMeta(logQuery); + return devLogMetaVoPageBean; + } + + /** + * 获取设备事件的统计信息 + * + * @param productKey 产品key + * @param iotId 设备id + */ + @GetMapping("/event/total/{productKey}/{iotId}") + public TableDataInfo getEventTotal( + @PathVariable("productKey") String productKey, @PathVariable("iotId") String iotId) { + PageBean devEventsPageBean = + iIoTDeviceDataService.queryEventTotal(productKey, iotId); + return getDataTable( + devEventsPageBean.getList(), Integer.parseInt(devEventsPageBean.getTotalCount() + "")); + } + + /** 查询设备日志列表 */ + @GetMapping("/list") + public TableDataInfo list(@Validated LogQuery logQuery) { + PageBean devLogVoPageBean = iIoTDeviceDataService.pageList(logQuery); + return getDataTable( + devLogVoPageBean.getList(), Integer.parseInt(devLogVoPageBean.getTotalCount() + "")); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceLogV1Controller.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceLogV1Controller.java new file mode 100644 index 0000000..1ae552f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceLogV1Controller.java @@ -0,0 +1,70 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.web; + +import cn.universal.admin.platform.service.IIoTDevLogService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.bo.IoTDeviceLogBO; +import cn.universal.persistence.entity.vo.IoTDeviceLogVO; +import cn.universal.persistence.page.TableDataInfo; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 设备日志Controller @Author ruoyi + * + * @since 2025-09-08 + */ +// @Tag(name = "设备日志控制器", description = "设备日志管理") +@RestController +@RequestMapping("/admin/v1/device/log") +public class IoTDeviceLogV1Controller extends BaseController { + + @Autowired private IIoTDevLogService devLogService; + + /** 查询设备日志列表 */ + // @Operation(summary = "查询设备日志列表") + @GetMapping("/list") + public TableDataInfo list(@Validated IoTDeviceLogBO bo) { + startPage(); + List ioTDeviceLogVOS = devLogService.queryPageList(bo); + return getDataTable(ioTDeviceLogVOS); + } + + /** 获取设备日志详细信息 */ + // @Operation(summary = "获取设备日志详细信息") + // @GetMapping("/id") + // public AjaxResult getInfo(@NotNull(message = "主键不能为空") Long id) { + // return AjaxResult.success(devLogService.queryById(id)); + // } + + /** + * 获取设备事件的统计信息 + * + * @param productId 产品Id + * @param iotId 设备id + */ + // @Operation(summary = "获取设备事件的统计信息") + @GetMapping("/event/total/{productId}/{iotId}") + public TableDataInfo getEventTotal( + @PathVariable("productId") String productId, @PathVariable("iotId") String iotId) { + List devLogVos = devLogService.queryEventTotal(productId, iotId); + return getDataTable(devLogVos); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceProtocolController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceProtocolController.java new file mode 100644 index 0000000..0413cce --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceProtocolController.java @@ -0,0 +1,183 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.web; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.ExcelUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.IoTDeviceProtocolService; +import cn.universal.admin.system.service.impl.IoTDeviceMessageCodecService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.common.exception.CodecException; +import cn.universal.common.exception.IoTException; +import cn.universal.ossm.service.ISysOssService; +import cn.universal.persistence.entity.IoTDeviceProtocol; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.bo.IoTDeviceProtocolBO; +import cn.universal.persistence.entity.vo.IoTProtocolVO; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 设备协议Controller @Author ruoyi + * + * @since 2023-01-06 + */ +@RestController +@RequestMapping("/admin/v1/protocol") +public class IoTDeviceProtocolController extends BaseController { + + @Autowired private IoTDeviceProtocolService devProtocolService; + @Resource private IoTDeviceMessageCodecService ioTDeviceMessageCodecService; + @Resource private ISysOssService ossService; + + /** 查询设备协议列表 */ + @GetMapping("/list") + public TableDataInfo list(IoTDeviceProtocol ioTDeviceProtocol) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + startPage(); + List list = + devProtocolService.selectDevProtocolList(ioTDeviceProtocol, iotUser); + return getDataTable(list); + } + + /** 查询设备协议列表 */ + @GetMapping("/list/ids") + public AjaxResult> idList(IoTDeviceProtocol ioTDeviceProtocol) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + List list = + devProtocolService.selectDevProtocolList(ioTDeviceProtocol, iotUser); + List list1 = list.stream().map(IoTDeviceProtocol::getId).collect(Collectors.toList()); + return AjaxResult.success(list1); + } + + /** 导出设备协议列表 */ + @PostMapping("/export") + @Log(title = "导出设备协议列表", businessType = BusinessType.EXPORT) + public void export(HttpServletResponse response, IoTDeviceProtocol ioTDeviceProtocol) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + List list = + devProtocolService.selectDevProtocolList(ioTDeviceProtocol, iotUser); + ExcelUtil util = new ExcelUtil(IoTDeviceProtocol.class); + util.exportExcel(response, list, "设备协议数据"); + } + + /** 获取设备协议详细信息 */ + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") String id) { + if (StrUtil.isBlank(id)) { + return AjaxResult.error("id is null"); + } + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + IoTDeviceProtocol ioTDeviceProtocol = + devProtocolService.selectDevProtocolById( + id, iotUser.isAdmin() ? null : iotUser.getUnionId()); + IoTProtocolVO devProductVO = BeanUtil.toBean(ioTDeviceProtocol, IoTProtocolVO.class); + JSONObject config = JSONUtil.parseObj(ioTDeviceProtocol.getConfiguration()); + if ("jar".equals(ioTDeviceProtocol.getType())) { + devProductVO.setFileName(config.getStr("provider")); + devProductVO.setUrl(config.getStr("location")); + } else { + devProductVO.setJscript(config.getStr("location")); + devProductVO.setNeedBs4Decode(config.getStr("needBs4Decode")); + } + + return AjaxResult.success(devProductVO); + } + + /** 新增设备协议 */ + @PostMapping + @Log(title = "新增设备协议", businessType = BusinessType.INSERT) + public AjaxResult add(@RequestBody IoTDeviceProtocolBO ioTDeviceProtocolBO) { + if (devProtocolService.selectDevProtocolById(ioTDeviceProtocolBO.getId(), null) != null) { + throw new IoTException("该产品已存在对应协议!"); + } + if (devProtocolService.countByProvider(ioTDeviceProtocolBO.getFileName()) > 0) { + throw new IoTException("该包名已存在,请修改后重试!"); + } + IoTDeviceProtocol ioTDeviceProtocol = + BeanUtil.toBean(ioTDeviceProtocolBO, IoTDeviceProtocol.class); + JSONObject object = new JSONObject(); + if (!"jar".equals(ioTDeviceProtocolBO.getType())) { + object.set("needBs4Decode", false); + object.set("location", ioTDeviceProtocolBO.getJscript()); + object.set("provider", ioTDeviceProtocolBO.getId()); + } else { + ioTDeviceProtocol.setState((byte) 0); + object.set("provider", ioTDeviceProtocolBO.getFileName()); + object.set("location", ioTDeviceProtocolBO.getUrl()); + } + + ioTDeviceProtocol.setConfiguration(object.toString()); + return toAjax(devProtocolService.insertDevProtocol(ioTDeviceProtocol)); + } + + /** 修改设备协议 */ + @PutMapping + @Log(title = "修改设备协议", businessType = BusinessType.UPDATE) + public AjaxResult edit(@RequestBody IoTDeviceProtocolBO ioTDeviceProtocolBO) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + return toAjax(devProtocolService.updateDevProtocol(ioTDeviceProtocolBO, iotUser)); + } + + /** 删除设备协议 */ + @DeleteMapping("/{ids}") + @Log(title = "删除设备协议", businessType = BusinessType.DELETE) + public AjaxResult remove(@PathVariable String[] ids) { + IoTDeviceProtocol ioTDeviceProtocol = + devProtocolService.selectDevProtocolById( + ids[0], loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + if (ObjectUtil.isNull(ioTDeviceProtocol)) { + throw new IoTException("该协议没有操作权限!"); + } + return toAjax(devProtocolService.deleteDevProtocolByIds(ids)); + } + + /** 编解码测试 */ + @PostMapping("/codec") + public AjaxResult codec(@RequestBody IoTDeviceProtocolBO ioTDeviceProtocolBO) { + + try { + if (ioTDeviceProtocolBO.getDecode()) { + return AjaxResult.success( + ioTDeviceMessageCodecService.decode( + ioTDeviceProtocolBO.getId(), ioTDeviceProtocolBO.getPayload())); + } else { + return AjaxResult.success( + ioTDeviceMessageCodecService.encode( + ioTDeviceProtocolBO.getId(), ioTDeviceProtocolBO.getPayload())); + } + } catch (CodecException e) { + return AjaxResult.success(e.getMsg()); + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceSubscribeController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceSubscribeController.java new file mode 100644 index 0000000..8569ae4 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceSubscribeController.java @@ -0,0 +1,88 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.web; + +import cn.hutool.core.util.StrUtil; +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.system.service.IoTDeviceSubscribeService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.common.domain.R; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.IoTDeviceSubscribe; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 设备分组Controller + * + * @since 2025-12-30 + */ +@RestController +@Tag(name = "网络组件", description = "mqtt,http..") +@RequestMapping("/admin/v1/subscribe") +public class IoTDeviceSubscribeController extends BaseController { + + @Autowired private IoTDeviceSubscribeService devSubscribeService; + + /** 查询订阅列表 */ + @GetMapping(value = "/list") + @Log(title = "查询订阅列表", businessType = BusinessType.OTHER) + public TableDataInfo list(IoTDeviceSubscribe sub) { + startPage(); + if (StrUtil.isBlank(sub.getProductKey()) || StrUtil.isBlank(sub.getIotId())) { + throw new IoTException("缺少必要参数"); + } + List list = devSubscribeService.selectDevSubscribeList(sub); + return getDataTable(list); + } + + /** 查看订阅详情 */ + @GetMapping(value = "/{id}") + @Log(title = "查看订阅详情", businessType = BusinessType.OTHER) + public AjaxResult info(@PathVariable("id") String id) { + return AjaxResult.success(devSubscribeService.selectDevInstanceById(id)); + } + + /** 修改订阅状态 */ + @PostMapping(value = "/update") + @Log(title = "修改订阅状态", businessType = BusinessType.OTHER) + public R updateSubscribe(@RequestBody IoTDeviceSubscribe sub) { + return devSubscribeService.updateDevSubscribe(sub); + } + + /** 新增订阅 */ + @PostMapping(value = "/add") + @Log(title = "设备新增订阅", businessType = BusinessType.OTHER) + public R bindSubscribe(@RequestBody IoTDeviceSubscribe sub) { + return devSubscribeService.insertDevSubscribe(sub); + } + + @PostMapping(value = "/delete/{productKey}/{iotId}") + @Log(title = "设备删除订阅", businessType = BusinessType.OTHER) + public R unBindSubscribe( + @PathVariable("productKey") String productKey, + @PathVariable("iotId") String iotId, + @RequestBody Long[] ids) { + return devSubscribeService.deleteDevSubscribe(productKey, iotId, ids); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTProductCategoryController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTProductCategoryController.java new file mode 100644 index 0000000..297fb78 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTProductCategoryController.java @@ -0,0 +1,148 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.web; + +import cn.hutool.core.util.StrUtil; +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.platform.service.IIoTProductSortService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.IoTProductSort; +import cn.universal.persistence.entity.bo.RuleModelBO.IoTProductSortBO; +import cn.universal.persistence.mapper.IoTProductMapper; +import cn.universal.persistence.query.AjaxResult; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 产品分类Controller @Author Aleo + * + * @since 2025-12-29 + */ +@RestController +@RequestMapping("/admin/sort") +public class IoTProductCategoryController extends BaseController { + + @Autowired private IIoTProductSortService devProductSortService; + @Resource private IoTProductMapper ioTProductMapper; + + /** 查询产品分类列表 */ + @GetMapping("/list") + public AjaxResult list(IoTProductSort ioTProductSort) { + List list = devProductSortService.selectDevProductSortList(ioTProductSort); + return AjaxResult.success(list); + } + + /** 获取产品分类树结构 */ + @GetMapping("/tree") + public AjaxResult getTree() { + List tree = devProductSortService.getProductSortTree(); + return AjaxResult.success(tree); + } + + /** 获取产品分类详细信息 */ + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") String id) { + return AjaxResult.success(devProductSortService.selectDevProductSortById(id)); + } + + /** 获取产品分类详细信息 */ + @PostMapping(value = "/info") + public AjaxResult getInfo(@RequestBody IoTProductSort ioTProductSort) { + if (StrUtil.isBlank(ioTProductSort.getId())) { + AjaxResult.success(); + } + return AjaxResult.success( + devProductSortService.selectDevProductSortById(ioTProductSort.getId())); + } + + /** 新增产品分类(使用BO) */ + @PostMapping("/add") + @Log(title = "新增产品分类", businessType = BusinessType.INSERT) + public AjaxResult addWithBo(@Valid @RequestBody IoTProductSortBO bo) { + try { + int result = devProductSortService.insertDevProductSort(bo); + return toAjax(result); + } catch (IllegalArgumentException e) { + return AjaxResult.error(e.getMessage()); + } catch (Exception e) { + return AjaxResult.error("新增产品分类失败:" + e.getMessage()); + } + } + + /** 修改产品分类(使用BO) */ + @PutMapping("/edit") + @Log(title = "修改产品分类", businessType = BusinessType.UPDATE) + public AjaxResult editWithBo(@Valid @RequestBody IoTProductSortBO bo) { + try { + int result = devProductSortService.updateDevProductSort(bo); + return toAjax(result); + } catch (IllegalArgumentException e) { + return AjaxResult.error(e.getMessage()); + } catch (Exception e) { + return AjaxResult.error("修改产品分类失败:" + e.getMessage()); + } + } + + /** 新增产品分类(原有方法,保持兼容性) */ + @PostMapping + @Log(title = "新增产品分类", businessType = BusinessType.INSERT) + public AjaxResult add(@RequestBody IoTProductSort ioTProductSort) { + return toAjax(devProductSortService.insertDevProductSort(ioTProductSort)); + } + + /** 修改产品分类(原有方法,保持兼容性) */ + @PutMapping + @Log(title = "修改产品分类", businessType = BusinessType.UPDATE) + public AjaxResult edit(@RequestBody IoTProductSort ioTProductSort) { + return toAjax(devProductSortService.updateDevProductSort(ioTProductSort)); + } + + /** 删除产品分类 */ + @DeleteMapping("/{ids}") + @Log(title = "删除产品分类", businessType = BusinessType.DELETE) + public AjaxResult remove(@PathVariable String[] ids) { + for (String id : ids) { + IoTProductSort ioTProductSort = devProductSortService.selectDevProductSortById(id); + if (ioTProductSort.getHasChild() == 1) { + throw new IoTException("该分类下有子分类"); + } + IoTProduct ioTProduct = IoTProduct.builder().classifiedId(id).build(); + List ioTProductList = ioTProductMapper.select(ioTProduct); + if (!ioTProductList.isEmpty()) { + throw new IoTException("该分类下有产品"); + } + } + return toAjax(devProductSortService.deleteDevProductSortByIds(ids)); + } + + /** 获取菜单下拉树列表 */ + @GetMapping("/treeselect") + public AjaxResult treeselect(IoTProductSort productSort) { + List productSortList = + devProductSortService.selectDevProductSortList(productSort); + return AjaxResult.success(devProductSortService.buildProductSortTree(productSortList)); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTProductController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTProductController.java new file mode 100644 index 0000000..bab3f26 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTProductController.java @@ -0,0 +1,745 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.web; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Validator; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.network.service.INetworkService; +import cn.universal.admin.platform.service.IIoTDeviceService; +import cn.universal.admin.platform.service.IIoTProductService; +import cn.universal.admin.platform.service.impl.IoTProductServiceImpl; +import cn.universal.admin.system.service.ISysDictTypeService; +import cn.universal.admin.system.service.IoTDeviceProtocolService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.common.constant.IoTConstant.ProtocolModule; +import cn.universal.common.constant.IoTConstant.TcpFlushType; +import cn.universal.common.exception.IoTException; +import cn.universal.core.service.IoTDownlFactory; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.IoTDeviceFunction; +import cn.universal.persistence.entity.IoTDeviceProperties; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.admin.SysDictData; +import cn.universal.persistence.entity.bo.IoTProductBO; +import cn.universal.persistence.entity.bo.IoTProductImportBO; +import cn.universal.persistence.entity.vo.IoTDeviceModelVO; +import cn.universal.persistence.entity.vo.IoTProductExportVO; +import cn.universal.persistence.entity.vo.IoTProductVO; +import cn.universal.persistence.mapper.IoTProductMapper; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import cn.universal.persistence.query.IoTProductQuery; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +/** + * 设备产品管理控制器 + * + * @since 2025-12-24 + */ +@RestController +@RequestMapping("/admin/v1/product") +public class IoTProductController extends BaseController { + + @Autowired + private IIoTProductService devProductService; + @Autowired + private IIoTDeviceService iIotDeviceService; + @Autowired + private ISysDictTypeService dictTypeService; + @Resource + private INetworkService iNetworkService; + @Resource + private IoTDeviceProtocolService ioTDeviceProtocolService; + @Resource + private IoTProductMapper ioTProductMapper; + @Autowired + private IoTProductServiceImpl ioTProductServiceImpl; + @Autowired + private ApplicationEventPublisher eventPublisher; + + /** + * 查询设备产品列表 + */ + @GetMapping("/list") + public TableDataInfo list(IoTProductQuery query) { + // 只能看自己设备,除非是特殊设置和超管 + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.viewAllProduct()) { + query.setSelf(true); + } + if (iotUser.isAdmin()) { + query.setSelf(false); + } + if (query.isSelf()) { + query.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + } + if (query.isHasDevice()) { + query.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + } + startPage(); + List list = devProductService.selectDevProductV2List(query); + return getDataTable(list); + } + + /** + * 获取产品详情通过 ProductKey + */ + @GetMapping("/get/pro/{key}") + public AjaxResult getProByKey(@PathVariable("key") String key) { + return devProductService.selectDevProductByKey(key); + } + + /** + * 查询设备产品列表 + */ + @GetMapping("/v2/list") + public TableDataInfo v2List(IoTProductQuery query) { + // 只能看自己设备,除非是特殊设置和超管 + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.viewAllProduct()) { + query.setSelf(true); + } + if (iotUser.isAdmin()) { + query.setSelf(false); + } + if (query.isSelf()) { + query.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + } + if (query.isHasDevice()) { + query.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + } + startPage(); + List list = devProductService.selectDevProductV2List(query); + return getDataTable(list); + } + + /** + * 查询设备产品列表 + */ + @GetMapping("/v3/list") + public TableDataInfo v3List(IoTProductQuery query) { + // 只能看自己设备,除非是特殊设置和超管 + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.viewAllProduct()) { + query.setSelf(true); + } + if (iotUser.isAdmin()) { + query.setSelf(false); + } + if (query.isSelf()) { + query.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + } + if (query.isHasDevice()) { + query.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + } + query.setState(0); + List list = devProductService.selectDevProductV3List(query); + + return getDataTable(list); + } + + /** + * 查询设备产品列表 + */ + @GetMapping("/v4/list") + public TableDataInfo v4List(IoTProductQuery query) { + // 只能看自己设备,除非是特殊设置和超管 + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.viewAllProduct()) { + query.setSelf(true); + } + if (iotUser.isAdmin()) { + query.setSelf(false); + } + if (query.isSelf()) { + query.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + } + if (query.isHasDevice()) { + query.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + } + query.setState(0); + List list = devProductService.selectDevProductV4List(query); + + return getDataTable(list); + } + + /** + * 查询所有设备产品列表 + */ + @GetMapping("/all/list") + public TableDataInfo v3List() { + List list = devProductService.selectDevProductAllList(); + return getDataTable(list); + } + + /** + * 物模型新增内容 + */ + @PostMapping("/metadata/add") + @Log(title = "物模型新增内容", businessType = BusinessType.INSERT) + public AjaxResult metadataAdd(@RequestBody IoTProductBO ioTProductBO) { + return toAjax(devProductService.insertMetadata(ioTProductBO)); + } + + /** + * 物模型修改内容 + */ + @PostMapping("/metadata/update") + @Log(title = "物模型修改内容", businessType = BusinessType.UPDATE) + public AjaxResult metadataUpdate(@RequestBody IoTProductBO ioTProductBO) { + return toAjax(devProductService.updateMetadata(ioTProductBO)); + } + + /** + * 物模型删除内容 + */ + @PostMapping("/metadata/delete") + @Log(title = "物模型删除内容", businessType = BusinessType.DELETE) + public AjaxResult metadataDelete(@RequestBody IoTProductBO ioTProductBO) { + return toAjax(devProductService.deleteMetadata(ioTProductBO)); + } + + /** + * 物模型查询内容 + */ + @PostMapping("/metadata/get") + public AjaxResult metadataGet(@RequestBody IoTProductBO ioTProductBO) { + return AjaxResult.success(devProductService.getMetadata(ioTProductBO)); + } + + /** 导出设备产品列表 */ + // @PostMapping("/export") + // public void export(HttpServletResponse response, IoTProduct ioTProduct) + // { + // List list = devProductService.selectDevProductList(ioTProduct); + // ExcelUtil util = new ExcelUtil(IoTProduct.class); + // util.exportExcel(response, list, "设备产品数据"); + // } + + /** + * 获取设备产品详细信息 + */ + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") String id) { + return AjaxResult.success(devProductService.selectDevProductById(id)); + } + + /** + * 查询可绑定网关列表 + */ + @Operation(summary = "查询可绑定网关产品列表") + @GetMapping("/gateway/list") + public AjaxResult> getGatewayList() { + List list = devProductService.getGatewayList(); + return AjaxResult.success(list); + } + + /** + * 查询可绑定网关子设备产品列表 + */ + @Operation(summary = "查询可绑定网关子设备产品列表") + @GetMapping("/subdevice/{productKey}") + public AjaxResult> getGatewaySubDeviceList( + @PathVariable("productKey") String productKey) { + List list = devProductService.getGatewaySubDeviceList(productKey); + return AjaxResult.success(list); + } + + /** + * 查询电信公共产品列表 + */ + @PostMapping(value = "/ctwing/pubpro") + public AjaxResult getCtwingPubPro(@RequestBody String downRequest) { + String creatorId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId(); + JSONObject jsonObject = JSONUtil.parseObj(downRequest); + JSONObject downProRequest = new JSONObject(); + Map data = new HashMap<>(); + data.put("searchValue", jsonObject.getStr("searchValue")); + data.put("pageNow", jsonObject.getInt("pageNum")); + data.put("pageSize", jsonObject.getInt("pageSize")); + downProRequest.set("creatorId", creatorId); + downProRequest.set("cmd", "PUBPRO_GET"); + downProRequest.set("data", data); + return AjaxResult.success( + IoTDownlFactory.safeInvokeDown( + ProtocolModule.ctaiot.name(), "downPro", downProRequest.toString())); + } + + /** + * 新增设备产品 + */ + @PostMapping + @Log(title = "新增产品", businessType = BusinessType.INSERT) + public AjaxResult add(@RequestBody IoTProduct ioTProduct) { + String unionId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId(); + ioTProduct.setCreatorId(unionId); + return toAjax(devProductService.insertDevProduct(ioTProduct)); + } + + /** + * 新增设备产品NB + */ + @PostMapping(value = "/nb") + @Log(title = "新增产品NB", businessType = BusinessType.INSERT) + public AjaxResult addNb(@RequestBody String downRequest) { + String creatorId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId(); + JSONObject jsonObject = JSONUtil.parseObj(downRequest); + IoTProduct ioTProduct = IoTProduct.builder().name(jsonObject.getStr("name")).build(); + List ioTProductList = devProductService.selectDevProductList(ioTProduct); + if (!ioTProductList.isEmpty()) { + throw new IoTException("产品名称已存在"); + } + JSONObject downProRequest = new JSONObject(); + Map data = new HashMap<>(); + data.put("productName", jsonObject.getStr("name")); + data.put("tupDeviceModel", jsonObject.getStr("productId")); + data.put("tupIsThrough", jsonObject.getInt("tupIsThrough")); + data.put("payloadFormat", jsonObject.getInt("payloadFormat")); + data.put("powerModel", jsonObject.getInt("powerModel")); + data.put("lwm2mEdrxTime", jsonObject.getStr("lwm2mEdrxTime")); + + downProRequest.set("creatorId", creatorId); + downProRequest.set("cmd", "pro_add"); + downProRequest.set("companyNo", jsonObject.getStr("companyNo")); + downProRequest.set("classifiedId", jsonObject.getStr("classifiedId")); + downProRequest.set("classifiedName", jsonObject.getStr("classifiedName")); + downProRequest.set("data", data); + return AjaxResult.success( + IoTDownlFactory.safeInvokeDown( + ProtocolModule.ctaiot.name(), "downPro", downProRequest.toString())); + } + + /** + * 新增电信公共产品 + */ + @PostMapping(value = "/ctwing/pubproadd") + @Log(title = "新增电信公共产品", businessType = BusinessType.INSERT) + public AjaxResult addCtwingPubPro(@RequestBody String downRequest) { + String creatorId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId(); + JSONObject jsonObject = JSONUtil.parseObj(downRequest); + JSONObject downProRequest = new JSONObject(); + Map data = new HashMap<>(); + Integer publicProductID = Integer.parseInt(jsonObject.getStr("publicProductID")); + data.put("publicProductId", publicProductID); + data.put("productId", jsonObject.getStr("productId")); + downProRequest.set("creatorId", creatorId); + downProRequest.set("classifiedId", jsonObject.getStr("classifiedId")); + downProRequest.set("classifiedName", jsonObject.getStr("classifiedName")); + downProRequest.set("companyNo", jsonObject.getStr("companyNo")); + downProRequest.set("cmd", "PUBPRO_ADD"); + downProRequest.set("data", data); + return AjaxResult.success( + IoTDownlFactory.safeInvokeDown( + ProtocolModule.ctaiot.name(), "downPro", downProRequest.toString())); + } + + /** + * 修改设备产品 + */ + @PostMapping(value = "/updateNetworkUnionId") + @Log(title = "修改网络组件", businessType = BusinessType.UPDATE) + public AjaxResult editNetwork(@RequestBody IoTProductBO ioTProductBO) { + if (ioTProductBO.getId() == null) { + throw new IoTException("id不能为空"); + } + // if (StrUtil.isBlank(ioTProductBO.getNetworkUnionId())) { + // throw new IoTException("networkUnionId不能为空"); + // } + return toAjax( + ioTProductServiceImpl.updateNetworkUnionId( + ioTProductBO.getId(), ioTProductBO.getNetworkUnionId())); + } + + /** + * 子设备绑定网关 + */ + @Operation(summary = "子设备绑定网关") + @PutMapping("/gateway") + @Log(title = "子设备绑定网关", businessType = BusinessType.UPDATE) + public AjaxResult editGateway(@RequestBody IoTProductBO ioTProductBO) { + Long id = ioTProductBO.getId(); + String gwProductKey = ioTProductBO.getGwProductKey(); + logger.info("子设备产品绑定网关,id={},gwProductKey={}", id, gwProductKey); + IoTProductVO productVO = + devProductService.selectDevProductById(String.valueOf(ioTProductBO.getId())); + if (productVO == null) { + return AjaxResult.error("网关产品不存在"); + } + productVO.setGwProductKey(gwProductKey); + + return toAjax(devProductService.updateDevProduct(productVO)); + } + + /** + * 修改设备产品 + */ + @PutMapping + @Log(title = "修改设备产品", businessType = BusinessType.UPDATE) + public AjaxResult edit(@RequestBody IoTProductBO ioTProductBO) { + if (ProtocolModule.ctaiot.name().equals(ioTProductBO.getThirdPlatform())) { + IoTProduct pro = devProductService.selectDevProductById(ioTProductBO.getId().toString()); + if (pro == null) { + throw new IoTException("没有该产品"); + } + Map data = new HashMap<>(); + JSONObject downProRequest = new JSONObject(); + JSONObject jsonObject = JSONUtil.parseObj(pro.getConfiguration()); + Integer powerModel = ioTProductBO.getPowerModel(); + data.put("powerModel", ioTProductBO.getPowerModel()); + data.put("productName", ioTProductBO.getName()); + data.put("productId", jsonObject.getStr("productId")); + data.put("classifiedId", ioTProductBO.getClassifiedId()); + if (powerModel == 3) { + data.put("lwm2mEdrxTime", ioTProductBO.getLwm2mEdrxTime()); + } + downProRequest.set("cmd", "pro_update"); + downProRequest.set("data", data); + downProRequest.set("productKey", ioTProductBO.getProductKey()); + return AjaxResult.success( + IoTDownlFactory.safeInvokeDown( + ProtocolModule.ctaiot.name(), "downPro", downProRequest.toString())); + } + IoTProduct ioTProduct = BeanUtil.toBean(ioTProductBO, IoTProduct.class); + return toAjax(devProductService.updateDevProduct(ioTProduct)); + } + + /** + * 修改产品配置信息 + */ + @PutMapping("/config") + @Log(title = "修改产品配置信息", businessType = BusinessType.UPDATE) + public AjaxResult editConfig(@RequestBody IoTProductVO devProduct) { + return toAjax(devProductService.updateDevProductConfig(devProduct)); + } + + /** + * 修改产品其他配置信息 + */ + @PutMapping("/otherConfig") + @Log(title = "修改产品其他配置信息", businessType = BusinessType.UPDATE) + public AjaxResult editOtherConfig(@RequestBody String otherConfig) { + return toAjax(devProductService.updateDevProductOtherConfig(otherConfig)); + } + + /** + * 修改产品存储策略 + */ + @PutMapping("/storeConfig") + @Log(title = "修改产品存储策略", businessType = BusinessType.UPDATE) + public AjaxResult editOtherConfig(@RequestBody IoTProductVO devProduct) { + return toAjax(devProductService.updateDevProductStoreConfig(devProduct)); + } + + /** + * 删除设备产品 + */ + @DeleteMapping("/{ids}") + @Log(title = "删除设备产品", businessType = BusinessType.DELETE) + public AjaxResult remove(@PathVariable String[] ids) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + for (String id : ids) { + IoTProduct ioTProduct = devProductService.selectDevProductById(id); + if (ioTProduct == null) { + throw new IoTException("没有该产品"); + } + if (!iotUser.isAdmin() && !ioTProduct.getCreatorId().equals(iotUser.getUnionId())) { + throw new IoTException("您没有权限操作此产品!"); + } + IoTDevice ioTDevice = IoTDevice.builder().productKey(ioTProduct.getProductKey()).build(); + List ioTDeviceList = + iIotDeviceService.selectDevInstanceList( + ioTDevice, loginIoTUnionUser(SecurityUtils.getUnionId())); + if (!ioTDeviceList.isEmpty()) { + throw new IoTException("该产品下还有设备"); + } + if (ProtocolModule.ctaiot.name().equals(ioTProduct.getThirdPlatform())) { + Map data = new HashMap<>(); + JSONObject downProRequest = new JSONObject(); + JSONObject object = JSONUtil.parseObj(ioTProduct.getConfiguration()); + data.put("productId", object.getStr("productId")); + data.put("masterKey", object.getStr("masterKey")); + downProRequest.set("cmd", "pro_del"); + downProRequest.set("data", data); + downProRequest.set("productKey", ioTProduct.getProductKey()); + + return AjaxResult.success( + IoTDownlFactory.safeInvokeDown( + ProtocolModule.ctaiot.name(), "downPro", downProRequest.toString())); + } + // tcp产品删除时同时删除network表 + if (ProtocolModule.tcp.name().equals(ioTProduct.getThirdPlatform())) { + // 停止监听 + devProductService.flushNettyServer( + ioTProduct.getConfiguration(), ioTProduct.getProductKey(), TcpFlushType.close); + iNetworkService.del(ioTProduct.getProductKey()); + } + // 删除协议 + ioTDeviceProtocolService.deleteDevProtocolById(ioTProduct.getProductKey()); + } + return toAjax(devProductService.deleteDevProductByIds(ids)); + } + + /** + * 查询设备产品物模型属性列表 + */ + @GetMapping("/properties/{id}") + public AjaxResult getPropertiesList(@PathVariable("id") String id) { + List list = devProductService.selectDevProperties(id); + return AjaxResult.success(list); + } + + /** + * 查询设备产品物模型属性列表(功能下发属性) + */ + @GetMapping("/functionProperties/{id}") + public AjaxResult getFunctionPropertiesList(@PathVariable("id") String id) { + List list = devProductService.selectDevProperties(id); + // 过滤可读写属性 + List dataRw = + list.stream().filter((item) -> "rw".equals(item.getMode())).collect(Collectors.toList()); + // 获取设备公共字段 + List data = dictTypeService.selectDictDataByType("device_common_property"); + // 根据公共字段过滤 + if (Validator.isNotNull(data)) { + for (SysDictData datum : data) { + for (int i = 0; i < dataRw.size(); i++) { + if (dataRw.get(i).getId().equals(datum.getDictValue())) { + dataRw.remove(i); + break; + } + } + dataRw.add( + IoTDeviceProperties.builder() + .description(datum.getRemark() + "(公共字段)") + .id(datum.getDictValue()) + .type("string") + .mode("r") + .name(datum.getDictLabel()) + .build()); + } + } + return AjaxResult.success(dataRw); + } + + /** + * 查询设备产品物模型事件列表 + */ + @GetMapping("/events/{id}") + public AjaxResult getEventsList(@PathVariable("id") String id) { + List list = devProductService.selectDevEvents(id); + return AjaxResult.success(list); + } + + /** + * 查询设备产品物模型方法列表 + */ + @GetMapping("/functions/{id}") + public AjaxResult getFunctionsList(@PathVariable("id") String id) { + List list = devProductService.selectDevFunctions(id); + return AjaxResult.success(list); + } + + /** + * 根据产品key查model配置 + */ + @GetMapping("/model/{id}") + public AjaxResult getmodel(@PathVariable("id") String id) { + IoTDeviceModelVO ioTDeviceModelVO = devProductService.getModelByProductKey(id); + return AjaxResult.success(ioTDeviceModelVO); + } + + @GetMapping("/metadata/{devId}") + public AjaxResult getMetaByDevId(@PathVariable("devId") String devId) { + return AjaxResult.success(devProductService.selectMetadataByDevId(devId)); + } + + @GetMapping("/devnumber") + public AjaxResult countDevNumberByProductKey() { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + Map products = + devProductService.countDevNumberByProductKey( + iotUser.isAdmin() ? null : iotUser.getUnionId()); + return AjaxResult.success(products); + } + + /** + * 导出设备产品列表 + */ + @PostMapping("/export") + @Log(title = "产品导出", businessType = BusinessType.EXPORT) + public void export(HttpServletResponse response, IoTProductQuery query) { + // 只能看自己设备,除非是特殊设置和超管 + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + query.setSelf(!iotUser.isAdmin()); + if (query.isSelf()) { + query.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + } + if (query.isHasDevice()) { + query.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + } + List list = devProductService.selectAllDevProductV2List(query); + + // 设置响应头为文件下载格式 + response.setContentType("application/octet-stream"); + response.setCharacterEncoding("utf-8"); + response.setHeader("Content-Disposition", "attachment; filename=products_export.json"); + // 添加缓存控制头,防止浏览器缓存 + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + // 强制下载,防止浏览器直接显示 + response.setHeader("X-Content-Type-Options", "nosniff"); + response.setHeader("X-Download-Options", "noopen"); + // 确保响应不会被压缩 + response.setHeader("Content-Encoding", "identity"); + + try { + // 将数据转换为JSON格式并写入响应 + String jsonData = JSONUtil.toJsonPrettyStr(list); + byte[] bytes = jsonData.getBytes("UTF-8"); + response.setContentLength(bytes.length); + + // 使用缓冲输出流确保数据完整写入 + try (java.io.OutputStream out = response.getOutputStream()) { + out.write(bytes); + out.flush(); + } + } catch (Exception e) { + throw new IoTException("导出失败:" + e.getMessage()); + } + } + + /** + * 批量导入产品 + */ + @PostMapping("/import") + @Log(title = "产品批量导入", businessType = BusinessType.IMPORT) + @Transactional(rollbackFor = Exception.class) + public AjaxResult importProduct(MultipartFile file) throws Exception { + // 模板 + // 只能看自己设备,除非是特殊设置和超管 + String unionId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId(); + try { + // 读取JSON文件内容 + String jsonContent = new String(file.getBytes(), "UTF-8"); + + // 解析JSON数据 + List productImportBos = + JSONUtil.toList(jsonContent, IoTProductImportBO.class); + + if (productImportBos == null || productImportBos.isEmpty()) { + return AjaxResult.error("导入文件为空或格式不正确"); + } + + String result = devProductService.importProduct(productImportBos, unionId); + return AjaxResult.success(result); + } catch (Exception e) { + return AjaxResult.error("导入失败:" + e.getMessage()); + } + } + + /** + * 下载导入模版 + */ + @PostMapping("/import/template") + public void downloadTemplate(HttpServletResponse response) { + List list = new ArrayList<>(); + // 设置响应头为文件下载格式 + response.setContentType("application/octet-stream"); + response.setCharacterEncoding("utf-8"); + response.setHeader("Content-Disposition", "attachment; filename=products_import_template.json"); + // 添加缓存控制头,防止浏览器缓存 + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + // 强制下载,防止浏览器直接显示 + response.setHeader("X-Content-Type-Options", "nosniff"); + response.setHeader("X-Download-Options", "noopen"); + // 确保响应不会被压缩 + response.setHeader("Content-Encoding", "identity"); + + try { + // 将数据转换为JSON格式并写入响应 + String jsonData = JSONUtil.toJsonPrettyStr(list); + byte[] bytes = jsonData.getBytes("UTF-8"); + response.setContentLength(bytes.length); + + // 使用缓冲输出流确保数据完整写入 + try (java.io.OutputStream out = response.getOutputStream()) { + out.write(bytes); + out.flush(); + } + } catch (Exception e) { + throw new IoTException("模板下载失败:" + e.getMessage()); + } + } + + /** + * 产品模糊搜索接口 + * + * @param query 搜索关键字 + * @return productKey和name列表 + */ + @GetMapping("/search") + public TableDataInfo searchProductKeyAndName(IoTProductQuery query) { + List> maps = ioTProductMapper.searchProductKeyAndName(query); + return getDataTable(maps); + } + + /** + * 绑定证书到产品 + */ + @PostMapping("/bindCertificate") + @Log(title = "绑定证书", businessType = BusinessType.UPDATE) + public AjaxResult bindCertificate(@RequestParam String productKey, @RequestParam String sslKey) { + int result = devProductService.bindCertificate(productKey, sslKey); + return toAjax(result); + } + + /** + * 解绑证书 + */ + @PostMapping("/unbindCertificate") + @Log(title = "解绑证书", businessType = BusinessType.UPDATE) + public AjaxResult unbindCertificate(@RequestParam String productKey) { + int result = devProductService.unbindCertificate(productKey); + return toAjax(result); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTUserAppController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTUserAppController.java new file mode 100644 index 0000000..a6164d5 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTUserAppController.java @@ -0,0 +1,326 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.web; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.Header; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.ExcelUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.platform.service.IIoTDeviceService; +import cn.universal.admin.system.service.IIoTUserApplicationService; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.admin.system.service.IOAuthClientDetailsService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.IoTUserApplication; +import cn.universal.persistence.entity.OAuth2ClientDetails; +import cn.universal.persistence.entity.vo.IoTDeviceCountVO; +import cn.universal.persistence.entity.vo.IoTUserApplicationVO; +import cn.universal.persistence.mapper.IoTUserApplicationMapper; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 用户应用信息管理控制器 + * + * @since 2025-12-30 + */ +@RestController +@Tag(name = "用户应用信息管理", description = "用户应用信息管理") +@RequestMapping("/admin/v1/apply") +@Slf4j +public class IoTUserAppController extends BaseController { + + @Autowired private IIoTUserApplicationService iotUserApplicationService; + @Resource private IoTUserApplicationMapper iotUserApplicationMapper; + @Resource private IOAuthClientDetailsService oauthClientDetailsService; + @Resource private IIoTDeviceService iIotDeviceService; + @Resource private IIotUserService iIotUserService; + + /** 查询用户应用信息列表 */ + @Operation(summary = "查询用户应用信息列表") + @GetMapping("/list") + public TableDataInfo list(IoTUserApplication iotUserApplication) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + startPage(); + List list = + iotUserApplicationService.selectIotUserApplicationList(iotUserApplication, iotUser); + List map = iIotDeviceService.countDevByApplication(null); + Map map1 = + map.stream() + .collect( + Collectors.toMap(IoTDeviceCountVO::getApplication, IoTDeviceCountVO::getDevNum)); + list.forEach( + applicationVo -> { + applicationVo.setDevNum( + map1.get(applicationVo.getAppUniqueId()) != null + ? Integer.parseInt(map1.get(applicationVo.getAppUniqueId())) + : 0); + }); + // 按设备数量降序排序 + List sort = + CollectionUtil.sort(list, Comparator.comparing(IoTUserApplicationVO::getDevNum).reversed()); + return getDataTable(sort); + } + + /** 导出用户应用信息列表 */ + @Operation(summary = "导出用户应用信息列表") + @PostMapping("/export") + @Log(title = "导出用户应用信息列表", businessType = BusinessType.EXPORT) + public void export(HttpServletResponse response, IoTUserApplication iotUserApplication) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + iotUserApplication.setUnionId( + iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()).isAdmin() + ? null + : SecurityUtils.getUnionId()); + List list = + iotUserApplicationService.selectIotUserApplicationList(iotUserApplication, iotUser); + List map = iIotDeviceService.countDevByApplication(null); + Map map1 = + map.stream() + .collect( + Collectors.toMap(IoTDeviceCountVO::getApplication, IoTDeviceCountVO::getDevNum)); + list.forEach( + applicationVo -> { + applicationVo.setDevNum( + map1.get(applicationVo.getAppUniqueId()) != null + ? Integer.parseInt(map1.get(applicationVo.getAppUniqueId())) + : 0); + }); + ExcelUtil util = + new ExcelUtil(IoTUserApplicationVO.class); + util.exportExcel(response, list, "用户应用信息数据"); + } + + /** 获取用户应用信息详细信息 */ + @Operation(summary = "获取用户应用信息详细信息") + @GetMapping(value = "/{appUniqueId}") + public AjaxResult getInfo(@PathVariable("appUniqueId") String appUniqueId) { + IoTUserApplication application = + iotUserApplicationService.selectIotUserApplicationById(appUniqueId); + IoTUser user = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!user.isAdmin() + && ObjectUtil.isNotEmpty(application) + && !application.getUnionId().equals(user.getUnionId())) { + throw new IoTException("你无权操作"); + } + return AjaxResult.success(application); + } + + /** 新增用户应用信息 */ + @Operation(summary = "新增用户应用信息") + @PostMapping + @Transactional + @Log(title = "新增用户应用信息", businessType = BusinessType.INSERT) + public AjaxResult add(@RequestBody IoTUserApplication iotUserApplication) { + String appId = RandomUtil.randomString(16); + // 检查appid是否重复 + while (!CollectionUtils.isEmpty( + iotUserApplicationService.selectIotUserApplicationList( + IoTUserApplication.builder().appId(appId).build(), + IoTUser.builder().identity(0).build()))) { + appId = RandomUtil.randomString(16); + } + iotUserApplication.setAppUniqueId(String.valueOf(IdUtil.getSnowflake().nextId())); + iotUserApplication.setAppId(appId); + iotUserApplication.setAppSecret(RandomUtil.randomString(32)); + iotUserApplication.setCreateDate(new Date()); + iotUserApplication.setUnionId(SecurityUtils.getUnionId()); + if (iotUserApplicationService.insertIotAppUser(iotUserApplication) == 0) { + throw new IoTException("新增应用失败!"); + } + if (oauthClientDetailsService.insert( + OAuth2ClientDetails.builder() + .clientId(iotUserApplication.getAppId()) + .clientSecret(new BCryptPasswordEncoder().encode(iotUserApplication.getAppSecret())) + .scope("all") + .AuthorizedGrantTypes("client_credentials") + .webServerRedirectUri("http://localhost:9091/client-authcode/login") + .accessTokenValidity(7200) + .refreshTokenValidity(36000) + .autoapprove("1") + .iotUnionId(iotUserApplication.getUnionId()) + .build()) + == 0) { + throw new IoTException("认证信息新增失败!"); + } + return AjaxResult.success(); + } + + /** 重置密钥 */ + @Operation(summary = "重置密钥") + @PutMapping(value = "/reset") + @Transactional + @Log(title = "重置密钥", businessType = BusinessType.UPDATE) + public AjaxResult resetSecret(@RequestBody IoTUserApplication iotUserApplication) { + IoTUserApplication oldapplication = + iotUserApplicationService.selectIotUserApplicationById(iotUserApplication.getAppUniqueId()); + IoTUser user = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!user.isAdmin() + && ObjectUtil.isNotEmpty(oldapplication) + && !oldapplication.getUnionId().equals(user.getUnionId())) { + throw new IoTException("你无权操作"); + } + // iotUserApplication.setAppId(RandomUtil.randomString(16)); + iotUserApplication.setAppSecret(RandomUtil.randomString(32)); + iotUserApplication.setUnionId(user.getUnionId()); + if (iotUserApplicationService.updateIotUserApplication(iotUserApplication) == 0) { + throw new IoTException("应用密钥重置失败"); + } + + return AjaxResult.success(); + } + + /** 修改用户应用信息 */ + @Operation(summary = "修改应用信息") + @PutMapping + @Log(title = "修改用户应用信息", businessType = BusinessType.UPDATE) + public AjaxResult> edit(@RequestBody IoTUserApplication iotUserApplication) { + log.info( + "修改用户应用信息,操作人={},参数={}", + SecurityUtils.getUnionId(), + JSONUtil.toJsonStr(iotUserApplication)); + IoTUser iotUser = iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()); + IoTUserApplication app = + iotUserApplicationService.selectIotUserApplicationById(iotUserApplication.getAppUniqueId()); + if (app != null + && iotUser != null + && !app.getUnionId().equalsIgnoreCase(iotUser.getUnionId()) + && !iotUser.isAdmin()) { + throw new IoTException("无权限操作"); + } + int appCount = iotUserApplicationMapper.countMqtt(app.getUnionId()); + Map map = new HashMap<>(); + map.put("message", "success"); + iotUserApplication.setUnionId(app.getUnionId()); + iotUserApplication.setUpTopic(app.getUnionId() + StrUtil.C_SLASH + app.getAppId()); + iotUserApplicationService.updateIotUserApplication(iotUserApplication); + return AjaxResult.success(map); + } + + /** 删除用户应用信息 */ + @Operation(summary = "删除用户应用信息") + @DeleteMapping("/{appUniqueId}") + @Transactional + @Log(title = "删除用户应用信息", businessType = BusinessType.DELETE) + public AjaxResult remove(@PathVariable String[] appUniqueId) { + log.info("删除用户应用信息,操作人={},参数={}", SecurityUtils.getUnionId(), JSONUtil.toJsonStr(appUniqueId)); + + if (iIotDeviceService.selectDevByAppUniqueId(appUniqueId) > 0) { + throw new IoTException("该应用下有绑定设备,禁止删除!"); + } + List list = + iotUserApplicationService.selectIotUserApplicationList( + IoTUserApplication.builder().appUniqueId(appUniqueId[0]).build(), + loginIoTUnionUser(SecurityUtils.getUnionId())); + if (CollectionUtils.isEmpty(list)) { + throw new IoTException("您没有权限操作此应用!"); + } + List applications = + iotUserApplicationService.selectIotUserApplicationByIds(appUniqueId); + if (oauthClientDetailsService.deleteByClientIds( + applications.stream().map(IoTUserApplication::getAppId).toArray(String[]::new)) + == 0) { + throw new IoTException("应用对应授权信息删除失败!"); + } + if (iotUserApplicationService.deleteIotUserApplicationByIds(appUniqueId) == 0) { + throw new IoTException("应用删除失败!"); + } + return AjaxResult.success(); + } + + @Operation(summary = "推送地址测试") + @GetMapping(value = "/check") + public AjaxResult checkUrl(IoTUserApplication iotUserApplication) { + String url = iotUserApplication.getNotifyUrl(); + BaseUPRequest downRequest = new BaseUPRequest(); + downRequest.setIotId("1111111111"); + downRequest.setDeviceName("测试"); + downRequest.setDeviceId("11111111"); + downRequest.setDebug(true); + downRequest.setTime(new Date().getTime()); + try { + HttpRequest request = HttpUtil.createPost(url); + request.timeout(3000); + String timestamp = String.valueOf(System.currentTimeMillis()); + String signature = String.valueOf((timestamp + url).hashCode()); + request.header("X-Timestamp", timestamp); + request.header("X-Signature", signature); + request.header(Header.CONTENT_TYPE, "application/json"); + request.body(JSONUtil.toJsonStr(downRequest)); + HttpResponse postBody = request.execute(); + if (postBody == null) { + throw new IoTException("该地址无法正常访问!"); + } + if (postBody.getStatus() != 200) { + throw new IoTException("该地址无法正常访问! 错误码:" + postBody.getStatus()); + } + return AjaxResult.success("200"); + } catch (Exception e) { + return AjaxResult.success(e.getMessage()); + } + } + + /** HTTP推送启/停用 */ + @Operation(summary = "HTTP推送启/停用") + @PostMapping(value = "/enable") + public AjaxResult HttpEnable(@RequestBody IoTUserApplication iotUserApplication) { + IoTUser iotUser = iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()); + IoTUserApplication app = + iotUserApplicationService.selectIotUserApplicationById(iotUserApplication.getAppUniqueId()); + if (app != null + && iotUser != null + && !app.getUnionId().equalsIgnoreCase(iotUser.getUnionId()) + && !iotUser.isAdmin()) { + throw new IoTException("无权限操作"); + } + iotUserApplication.setUnionId(app.getUnionId()); + iotUserApplicationService.updateIotUserApplication(iotUserApplication); + return AjaxResult.success(); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTUserController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTUserController.java new file mode 100644 index 0000000..a6baa50 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTUserController.java @@ -0,0 +1,273 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * + * @Author: Aleo + * + * @Email: wo8335224@gmail.com + * + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.platform.web; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.ExcelUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.IIoTUserApplicationService; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.admin.system.service.ISysRoleService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.constant.IoTUserConstants; +import cn.universal.common.exception.IoTException; +import cn.universal.common.utils.StringUtils; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.admin.SysRole; +import cn.universal.persistence.entity.bo.IoTUserBO; +import cn.universal.persistence.entity.vo.IoTUserVO; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * IoT用户管理控制器 + * + * @version 1.0 @Author Aleo + * @since 2025/8/12 19:11 + */ +@RestController +@RequestMapping("admin/system/user") +@Slf4j +public class IoTUserController extends BaseController { + + @Resource private IIotUserService iIotUserService; + @Resource private ISysRoleService iSysRoleService; + + @Resource private IIoTUserApplicationService iotUserApplicationService; + + /** 分页查询用户列表 */ + @GetMapping("/list") + public TableDataInfo pageList(IoTUser user) { + log.info("当前用户={}", SecurityUtils.getUnionId()); + if (iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()).isAdmin()) { + user.setCreateBy(null); + } else { + user.setCreateBy(SecurityUtils.getUnionId()); + } + startPage(); + List userList = iIotUserService.selectUserList(user); + return getDataTable(userList); + } + + @Operation(summary = "导出用户应用信息列表") + @PostMapping("/export") + @Log(title = "导出用户应用信息列表", businessType = BusinessType.EXPORT) + public void export(HttpServletResponse response, IoTUser iotUser) { + log.info("导出用户,操作人={},参数={}", SecurityUtils.getUnionId(), JSONUtil.toJsonStr(iotUser)); + if (iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()).isAdmin()) { + iotUser.setCreateBy(null); + } else { + iotUser.setCreateBy(SecurityUtils.getUnionId()); + } + List list = iIotUserService.selectUserList(iotUser); + ExcelUtil util = new ExcelUtil(IoTUser.class); + util.exportExcel(response, list, "用户信息数据"); + } + + /** 根据用户编号获取详细信息 */ + @GetMapping(value = {"/", "/{id}"}) + public AjaxResult getInfo(@PathVariable(value = "id", required = false) Long id) { + + JSONObject jsonObject = new JSONObject(); + List roles = iSysRoleService.selectRoleAll(); + IoTUser currentUser = iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()); + jsonObject.set( + "roles", + currentUser.isAdmin() + ? roles + : roles.stream() + .filter(r -> SecurityUtils.getUnionId().equals(r.getCreateBy())) + .collect(Collectors.toList())); + if (StringUtils.isNotNull(id)) { + IoTUser iotUser = iIotUserService.selectUserById(id); + if (!SecurityUtils.getUnionId().equalsIgnoreCase(iotUser.getParentUnionId()) + && !currentUser.isAdmin()) { + throw new IoTException("你无权查询用户"); + } + IoTUserVO iotUserVO = BeanUtil.toBean(iotUser, IoTUserVO.class); + iotUserVO.setRoleIds(iSysRoleService.selectRoleListByUnionId(iotUser.getUnionId())); + jsonObject.set("data", iotUserVO); + } + + return AjaxResult.success(jsonObject); + } + + /** 新增用户 */ + @Operation(summary = "新增用户") + @PostMapping + @Log(title = "新增用户", businessType = BusinessType.INSERT) + public AjaxResult add(@Validated @RequestBody IoTUserBO userbo) { + log.info("新增用户,操作人={},参数={}", SecurityUtils.getUnionId(), JSONUtil.toJsonStr(userbo)); + String unionId = SecurityUtils.getUnionId(); + IoTUser parentUser = iIotUserService.selectUserByUnionId(unionId); + if (!parentUser.isAdmin()) { + throw new IoTException("你无权操作"); + } + IoTUser user = BeanUtil.toBean(userbo, IoTUser.class); + if (Objects.isNull(user.getMobile())) { + return AjaxResult.error("新增用户'" + user.getUsername() + "'失败,手机号码不得为空"); + } + if (IoTUserConstants.NOT_UNIQUE.equals(iIotUserService.checkUserNameUnique(user))) { + return AjaxResult.error("新增用户'" + user.getUsername() + "'失败,用户名已存在"); + } else if (Validator.isNotEmpty(user.getMobile()) + && IoTUserConstants.NOT_UNIQUE.equals(iIotUserService.checkPhoneUnique(user))) { + return AjaxResult.error("新增用户'" + user.getUsername() + "'失败,手机号码已存在"); + } + if (StrUtil.isNotEmpty(user.getPassword())) { + Matcher matcher = IoTConstant.pattern.matcher(user.getPassword()); + if (!matcher.matches()) { + throw new IoTException("密码中必须包含字母、数字、特殊字符,至少8个字符,最多30个字符"); + } + } + + userbo.setCreateBy(unionId); + if (!parentUser.isAdmin()) { + userbo.setParentUnionId(unionId); + } + userbo.setIdentity(parentUser.getIdentity() + 1); + userbo.setCreateDate(new Date()); + userbo.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + // userbo.setPassword(user.getPassword()); + userbo.setUnionId(userbo.getUsername()); + userbo.setRegisterFrom("admin"); + userbo.setDeleted(0); + if (parentUser.isAdmin()) { + userbo.setLicense(5000); + userbo.setLicenseTotalAmount(5000); + } + return toAjax(iIotUserService.insertUser(userbo)); + } + + /** 修改用户 */ + @Operation(summary = "修改用户") + @PutMapping + @Log(title = "修改用户", businessType = BusinessType.UPDATE) + public AjaxResult edit(@Validated @RequestBody IoTUserBO userbo) { + log.info("修改用户,操作人={},参数={}", SecurityUtils.getUnionId(), JSONUtil.toJsonStr(userbo)); + IoTUser user = BeanUtil.toBean(userbo, IoTUser.class); + iIotUserService.checkUserAllowed(user); + IoTUser updateUser = iIotUserService.selectUserById(userbo.getId()); + IoTUser iotUser = iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()); + // 非超管身份只能操作自己和子账户 + if (!SecurityUtils.getUnionId().equalsIgnoreCase(updateUser.getParentUnionId()) + && (iotUser == null || !user.getId().equals(iotUser.getId())) + && !iotUser.isAdmin()) { + return error("你无权操作"); + } + if (Validator.isEmpty(user.getMobile())) { + return error("修改用户'" + user.getUsername() + "'失败,手机号码不能为空"); + } + if (IoTUserConstants.NOT_UNIQUE.equals(iIotUserService.checkPhoneUnique(user))) { + return error("修改用户'" + user.getUsername() + "'失败,手机号码已存在"); + } + + if ("".equals(userbo.getPassword())) { + userbo.setPassword(null); + } else if (userbo.getPassword() != null) { + // Pattern pattern = Pattern.compile( + // "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@#$%^&+=])[0-9a-zA-Z@#$%^&+=]{8,30}$"); + Matcher matcher = IoTConstant.pattern.matcher(user.getPassword()); + if (!matcher.matches()) { + throw new IoTException("密码中必须包含字母、数字、特殊字符,至少8个字符,最多30个字符"); + } + userbo.setPassword(SecurityUtils.encryptPassword(userbo.getPassword())); + } + userbo.setUpdateBy(SecurityUtils.getUnionId()); + userbo.setUpdateDate(new Date()); + int count = iIotUserService.updateUser(userbo); + if (count > 0) { + iotUserApplicationService.EnDisableIoTUser( + userbo.getUnionId(), IoTConstant.NORMAL.toString().equals(iotUser.getStatus())); + } + return toAjax(count); + } + + /** 删除用户 */ + @Operation(summary = "删除用户") + @DeleteMapping("/{userIds}") + @Log(title = "删除用户", businessType = BusinessType.DELETE) + public AjaxResult remove(@PathVariable Long[] userIds) { + if (userIds != null && userIds.length > 1) { + return error("不允许批量删除"); + } + boolean isAdmin = iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()).isAdmin(); + IoTUser deleteUser = iIotUserService.selectUserById(userIds[0]); + if (!SecurityUtils.getUnionId().equalsIgnoreCase(deleteUser.getParentUnionId()) && !isAdmin) { + return error("你无权删除用户"); + } + return toAjax(iIotUserService.deleteUserByIds(userIds)); + } + + /** 根据用户编号获取授权角色 */ + @GetMapping("/authRole/{id}") + public AjaxResult authRole(@PathVariable("id") Long id) { + log.info("授权用户,操作人={},参数={}", SecurityUtils.getUnionId(), id); + + JSONObject object = new JSONObject(); + IoTUser currentUser = iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()); + IoTUser user = iIotUserService.selectUserById(id); + if (!SecurityUtils.getUnionId().equalsIgnoreCase(user.getParentUnionId()) + && !currentUser.isAdmin()) { + return error("你无权操作用户"); + } + List roles = iSysRoleService.selectRolesByUnionId(user.getUnionId()); + object.set("user", user); + object.set( + "roles", + user.isAdmin() + ? roles + : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + return AjaxResult.success(object); + } + + /** 用户授权角色 */ + @PutMapping("/authRole") + public AjaxResult insertAuthRole(String unionId, Long[] roleIds) { + IoTUser currentUser = iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()); + IoTUser user = iIotUserService.selectUserByUnionId(unionId); + if (!SecurityUtils.getUnionId().equalsIgnoreCase(user.getParentUnionId()) + && !currentUser.isAdmin()) { + return error("你无权操作用户"); + } + iIotUserService.insertUserAuth(unionId, roleIds); + return success(); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/monitor/SysLoginLogController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/monitor/SysLoginLogController.java new file mode 100644 index 0000000..696154d --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/monitor/SysLoginLogController.java @@ -0,0 +1,77 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.monitor; + +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.ExcelUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.ISysLogininforService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.SysLogininfor; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 系统访问记录 @Author ruoyi */ +@RestController +@RequestMapping("/admin/monitor/logininfor") +public class SysLoginLogController extends BaseController { + + @Resource private ISysLogininforService logininforService; + + @GetMapping("/list") + public TableDataInfo list(SysLogininfor logininfor) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin()) { + logininfor.setUserName(iotUser.getUnionId()); + } + startPage(); + List sysLogininfors = logininforService.selectPageLogininforList(logininfor); + return getDataTable(sysLogininfors); + } + + @Log(title = "登录日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysLogininfor logininfor) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin()) { + logininfor.setUserName(iotUser.getUnionId()); + } + List list = logininforService.selectLogininforList(logininfor); + ExcelUtil util = new ExcelUtil(SysLogininfor.class); + util.exportExcel(response, list, "登录日志"); + } + + @Log(title = "登录日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{infoIds}") + public AjaxResult remove(@PathVariable Long[] infoIds) { + return toAjax(logininforService.deleteLogininforByIds(infoIds)); + } + + @Log(title = "登录日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() { + logininforService.cleanLogininfor(); + return AjaxResult.success(); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/monitor/SysOperlogController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/monitor/SysOperlogController.java new file mode 100644 index 0000000..18f8f14 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/monitor/SysOperlogController.java @@ -0,0 +1,61 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.monitor; + +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.ExcelUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.ISysOperLogService; +import cn.universal.admin.system.web.BaseController; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.SysOperLog; +import cn.universal.persistence.page.TableDataInfo; +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 操作日志记录 @Author ruoyi */ +@RestController +@RequestMapping("/admin/monitor/operlog") +public class SysOperlogController extends BaseController { + + @Autowired private ISysOperLogService operLogService; + + @GetMapping("/list") + public TableDataInfo list(SysOperLog operLog) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin()) { + operLog.setOperName(iotUser.getUnionId()); + } + startPage(); + List sysOperLogs = operLogService.selectPageOperLogList(operLog); + return getDataTable(sysOperLogs); + } + + @Log(title = "操作日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysOperLog operLog) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin()) { + operLog.setOperName(iotUser.getUnionId()); + } + List list = operLogService.selectOperLogList(operLog); + ExcelUtil util = new ExcelUtil(SysOperLog.class); + util.exportExcel(response, list, "操作日志"); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/oss/SysOssController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/oss/SysOssController.java new file mode 100644 index 0000000..8b23dcd --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/oss/SysOssController.java @@ -0,0 +1,128 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.oss; + +import cn.hutool.core.convert.Convert; +import cn.hutool.http.HttpUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.web.BaseController; +import cn.universal.common.exception.IoTException; +import cn.universal.common.utils.FileUtils; +import cn.universal.ossm.entity.SysOss; +import cn.universal.ossm.entity.bo.SysOssBo; +import cn.universal.ossm.service.ISysOssService; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.NotEmpty; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.util.CollectionUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +/** 文件上传 控制层 @Author Lion Li */ +@Validated +@Tag(name = "OSS云存储控制器", description = "OSS云存储管理") +@RestController +@RequestMapping("/admin/system/oss") +public class SysOssController extends BaseController { + + @Resource private ISysOssService iSysOssService; + + @Value("${codec.path:cn-universal}") + private String prePath; + + /** 查询OSS云存储列表 */ + @Operation(summary = "查询OSS云存储列表") + @GetMapping("/list") + public TableDataInfo list(@Validated SysOssBo bo) { + startPage(); + return getDataTable(iSysOssService.queryPageList(bo)); + } + + /** 上传OSS云存储 */ + @Operation(summary = "上传OSS云存储") + @PostMapping("/upload") + public AjaxResult> upload( + @Parameter(name = "file", description = "文件", required = true) @RequestPart("file") + MultipartFile file) { + if (file.isEmpty()) { + throw new IoTException("上传文件不能为空"); + } + List sysOsses = + iSysOssService.queryPageList( + SysOssBo.builder().originalName(prePath + file.getOriginalFilename()).build()); + String unionId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId(); + if (!CollectionUtils.isEmpty(sysOsses)) { + // 过滤出不是自己创建的同名文件 + List existOss = + sysOsses.stream() + .filter(sysOss -> !unionId.equals(sysOss.getCreateBy())) + .collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(existOss)) { + throw new IoTException("已存在同名文件!请修改后重试!"); + } + } + // String unionId = SecurityUtils.getUnionId(); + SysOss oss = iSysOssService.upload(file, unionId); + Map map = new HashMap<>(2); + map.put("url", oss.getUrl()); + String name = oss.getOriginalName(); + map.put("fileName", name.substring(name.lastIndexOf('/') + 1, name.lastIndexOf("."))); + return AjaxResult.success(map); + } + + @Operation(summary = "下载OSS云存储") + @GetMapping("/download/{ossId}") + public void download(@PathVariable Long ossId, HttpServletResponse response) throws IOException { + SysOss sysOss = iSysOssService.getById(ossId); + if (sysOss == null) { + throw new IoTException("文件数据不存在!"); + } + response.reset(); + response.addHeader("Access-Control-Allow-Origin", "*"); + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition"); + FileUtils.setAttachmentResponseHeader( + response, URLEncoder.encode(sysOss.getOriginalName(), StandardCharsets.UTF_8.toString())); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8"); + long data = HttpUtil.download(sysOss.getUrl(), response.getOutputStream(), false); + response.setContentLength(Convert.toInt(data)); + } + + /** 删除OSS云存储 */ + @Operation(summary = "删除OSS云存储") + @DeleteMapping("/{ossIds}") + public AjaxResult remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ossIds) { + return toAjax(iSysOssService.deleteWithValidByIds(Arrays.asList(ossIds), true) ? 1 : 0); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/permission/SysPermissionService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/permission/SysPermissionService.java new file mode 100644 index 0000000..fc73810 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/permission/SysPermissionService.java @@ -0,0 +1,58 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.permission; + +import cn.universal.admin.system.service.ISysMenuService; +import cn.universal.admin.system.service.ISysRoleService; +import cn.universal.persistence.entity.IoTUser; +import java.util.HashSet; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** 用户权限处理服务 */ +@Component +public class SysPermissionService { + + @Autowired private ISysRoleService roleService; + + @Autowired private ISysMenuService menuService; + + /** 获取角色数据权限 */ + public Set getRolePermission(IoTUser user) { + Set roles = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) { + roles.add("admin"); + } else { + roles.addAll(roleService.selectRolePermissionByUnionId(user.getUnionId())); + } + return roles; + } + + /** + * 获取菜单数据权限 + * + * @param user 用户信息 + * @return 菜单权限信息 + */ + public Set getMenuPermission(IoTUser user) { + Set perms = new HashSet(); + if (user.isAdmin()) { + perms.add("*:*:*"); + } else { + perms.addAll(menuService.selectMenuPermsByUserId(user.getUnionId())); + } + return perms; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/quartz/SysJobController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/quartz/SysJobController.java new file mode 100644 index 0000000..7b31802 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/quartz/SysJobController.java @@ -0,0 +1,110 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.quartz; + +import cn.hutool.core.util.StrUtil; +import cn.universal.admin.common.utils.ExcelUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.web.BaseController; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import cn.universal.quartz.constant.Constants; +import cn.universal.quartz.domain.SysJob; +import cn.universal.quartz.service.ISysJobService; +import cn.universal.quartz.util.CronUtils; +import java.util.List; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 调度任务信息操作处理 @Author ruoyi */ +@RestController +@RequestMapping("/admin/quartz/job") +public class SysJobController extends BaseController { + + @Autowired private ISysJobService jobService; + + /** 查询定时任务列表 */ + @GetMapping("/list") + public TableDataInfo list(SysJob sysJob) { + startPage(); + return getDataTable(jobService.selectJobList(sysJob)); + } + + /** 导出定时任务列表 */ + @GetMapping("/export") + public AjaxResult export(SysJob sysJob) { + List list = jobService.selectJobList(sysJob); + ExcelUtil util = new ExcelUtil(SysJob.class); + return util.exportExcel(list, "定时任务"); + } + + /** 获取定时任务详细信息 */ + @GetMapping(value = "/{jobId}") + public AjaxResult getInfo(@PathVariable("jobId") Long jobId) { + return AjaxResult.success(jobService.selectJobById(jobId)); + } + + /** 新增定时任务 */ + @PostMapping + public AjaxResult add(@RequestBody SysJob sysJob) throws SchedulerException { + if (!CronUtils.isValid(sysJob.getCronExpression())) { + return AjaxResult.error("新增任务'" + sysJob.getJobName() + "'失败,Cron表达式不正确"); + } else if (StrUtil.containsIgnoreCase(sysJob.getInvokeTarget(), Constants.LOOKUP_RMI)) { + return AjaxResult.error("新增任务'" + sysJob.getJobName() + "'失败,目标字符串不允许'rmi://'调用"); + } + sysJob.setCreateBy(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + return toAjax(jobService.insertJob(sysJob)); + } + + /** 修改定时任务 */ + @PutMapping + public AjaxResult edit(@RequestBody SysJob sysJob) throws SchedulerException { + if (!CronUtils.isValid(sysJob.getCronExpression())) { + return AjaxResult.error("修改任务'" + sysJob.getJobName() + "'失败,Cron表达式不正确"); + } else if (StrUtil.containsIgnoreCase(sysJob.getInvokeTarget(), Constants.LOOKUP_RMI)) { + return AjaxResult.error("修改任务'" + sysJob.getJobName() + "'失败,目标字符串不允许'rmi://'调用"); + } + sysJob.setUpdateBy(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + return toAjax(jobService.updateJob(sysJob)); + } + + /** 定时任务状态修改 */ + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException { + SysJob newJob = jobService.selectJobById(job.getJobId()); + newJob.setStatus(job.getStatus()); + return toAjax(jobService.changeStatus(newJob)); + } + + /** 定时任务立即执行一次 */ + @PutMapping("/run") + public AjaxResult run(@RequestBody SysJob job) throws SchedulerException { + jobService.run(job); + return AjaxResult.success(); + } + + /** 删除定时任务 */ + @DeleteMapping("/{jobIds}") + public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException { + jobService.deleteJobByIds(jobIds); + return AjaxResult.success(); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/quartz/SysJobLogController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/quartz/SysJobLogController.java new file mode 100644 index 0000000..4fd9b7a --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/quartz/SysJobLogController.java @@ -0,0 +1,69 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.quartz; + +import cn.universal.admin.common.utils.ExcelUtil; +import cn.universal.admin.system.web.BaseController; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import cn.universal.quartz.domain.SysJobLog; +import cn.universal.quartz.service.ISysJobLogService; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 调度日志操作处理 @Author ruoyi */ +@RestController +@RequestMapping("/admin/quartz/jobLog") +public class SysJobLogController extends BaseController { + + @Autowired private ISysJobLogService jobLogService; + + /** 查询定时任务调度日志列表 */ + @GetMapping("/list") + public TableDataInfo list(SysJobLog sysJobLog) { + startPage(); + return getDataTable(jobLogService.selectJobLogList(sysJobLog)); + } + + /** 导出定时任务调度日志列表 */ + @GetMapping("/export") + public AjaxResult export(SysJobLog sysJobLog) { + List list = jobLogService.selectJobLogList(sysJobLog); + ExcelUtil util = new ExcelUtil(SysJobLog.class); + return util.exportExcel(list, "调度日志"); + } + + /** 根据调度编号获取详细信息 */ + @GetMapping(value = "/{configId}") + public AjaxResult getInfo(@PathVariable Long jobLogId) { + return AjaxResult.success(jobLogService.selectJobLogById(jobLogId)); + } + + /** 删除定时任务调度日志 */ + @DeleteMapping("/{jobLogId}") + public AjaxResult remove(@PathVariable Long jobLogId) { + return toAjax(jobLogService.deleteJobLogById(jobLogId)); + } + + /** 清空定时任务调度日志 */ + @DeleteMapping("/clean") + public AjaxResult clean() { + jobLogService.cleanJobLog(); + return AjaxResult.success(); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/AsyncService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/AsyncService.java new file mode 100644 index 0000000..965ba47 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/AsyncService.java @@ -0,0 +1,111 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.universal.common.constant.Constants; +import cn.universal.common.utils.IPUtils; +import cn.universal.common.utils.ServletUtils; +import cn.universal.persistence.entity.SysLogininfor; +import cn.universal.persistence.entity.SysOperLog; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** 异步工厂(产生任务用) @Author Lion Li */ +@Slf4j +@Async +@Component +public class AsyncService { + + @Autowired private ISysLogininforService iSysLogininforService; + + @Autowired private ISysOperLogService iSysOperLogService; + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param status 状态 + * @param message 消息 + * @param request HTTP请求对象,可以为null + */ + public void recordLogininfor( + final String username, + final String status, + final String message, + final HttpServletRequest request) { + HttpServletRequest httpRequest = request; + // 如果传入的request为null,尝试从RequestContextHolder获取 + if (httpRequest == null) { + try { + httpRequest = ServletUtils.getRequest(); + } catch (Exception e) { + log.warn("无法获取HttpServletRequest,使用默认值记录登录信息: {}", e.getMessage()); + // 创建默认的登录信息记录 + SysLogininfor logininfor = new SysLogininfor(); + logininfor.setUserName(username); + logininfor.setIpaddr("unknown"); + logininfor.setLoginLocation("unknown"); + logininfor.setBrowser("unknown"); + logininfor.setOs("unknown"); + logininfor.setMsg(message); + // 日志状态 + if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status)) { + logininfor.setStatus(Constants.SUCCESS); + } else if (Constants.LOGIN_FAIL.equals(status)) { + logininfor.setStatus(Constants.FAIL); + } + // 插入数据 + iSysLogininforService.insertLogininfor(logininfor); + return; + } + } + + // 使用获取到的request参数 + final UserAgent userAgent = UserAgentUtil.parse(httpRequest.getHeader("User-Agent")); + final String ip = IPUtils.getIpAddr(httpRequest); + // 操作系统 + String os = userAgent.getOs().getName(); + // 浏览器 + String browser = userAgent.getBrowser().getName(); + SysLogininfor login = new SysLogininfor(); + login.setUserName(username); + login.setIpaddr(ip); + login.setBrowser(browser); + login.setOs(os); + login.setMsg(message); + // 日志状态 + if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status)) { + login.setStatus(Constants.SUCCESS); + } else if (Constants.LOGIN_FAIL.equals(status)) { + login.setStatus(Constants.FAIL); + } + // 插入数据 + iSysLogininforService.insertLogininfor(login); + } + + /** + * 操作日志记录 + * + * @param operLog 操作日志信息 + */ + public void recordOper(final SysOperLog operLog) { + // 远程查询操作地点 + operLog.setOperLocation(operLog.getOperIp()); + iSysOperLogService.insertOperlog(operLog); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IIoTUserApplicationService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IIoTUserApplicationService.java new file mode 100644 index 0000000..cafd4a5 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IIoTUserApplicationService.java @@ -0,0 +1,70 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.IoTUserApplication; +import cn.universal.persistence.entity.vo.IoTUserApplicationVO; +import java.util.List; + +/** + * 用户应用信息Service接口 + * + * @since 2025-12-30 + */ +public interface IIoTUserApplicationService { + + /** 启停用 */ + boolean EnDisableIoTUser(String unionId, boolean isEnable); + + String selectApplicationName(String applicationId); + + /** 查询用户应用信息 */ + IoTUserApplication selectIotUserApplicationById(String appUniqueId); + + /** 查询用户应用信息列表 */ + List selectIotUserApplicationList( + IoTUserApplication iotUserApplication, IoTUser iotUser); + + /** 新增用户应用信息 */ + int insertIotAppUser(IoTUserApplication iotUserApplication); + + /** + * 修改用户应用信息 + * + * @param iotUserApplication 用户应用信息 + * @return 结果 + */ + int updateIotUserApplication(IoTUserApplication iotUserApplication); + + /** + * 批量删除用户应用信息 + * + * @param appUniqueId 需要删除的数据ID + * @return 结果 + */ + int deleteIotUserApplicationByIds(String[] appUniqueId); + + /** 根据主键批量查询 */ + List selectIotUserApplicationByIds(String[] appUniqueId); + + /** + * 删除用户应用信息信息 + * + * @param appUniqueId 用户应用信息ID + * @return 结果 + */ + int deleteIotUserApplicationById(String appUniqueId); + + List selectApplicationList(IoTUserApplication application, IoTUser iotUser); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IIotUserService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IIotUserService.java new file mode 100644 index 0000000..9fe6b67 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IIotUserService.java @@ -0,0 +1,238 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.bo.IoTUserBO; +import java.util.List; + +/** 用户业务层接口 */ +public interface IIotUserService { + + // TableDataInfo selectPageUserList(IoTUserBO user); + + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(IoTUser user); + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(IoTUserBO user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(IoTUserBO user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public IoTUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public IoTUser selectUserById(Long userId); + + /** + * 通过用户手机号查询用户 + * + * @param mobile 用户手机号 + * @return 用户对象信息 + */ + public IoTUser selectUserByMobile(String mobile); + + /** + * 通过用户unionId查询用户 + * + * @param + * @return 用户对象信息 + */ + public IoTUser selectUserByUnionId(String unionId); + + /** + * 查询用户 + * + * @param iotUser 查询条件 + * @return + */ + IoTUser queryOne(IoTUser iotUser); + + /** + * 根据用户ID查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + // public String selectUserRoleGroup(String userName); + + /** + * 根据用户ID查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + // public String selectUserPostGroup(String userName); + + /** + * 校验用户名称是否唯一 + * + * @param user 用户 + * @return 结果 + */ + public String checkUserNameUnique(IoTUser user); + + /** + * 校验手机号码是否唯一 + * + * @param user 用户 + * @return 结果 + */ + public String checkPhoneUnique(IoTUser user); + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + // public String checkEmailUnique(IoTUser user); + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + public void checkUserAllowed(IoTUser user); + + /** + * 新增用户信息 + * + * @param userbo 用户信息 + * @return 结果 + */ + public int insertUser(IoTUserBO userbo); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(IoTUserBO user); + + void updateUserById(IoTUser iotUser); + + /** + * 用户授权角色 + * + * @param unionId 用户ID + * @param roleIds 角色组 + */ + public void insertUserAuth(String unionId, Long[] roleIds); + + /** + * 授权子账户 + * + * @param id + * @param unionId + */ + public void addSubAccount(Long id, String unionId, int isSub); + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + // public int updateUserStatus(IoTUser user); + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + // public int updateUserProfile(IoTUser user); + + /** + * 修改用户头像 + * + * @param userName 用户名 + * @param avatar 头像地址 + * @return 结果 + */ + // public boolean updateUserAvatar(String userName, String avatar); + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + // public int resetPwd(IoTUser user); + + /** + * 重置用户密码 + * + * @param userName 用户名 + * @param password 密码 + * @return 结果 + */ + // public int resetUserPwd(String userName, String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + // public String importUser(List userList, Boolean isUpdateSupport, String operName); + + /*public int addUser(IoTUserBO sysUserBo, IoTUser user);*/ +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IOAuthClientDetailsService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IOAuthClientDetailsService.java new file mode 100644 index 0000000..99e12db --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IOAuthClientDetailsService.java @@ -0,0 +1,25 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.persistence.entity.OAuth2ClientDetails; + +/** OAuth2 */ +public interface IOAuthClientDetailsService { + + int insert(OAuth2ClientDetails OAuth2ClientDetails); + + int updateSecret(OAuth2ClientDetails OAuth2ClientDetails); + + int deleteByClientIds(String[] ids); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IPWhitelistService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IPWhitelistService.java new file mode 100644 index 0000000..ebff03f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IPWhitelistService.java @@ -0,0 +1,60 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.common.utils.IPUtil; +import jakarta.annotation.Resource; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +/** + * 白名单服务 + * + * @version 1.0 + * @since 2023/8/11 + */ +@Component +public class IPWhitelistService { + + private static final String REDIS_WHITE_KEY = "whitelist"; + + @Value("${whitelist.enable:true}") + private boolean whiteEnable; + + @Resource private ISysDictDataService iSysDictDataService; + @Resource private StringRedisTemplate stringRedisTemplate; + + public boolean isWhitelisted(List whitelistKeys, String ipAddress) { + if (!whiteEnable) { + return Boolean.TRUE; + } + boolean exist = stringRedisTemplate.hasKey(REDIS_WHITE_KEY + ":" + ipAddress); + if (exist) { + return Boolean.TRUE; + } + Set ipLists = iSysDictDataService.selectWhitelist(whitelistKeys); + boolean perMit = IPUtil.checkIPWhitelist(ipAddress, ipLists); + if (perMit) { + stringRedisTemplate + .opsForValue() + .set(REDIS_WHITE_KEY + ":" + ipAddress, "1", 1, TimeUnit.HOURS); + return Boolean.TRUE; + } else { + return Boolean.FALSE; + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysConfigService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysConfigService.java new file mode 100644 index 0000000..5e8ed60 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysConfigService.java @@ -0,0 +1,92 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.persistence.entity.admin.SysConfig; +import java.util.List; + +/** 参数配置服务层接口 */ +public interface ISysConfigService { + + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数键值 + */ + public String selectConfigByKey(String configKey); + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + public boolean selectCaptchaOnOff(); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + * @return 结果 + */ + public void deleteConfigByIds(Long[] configIds); + + /** 加载参数缓存数据 */ + public void loadingConfigCache(); + + /** 清空参数缓存数据 */ + public void clearConfigCache(); + + /** 重置参数缓存数据 */ + public void resetConfigCache(); + + /** + * 校验参数键名是否唯一 + * + * @param config 参数信息 + * @return 结果 + */ + public String checkConfigKeyUnique(SysConfig config); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysDictDataService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysDictDataService.java new file mode 100644 index 0000000..3bf97c3 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysDictDataService.java @@ -0,0 +1,92 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.persistence.entity.admin.SysDictData; +import cn.universal.persistence.entity.admin.vo.SysDictDataVo; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** 字典数据业务层接口 */ +public interface ISysDictDataService { + + List selectPageDictDataList(SysDictDataVo dictData); + + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictDataVo dictData); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(String dictType, String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + * @return 结果 + */ + public void deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); + + /** + * 根据字典类型数组查询字典数据信息 + * + * @param dictType + * @return + */ + Map selectDictDataByTypes(String[] dictType); + + int insertSaleBuyDictData(SysDictData dict); + + /** + * 根据label标签,查找白名单 + * + * @param labels + * @return + */ + public Set selectWhitelist(List labels); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysDictTypeService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysDictTypeService.java new file mode 100644 index 0000000..b9b375f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysDictTypeService.java @@ -0,0 +1,103 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.persistence.entity.admin.SysDictData; +import cn.universal.persistence.entity.admin.SysDictType; +import java.util.List; + +/** 字典类型业务层接口 */ +public interface ISysDictTypeService { + + List selectPageDictTypeList(SysDictType dictType); + + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 批量删除字典信息 + * + * @param dictIds 需要删除的字典ID + * @return 结果 + */ + public void deleteDictTypeByIds(Long[] dictIds); + + /** 加载字典缓存数据 */ + public void loadingDictCache(); + + /** 清空字典缓存数据 */ + public void clearDictCache(); + + /** 重置字典缓存数据 */ + public void resetDictCache(); + + /** + * 新增保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public String checkDictTypeUnique(SysDictType dictType); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysLogininforService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysLogininforService.java new file mode 100644 index 0000000..d2d9641 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysLogininforService.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.persistence.entity.SysLogininfor; +import java.util.List; + +/** 系统访问日志情况信息 服务层 @Author ruoyi */ +public interface ISysLogininforService { + + List selectPageLogininforList(SysLogininfor logininfor); + + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** 清空系统登录日志 */ + public void cleanLogininfor(); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysMenuService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysMenuService.java new file mode 100644 index 0000000..b780bf7 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysMenuService.java @@ -0,0 +1,151 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.persistence.entity.admin.SysMenu; +import cn.universal.persistence.entity.admin.TreeSelect; +import cn.universal.persistence.entity.admin.vo.RouterVo; +import java.util.List; +import java.util.Set; + +/** 菜单业务层接口 */ +public interface ISysMenuService { + + /** + * 根据用户查询系统菜单列表 + * + * @param unionId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(String unionId); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @param unionId 用户unionId + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu, String unionId); + + /** + * 根据用户ID查询权限 + * + * @param unionId 用户unionId + * @return 权限列表 + */ + public Set selectMenuPermsByUserId(String unionId); + + /** + * 根据用户ID查询菜单树信息 + * + * @param unionId 用户unionId + * @return 菜单列表 + */ + public List selectMenuTreeByUnionId(String unionId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(Long roleId); + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + public List buildMenus(List menus); + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + public List buildMenuTree(List menus); + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + public List buildMenuTreeSelect(List menus); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean hasChildByMenuId(Long menuId); + + /** + * 查询菜单是否存在角色 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkMenuExistRole(Long menuId); + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + public String checkMenuNameUnique(SysMenu menu); + + /** + * 执行菜单脚本 + * + * @param sql 菜单脚本 + */ + void execMenuSql(String sql); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysOperLogService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysOperLogService.java new file mode 100644 index 0000000..80fd541 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysOperLogService.java @@ -0,0 +1,56 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.persistence.entity.SysOperLog; +import java.util.List; + +/** 操作日志 服务层 @Author ruoyi */ +public interface ISysOperLogService { + + List selectPageOperLogList(SysOperLog operLog); + + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** 清空操作日志 */ + public void cleanOperLog(); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysRoleService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysRoleService.java new file mode 100644 index 0000000..44a79f7 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/ISysRoleService.java @@ -0,0 +1,184 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.persistence.entity.admin.SysRole; +import cn.universal.persistence.entity.admin.SysUserRole; +import java.util.List; +import java.util.Set; + +/** 角色业务层 @Author ruoyi */ +public interface ISysRoleService { + + List selectPageRoleList(SysRole role); + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色列表 + * + * @param unionId 用户unionId + * @return 角色列表 + */ + public List selectRolesByUnionId(String unionId); + + /** + * 根据用户ID查询角色权限 + * + * @param unionId 用户unionId + * @return 权限列表 + */ + public Set selectRolePermissionByUnionId(String unionId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param unionId 用户unionId + * @return 选中角色ID列表 + */ + public List selectRoleListByUnionId(String unionId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public String checkRoleNameUnique(SysRole role); + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public String checkRoleKeyUnique(SysRole role); + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + public void checkRoleAllowed(SysRole role); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRoleStatus(SysRole role); + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + // public int authDataScope(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteAuthUser(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param unionIds 需要取消授权的用户数据ID + * @return 结果 + */ + public int deleteAuthUsers(Long roleId, String[] unionIds); + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param unionIds 需要删除的用户数据ID + * @return 结果 + */ + public int insertAuthUsers(Long roleId, String[] unionIds); + + /** + * 根据用户ID查询用户所属角色组 + * + * @param unionId 用户名 + * @return 结果 + */ + public String selectUserRoleGroup(String unionId); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IndexQueryService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IndexQueryService.java new file mode 100644 index 0000000..236cca2 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IndexQueryService.java @@ -0,0 +1,26 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.persistence.dto.IndexQueryDTO; + +/** + * 首页统计服务 + * + * @since 2023/8/12 16:51 + */ +public interface IndexQueryService { + + /** 查询首页数量 */ + IndexQueryDTO queryIndexQty(String creator); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IoTDeviceProtocolService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IoTDeviceProtocolService.java new file mode 100644 index 0000000..14c6019 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IoTDeviceProtocolService.java @@ -0,0 +1,95 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.admin.common.service.BaseService; +import cn.universal.persistence.entity.IoTDeviceProtocol; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.bo.IoTDeviceProtocolBO; +import java.util.List; +import java.util.Set; + +/** + * 设备协议Service接口 + * + * @since 2023-01-06 + */ +public interface IoTDeviceProtocolService extends BaseService { + + Set method = + Set.of( + "decode", + "encode", + "preDecode", + "codecAdd", + "codecDelete", + "codecUpdate", + "codecQuery", + "iotToYour", + "yourToIot"); + + int countProtocol(String id); + + /** + * 查询设备协议 + * + * @param id 设备协议主键 + * @return 设备协议 + */ + public IoTDeviceProtocol selectDevProtocolById(String id, String unionId); + + /** 包名查重 */ + int countByProvider(String provider); + + /** + * 查询设备协议列表 + * + * @param ioTDeviceProtocol 设备协议 + * @return 设备协议集合 + */ + public List selectDevProtocolList( + IoTDeviceProtocol ioTDeviceProtocol, IoTUser iotUser); + + /** + * 新增设备协议 + * + * @param ioTDeviceProtocol 设备协议 + * @return 结果 + */ + public int insertDevProtocol(IoTDeviceProtocol ioTDeviceProtocol); + + public int insertList(List ioTDeviceProtocolList); + + /** + * 修改设备协议 + * + * @return 结果 + */ + public int updateDevProtocol(IoTDeviceProtocolBO ioTDeviceProtocolBO, IoTUser iotUser); + + /** + * 批量删除设备协议 + * + * @param ids 需要删除的设备协议主键集合 + * @return 结果 + */ + public int deleteDevProtocolByIds(String[] ids); + + /** + * 删除设备协议信息 + * + * @param id 设备协议主键 + * @return 结果 + */ + public int deleteDevProtocolById(String id); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IoTDeviceSubscribeService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IoTDeviceSubscribeService.java new file mode 100644 index 0000000..a3a5e77 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IoTDeviceSubscribeService.java @@ -0,0 +1,32 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service; + +import cn.universal.admin.common.service.BaseService; +import cn.universal.common.domain.R; +import cn.universal.persistence.entity.IoTDeviceSubscribe; +import java.util.List; + +/** 设备订阅 */ +public interface IoTDeviceSubscribeService extends BaseService { + + List selectDevSubscribeList(IoTDeviceSubscribe sub); + + IoTDeviceSubscribe selectDevInstanceById(String id); + + R updateDevSubscribe(IoTDeviceSubscribe sub); + + R insertDevSubscribe(IoTDeviceSubscribe sub); + + R deleteDevSubscribe(String productKey, String iotId, Long[] ids); +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IndexQueryServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IndexQueryServiceImpl.java new file mode 100644 index 0000000..f863b64 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IndexQueryServiceImpl.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.universal.admin.system.service.IndexQueryService; +import cn.universal.persistence.dto.IndexQueryDTO; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.IoTUserApplication; +import cn.universal.persistence.entity.RuleModel; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.IoTProductMapper; +import cn.universal.persistence.mapper.IoTUserApplicationMapper; +import cn.universal.persistence.mapper.RuleModelMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +/** 首页统计 */ +@Service +public class IndexQueryServiceImpl implements IndexQueryService { + + @Resource private IoTDeviceMapper ioTDeviceMapper; + + @Resource private RuleModelMapper ruleModelMapper; + + @Resource private IoTProductMapper ioTProductMapper; + + @Resource private IoTUserApplicationMapper iotUserApplicationMapper; + + @Override + public IndexQueryDTO queryIndexQty(String creator) { + IndexQueryDTO qty = new IndexQueryDTO(); + IoTProduct product = new IoTProduct(); + product.setCreatorId(creator); + qty.setProduct(ioTProductMapper.selectCount(product)); + + RuleModel ruleModel = new RuleModel(); + ruleModel.setCreatorId(creator); + qty.setRule(ruleModelMapper.selectCount(ruleModel)); + + IoTUserApplication userApplication = new IoTUserApplication(); + userApplication.setUnionId(creator); + userApplication.setDeleted(0); + qty.setApps(iotUserApplicationMapper.selectCount(userApplication)); + + IoTDevice ioTDevice = new IoTDevice(); + ioTDevice.setCreatorId(creator); + qty.setDevice(ioTDeviceMapper.selectCount(ioTDevice)); + ioTDevice.setState(true); + qty.setOnline(ioTDeviceMapper.selectCount(ioTDevice)); + qty.setOffline(qty.getDevice() - qty.getOnline()); + + return qty; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTDeviceMessageCodecService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTDeviceMessageCodecService.java new file mode 100644 index 0000000..a828b8a --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTDeviceMessageCodecService.java @@ -0,0 +1,112 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import cn.universal.common.exception.CodecException; +import cn.universal.core.protocol.jar.ProtocolCodecJar; +import cn.universal.core.protocol.jscrtipt.ProtocolCodecJscript; +import cn.universal.core.protocol.magic.ProtocolCodecMagic; +import cn.universal.core.protocol.support.ProtocolCodecSupport; +import cn.universal.core.protocol.support.ProtocolSupportDefinition; +import cn.universal.core.service.ICodecService; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** 在线编解码转换测试 */ +@Service("messageCodecService") +@Slf4j +public class IoTDeviceMessageCodecService { + + @Resource private IoTProductDeviceService iotProductDeviceService; + + @Autowired private ICodecService codecService; + + // 预解析 + public Object preDecode(String productKey, Object payload) { + if (productKey == null || payload == null) { + return null; + } + try { + Object result = codecService.preDecode(productKey, JSONUtil.toJsonStr(payload)); + log.info("产品编号={} 原始报文={} , preDecode解码={}", productKey, payload, JSONUtil.toJsonStr(result)); + return result; + } catch (Exception e) { + log.info( + "产品编号={} 原始报文={} , preDecode解码异常", productKey, payload, ExceptionUtil.getRootCause(e)); + return payload; + } + } + + // 上行解码 + public Object decode(String productKey, String payload) throws CodecException { + if (StrUtil.isBlank(payload)) { + return null; + } + try { + Object result = codecService.decode(productKey, payload); + log.info("产品编号={} 原始报文={} , 解码={}", productKey, payload, JSONUtil.toJsonStr(result)); + return result; + } catch (Exception e) { + log.error("产品编号={} 原始报文={} , 解码异常", productKey, payload, e); + return null; + } + } + + // 下行编码 + public String encode(String productKey, String payload) throws CodecException { + try { + String result = codecService.encode(productKey, payload); + log.info("产品编号={} 原始报文={} , 编码={}", productKey, payload, JSONUtil.toJsonStr(result)); + return result; + } catch (Exception e) { + log.error("产品编号={} 原始报文={} , 编码异常", productKey, payload, e); + return null; + } + } + + /** + * 查询产品的解码 + * + * @param productKey + * @return + */ + protected ProtocolSupportDefinition selectDevProtocolDef(String productKey) { + return iotProductDeviceService.selectProtocolDef(productKey); + } + + /** + * 获取编解码插件,工程模式 + * + * @param supportType + * @return + */ + protected ProtocolCodecSupport getProtocolCodecProvider(String supportType) { + if (supportType == null) { + return null; + } + if (supportType.equalsIgnoreCase("jar")) { + return ProtocolCodecJar.getInstance(); + } else if (supportType.equalsIgnoreCase("jscript")) { + return ProtocolCodecJscript.getInstance(); + } else if (supportType.equalsIgnoreCase("magic")) { + return ProtocolCodecMagic.getInstance(); + } + return null; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTDeviceProtocolServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTDeviceProtocolServiceImpl.java new file mode 100644 index 0000000..6fefe70 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTDeviceProtocolServiceImpl.java @@ -0,0 +1,283 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.service.BaseServiceImpl; +import cn.universal.admin.system.service.IoTDeviceProtocolService; +import cn.universal.common.constant.IoTConstant.ProductFlushType; +import cn.universal.common.event.EventTopics; +import cn.universal.common.event.ProtocolUpdatedEvent; +import cn.universal.common.event.processer.EventPublisher; +import cn.universal.common.exception.IoTException; +import cn.universal.core.protocol.jar.ProtocolCodecJar; +import cn.universal.core.protocol.jscrtipt.JsMethodExtractor; +import cn.universal.core.protocol.jscrtipt.ProtocolCodecJscript; +import cn.universal.core.protocol.magic.MagicScriptMethodExtractor; +import cn.universal.core.protocol.magic.ProtocolCodecMagic; +import cn.universal.dm.device.service.impl.IoTCacheRemoveService; +import cn.universal.ossm.service.ISysOssService; +import cn.universal.persistence.entity.IoTDeviceProtocol; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.bo.IoTDeviceProtocolBO; +import cn.universal.persistence.mapper.IoTDeviceProtocolMapper; +import jakarta.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 设备协议Service业务层处理 @Author ruoyi + * + * @since 2023-01-06 + */ +@Service +@Slf4j +public class IoTDeviceProtocolServiceImpl extends BaseServiceImpl + implements IoTDeviceProtocolService { + + @Resource private IoTDeviceProtocolMapper ioTDeviceProtocolMapper; + @Resource private ISysOssService ossService; + + @Resource private IoTCacheRemoveService iotCacheRemoveService; + + @Resource private EventPublisher eventPublisher; + + @Resource private ApplicationEventPublisher applicationEventPublisher; + + @Override + public int countProtocol(String id) { + return ioTDeviceProtocolMapper.selectCount(IoTDeviceProtocol.builder().id(id).build()); + } + + /** + * 查询设备协议 + * + * @param id 设备协议主键 + * @return 设备协议 + */ + @Override + public IoTDeviceProtocol selectDevProtocolById(String id, String unionId) { + return ioTDeviceProtocolMapper.selectDevProtocolById(id, unionId); + } + + @Override + public int countByProvider(String provider) { + return ioTDeviceProtocolMapper.countByProvider(provider); + } + + /** + * 查询设备协议列表 + * + * @param ioTDeviceProtocol 设备协议 + * @return 设备协议 + */ + @Override + @Transactional + public List selectDevProtocolList( + IoTDeviceProtocol ioTDeviceProtocol, IoTUser iotUser) { + return ioTDeviceProtocolMapper.selectDevProtocolList( + ioTDeviceProtocol, iotUser.isAdmin() ? null : iotUser.getUnionId()); + } + + /** + * 新增设备协议 + * + * @param ioTDeviceProtocol 设备协议 + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = {"iot_dev_product_list", "iot_protocol_def", "selectProtocolDefNoScript"}, + allEntries = true) + public int insertDevProtocol(IoTDeviceProtocol ioTDeviceProtocol) { + return ioTDeviceProtocolMapper.insertDevProtocol(ioTDeviceProtocol); + } + + @Override + public int insertList(List ioTDeviceProtocolList) { + return ioTDeviceProtocolMapper.insertProtocolList(ioTDeviceProtocolList); + } + + /** + * 修改设备协议 + * + * @param ioTDeviceProtocolBO 设备协议 + * @return 结果 + */ + @Override + @Transactional + @CacheEvict( + cacheNames = {"iot_dev_product_list", "iot_protocol_def", "selectProtocolDefNoScript"}, + allEntries = true) + public int updateDevProtocol(IoTDeviceProtocolBO ioTDeviceProtocolBO, IoTUser iotUser) { + IoTDeviceProtocol ioTDeviceProtocol = + BeanUtil.toBean(ioTDeviceProtocolBO, IoTDeviceProtocol.class); + IoTDeviceProtocol oldIoTDeviceProtocol = + ioTDeviceProtocolMapper.selectDevProtocolById( + ioTDeviceProtocol.getId(), iotUser.getIdentity() != 0 ? iotUser.getUnionId() : null); + if (ObjectUtil.isNull(oldIoTDeviceProtocol)) { + throw new IoTException("该协议没有操作权限!"); + } + JSONObject config = JSONUtil.parseObj(oldIoTDeviceProtocol.getConfiguration()); + if ("jar".equals(ioTDeviceProtocolBO.getType())) { + // 状态变更 + if (ioTDeviceProtocolBO.getUrl() == null) { + if (iotUser.getIdentity() != 0) { + throw new IoTException("请联系管理员审核插件后发布"); + } + return ioTDeviceProtocolMapper.updateDevProtocol(ioTDeviceProtocol); + + } else { + if (!ioTDeviceProtocolBO.getFileName().equals(config.getStr("provider"))) { + ioTDeviceProtocol.setState((byte) 0); + } + if ("jscript".equals(oldIoTDeviceProtocol.getType())) { + ioTDeviceProtocol.setState((byte) 0); + } + } + } + + if ("jar".equals(oldIoTDeviceProtocol.getType())) { + // 删除旧oss资源 + if (!config.getStr("location").equals(ioTDeviceProtocolBO.getUrl())) { + // JSONObject object1 = JSONUtil.parseObj(oldIoTDeviceProtocol.getConfiguration()); + String[] a = {config.getStr("location")}; + ossService.deleteWithValidByUrls(a, false); + ioTDeviceProtocol.setState((byte) 0); + } + } + JSONObject object = new JSONObject(); + String provider = oldIoTDeviceProtocol.getId(); + if ("jscript".equals(ioTDeviceProtocolBO.getType())) { + provider = ioTDeviceProtocolBO.getId(); + object.set("needBs4Decode", ioTDeviceProtocolBO.getNeedBs4Decode()); + object.set("location", ioTDeviceProtocolBO.getJscript()); + object.set("provider", provider); + Set strings = JsMethodExtractor.extractMethodNames(ioTDeviceProtocolBO.getJscript()); + Collection intersection = CollUtil.intersection(method, strings); + if (CollUtil.isEmpty(intersection)) { + throw new IoTException("预编译失败,没有包含任何一个有效方法"); + } + object.set("supportMethods", intersection); + + } else if ("magic".equals(ioTDeviceProtocolBO.getType())) { + Set supportMethods = + MagicScriptMethodExtractor.extractSupportMethods( + ioTDeviceProtocolBO.getJscript(), method); + object.set("needBs4Decode", ioTDeviceProtocolBO.getNeedBs4Decode()); + object.set("location", ioTDeviceProtocolBO.getJscript()); + object.set("provider", provider); + if (CollUtil.isEmpty(supportMethods)) { + throw new IoTException("预编译失败,没有包含任何一个有效方法"); + } + object.put("supportMethods", supportMethods); + + } else { + provider = ioTDeviceProtocolBO.getFileName(); + object.set("provider", ioTDeviceProtocolBO.getFileName()); + object.set("location", ioTDeviceProtocolBO.getUrl()); + } + ioTDeviceProtocol.setConfiguration(object.toString()); + int rows = ioTDeviceProtocolMapper.updateDevProtocol(ioTDeviceProtocol); + if (rows > 0) { + JSONObject object1 = new JSONObject(); + object1.set("type", ProductFlushType.script.name()); + object1.set("provider", provider); + if ("jscript".equals(ioTDeviceProtocolBO.getType())) { + object1.set("customType", "jscript"); + eventPublisher.publishEvent(EventTopics.PROTOCOL_UPDATED, object1); + // ProtocolCodecJscript.getInstance().remove(provider); + } else if ("magic".equals(ioTDeviceProtocolBO.getType())) { + object1.set("customType", "magic"); + eventPublisher.publishEvent(EventTopics.PROTOCOL_UPDATED, object1); + } else { + object1.set("customType", "jar"); + eventPublisher.publishEvent(EventTopics.PROTOCOL_UPDATED, object1); + } + // 清除协议相关的所有缓存 + iotCacheRemoveService.removeDevProtocolCache(); + + // 发布协议更新事件,通知相关组件清理缓存 + applicationEventPublisher.publishEvent( + new ProtocolUpdatedEvent(this, ioTDeviceProtocol.getId(), ioTDeviceProtocolBO.getType())); + } + return rows; + } + + /** + * 批量删除设备协议 + * + * @param ids 需要删除的设备协议主键 + * @return 结果 + */ + @Override + @Transactional + @CacheEvict( + cacheNames = {"iot_dev_product_list", "iot_protocol_def", "selectProtocolDefNoScript"}, + allEntries = true) + public int deleteDevProtocolByIds(String[] ids) { + List list = ioTDeviceProtocolMapper.selectDevProtocolByIds(ids); + String[] a = + list.stream() + .filter(s -> "jar".equals(s.getType())) + .map( + i -> { + JSONObject object1 = JSONUtil.parseObj(i.getConfiguration()); + return object1.getStr("location"); + }) + .toArray(String[]::new); + ossService.deleteWithValidByUrls(a, false); + int rs = ioTDeviceProtocolMapper.deleteDevProtocolByIds(ids); + // 删除编解码缓存 + list.stream() + .forEach( + s -> { + String provider = null; + if ("jscript".equals(s.getType())) { + provider = s.getId(); + ProtocolCodecJscript.getInstance().remove(provider); + } else if ("magic".equals(s.getType())) { + provider = s.getId(); + ProtocolCodecMagic.getInstance().remove(provider); + } else { + provider = s.toDefinition().getProvider(); + ProtocolCodecJar.getInstance().remove(provider); + } + }); + return rs; + } + + /** + * 删除设备协议信息 + * + * @param id 设备协议主键 + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = {"iot_dev_product_list", "iot_protocol_def", "selectProtocolDefNoScript"}, + allEntries = true) + public int deleteDevProtocolById(String id) { + return ioTDeviceProtocolMapper.deleteDevProtocolById(id); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTDeviceSubscribeServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTDeviceSubscribeServiceImpl.java new file mode 100644 index 0000000..d65cca4 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTDeviceSubscribeServiceImpl.java @@ -0,0 +1,135 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.hutool.core.util.StrUtil; +import cn.universal.admin.common.service.BaseServiceImpl; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.IoTDeviceSubscribeService; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.constant.IoTConstant.DeviceSubscribe; +import cn.universal.common.domain.R; +import cn.universal.common.exception.IoTErrorCode; +import cn.universal.dm.device.service.impl.IoTCacheRemoveService; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTDeviceSubscribe; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.IoTDeviceSubscribeMapper; +import jakarta.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import org.springframework.stereotype.Service; + +/** + * subscribe @Author ruoyi + * + * @since 2023-01-06 + */ +@Service +public class IoTDeviceSubscribeServiceImpl extends BaseServiceImpl + implements IoTDeviceSubscribeService { + + @Resource private IoTDeviceSubscribeMapper ioTDeviceSubscribeMapper; + + @Resource private IoTDeviceMapper ioTDeviceMapper; + + @Resource private IoTCacheRemoveService iotCacheRemoveService; + + @Override + public List selectDevSubscribeList(IoTDeviceSubscribe sub) { + List subscribes = ioTDeviceSubscribeMapper.selectSubscribesBO(sub); + subscribes.forEach( + (item) -> { + if (StrUtil.isNotBlank(item.getUrl()) && StrUtil.isNotBlank(item.getTopic())) { + item.setUrl(item.getUrl() + "," + item.getTopic()); + } else if (StrUtil.isNotBlank(item.getUrl())) { + item.setUrl(item.getUrl()); + } else if (StrUtil.isNotBlank(item.getTopic())) { + item.setUrl(item.getTopic()); + } + }); + return subscribes; + } + + @Override + public IoTDeviceSubscribe selectDevInstanceById(String id) { + return ioTDeviceSubscribeMapper.selectByPrimaryKey(id); + } + + @Override + public R updateDevSubscribe(IoTDeviceSubscribe sub) { + int update = ioTDeviceSubscribeMapper.updateByPrimaryKey(sub); + if (update >= 1) { + return R.ok(); + } + return R.error("更新失败"); + } + + @Override + public R insertDevSubscribe(IoTDeviceSubscribe sub) { + IoTDeviceSubscribe build = + IoTDeviceSubscribe.builder() + .subType(DeviceSubscribe.DEVICE.name()) + .iotId(sub.getIotId()) + .productKey(sub.getProductKey()) + .build(); + int i = ioTDeviceSubscribeMapper.selectCount(build); + if (i > IoTConstant.MAX_DEV_MSG_SUBSCRIBE_NUM) { + return R.error( + IoTErrorCode.DEV_SUBSCRIBE_REPEAT_ERROR.getCode(), + IoTErrorCode.DEV_SUBSCRIBE_REPEAT_ERROR.getName()); + } + IoTDevice ioTDevice = + ioTDeviceMapper.selectOne(IoTDevice.builder().iotId(sub.getIotId()).build()); + if (ioTDevice != null) { + sub.setDeviceId(ioTDevice.getDeviceId()); + } + sub.setCreater(SecurityUtils.getUnionId()); + sub.setInstance("0"); + + sub.setCreateDate(new Date()); + sub.setEnabled(true); + int insert = ioTDeviceSubscribeMapper.insert(sub); + if (insert >= 1) { + iotCacheRemoveService.removeIotDeviceSubscribeCache(); + return R.ok(); + } + return R.error("添加失败"); + } + + @Override + public R deleteDevSubscribe(String productKey, String iotId, Long[] ids) { + IoTDevice ioTDevice = + ioTDeviceMapper.selectOne(IoTDevice.builder().iotId(iotId).productKey(productKey).build()); + if (Objects.isNull(ioTDevice)) { + return R.error("删除失败,设备不存在"); + } + if (!SecurityUtils.getUnionId().equals(ioTDevice.getCreatorId())) { + return R.error("权限不足"); + } + StringBuffer str = new StringBuffer(); + for (int i = 0; i < ids.length; i++) { + str.append(ids[i]); + if ((i + 1) < ids.length) { + str.append(","); + } + } + int delete = ioTDeviceSubscribeMapper.deleteByIds(str.toString()); + if (delete >= 1) { + iotCacheRemoveService.removeIotDeviceSubscribeCache(); + return R.ok(); + } + return R.error("删除失败"); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTUserApplicationServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTUserApplicationServiceImpl.java new file mode 100644 index 0000000..6586b10 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTUserApplicationServiceImpl.java @@ -0,0 +1,215 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.universal.admin.common.service.BaseServiceImpl; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.IIoTUserApplicationService; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.IoTUserApplication; +import cn.universal.persistence.entity.OAuth2ClientDetails; +import cn.universal.persistence.entity.vo.IoTUserApplicationVO; +import cn.universal.persistence.mapper.IoTUserApplicationMapper; +import cn.universal.persistence.mapper.OauthClientDetailsMapper; +import jakarta.annotation.Resource; +import java.util.List; +import java.util.Objects; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import tk.mybatis.mapper.entity.Example; + +/** + * 用户应用信息Service业务层处理 + * + * @since 2025-12-30 + */ +@Service +public class IoTUserApplicationServiceImpl extends BaseServiceImpl + implements IIoTUserApplicationService { + + @Resource private IoTUserApplicationMapper iotUserApplicationMapper; + @Resource private IIotUserService iIotUserService; + + @Resource private OauthClientDetailsMapper oauthClientDetailsMapper; + + @Override + public boolean EnDisableIoTUser(String unionId, boolean isEnable) { + List select = + iotUserApplicationMapper.select(IoTUserApplication.builder().unionId(unionId).build()); + if (CollectionUtil.isNotEmpty(select)) { + // 处理应用 + for (IoTUserApplication iotUserApplication : select) { + if (isEnable) { + iotUserApplication.setAppStatus(IoTConstant.NORMAL); + } else { + iotUserApplication.setAppStatus(IoTConstant.UN_NORMAL); + } + iotUserApplicationMapper.updateByPrimaryKeySelective(iotUserApplication); + } + } + // 处理oauth2鉴权 + List oauth2 = + oauthClientDetailsMapper.select(OAuth2ClientDetails.builder().iotUnionId(unionId).build()); + if (CollectionUtil.isNotEmpty(oauth2)) { + for (OAuth2ClientDetails details : oauth2) { + OAuth2ClientDetails build = OAuth2ClientDetails.builder().build(); + if (isEnable) { + build.setAuthorizedGrantTypes("client_credentials"); + } else { + build.setAuthorizedGrantTypes(""); + } + Example example = new Example(OAuth2ClientDetails.class); + example.createCriteria().andEqualTo("clientId", details.getClientId()); + oauthClientDetailsMapper.updateByExampleSelective(build, example); + } + } + return true; + } + + @Override + @Cacheable(cacheNames = "iot_app_user_name", key = "#applicationId") + public String selectApplicationName(String applicationId) { + IoTUserApplication iotUserApplication = + iotUserApplicationMapper.selectOne( + IoTUserApplication.builder().appUniqueId(applicationId).build()); + return iotUserApplication == null ? "" : iotUserApplication.getAppName(); + } + + /** + * 查询用户应用信息 + * + * @param appUniqueId 用户应用信息ID + * @return 用户应用信息 + */ + @Override + public IoTUserApplication selectIotUserApplicationById(String appUniqueId) { + return checkSelf(appUniqueId); + } + + public IoTUserApplication checkSelf(String appUniqueId) { + IoTUserApplication a = IoTUserApplication.builder().appUniqueId(appUniqueId).build(); + String unionId = queryIotUser(SecurityUtils.getUnionId()).getUnionId(); + if (!iIotUserService.selectUserByUnionId(unionId).isAdmin()) { + a.setUnionId(unionId); + } + IoTUserApplication iotUserApplication = iotUserApplicationMapper.selectOne(a); + if (Objects.isNull(iotUserApplication)) { + throw new IoTException("您没有权限操作此应用!"); + } + return iotUserApplication; + } + + /** + * 查询用户应用信息列表 + * + * @param iotUserApplication 用户应用信息 + * @return 用户应用信息 + */ + @Override + public List selectIotUserApplicationList( + IoTUserApplication iotUserApplication, IoTUser iotUser) { + iotUserApplication.setUnionId(iotUser.isAdmin() ? null : iotUser.getUnionId()); + return iotUserApplicationMapper.selectIotUserApplicationList(iotUserApplication); + } + + /** + * 新增用户应用信息 + * + * @param iotUserApplication 用户应用信息 + * @return 结果 + */ + @Override + public int insertIotAppUser(IoTUserApplication iotUserApplication) { + IoTUser iotUser = queryIotUser(iotUserApplication.getUnionId()); + iotUserApplication.setUnionId(iotUser.getUnionId()); + return iotUserApplicationMapper.insertIotUserApplication(iotUserApplication); + } + + /** + * 修改用户应用信息 + * + * @param iotUserApplication 用户应用信息 + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount" + }, + allEntries = true) + public int updateIotUserApplication(IoTUserApplication iotUserApplication) { + return iotUserApplicationMapper.updateIotUserApplication(iotUserApplication); + } + + /** + * 删除用户应用信息对象 + * + * @param appUniqueId 需要删除的数据ID + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount" + }, + allEntries = true) + public int deleteIotUserApplicationByIds(String[] appUniqueId) { + return iotUserApplicationMapper.deleteIotUserApplicationByIds(appUniqueId); + } + + @Override + public List selectIotUserApplicationByIds(String[] appUniqueId) { + String a = String.join(",", appUniqueId); + return iotUserApplicationMapper.selectByIds(a); + } + + /** + * 删除用户应用信息信息 x + * + * @param appUniqueId 用户应用信息ID + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount" + }, + allEntries = true) + public int deleteIotUserApplicationById(String appUniqueId) { + return iotUserApplicationMapper.deleteIotUserApplicationById(appUniqueId); + } + + @Override + public List selectApplicationList( + IoTUserApplication application, IoTUser iotUser) { + application.setUnionId(iotUser.isAdmin() ? null : iotUser.getUnionId()); + return iotUserApplicationMapper.selectApplicationList(application); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTUserServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTUserServiceImpl.java new file mode 100644 index 0000000..72f215f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTUserServiceImpl.java @@ -0,0 +1,279 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * + * @Author: Aleo + * + * @Email: wo8335224@gmail.com + * + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.IdUtil; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.admin.system.service.ISysRoleService; +import cn.universal.common.constant.IoTUserConstants; +import cn.universal.common.exception.IoTException; +import cn.universal.common.utils.StringUtils; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.admin.SysUserRole; +import cn.universal.persistence.entity.bo.IoTUserBO; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.IoTUserMapper; +import cn.universal.persistence.mapper.admin.SysUserRoleMapper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import tk.mybatis.mapper.entity.Example; + +/** 用户 业务层处理 @Author ruoyi */ +@Slf4j +@Service +public class IoTUserServiceImpl implements IIotUserService { + + @Resource private IoTUserMapper iotUserMapper; + @Resource private SysUserRoleMapper userRoleMapper; + // @Resource + // private OauthClientDetailsMapper oauthClientDetailsMapper; + @Resource private IoTDeviceMapper ioTDeviceMapper; + @Resource private ISysRoleService iSysRoleService; + + @Override + // @DataScope(alias = "", created = "u.create_by") + public List selectUserList(IoTUser user) { + return iotUserMapper.selectList(user); + } + + @Override + public IoTUser selectUserById(Long userId) { + return iotUserMapper.selectByPrimaryKey(userId); + } + + @Override + public IoTUser selectUserByMobile(String mobile) { + return iotUserMapper.selectUserByMobile(mobile); + } + + @Override + @Cacheable(cacheNames = "iot_user_info", key = "#unionId", unless = "#result == null") + public IoTUser selectUserByUnionId(String unionId) { + Example ex = new Example(IoTUser.class); + ex.createCriteria().andEqualTo("unionId", unionId).andEqualTo("deleted", 0); + ; + return iotUserMapper.selectOneByExample(ex); + } + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + public List selectAllocatedList(IoTUserBO user) { + return iotUserMapper.selectAllocatedList(user); + } + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + public List selectUnallocatedList(IoTUserBO user) { + return iotUserMapper.selectUnallocatedList(user); + } + + @Override + public IoTUser queryOne(IoTUser iotUser) { + return iotUserMapper.selectOne(iotUser); + } + + @Override + @Cacheable(cacheNames = "selectUserByUserName", key = "#userName", unless = "#result == null") + public IoTUser selectUserByUserName(String userName) { + Example ex = new Example(IoTUser.class); + ex.createCriteria().andEqualTo("username", userName).andEqualTo("deleted", 0); + return iotUserMapper.selectOneByExample(ex); + } + + @Override + public String checkUserNameUnique(IoTUser user) { + Long userId = StringUtils.isNull(user.getId()) ? -1L : user.getId(); + // unionId必须唯一 新增是unionId为usrname + List users = + iotUserMapper.selectList(IoTUser.builder().unionId(user.getUsername()).build()); + if (!CollectionUtils.isEmpty(users) && users.get(0).getId().longValue() != userId.longValue()) { + return IoTUserConstants.NOT_UNIQUE; + } + return IoTUserConstants.UNIQUE; + } + + @Override + public String checkPhoneUnique(IoTUser user) { + Long userId = StringUtils.isNull(user.getId()) ? -1L : user.getId(); + List users = + iotUserMapper.selectList(IoTUser.builder().mobile(user.getMobile()).build()); + if (!CollectionUtils.isEmpty(users) && users.get(0).getId().longValue() != userId.longValue()) { + return IoTUserConstants.NOT_UNIQUE; + } + return IoTUserConstants.UNIQUE; + } + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + @Override + public void checkUserAllowed(IoTUser user) { + if (Validator.isNotNull(user.getId()) && user.isAdmin()) { + throw new IoTException("不允许操作超级管理员用户"); + } + } + + @Override + @Transactional + public int insertUser(IoTUserBO userbo) { + userbo.setId(IdUtil.getSnowflake().nextId()); + IoTUser iotUser = BeanUtil.toBean(userbo, IoTUser.class); + int rows = iotUserMapper.insert(iotUser); + insertUserRole(userbo); + return rows; + } + + @Override + @CacheEvict( + cacheNames = {"iot_user_info"}, + key = "#userbo.unionId") + @Transactional + public int updateUser(IoTUserBO userbo) { + IoTUser iotUser = BeanUtil.toBean(userbo, IoTUser.class); + if (iotUser.getPassword() == null || "".equals(iotUser.getPassword())) { + iotUser.setPassword(null); + deleteUserRoleByUnionId(userbo); + insertUserRole(userbo); + } + return iotUserMapper.updateByPrimaryKeySelective(iotUser); + } + + @Override + @CacheEvict( + cacheNames = {"iot_user_info"}, + key = "#iotUser.unionId") + public void updateUserById(IoTUser iotUser) { + iotUserMapper.updateByPrimaryKeySelective(iotUser); + } + + @Override + public int deleteUserById(Long userId) { + return iotUserMapper.deleteByPrimaryKey(userId); + } + + @Override + public int deleteUserByIds(Long[] userIds) { + String ids = ""; + for (int i = 0; i < userIds.length; i++) { + checkUserAllowed(new IoTUser(userIds[i])); + if (i == 0) { + ids = ids + userIds[i]; + } else { + ids = ids + "," + userIds[i]; + } + } + + List list = iotUserMapper.selectByIds(ids); + String[] unionIds = + list.stream() + .map(IoTUser::getUnionId) + .collect(Collectors.toList()) + .toArray(new String[list.size()]); + Example ex = new Example(IoTDevice.class); + ex.createCriteria().andIn("creatorId", Arrays.asList(unionIds)); + int count = ioTDeviceMapper.selectCountByExample(ex); + if (count > 0) { + throw new IoTException("该账号下存在设备,不允许删除!"); + } + userRoleMapper.deleteUserRoleByUserIds(unionIds); + return iotUserMapper.deleteByIds(ids); + } + + /** + * 新增用户角色信息 + * + * @param user 用户对象 + */ + public void insertUserRole(IoTUserBO user) { + Long[] roles = user.getRoleIds(); + if (StringUtils.isNotNull(roles)) { + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long roleId : roles) { + SysUserRole ur = new SysUserRole(); + ur.setUnionId(user.getUnionId()); + ur.setRoleId(roleId); + list.add(ur); + } + if (list.size() > 0) { + userRoleMapper.insertList(list); + } + } + } + + /** + * 删除用户角色信息 + * + * @param user 用户对象 + */ + public void deleteUserRoleByUnionId(IoTUserBO user) { + if (user.getRoleIds() != null) { + Example ex = new Example(SysUserRole.class); + ex.createCriteria().andEqualTo("unionId", user.getUnionId()); + userRoleMapper.deleteByExample(ex); + } + } + + /** + * 用户授权角色 + * + * @param unionId 用户ID + * @param roleIds 角色组 + */ + @Override + @Transactional + public void insertUserAuth(String unionId, Long[] roleIds) { + Example ex = new Example(SysUserRole.class); + ex.createCriteria().andEqualTo("unionId", unionId); + userRoleMapper.deleteByExample(ex); + insertUserRole(IoTUserBO.builder().unionId(unionId).roleIds(roleIds).build()); + } + + @Override + @Transactional + public void addSubAccount(Long id, String unionId, int isSub) { + // isSub为1时为授权绑定 否则为取消授权 + if (isSub == 1) { + updateUserById(IoTUser.builder().id(id).parentUnionId(unionId).build()); + } else { + updateUserById(IoTUser.builder().id(id).parentUnionId("").build()); + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/OAuth2ClientDetailsServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/OAuth2ClientDetailsServiceImpl.java new file mode 100644 index 0000000..7ce4bf6 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/OAuth2ClientDetailsServiceImpl.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.universal.admin.system.service.IOAuthClientDetailsService; +import cn.universal.persistence.entity.OAuth2ClientDetails; +import cn.universal.persistence.mapper.OauthClientDetailsMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** 用户 业务层处理 @Author ruoyi */ +@Slf4j +@Service +public class OAuth2ClientDetailsServiceImpl implements IOAuthClientDetailsService { + + @Resource private OauthClientDetailsMapper oauthClientDetailsMapper; + + @Override + public int insert(OAuth2ClientDetails OAuth2ClientDetails) { + return oauthClientDetailsMapper.insert(OAuth2ClientDetails); + } + + @Override + public int updateSecret(OAuth2ClientDetails OAuth2ClientDetails) { + return oauthClientDetailsMapper.updateByPrimaryKeySelective(OAuth2ClientDetails); + } + + @Override + public int deleteByClientIds(String[] ids) { + // String a=String.join(",",ids); + return oauthClientDetailsMapper.deleteByClientIds(ids); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysConfigServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysConfigServiceImpl.java new file mode 100644 index 0000000..050937c --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysConfigServiceImpl.java @@ -0,0 +1,204 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.hutool.core.convert.Convert; +import cn.universal.admin.system.service.ISysConfigService; +import cn.universal.common.constant.Constants; +import cn.universal.common.constant.IoTUserConstants; +import cn.universal.common.exception.IoTException; +import cn.universal.common.utils.StringUtils; +import cn.universal.persistence.entity.admin.SysConfig; +import cn.universal.persistence.mapper.admin.SysConfigMapper; +import io.netty.util.internal.StringUtil; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import java.util.List; +import org.springframework.stereotype.Service; + +/** 参数配置 服务层实现 @Author ruoyi */ +@Service +public class SysConfigServiceImpl implements ISysConfigService { + + @Resource private SysConfigMapper configMapper; + + /** 项目启动时,初始化参数到缓存 */ + @PostConstruct + public void init() { + loadingConfigCache(); + } + + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + @Override + public SysConfig selectConfigById(Long configId) { + SysConfig config = new SysConfig(); + config.setConfigId(configId); + return configMapper.selectConfig(config); + } + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数key + * @return 参数键值 + */ + @Override + public String selectConfigByKey(String configKey) { + // String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey))); + // if (StringUtils.isNotEmpty(configValue)) + // { + // return configValue; + // } + SysConfig config = new SysConfig(); + config.setConfigKey(configKey); + SysConfig retConfig = configMapper.selectConfig(config); + if (StringUtils.isNotNull(retConfig)) { + // redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue()); + return retConfig.getConfigValue(); + } + return StringUtil.EMPTY_STRING; + } + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + @Override + public boolean selectCaptchaOnOff() { + String captchaOnOff = selectConfigByKey("sys.account.captchaOnOff"); + if (StringUtils.isEmpty(captchaOnOff)) { + return true; + } + return Convert.toBool(captchaOnOff); + } + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + @Override + public List selectConfigList(SysConfig config) { + return configMapper.selectConfigList(config); + } + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int insertConfig(SysConfig config) { + int row = configMapper.insertConfig(config); + // if (row > 0) + // { + // redisCache.setCacheObject(getCacheKey(config.getConfigKey()), + // config.getConfigValue()); + // } + return row; + } + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int updateConfig(SysConfig config) { + int row = configMapper.updateConfig(config); + // if (row > 0) + // { + // redisCache.setCacheObject(getCacheKey(config.getConfigKey()), + // config.getConfigValue()); + // } + return row; + } + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + * @return 结果 + */ + @Override + public void deleteConfigByIds(Long[] configIds) { + for (Long configId : configIds) { + SysConfig config = selectConfigById(configId); + if (IoTUserConstants.YES.equals(config.getConfigType())) { + throw new IoTException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey())); + } + configMapper.deleteConfigById(configId); + // redisCache.deleteObject(getCacheKey(config.getConfigKey())); + } + } + + /** 加载参数缓存数据 */ + @Override + public void loadingConfigCache() { + // List configsList = configMapper.selectConfigList(new SysConfig()); + // for (SysConfig config : configsList) + // { + // redisCache.setCacheObject(getCacheKey(config.getConfigKey()), + // config.getConfigValue()); + // } + } + + /** 清空参数缓存数据 */ + @Override + public void clearConfigCache() { + // Collection keys = redisCache.keys(Constants.SYS_CONFIG_KEY + "*"); + // redisCache.deleteObject(keys); + } + + /** 重置参数缓存数据 */ + @Override + public void resetConfigCache() { + clearConfigCache(); + loadingConfigCache(); + } + + /** + * 校验参数键名是否唯一 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public String checkConfigKeyUnique(SysConfig config) { + Long configId = StringUtils.isNull(config.getConfigId()) ? -1L : config.getConfigId(); + SysConfig info = configMapper.checkConfigKeyUnique(config.getConfigKey()); + if (StringUtils.isNotNull(info) && info.getConfigId().longValue() != configId.longValue()) { + return IoTUserConstants.NOT_UNIQUE; + } + return IoTUserConstants.UNIQUE; + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + private String getCacheKey(String configKey) { + return Constants.SYS_CONFIG_KEY + configKey; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysDictDataServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysDictDataServiceImpl.java new file mode 100644 index 0000000..fff2dd4 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysDictDataServiceImpl.java @@ -0,0 +1,241 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.ISysDictDataService; +import cn.universal.admin.system.service.ISysDictTypeService; +import cn.universal.persistence.entity.admin.SysDictData; +import cn.universal.persistence.entity.admin.vo.SysDictDataVo; +import cn.universal.persistence.entity.admin.vo.SysDictDataVos; +import cn.universal.persistence.mapper.admin.SysDictDataMapper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import tk.mybatis.mapper.entity.Example; +import tk.mybatis.mapper.entity.Example.Criteria; + +/** 字典 业务层处理 @Author ruoyi */ +@Service +public class SysDictDataServiceImpl implements ISysDictDataService { + + @Resource private SysDictDataMapper sysDictDataMapper; + + @Autowired private ISysDictTypeService dictTypeService; + + @Override + // @Cacheable(cacheNames = "selectPageDictDataList", unless = "#result == null", keyGenerator = + // "redisKeyGenerate") + public List selectPageDictDataList(SysDictDataVo dictData) { + return sysDictDataMapper.selectDictData(dictData); + } + + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + @Override + // @Cacheable(cacheNames = "selectDictDataList", unless = "#result == null", keyGenerator = + // "redisKeyGenerate") + public List selectDictDataList(SysDictDataVo dictData) { + return sysDictDataMapper.selectDictData(dictData); + } + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + @Override + @Cacheable( + cacheNames = "selectDictLabel", + key = "''+#dictType+':'+#dictValue", + unless = "#result == null") + public String selectDictLabel(String dictType, String dictValue) { + Example ex = new Example(SysDictData.class); + ex.createCriteria().andEqualTo("dictType", dictType).andEqualTo("dictValue", dictValue); + SysDictData sysDictData = sysDictDataMapper.selectOneByExample(ex); + return sysDictData.getDictLabel(); + } + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + @Override + @Cacheable(cacheNames = "selectDictDataById", key = "''+#dictCode", unless = "#result == null") + public SysDictData selectDictDataById(Long dictCode) { + return sysDictDataMapper.selectByPrimaryKey(dictCode); + } + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = { + "selectDictDataById", + "selectDictLabel", + "selectDictDataByTypes", + "selectWhitelist", + "selectDictTypeByType" + }) + public void deleteDictDataByIds(Long[] dictCodes) { + for (Long dictCode : dictCodes) { + SysDictData data = selectDictDataById(dictCode); + sysDictDataMapper.deleteByPrimaryKey(dictCode); + List dictDatas = sysDictDataMapper.selectDictDataByType(data.getDictType()); + } + } + + /** + * 新增保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = { + "selectDictDataById", + "selectDictLabel", + "selectDictDataByTypes", + "selectWhitelist", + "selectDictTypeByType" + }) + public int insertDictData(SysDictData data) { + int row = sysDictDataMapper.insert(data); + return row; + } + + /** + * 修改保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = { + "selectDictDataById", + "selectDictLabel", + "selectDictDataByTypes", + "selectWhitelist", + "selectDictTypeByType" + }) + public int updateDictData(SysDictData data) { + int row = sysDictDataMapper.updateByPrimaryKey(data); + return row; + } + + /** 根据字典类型数组查询字典数据信息 */ + @Override + @Cacheable( + cacheNames = "selectDictDataByTypes", + unless = "#result == null", + keyGenerator = "redisKeyGenerate") + public Map selectDictDataByTypes(String[] dictType) { + Map data = new HashMap<>(); + for (int i = 0; i < dictType.length; i++) { + if (!"model_unit".equals(dictType[i])) { + data.put(dictType[i], dictTypeService.selectDictDataByType(dictType[i])); + } else { + List unitDictData = dictTypeService.selectDictDataByType(dictType[i]); + List dataVos = new ArrayList<>(); + for (SysDictData dict : unitDictData) { + SysDictDataVos dataVo = new SysDictDataVos(); + List dictData = dictTypeService.selectDictDataByType(dict.getDictLabel()); + dataVo.setId(dict.getDictLabel()); + dataVo.setName(dict.getDictValue()); + if (dictData != null && dictData.size() > 0) { + dataVo.setArray(dictData); + } + dataVos.add(dataVo); + } + data.put(dictType[i], dataVos); + } + } + return data; + } + + /** 新增字典类型(进销人员) */ + @Override + @CacheEvict( + cacheNames = { + "selectDictDataById", + "selectDictLabel", + "selectDictDataByTypes", + "selectWhitelist", + "selectDictTypeByType" + }) + public int insertSaleBuyDictData(SysDictData data) { + data.setDictValue(UUID.randomUUID().toString().replace("-", "")); + data.setDictSort(0L); + data.setCreateBy(SecurityUtils.getUnionId()); + data.setCreateTime(new Date()); + data.setStatus("0"); + int row = sysDictDataMapper.insert(data); + if (row > 0) { + List dictDatas = sysDictDataMapper.selectDictDataByType(data.getDictType()); + } + return row; + } + + @Override + @Cacheable( + cacheNames = "selectWhitelist", + keyGenerator = "redisKeyGenerate", + unless = "#result == null") + public Set selectWhitelist(List labels) { + Example ex = new Example(SysDictData.class); + Criteria criteria = ex.createCriteria().andEqualTo("dictType", "whitelist"); + if (CollectionUtil.isNotEmpty(labels)) { + criteria.andIn("dictLabel", labels); + } + List sysDictData = sysDictDataMapper.selectByExample(ex); + Set whitelist = new HashSet<>(); + if (CollectionUtil.isNotEmpty(sysDictData)) { + sysDictData.stream() + .filter(s -> StrUtil.isNotBlank(s.getDictValue())) + .forEach( + s -> { + String[] ls = s.getDictValue().split(","); + for (String v : ls) { + whitelist.add(v); + } + }); + } + return whitelist; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysDictTypeServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysDictTypeServiceImpl.java new file mode 100644 index 0000000..806cfe9 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysDictTypeServiceImpl.java @@ -0,0 +1,267 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Validator; +import cn.universal.admin.common.utils.DictUtils; +import cn.universal.admin.system.service.ISysDictTypeService; +import cn.universal.common.constant.IoTUserConstants; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.admin.SysDictData; +import cn.universal.persistence.entity.admin.SysDictType; +import cn.universal.persistence.mapper.admin.SysDictDataMapper; +import cn.universal.persistence.mapper.admin.SysDictTypeMapper; +import jakarta.annotation.Resource; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import tk.mybatis.mapper.entity.Example; + +/** 字典 业务层处理 @Author ruoyi */ +@Service +public class SysDictTypeServiceImpl + implements ISysDictTypeService, ApplicationListener { + + @Resource private SysDictDataMapper dictDataMapper; + @Resource private SysDictTypeMapper dictTypeMapper; + + /** 项目启动时,初始化字典到缓存 */ + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + // loadingDictCache(); + } + + @Override + // @Cacheable(cacheNames = "selectPageDictTypeList", unless = "#result == null", keyGenerator = + // "redisKeyGenerate") + public List selectPageDictTypeList(SysDictType dictType) { + return dictTypeMapper.selectDictTypeList(dictType); + } + + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + @Override + // @Cacheable(cacheNames = "selectDictTypeList", unless = "#result == null", keyGenerator = + // "redisKeyGenerate") + public List selectDictTypeList(SysDictType dictType) { + Map params = dictType.getParams(); + return dictTypeMapper.selectDictTypeList(dictType); + } + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + @Override + @Cacheable( + cacheNames = "selectDictTypeAll", + unless = "#result == null", + keyGenerator = "redisKeyGenerate") + public List selectDictTypeAll() { + return dictTypeMapper.selectAll(); + } + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + @Override + @Cacheable(cacheNames = "selectDictDataByType", key = "''+#dictType", unless = "#result == null") + public List selectDictDataByType(String dictType) { + List dictDatas = dictDataMapper.selectDictDataByType(dictType); + if (CollUtil.isNotEmpty(dictDatas)) { + return dictDatas; + } + return null; + } + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + @Override + @Cacheable(cacheNames = "selectDictTypeById", key = "''+#dictId", unless = "#result == null") + public SysDictType selectDictTypeById(Long dictId) { + return dictTypeMapper.selectByPrimaryKey(dictId); + } + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + @Override + @Cacheable(cacheNames = "selectDictTypeByType", key = "''+#dictType", unless = "#result == null") + public SysDictType selectDictTypeByType(String dictType) { + Example ex = new Example(SysDictType.class); + ex.createCriteria().andEqualTo("dictType", dictType); + return dictTypeMapper.selectOneByExample(ex); + } + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = { + "selectDictTypeList", + "selectDictTypeAll", + "selectDictDataByType", + "selectDictTypeById", + "selectDictTypeByType", + "selectPageDictTypeList" + }) + public void deleteDictTypeByIds(Long[] dictIds) { + for (Long dictId : dictIds) { + SysDictType dictType = selectDictTypeById(dictId); + Example ex = new Example(SysDictData.class); + ex.createCriteria().andEqualTo("dictType", dictType.getDictType()); + if (dictDataMapper.selectCountByExample(ex) > 0) { + throw new IoTException(String.format("%1$s已分配,不能删除", dictType.getDictName())); + } + DictUtils.removeDictCache(dictType.getDictType()); + } + List ids = Arrays.asList(dictIds); + String ids1 = ids.stream().map(Object::toString).collect(Collectors.joining(",")); + dictTypeMapper.deleteByIds(ids1); + } + + /** 加载字典缓存数据 */ + @Override + public void loadingDictCache() { + List dictTypeList = selectDictTypeAll(); + for (SysDictType dictType : dictTypeList) { + dictDataMapper.selectDictDataByType(dictType.getDictType()); + } + } + + /** 清空字典缓存数据 */ + @Override + @CacheEvict( + cacheNames = { + "selectDictTypeList", + "selectDictTypeAll", + "selectDictDataByType", + "selectDictTypeById", + "selectDictTypeByType", + "selectPageDictTypeList" + }) + public void clearDictCache() {} + + /** 重置字典缓存数据 */ + @Override + @CacheEvict( + cacheNames = { + "selectDictTypeList", + "selectDictTypeAll", + "selectDictDataByType", + "selectDictTypeById", + "selectDictTypeByType", + "selectPageDictTypeList" + }) + public void resetDictCache() {} + + /** + * 新增保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + @CacheEvict( + cacheNames = { + "selectDictTypeList", + "selectDictTypeAll", + "selectDictDataByType", + "selectDictTypeById", + "selectDictTypeByType", + "selectPageDictTypeList" + }) + public int insertDictType(SysDictType dict) { + int row = dictTypeMapper.insert(dict); + return row; + } + + /** + * 修改保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + @Transactional + @CacheEvict( + cacheNames = { + "selectDictTypeList", + "selectDictTypeAll", + "selectDictDataByType", + "selectDictTypeById", + "selectDictTypeByType", + "selectPageDictTypeList" + }) + public int updateDictType(SysDictType dict) { + SysDictType oldDict = dictTypeMapper.selectByPrimaryKey(dict.getDictId()); + Example ex = new Example(SysDictData.class); + ex.createCriteria().andEqualTo("dictType", oldDict.getDictType()); + SysDictData sysDictData = new SysDictData(); + sysDictData.setDictType(dict.getDictType()); + dictDataMapper.updateByExampleSelective(sysDictData, ex); + int row = dictTypeMapper.updateByPrimaryKey(dict); + return row; + } + + /** + * 校验字典类型称是否唯一 + * + * @param dict 字典类型 + * @return 结果 + */ + @Override + @Cacheable( + cacheNames = "checkDictTypeUnique", + unless = "#result == null", + keyGenerator = "redisKeyGenerate") + public String checkDictTypeUnique(SysDictType dict) { + Long dictId = Validator.isNull(dict.getDictId()) ? -1L : dict.getDictId(); + Example ex = new Example(SysDictType.class); + ex.createCriteria().andEqualTo("dictType", dict.getDictType()); + List dictTypes = dictTypeMapper.selectByExample(ex); + if (!CollectionUtils.isEmpty(dictTypes) + && dictTypes.get(0).getDictId().longValue() != dictId.longValue()) { + return IoTUserConstants.NOT_UNIQUE; + } + return IoTUserConstants.UNIQUE; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysLogininforServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysLogininforServiceImpl.java new file mode 100644 index 0000000..e630821 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysLogininforServiceImpl.java @@ -0,0 +1,73 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.universal.admin.common.service.BaseServiceImpl; +import cn.universal.admin.system.service.ISysLogininforService; +import cn.universal.persistence.entity.SysLogininfor; +import cn.universal.persistence.mapper.SysLogininforMapper; +import jakarta.annotation.Resource; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import org.springframework.stereotype.Service; + +/** 系统访问日志情况信息 服务层处理 @Author ruoyi */ +@Service +public class SysLogininforServiceImpl extends BaseServiceImpl implements ISysLogininforService { + + @Resource private SysLogininforMapper sysLogininforMapper; + + @Override + public List selectPageLogininforList(SysLogininfor logininfor) { + List sysLogininfors = sysLogininforMapper.selectList(logininfor); + return sysLogininfors; + } + + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + @Override + public void insertLogininfor(SysLogininfor logininfor) { + logininfor.setLoginTime(new Date()); + sysLogininforMapper.insert(logininfor); + } + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + @Override + public List selectLogininforList(SysLogininfor logininfor) { + List sysLogininfors = sysLogininforMapper.selectList(logininfor); + return sysLogininfors; + } + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + */ + @Override + public int deleteLogininforByIds(Long[] infoIds) { + return sysLogininforMapper.deleteByIds(Arrays.toString(infoIds)); + } + + /** 清空系统登录日志 */ + @Override + public void cleanLogininfor() {} +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysMenuServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..01fb76f --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,465 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.admin.system.service.ISysMenuService; +import cn.universal.common.constant.Constants; +import cn.universal.common.constant.IoTUserConstants; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.admin.SysMenu; +import cn.universal.persistence.entity.admin.SysRole; +import cn.universal.persistence.entity.admin.SysRoleMenu; +import cn.universal.persistence.entity.admin.TreeSelect; +import cn.universal.persistence.entity.admin.vo.MetaVo; +import cn.universal.persistence.entity.admin.vo.RouterVo; +import cn.universal.persistence.mapper.admin.SysMenuMapper; +import cn.universal.persistence.mapper.admin.SysRoleMapper; +import cn.universal.persistence.mapper.admin.SysRoleMenuMapper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** 菜单 业务层处理 @Author ruoyi */ +@Service +public class SysMenuServiceImpl implements ISysMenuService { + + public static final String PREMISSION_STRING = "perms[\"{0}\"]"; + + @Resource private SysRoleMapper roleMapper; + + @Resource private SysRoleMenuMapper roleMenuMapper; + @Resource private SysMenuMapper menuMapper; + @Resource private IIotUserService iIotUserService; + + /** + * 根据用户查询系统菜单列表 + * + * @param unionId 用户unionId + * @return 菜单列表 + */ + @Override + public List selectMenuList(String unionId) { + return selectMenuList(new SysMenu(), unionId); + } + + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + @Override + @Transactional + public List selectMenuList(SysMenu menu, String unionId) { + List menuList = null; + // 管理员显示所有菜单信息 + IoTUser iotUser = iIotUserService.selectUserByUnionId(unionId); + if (iotUser.isAdmin()) { + menuList = menuMapper.selectMenuList(menu); + } else { + menu.getParams().put("unionId", unionId); + menuList = menuMapper.selectMenuListByUserId(menu); + } + return menuList; + } + + /** + * 根据用户ID查询权限 + * + * @param unionId 用户unionId + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByUserId(String unionId) { + List perms = menuMapper.selectMenuPermsByUserId(unionId); + Set permsSet = new HashSet<>(); + for (String perm : perms) { + if (Validator.isNotEmpty(perm)) { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + @Override + public List selectMenuTreeByUnionId(String unionId) { + List menus = null; + IoTUser iotUser = iIotUserService.selectUserByUnionId(unionId); + if (iotUser.isAdmin()) { + menus = menuMapper.selectMenuTreeAll(); + } else { + menus = menuMapper.selectMenuTreeByUnionId(unionId); + } + return getChildPerms(menus, 0); + } + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + @Override + public List selectMenuListByRoleId(Long roleId) { + SysRole role = roleMapper.selectByPrimaryKey(roleId); + return menuMapper.selectMenuListByRoleId(roleId, role.getMenuCheckStrictly()); + } + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + @Override + public List buildMenus(List menus) { + List routers = new LinkedList(); + for (SysMenu menu : menus) { + RouterVo router = new RouterVo(); + router.setHidden("1".equals(menu.getVisible())); + router.setName(getRouteName(menu)); + router.setPath(getRouterPath(menu)); + router.setComponent(getComponent(menu)); + router.setMeta( + new MetaVo( + menu.getMenuName(), + menu.getIcon(), + StrUtil.equals("1", menu.getIsCache()), + menu.getPath())); + List cMenus = menu.getChildren(); + if (!cMenus.isEmpty() && IoTUserConstants.TYPE_DIR.equals(menu.getMenuType())) { + router.setAlwaysShow(true); + router.setRedirect("noRedirect"); + router.setChildren(buildMenus(cMenus)); + } else if (isMenuFrame(menu)) { + router.setMeta(null); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + children.setPath(menu.getPath()); + children.setComponent(menu.getComponent()); + children.setName(StrUtil.upperFirst(menu.getPath())); + children.setMeta( + new MetaVo( + menu.getMenuName(), + menu.getIcon(), + StrUtil.equals("1", menu.getIsCache()), + menu.getPath())); + childrenList.add(children); + router.setChildren(childrenList); + } else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) { + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon())); + router.setPath("/inner"); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + String routerPath = StringUtils.replace(menu.getPath(), Constants.HTTP, ""); + routerPath = StringUtils.replace(routerPath, Constants.HTTPS, ""); + children.setPath(routerPath); + children.setComponent(IoTUserConstants.INNER_LINK); + children.setName(StrUtil.upperFirst(routerPath)); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath())); + childrenList.add(children); + router.setChildren(childrenList); + } + routers.add(router); + } + return routers; + } + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + @Override + public List buildMenuTree(List menus) { + List returnList = new ArrayList(); + List tempList = new ArrayList(); + for (SysMenu dept : menus) { + tempList.add(dept.getMenuId()); + } + for (SysMenu menu : menus) { + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(menu.getParentId())) { + recursionFn(menus, menu); + returnList.add(menu); + } + } + if (returnList.isEmpty()) { + returnList = menus; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + @Override + public List buildMenuTreeSelect(List menus) { + List menuTrees = buildMenuTree(menus); + return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + @Override + public SysMenu selectMenuById(Long menuId) { + return menuMapper.selectByPrimaryKey(menuId); + } + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean hasChildByMenuId(Long menuId) { + int result = menuMapper.selectCount(SysMenu.builder().parentId(menuId).build()); + return result > 0; + } + + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean checkMenuExistRole(Long menuId) { + int result = roleMenuMapper.selectCount(SysRoleMenu.builder().menuId(menuId).build()); + return result > 0; + } + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int insertMenu(SysMenu menu) { + return menuMapper.insert(menu); + } + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int updateMenu(SysMenu menu) { + return menuMapper.updateByPrimaryKey(menu); + } + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public int deleteMenuById(Long menuId) { + return menuMapper.deleteByPrimaryKey(menuId); + } + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public String checkMenuNameUnique(SysMenu menu) { + Long menuId = Validator.isNull(menu.getMenuId()) ? -1L : menu.getMenuId(); + List info = + menuMapper.selectMenuList( + SysMenu.builder().menuName(menu.getMenuName()).parentId(menu.getParentId()).build()); + if (!CollectionUtils.isEmpty(info) + && info.get(0).getMenuId().longValue() != menuId.longValue()) { + return IoTUserConstants.NOT_UNIQUE; + } + return IoTUserConstants.UNIQUE; + } + + /** + * 获取路由名称 + * + * @param menu 菜单信息 + * @return 路由名称 + */ + public String getRouteName(SysMenu menu) { + String routerName = StrUtil.upperFirst(menu.getPath()); + // 非外链并且是一级目录(类型为目录) + if (isMenuFrame(menu)) { + routerName = StrUtil.EMPTY; + } + return routerName; + } + + /** + * 获取路由地址 + * + * @param menu 菜单信息 + * @return 路由地址 + */ + public String getRouterPath(SysMenu menu) { + String routerPath = menu.getPath(); + // 内链打开外网方式 + if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) { + routerPath = StringUtils.replace(routerPath, Constants.HTTP, ""); + routerPath = StringUtils.replace(routerPath, Constants.HTTPS, ""); + } + // 非外链并且是一级目录(类型为目录) + if (0 == menu.getParentId().intValue() + && IoTUserConstants.TYPE_DIR.equals(menu.getMenuType()) + && IoTUserConstants.NO_FRAME.equals(menu.getIsFrame())) { + routerPath = "/" + menu.getPath(); + } + // 非外链并且是一级目录(类型为菜单) + else if (isMenuFrame(menu)) { + routerPath = "/"; + } + return routerPath; + } + + /** + * 获取组件信息 + * + * @param menu 菜单信息 + * @return 组件信息 + */ + public String getComponent(SysMenu menu) { + String component = IoTUserConstants.LAYOUT; + if (StrUtil.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) { + component = menu.getComponent(); + } else if (StrUtil.isEmpty(menu.getComponent()) + && menu.getParentId().intValue() != 0 + && isInnerLink(menu)) { + component = IoTUserConstants.INNER_LINK; + } else if (StrUtil.isEmpty(menu.getComponent()) && isParentView(menu)) { + component = IoTUserConstants.PARENT_VIEW; + } + return component; + } + + /** + * 是否为菜单内部跳转 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isMenuFrame(SysMenu menu) { + return menu.getParentId().intValue() == 0 + && IoTUserConstants.TYPE_MENU.equals(menu.getMenuType()) + && menu.getIsFrame().equals(IoTUserConstants.NO_FRAME); + } + + /** + * 是否为内链组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isInnerLink(SysMenu menu) { + return menu.getIsFrame().equals(IoTUserConstants.NO_FRAME) && Validator.isUrl(menu.getPath()); + } + + /** + * 是否为parent_view组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isParentView(SysMenu menu) { + return menu.getParentId().intValue() != 0 + && IoTUserConstants.TYPE_DIR.equals(menu.getMenuType()); + } + + /** + * 根据父节点的ID获取所有子节点 + * + * @param list 分类表 + * @param parentId 传入的父节点ID + * @return String + */ + public List getChildPerms(List list, int parentId) { + List returnList = new ArrayList(); + for (SysMenu t : list) { + // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 + if (t.getParentId() == parentId) { + recursionFn(list, t); + returnList.add(t); + } + } + return returnList; + } + + /** 递归列表 */ + private void recursionFn(List list, SysMenu t) { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysMenu tChild : childList) { + if (hasChild(list, tChild)) { + recursionFn(list, tChild); + } + } + } + + /** 得到子节点列表 */ + private List getChildList(List list, SysMenu t) { + List tlist = new ArrayList(); + for (SysMenu n : list) { + if (n.getParentId().longValue() == t.getMenuId().longValue()) { + tlist.add(n); + } + } + return tlist; + } + + /** 判断是否有子节点 */ + private boolean hasChild(List list, SysMenu t) { + return getChildList(list, t).size() > 0; + } + + /** + * 执行菜单脚本 + * + * @param sql 菜单脚本 + */ + @Override + public void execMenuSql(String sql) { + menuMapper.execMenuSql(sql); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysOperLogServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysOperLogServiceImpl.java new file mode 100644 index 0000000..68c3833 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysOperLogServiceImpl.java @@ -0,0 +1,84 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.universal.admin.system.service.ISysOperLogService; +import cn.universal.persistence.entity.SysOperLog; +import cn.universal.persistence.mapper.SysOperLogMapper; +import jakarta.annotation.Resource; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import org.springframework.stereotype.Service; + +/** 操作日志 服务层处理 @Author ruoyi */ +@Service +public class SysOperLogServiceImpl implements ISysOperLogService { + + @Resource private SysOperLogMapper sysOperLogMapper; + + @Override + public List selectPageOperLogList(SysOperLog operLog) { + List sysOperLogs = sysOperLogMapper.selectList(operLog); + return sysOperLogs; + } + + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + @Override + public void insertOperlog(SysOperLog operLog) { + operLog.setOperTime(new Date()); + sysOperLogMapper.insert(operLog); + } + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + @Override + public List selectOperLogList(SysOperLog operLog) { + List sysOperLogs = sysOperLogMapper.selectList(operLog); + return sysOperLogs; + } + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + @Override + public int deleteOperLogByIds(Long[] operIds) { + return sysOperLogMapper.deleteByIds(Arrays.toString(operIds)); + } + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + @Override + public SysOperLog selectOperLogById(Long operId) { + return sysOperLogMapper.selectByPrimaryKey(operId); + } + + /** 清空操作日志 */ + @Override + public void cleanOperLog() {} +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysRoleServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..6100050 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,418 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.universal.admin.common.annotation.DataScope; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.ISysRoleService; +import cn.universal.common.constant.IoTUserConstants; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.admin.SysRole; +import cn.universal.persistence.entity.admin.SysRoleMenu; +import cn.universal.persistence.entity.admin.SysUserRole; +import cn.universal.persistence.mapper.admin.SysRoleMapper; +import cn.universal.persistence.mapper.admin.SysRoleMenuMapper; +import cn.universal.persistence.mapper.admin.SysUserRoleMapper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import tk.mybatis.mapper.entity.Example; + +/** 角色 业务层处理 @Author ruoyi */ +@Service +public class SysRoleServiceImpl implements ISysRoleService { + + @Resource private SysRoleMenuMapper roleMenuMapper; + + @Resource private SysUserRoleMapper userRoleMapper; + @Resource private SysRoleMapper roleMapper; + + @Override + // @DataScope(alias = "ur", created = "r.create_by") + public List selectPageRoleList(SysRole role) { + return roleMapper.selectRoleList(role); + } + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + @Override + @DataScope(alias = "ur", created = "r.create_by") + public List selectRoleList(SysRole role) { + return roleMapper.selectRoleList(role); + } + + /** + * 根据用户ID查询角色 + * + * @param unionId 用户unionId + * @return 角色列表 + */ + @Override + public List selectRolesByUnionId(String unionId) { + List userRoles = roleMapper.selectRolePermissionByUnionId(unionId); + List roles = roleMapper.selectRolePermissionByUnionId(SecurityUtils.getUnionId()); + if (CollectionUtils.isEmpty(roles) || roles.get(0).getRoleId() == 1) { + roles = roleMapper.selectAll(); + } + for (SysRole role : roles) { + for (SysRole userRole : userRoles) { + if (role.getRoleId().longValue() == userRole.getRoleId().longValue()) { + role.setFlag(true); + } + } + } + return roles; + } + + /** + * 根据用户ID查询权限 + * + * @param unionId 用户unionId + * @return 权限列表 + */ + @Override + public Set selectRolePermissionByUnionId(String unionId) { + List perms = roleMapper.selectRolePermissionByUnionId(unionId); + Set permsSet = new HashSet<>(); + for (SysRole perm : perms) { + if (Validator.isNotNull(perm)) { + permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(","))); + } + } + return permsSet; + } + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + @Override + public List selectRoleAll() { + return roleMapper.selectRoleList(new SysRole()); + } + + /** + * 根据用户ID获取角色选择框列表 + * + * @param unionId 用户unionId + * @return 选中角色ID列表 + */ + @Override + public List selectRoleListByUnionId(String unionId) { + return roleMapper.selectRoleListByUnionId(unionId); + } + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + @Override + public SysRole selectRoleById(Long roleId) { + return roleMapper.selectByPrimaryKey(roleId); + } + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public String checkRoleNameUnique(SysRole role) { + Long roleId = Validator.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + Example ex = new Example(SysRole.class); + ex.createCriteria().andEqualTo("roleName", role.getRoleName()); + List roles = roleMapper.selectByExample(ex); + // List roles = roleMapper + // .selectRoleList(SysRole.builder().roleName(role.getRoleName()).build()); + if (!CollectionUtils.isEmpty(roles) + && roles.get(0).getRoleId().longValue() != roleId.longValue()) { + return IoTUserConstants.NOT_UNIQUE; + } + return IoTUserConstants.UNIQUE; + } + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public String checkRoleKeyUnique(SysRole role) { + Long roleId = Validator.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + Example ex = new Example(SysRole.class); + ex.createCriteria().andEqualTo("roleKey", role.getRoleKey()); + List roles = roleMapper.selectByExample(ex); + if (!CollectionUtils.isEmpty(roles) + && roles.get(0).getRoleId().longValue() != roleId.longValue()) { + return IoTUserConstants.NOT_UNIQUE; + } + return IoTUserConstants.UNIQUE; + } + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + @Override + public void checkRoleAllowed(SysRole role) { + if (Validator.isNotNull(role.getRoleId()) && role.isAdmin()) { + throw new IoTException("不允许操作超级管理员角色"); + } + } + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + public int countUserRoleByRoleId(Long roleId) { + // 当前用户!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // Long userId = SecurityUtils.getLoginUser().getUser().getUserId(); + Example ex = new Example(SysUserRole.class); + ex.createCriteria().andEqualTo("roleId", roleId); + return userRoleMapper.selectCountByExample(ex); + // .andNotEqualTo("userId", userId)); + } + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int insertRole(SysRole role) { + // 新增角色信息 + role.setDataScope("4"); + role.setCreateTime(new Date()); + roleMapper.insertUseGeneratedKeys(role); + return insertRoleMenu(role); + } + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int updateRole(SysRole role) { + // 修改角色信息 + role.setUpdateTime(new Date()); + roleMapper.updateByPrimaryKey(role); + // 删除角色与菜单关联 + Example ex = new Example(SysRoleMenu.class); + ex.createCriteria().andEqualTo("roleId", role.getRoleId()); + roleMenuMapper.deleteByExample(ex); + return insertRoleMenu(role); + } + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public int updateRoleStatus(SysRole role) { + return roleMapper.updateByPrimaryKey(role); + } + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + // @Override + // @Transactional + // public int authDataScope(SysRole role) { + // // 修改角色信息 + // roleMapper.updateByPrimaryKey(role); + // // 删除角色与部门关联 + // roleDeptMapper + // .delete(new LambdaQueryWrapper().eq(SysRoleDept::getRoleId, + // role.getRoleId())); + // // 新增角色和部门信息(数据权限) + // return insertRoleDept(role); + // } + + /** + * 新增角色菜单信息 + * + * @param role 角色对象 + */ + public int insertRoleMenu(SysRole role) { + int rows = 1; + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long menuId : role.getMenuIds()) { + SysRoleMenu rm = new SysRoleMenu(); + rm.setRoleId(role.getRoleId()); + rm.setMenuId(menuId); + list.add(rm); + } + if (list.size() > 0) { + rows = roleMenuMapper.insertList(list); + } + return rows; + } + + /** + * 新增角色部门信息(数据权限) + * + * @param role 角色对象 + */ + // public int insertRoleDept(SysRole role) { + // int rows = 1; + // // 新增角色与部门(数据权限)管理 + // List list = new ArrayList(); + // for (Long deptId : role.getDeptIds()) { + // SysRoleDept rd = new SysRoleDept(); + // rd.setRoleId(role.getRoleId()); + // rd.setDeptId(deptId); + // list.add(rd); + // } + // if (list.size() > 0) { + // rows = roleDeptMapper.insertAll(list); + // } + // return rows; + // } + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleById(Long roleId) { + // 删除角色与菜单关联 + roleMenuMapper.deleteByPrimaryKey(roleId); + // 删除角色与部门关联 + // roleDeptMapper.delete(new LambdaQueryWrapper().eq(SysRoleDept::getRoleId, + // roleId)); + return roleMapper.deleteByPrimaryKey(roleId); + } + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleByIds(Long[] roleIds) { + for (Long roleId : roleIds) { + checkRoleAllowed(new SysRole(roleId)); + SysRole role = selectRoleById(roleId); + if (countUserRoleByRoleId(roleId) > 0) { + throw new IoTException(String.format("%1$s已分配,不能删除", role.getRoleName())); + } + } + List ids = Arrays.asList(roleIds); + String ids1 = ids.stream().map(Object::toString).collect(Collectors.joining(",")); + // 删除角色与菜单关联 + Example ex = new Example(SysRoleMenu.class); + ex.createCriteria().andIn("roleId", ids); + roleMenuMapper.deleteByExample(ex); + // 删除角色与部门关联 + // roleDeptMapper.delete(new LambdaQueryWrapper().in(SysRoleDept::getRoleId, + // ids)); + return roleMapper.deleteByIds(ids1); + } + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + @Override + public int deleteAuthUser(SysUserRole userRole) { + return userRoleMapper.delete( + SysUserRole.builder().unionId(userRole.getUnionId()).roleId(userRole.getRoleId()).build()); + } + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param unionIds 需要取消授权的用户数据ID + * @return 结果 + */ + @Override + public int deleteAuthUsers(Long roleId, String[] unionIds) { + Example ex = new Example(SysUserRole.class); + ex.createCriteria().andEqualTo("roleId", roleId).andIn("unionId", Arrays.asList(unionIds)); + return userRoleMapper.deleteByExample(ex); + } + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param unionIds 需要删除的用户数据ID + * @return 结果 + */ + @Override + public int insertAuthUsers(Long roleId, String[] unionIds) { + // 新增用户与角色管理 + int rows = 1; + List list = new ArrayList(); + for (String unionId : unionIds) { + SysUserRole ur = new SysUserRole(); + ur.setUnionId(unionId); + ur.setRoleId(roleId); + list.add(ur); + } + if (list.size() > 0) { + rows = userRoleMapper.insertList(list); + } + return rows; + } + + @Override + public String selectUserRoleGroup(String unionId) { + List list = roleMapper.selectRolePermissionByUnionId(unionId); + if (CollectionUtils.isEmpty(list)) { + return ""; + } + return list.stream().map(SysRole::getRoleName).collect(Collectors.joining(",")); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/UserApplicationService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/UserApplicationService.java new file mode 100644 index 0000000..3bb5df3 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/UserApplicationService.java @@ -0,0 +1,131 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.service.impl; + +import cn.hutool.core.util.StrUtil; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.IoTUserApplication; +import cn.universal.persistence.entity.OAuth2ClientDetails; +import cn.universal.persistence.mapper.IoTUserApplicationMapper; +import cn.universal.persistence.mapper.IoTUserMapper; +import cn.universal.persistence.mapper.OauthClientDetailsMapper; +import jakarta.annotation.Resource; +import java.util.List; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +/** + * 用户APP鉴权服务类 + * + *

提供IoT平台中用户应用程序的认证和授权功能,包括: - OAuth2客户端信息查询 - 用户应用关联关系管理 - 应用ID和UnionID的映射查询 + * + *

所有查询方法都使用了Spring Cache缓存,提高查询性能 + * + * @version 1.0 @Author Aleo + * @since 2025/8/12 16:10 + */ +@Component +public class UserApplicationService { + + @Resource private IoTUserApplicationMapper applicationMapper; + + @Resource private IoTUserMapper iotUserMapper; + + @Resource private OauthClientDetailsMapper oauthClientDetailsMapper; + + /** + * 根据客户端ID查询OAuth2客户端详细信息 + * + *

用于验证客户端身份,获取客户端的授权信息、重定向URI等配置 结果会被缓存,提高频繁查询的性能 + * + * @param clientId OAuth2客户端ID,不能为空 + * @return OAuth2客户端详细信息,如果不存在返回null + * @throws IoTException 当clientId为空时抛出业务异常 + */ + @Cacheable(cacheNames = "iot_oauth_client", key = "''+#clientId", unless = "#result == null") + public OAuth2ClientDetails getOauthUnionId(String clientId) { + if (StrUtil.isBlank(clientId)) { + throw new IoTException("clientId can't be null"); + } + OAuth2ClientDetails clientDetails = OAuth2ClientDetails.builder().clientId(clientId).build(); + clientDetails = oauthClientDetailsMapper.selectOne(clientDetails); + if (clientDetails == null) { + return null; + } + return clientDetails; + } + + /** + * 根据UnionID查询用户关联的所有应用程序 + * + *

UnionID是用户在IoT平台中的唯一标识,一个用户可以关联多个应用 用于获取用户在所有应用中的权限和配置信息 + * + * @param unionId 用户在IoT平台中的唯一标识,不能为空 + * @return 用户关联的应用程序列表 + * @throws IoTException 当unionId为空或查询失败时抛出业务异常 + */ + @Cacheable(cacheNames = "iot_unionId", key = "''+#unionId", unless = "#result == null") + public List getUserAppByUnionId(String unionId) { + if (StrUtil.isBlank(unionId)) { + throw new IoTException("platform application user unionId can't be null"); + } + List applications = + applicationMapper.selectIotUserApplicationByUnionId(unionId); + if (applications == null) { + throw new IoTException("inner error,please connect admin"); + } + return applications; + } + + /** + * 根据应用ID查询应用详细信息(包含删除状态检查) + * + *

用于验证应用的有效性,只返回未删除的应用信息 如果应用不存在或已删除,会抛出业务异常 + * + * @param appId 应用ID,不能为空 + * @return 应用详细信息 + * @throws IoTException 当appId为空或应用不存在时抛出业务异常 + */ + @Cacheable(cacheNames = "iot_appid", key = "''+#appId", unless = "#result == null") + public IoTUserApplication getUserAppByAppid(String appId) { + if (StrUtil.isBlank(appId)) { + throw new IoTException("appId can't be null"); + } + IoTUserApplication queryObject = IoTUserApplication.builder().appId(appId).deleted(0).build(); + IoTUserApplication iotUser = applicationMapper.selectOne(queryObject); + + if (iotUser == null) { + throw new IoTException("inner error,please connect admin"); + } + return iotUser; + } + + /** + * 根据应用ID查询应用详细信息(不包含删除状态检查) + * + *

与getUserAppByAppid的区别是不检查删除状态,返回所有应用信息 适用于需要查看已删除应用的场景 + * + * @param appId 应用ID,不能为空 + * @return 应用详细信息,可能为null + * @throws IoTException 当appId为空时抛出业务异常 + */ + @Cacheable(cacheNames = "iot_appid", key = "''+#appId", unless = "#result == null") + public IoTUserApplication getIotUserByAppid(String appId) { + if (StrUtil.isBlank(appId)) { + throw new IoTException("appId can't be null"); + } + IoTUserApplication queryObject = IoTUserApplication.builder().appId(appId).deleted(0).build(); + IoTUserApplication iotUser = applicationMapper.selectOne(queryObject); + return iotUser; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/BaseController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/BaseController.java new file mode 100644 index 0000000..0d4d6aa --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/BaseController.java @@ -0,0 +1,143 @@ +package cn.universal.admin.system.web; + +import cn.universal.admin.common.utils.PageUtils; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.common.utils.StringUtils; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.page.PageDomain; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.page.TableSupport; +import cn.universal.persistence.query.AjaxResult; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jakarta.annotation.Resource; +import java.beans.PropertyEditorSupport; +import java.util.Date; +import java.util.List; +import org.apache.http.client.utils.DateUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; + +/** 数据处理 */ +public class BaseController { + + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Resource private IIotUserService iIotUserService; + + /** 日期格式字符串自动转换Date */ + @InitBinder + public void initBinder(WebDataBinder binder) { + // Date 类型转换 + binder.registerCustomEditor( + Date.class, + new PropertyEditorSupport() { + @Override + public void setAsText(String text) { + setValue(DateUtils.parseDate(text)); + } + }); + } + + @Cacheable(cacheNames = "user_parent_entity", key = "''+#unionId", unless = "#result == null") + public IoTUser loginIoTUnionUser(String unionId) { + IoTUser iotUser = iIotUserService.selectUserByUnionId(unionId); + if (iotUser.getParentUnionId() != null) { + iotUser = iIotUserService.selectUserByUnionId(iotUser.getParentUnionId()); + return iotUser; + } + return iotUser; + } + + public boolean isAdmin() { + IoTUser iotUser = iIotUserService.selectUserByUnionId(SecurityUtils.getUnionId()); + return iotUser.isAdmin(); + } + + /** 设置请求分页数据 */ + protected void startPage() { + PageUtils.startPage(); + } + + /** 设置请求排序数据 */ + protected void startOrderBy() { + PageDomain pageDomain = TableSupport.buildPageRequest(); + if (StringUtils.isNotEmpty(pageDomain.getOrderBy())) { + String orderBy = pageDomain.getOrderBy(); + PageHelper.orderBy(orderBy); + } + } + + /** 响应请求分页数据 */ + @SuppressWarnings({"rawtypes", "unchecked"}) + protected TableDataInfo getDataTable(List list, Integer total) { + TableDataInfo rspData = new TableDataInfo(); + // 请求成功返回0 + rspData.setCode(0); + rspData.setMsg("查询成功"); + rspData.setRows(list); + rspData.setTotal(total); + return rspData; + } + + /** 响应请求分页数据 */ + @SuppressWarnings({"rawtypes", "unchecked"}) + protected TableDataInfo getDataTable(List list) { + TableDataInfo rspData = new TableDataInfo(); + // 请求成功返回0 + rspData.setCode(0); + rspData.setMsg("查询成功"); + rspData.setRows(list); + rspData.setTotal((int) new PageInfo(list).getTotal()); + return rspData; + } + + /** 返回成功 */ + public AjaxResult success() { + return AjaxResult.success(); + } + + /** 返回失败消息 */ + public AjaxResult error() { + return AjaxResult.error(); + } + + /** 返回成功消息 */ + public AjaxResult success(String message) { + return AjaxResult.success(message); + } + + /** 返回失败消息 */ + public AjaxResult error(String message) { + return AjaxResult.error(message); + } + + /** + * 响应返回结果 + * + * @param rows 影响行数 + * @return 操作结果 + */ + protected AjaxResult toAjax(int rows) { + return rows > 0 ? AjaxResult.success() : AjaxResult.error(); + } + + /** + * 响应返回结果 + * + * @param result 结果 + * @return 操作结果 + */ + protected AjaxResult toAjax(boolean result) { + return result ? success() : error(); + } + + /** 页面跳转 */ + public String redirect(String url) { + return StringUtils.format("redirect:{}", url); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/CaptchaController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/CaptchaController.java new file mode 100644 index 0000000..475dcbc --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/CaptchaController.java @@ -0,0 +1,107 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.web; + +import cn.hutool.captcha.AbstractCaptcha; +import cn.hutool.captcha.CaptchaUtil; +import cn.hutool.captcha.generator.RandomGenerator; +import cn.hutool.core.util.IdUtil; +import cn.universal.common.domain.R; +import cn.universal.common.utils.RSAUtils; +import jakarta.annotation.Resource; +import java.awt.Color; +import java.awt.Font; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** 验证码操作处理 */ +@RestController +@Slf4j +public class CaptchaController { + + @Resource private StringRedisTemplate stringRedisTemplate; + + // 可配置参数 + private static final int WIDTH = 160; + private static final int HEIGHT = 60; + private static final int LENGTH = 4; + private static final String FONT_NAME = "Arial"; + private static final int FONT_SIZE = 48; + private static final Color BACKGROUND = Color.WHITE; + private static final String CAPTCHA_TYPE = "circle"; // 可选: line/shear/circle + + /** + * 获取验证码图片和公钥 + * + * @param type 可选参数,验证码类型(line/shear/circle) + */ + @GetMapping("/getCaptchaCode") + public R getCode(@RequestParam(value = "type", required = false) String type) throws IOException { + Map map = new HashMap<>(); + String uuid = IdUtil.simpleUUID(); + String verifyKey = "captcha_codes:" + uuid; + + // 生成验证码 + AbstractCaptcha captcha = createCaptcha(type); + captcha.createCode(); + stringRedisTemplate.opsForValue().set(verifyKey, captcha.getCode(), 2, TimeUnit.MINUTES); + map.put("uuid", uuid); + map.put("img", captcha.getImageBase64()); + + // 获取/生成RSA公钥 + try { + String publicKey = stringRedisTemplate.opsForValue().get("RSAPublicKey"); + if (publicKey == null) { + RSAUtils.genKeyPair(); + String rsaPublicKey = RSAUtils.keyMap.get(0); + String rsaPrivateKey = RSAUtils.keyMap.get(1); + publicKey = rsaPublicKey; + stringRedisTemplate.opsForValue().set("RSAPublicKey", rsaPublicKey, 24, TimeUnit.HOURS); + stringRedisTemplate.opsForValue().set("RSAPrivateKey", rsaPrivateKey, 24, TimeUnit.HOURS); + } + map.put("key", publicKey); + } catch (Exception e) { + log.error("rsa密钥生成失败,错误信息:{}", e.toString()); + } + return R.ok(map); + } + + /** 创建验证码对象,支持多种类型 */ + private AbstractCaptcha createCaptcha(String type) { + String useType = (type == null || type.isEmpty()) ? CAPTCHA_TYPE : type; + Font font = new Font(FONT_NAME, Font.BOLD, FONT_SIZE); + AbstractCaptcha captcha; + switch (useType) { + case "circle": + captcha = CaptchaUtil.createCircleCaptcha(WIDTH, HEIGHT, LENGTH, 20); + break; + case "shear": + captcha = CaptchaUtil.createShearCaptcha(WIDTH, HEIGHT, LENGTH, 4); + break; + default: + captcha = CaptchaUtil.createLineCaptcha(WIDTH, HEIGHT, LENGTH, 150); + break; + } + captcha.setFont(font); + captcha.setBackground(BACKGROUND); + captcha.setGenerator(new RandomGenerator(LENGTH)); + return captcha; + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/IndexController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/IndexController.java new file mode 100644 index 0000000..f331a69 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/IndexController.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.web; + +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.IndexQueryService; +import cn.universal.persistence.dto.IndexQueryDTO; +import cn.universal.persistence.query.AjaxResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 首页数据接口 + * + * @since 2023/8/12 16:51 + */ +@RestController +@Tag(name = "首页overview", description = "首页overview") +@RequestMapping("admin/index") +@Slf4j +public class IndexController extends BaseController { + + @Resource private IndexQueryService indexQueryService; + + @Operation(summary = "overview统计") + @GetMapping("/overview") + public AjaxResult index() { + boolean admin = loginIoTUnionUser(SecurityUtils.getUnionId()).isAdmin(); + IndexQueryDTO qtyDTO = + indexQueryService.queryIndexQty(admin ? null : SecurityUtils.getUnionId()); + return AjaxResult.success(qtyDTO); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/RuleController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/RuleController.java new file mode 100644 index 0000000..5edf199 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/RuleController.java @@ -0,0 +1,132 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.web; + +import cn.hutool.json.JSONObject; +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.persistence.entity.bo.RuleModelBO; +import cn.universal.persistence.entity.vo.RuleModelVO; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import cn.universal.rule.model.bo.RuleBo; +import cn.universal.rule.model.bo.RuleTargetTestBO; +import cn.universal.rule.service.RuleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import java.util.List; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 规则管理控制器 + * + * @since 2023/1/14 9:10 + */ +@RestController +@Tag(name = "规则引擎", description = "规则引擎") +@RequestMapping("/admin/v1/") +public class RuleController extends BaseController { + + @Resource private RuleService ruleService; + @Resource private IIotUserService iIotUserService; + + @Operation(summary = "查询规则列表") + @GetMapping("rule/list") + public TableDataInfo pageList(RuleModelBO ruleModelBo) { + ruleModelBo.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + startPage(); + List userList = ruleService.queryRuleListByBo(ruleModelBo); + return getDataTable(userList); + } + + @Operation(summary = "查询规则") + @GetMapping("rule") + public AjaxResult detail(@RequestParam @Parameter(description = "规则id") Long id) { + RuleModelVO ruleModel = + ruleService.queryVoByIdAndCreator( + id, loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + return AjaxResult.success(ruleModel); + } + + @Operation(summary = "新增规则") + @PostMapping("/rule") + @Log(title = "新增规则", businessType = BusinessType.INSERT) + public AjaxResult addRule(@RequestBody RuleBo ruleModelBo) { + ruleModelBo.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + Long ruleId = ruleService.addRule(ruleModelBo); + return AjaxResult.success(ruleId); + } + + @Operation(summary = "删除规则") + @DeleteMapping("/rule/{id}") + @Log(title = "删除规则", businessType = BusinessType.DELETE) + public AjaxResult deleteRule(@PathVariable @Parameter(description = "规则id") Long id) { + + ruleService.deletedRule(id, loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + return AjaxResult.success(); + } + + @Operation(summary = "更新规则") + @PutMapping("/rule") + @Log(title = "更新规则", businessType = BusinessType.UPDATE) + public AjaxResult updateRule(@RequestBody RuleBo ruleModelBo) { + ruleModelBo.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + ruleService.updateRule(ruleModelBo); + return AjaxResult.success(); + } + + @Operation(summary = "更新规则调用目标") + @PutMapping("/rule/targets") + @Log(title = "更新规则调用目标", businessType = BusinessType.UPDATE) + public AjaxResult updateRuleTargets(@RequestBody RuleBo ruleModelBo) { + ruleModelBo.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + ruleService.updateRuleTargets(ruleModelBo); + return AjaxResult.success(); + } + + @Operation(summary = "更新规则状态") + @PostMapping("rule/status") + @Log(title = "更新规则状态", businessType = BusinessType.UPDATE) + public AjaxResult changeStatus(@RequestBody RuleModelBO ruleModelBo) { + ruleModelBo.setCreatorId(loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId()); + ruleService.changeStatus(ruleModelBo); + return AjaxResult.success(); + } + + @Operation(summary = "sql测试") + @PostMapping("/rule/sql/test") + @Log(title = "sql测试", businessType = BusinessType.OTHER) + public AjaxResult sqlTest(@RequestBody RuleBo sqlBo) { + return AjaxResult.success(ruleService.testExecuteRule(sqlBo)); + } + + @Operation(summary = "调用目标测试") + @PostMapping("/rule/target/test") + @Log(title = "调用目标测试", businessType = BusinessType.OTHER) + public AjaxResult targetTest(@RequestBody RuleTargetTestBO testBo) { + Object result = ruleService.testRuleTarget(testBo); + return AjaxResult.success(result); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SceneLinkageController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SceneLinkageController.java new file mode 100644 index 0000000..b7a4089 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SceneLinkageController.java @@ -0,0 +1,150 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.web; + +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.ExcelUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.SceneLinkage; +import cn.universal.persistence.entity.bo.SceneLinkageBO; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import cn.universal.rule.model.ExeRunContext; +import cn.universal.rule.scene.service.SceneLinkageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Date; +import java.util.List; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 场景联动管理控制器 + * + * @since 2023-03-01 + */ +@RestController +@Tag(name = "场景联动管理", description = "场景联动管理") +@RequestMapping("/admin/v1/scene/linkage") +public class SceneLinkageController extends BaseController { + + @Resource private SceneLinkageService sceneLinkageService; + + /** 查询场景联动列表 */ + @Operation(summary = "查询场景联动列表") + @GetMapping("/list") + public TableDataInfo list(SceneLinkage sceneLinkage) { + IoTUser user = loginIoTUnionUser(SecurityUtils.getUnionId()); + startPage(); + sceneLinkage.setCreateBy(user.isAdmin() ? null : user.getUnionId()); + List list = sceneLinkageService.selectSceneLinkageList(sceneLinkage); + return getDataTable(list); + } + + /** 导出场景联动列表 */ + @Operation(summary = "导出场景联动列表") + @PostMapping("/export") + @Log(title = "导出场景联动列表", businessType = BusinessType.EXPORT) + public void export(HttpServletResponse response, SceneLinkage sceneLinkage) { + List list = sceneLinkageService.selectSceneLinkageList(sceneLinkage); + ExcelUtil util = new ExcelUtil(SceneLinkage.class); + util.exportExcel(response, list, "场景联动数据"); + } + + /** 获取场景联动详细信息 */ + @Operation(summary = "获取场景联动详细信息") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!sceneLinkageService.checkSelf(id, iotUser.getUnionId()) && !iotUser.isAdmin()) { + throw new IoTException("场景没有操作权限!"); + } + return AjaxResult.success(sceneLinkageService.selectSceneLinkageById(id)); + } + + /** 新增场景联动 */ + @Operation(summary = "新增场景联动") + @PostMapping + @Log(title = "新增场景联动", businessType = BusinessType.INSERT) + public AjaxResult add(@RequestBody SceneLinkageBO sceneLinkage) throws Exception { + String unionId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId(); + sceneLinkage.setCreateBy(unionId); + sceneLinkage.setCreateTime(new Date()); + sceneLinkage.setUpdateBy(unionId); + sceneLinkage.setUpdateTime(new Date()); + int rows = sceneLinkageService.insertSceneLinkage(sceneLinkage); + return AjaxResult.toAjax(rows); + } + + /** 修改场景联动 */ + @Operation(summary = "修改场景联动") + @PutMapping + @Log(title = "修改场景联动", businessType = BusinessType.UPDATE) + public AjaxResult edit(@RequestBody SceneLinkageBO sceneLinkage) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!sceneLinkageService.checkSelf(sceneLinkage.getId(), iotUser.getUnionId()) + && !iotUser.isAdmin()) { + throw new IoTException("场景没有操作权限!"); + } + sceneLinkage.setUpdateBy(SecurityUtils.getUnionId()); + sceneLinkage.setUpdateTime(new Date()); + return AjaxResult.toAjax(sceneLinkageService.updateSceneLinkage(sceneLinkage)); + } + + /** 删除场景联动 */ + @Operation(summary = "删除场景联动") + @DeleteMapping("/{ids}") + @Log(title = "删除场景联动", businessType = BusinessType.DELETE) + public AjaxResult remove(@PathVariable Long[] ids) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!sceneLinkageService.checkSelf(ids[0], iotUser.getUnionId()) && !iotUser.isAdmin()) { + throw new IoTException("场景没有操作权限!"); + } + return AjaxResult.toAjax(sceneLinkageService.deleteSceneLinkageByIds(ids)); + } + + /** 手动触发动作 */ + @Operation(summary = "手动触发动作") + @GetMapping(value = "/exec/{id}") + public AjaxResult manualExec(@PathVariable("id") Long id) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!sceneLinkageService.checkSelf(id, iotUser.getUnionId()) && !iotUser.isAdmin()) { + throw new IoTException("场景没有操作权限!"); + } + List runContexts = sceneLinkageService.functionDown(id); + logger.info("手动触发执行成功={}", runContexts); + return AjaxResult.success("触发成功"); + } + + /** 查询场景联动执行日志(分页) */ + @Operation(summary = "查询场景联动执行日志") + @GetMapping("/log/{sceneId}") + public TableDataInfo logList( + @PathVariable("sceneId") String sceneId, + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "10") int pageSize) { + return getDataTable(sceneLinkageService.getSceneLinkageLogPage(sceneId, pageNum, pageSize)); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysConfigController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysConfigController.java new file mode 100644 index 0000000..22f7ee3 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysConfigController.java @@ -0,0 +1,106 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.web; + +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.ISysConfigService; +import cn.universal.common.constant.IoTUserConstants; +import cn.universal.persistence.entity.admin.SysConfig; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 参数配置 信息操作处理 @Author ruoyi */ +@RestController +@RequestMapping("admin/system/config") +public class SysConfigController extends BaseController { + + @Autowired private ISysConfigService configService; + + /** 获取参数配置列表 */ + @GetMapping("/list") + public TableDataInfo list(SysConfig config) { + startPage(); + List list = configService.selectConfigList(config); + return getDataTable(list); + } + + // @PostMapping("/export") + // public void export(HttpServletResponse response, SysConfig config) + // { + // List list = configService.selectConfigList(config); + // ExcelUtil util = new ExcelUtil(SysConfig.class); + // util.exportExcel(response, list, "参数数据"); + // } + + /** 根据参数编号获取详细信息 */ + @GetMapping(value = "/{configId}") + public AjaxResult getInfo(@PathVariable Long configId) { + return AjaxResult.success(configService.selectConfigById(configId)); + } + + /** 根据参数键名查询参数值 */ + @GetMapping(value = "/configKey/{configKey}") + public AjaxResult getConfigKey(@PathVariable String configKey) { + return AjaxResult.success(configService.selectConfigByKey(configKey)); + } + + /** 新增参数配置 */ + @PostMapping + @Log(title = "新增参数配置", businessType = BusinessType.INSERT) + public AjaxResult add(@Validated @RequestBody SysConfig config) { + if (IoTUserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config))) { + return AjaxResult.error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setCreateBy(SecurityUtils.getUnionId()); + return toAjax(configService.insertConfig(config)); + } + + /** 修改参数配置 */ + @PutMapping + @Log(title = "修改参数配置", businessType = BusinessType.UPDATE) + public AjaxResult edit(@Validated @RequestBody SysConfig config) { + if (IoTUserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config))) { + return AjaxResult.error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setUpdateBy(SecurityUtils.getUnionId()); + return toAjax(configService.updateConfig(config)); + } + + /** 删除参数配置 */ + @DeleteMapping("/{configIds}") + @Log(title = "删除参数配置", businessType = BusinessType.DELETE) + public AjaxResult remove(@PathVariable Long[] configIds) { + configService.deleteConfigByIds(configIds); + return success(); + } + + /** 刷新参数缓存 */ + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() { + configService.resetConfigCache(); + return AjaxResult.success(); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysDictDataController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysDictDataController.java new file mode 100644 index 0000000..7d695a8 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysDictDataController.java @@ -0,0 +1,109 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.web; + +import cn.hutool.core.lang.Validator; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.ISysDictDataService; +import cn.universal.admin.system.service.ISysDictTypeService; +import cn.universal.persistence.entity.admin.SysDictData; +import cn.universal.persistence.entity.admin.vo.SysDictDataVo; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 数据字典信息 @Author ruoyi */ +@RestController +@RequestMapping("admin/system/dict/data") +public class SysDictDataController extends BaseController { + + @Autowired private ISysDictDataService dictDataService; + + @Autowired private ISysDictTypeService dictTypeService; + + @GetMapping("/list") + public TableDataInfo list(SysDictDataVo dictData) { + startPage(); + return getDataTable(dictDataService.selectPageDictDataList(dictData)); + } + + // @GetMapping("/export") + // public AjaxResult export(SysDictData dictData) { + // List list = dictDataService.selectDictDataList(dictData); + // ExcelUtil util = new ExcelUtil(SysDictData.class); + // return util.exportExcel(list, "字典数据"); + // } + + /** 查询字典数据详细 */ + @GetMapping(value = "/{dictCode}") + public AjaxResult getInfo(@PathVariable Long dictCode) { + return AjaxResult.success(dictDataService.selectDictDataById(dictCode)); + } + + /** 根据字典类型查询字典数据信息 */ + @GetMapping(value = "/type/{dictType}") + public AjaxResult dictType(@PathVariable String dictType) { + List data = dictTypeService.selectDictDataByType(dictType); + if (Validator.isNull(data)) { + data = new ArrayList(); + } + return AjaxResult.success(data); + } + + /** 根据字典类型数组查询字典数据信息 */ + @GetMapping(value = "/types/{dictType}") + public AjaxResult dictType(@PathVariable String[] dictType) { + return AjaxResult.success(dictDataService.selectDictDataByTypes(dictType)); + } + + /** 新增字典类型 */ + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictData dict) { + dict.setCreateBy(SecurityUtils.getUnionId()); + dict.setCreateTime(new Date()); + return toAjax(dictDataService.insertDictData(dict)); + } + + /** 新增字典类型(进销人员) */ + @PostMapping(value = "/sale") + public AjaxResult addSaleBuyPerson(@Validated @RequestBody SysDictData dict) { + return toAjax(dictDataService.insertSaleBuyDictData(dict)); + } + + /** 修改保存字典类型 */ + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictData dict) { + dict.setUpdateBy(SecurityUtils.getUnionId()); + dict.setUpdateTime(new Date()); + return toAjax(dictDataService.updateDictData(dict)); + } + + /** 删除字典类型 */ + @DeleteMapping("/{dictCodes}") + public AjaxResult remove(@PathVariable Long[] dictCodes) { + dictDataService.deleteDictDataByIds(dictCodes); + return success(); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysDictTypeController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysDictTypeController.java new file mode 100644 index 0000000..556b741 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysDictTypeController.java @@ -0,0 +1,102 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.web; + +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.ISysDictTypeService; +import cn.universal.common.constant.IoTUserConstants; +import cn.universal.persistence.entity.admin.SysDictType; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import java.util.Date; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 数据字典信息 @Author ruoyi */ +@RestController +@RequestMapping("admin/system/dict/type") +public class SysDictTypeController extends BaseController { + + @Autowired private ISysDictTypeService dictTypeService; + + @GetMapping("/list") + public TableDataInfo list(SysDictType dictType) { + startPage(); + return getDataTable(dictTypeService.selectPageDictTypeList(dictType)); + } + + // @GetMapping("/export") + // public AjaxResult export(SysDictType dictType) { + // List list = dictTypeService.selectDictTypeList(dictType); + // ExcelUtil util = new ExcelUtil(SysDictType.class); + // return util.exportExcel(list, "字典类型"); + // } + + /** 查询字典类型详细 */ + @GetMapping(value = "/{dictId}") + public AjaxResult getInfo(@PathVariable Long dictId) { + return AjaxResult.success(dictTypeService.selectDictTypeById(dictId)); + } + + /** 新增字典类型 */ + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictType dict) { + if (IoTUserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict))) { + return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setCreateBy(SecurityUtils.getUnionId()); + dict.setCreateTime(new Date()); + return toAjax(dictTypeService.insertDictType(dict)); + } + + /** 修改字典类型 */ + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictType dict) { + if (IoTUserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict))) { + return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setUpdateBy(SecurityUtils.getUnionId()); + dict.setUpdateTime(new Date()); + return toAjax(dictTypeService.updateDictType(dict)); + } + + /** 删除字典类型 */ + @DeleteMapping("/{dictIds}") + public AjaxResult remove(@PathVariable Long[] dictIds) { + dictTypeService.deleteDictTypeByIds(dictIds); + return success(); + } + + /** 刷新字典缓存 */ + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() { + dictTypeService.resetDictCache(); + return success(); + } + + /** 获取字典选择框列表 */ + @GetMapping("/optionselect") + public AjaxResult optionselect() { + List dictTypes = dictTypeService.selectDictTypeAll(); + return AjaxResult.success(dictTypes); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysMenuController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysMenuController.java new file mode 100644 index 0000000..656e2ad --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysMenuController.java @@ -0,0 +1,130 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.web; + +import cn.hutool.core.lang.Validator; +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.ISysMenuService; +import cn.universal.common.constant.IoTUserConstants; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.admin.SysMenu; +import cn.universal.persistence.query.AjaxResult; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 菜单信息 @Author ruoyi */ +@RestController +@RequestMapping("admin/system/menu") +public class SysMenuController extends BaseController { + + @Autowired private ISysMenuService menuService; + + /** 获取菜单列表 */ + @GetMapping("/list") + public AjaxResult list(SysMenu menu) { + List menus = menuService.selectMenuList(menu, SecurityUtils.getUnionId()); + return AjaxResult.success(menus); + } + + /** 根据菜单编号获取详细信息 */ + @GetMapping(value = "/{menuId}") + public AjaxResult getInfo(@PathVariable Long menuId) { + if (!isAdmin()) { + throw new IoTException("你无权操作"); + } + return AjaxResult.success(menuService.selectMenuById(menuId)); + } + + /** 获取菜单下拉树列表 */ + @GetMapping("/treeselect") + public AjaxResult treeselect(SysMenu menu) { + List menus = menuService.selectMenuList(menu, SecurityUtils.getUnionId()); + return AjaxResult.success(menuService.buildMenuTreeSelect(menus)); + } + + /** 加载对应角色菜单列表树 */ + @GetMapping(value = "/roleMenuTreeselect/{roleId}") + public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId) { + List menus = menuService.selectMenuList(SecurityUtils.getUnionId()); + Map ajax = new HashMap<>(); + ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId)); + ajax.put("menus", menuService.buildMenuTreeSelect(menus)); + return AjaxResult.success(ajax); + } + + /** 新增菜单 */ + + // @Log(title = "菜单管理", businessType = BusinessType.INSERT) + @PostMapping + @Log(title = "新增菜单", businessType = BusinessType.INSERT) + public AjaxResult add(@Validated @RequestBody SysMenu menu) { + if (!isAdmin()) { + throw new IoTException("你无权操作"); + } + if (IoTUserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) { + return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } else if (IoTUserConstants.YES_FRAME.equals(menu.getIsFrame()) + && !Validator.isUrl(menu.getPath())) { + return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + menu.setCreateBy(SecurityUtils.getUnionId()); + return toAjax(menuService.insertMenu(menu)); + } + + /** 修改菜单 */ + + // @Log(title = "菜单管理", businessType = BusinessType.UPDATE) + @PutMapping + @Log(title = "修改菜单", businessType = BusinessType.UPDATE) + public AjaxResult edit(@Validated @RequestBody SysMenu menu) { + if (IoTUserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) { + return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } else if (IoTUserConstants.YES_FRAME.equals(menu.getIsFrame()) + && !Validator.isUrl(menu.getPath())) { + return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } else if (menu.getMenuId().equals(menu.getParentId())) { + return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己"); + } + menu.setUpdateBy(SecurityUtils.getUnionId()); + return toAjax(menuService.updateMenu(menu)); + } + + /** 删除菜单 */ + @PreAuthorize("@ss.hasPermi('system:menu:remove')") + // @Log(title = "菜单管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{menuId}") + @Log(title = "删除菜单", businessType = BusinessType.DELETE) + public AjaxResult remove(@PathVariable("menuId") Long menuId) { + if (menuService.hasChildByMenuId(menuId)) { + return error("存在子菜单,不允许删除"); + } + if (menuService.checkMenuExistRole(menuId)) { + return error("菜单已分配,不允许删除"); + } + return toAjax(menuService.deleteMenuById(menuId)); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysProfileController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysProfileController.java new file mode 100644 index 0000000..71bce95 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysProfileController.java @@ -0,0 +1,127 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.web; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.PhoneUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.admin.system.service.ISysRoleService; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.constant.IoTUserConstants; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.bo.IoTUserBO; +import cn.universal.persistence.entity.bo.PasswordBO; +import cn.universal.persistence.query.AjaxResult; +import jakarta.annotation.Resource; +import java.util.Date; +import java.util.Objects; +import java.util.regex.Matcher; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 个人信息 */ +@RestController +@RequestMapping("/admin/system/user/profile") +public class SysProfileController extends BaseController { + + @Resource private IIotUserService iIotUserService; + @Resource private ISysRoleService iSysRoleService; + + /** 查询个人信息 */ + @GetMapping + public AjaxResult profile() { + JSONObject jsonObject = new JSONObject(); + String unionId = SecurityUtils.getUnionId(); + + IoTUser iotUser = iIotUserService.selectUserByUnionId(unionId); + // 手机号脱敏 + if (StrUtil.isNotEmpty(iotUser.getMobile())) { + iotUser.setMobile(PhoneUtil.hideBetween(iotUser.getMobile()).toString()); + } + jsonObject.set("user", iotUser); + jsonObject.set("roleGroup", iSysRoleService.selectRolesByUnionId(unionId)); + + return AjaxResult.success(jsonObject); + } + + /** 个人信息 */ + @PutMapping + public AjaxResult profile(@RequestBody IoTUserBO iotUserBo) { + String unionId = SecurityUtils.getUnionId(); + if (Validator.isEmpty(iotUserBo.getMobile())) { + return error("修改个人信息失败,手机号码不能为空"); + } + IoTUser user = BeanUtil.toBean(iotUserBo, IoTUser.class); + if (IoTUserConstants.NOT_UNIQUE.equals(iIotUserService.checkPhoneUnique(user))) { + return error("修改个人信息失败,手机号码已存在"); + } + IoTUser iotUser = iIotUserService.selectUserByUnionId(unionId); + iotUser.setAlias(iotUserBo.getAlias()); + iotUser.setEmail(iotUserBo.getEmail()); + iotUser.setMobile(iotUserBo.getMobile()); + iotUser.setUpdateBy(unionId); + iotUser.setUpdateDate(new Date()); + if (JSONUtil.isTypeJSON(iotUserBo.getCfg())) { + JSONObject origin = JSONUtil.parseObj(iotUser.getCfg()); + JSONObject latest = JSONUtil.parseObj(iotUserBo.getCfg()); + origin.set(IoTConstant.EXCLUSIVE_FIRST_LOGIN, latest.get(IoTConstant.EXCLUSIVE_FIRST_LOGIN)); + iotUser.setCfg(JSONUtil.toJsonStr(origin)); + } + iIotUserService.updateUserById(iotUser); + return AjaxResult.success(); + } + + /** 重置密码(修改密码) */ + @PutMapping("/updatePwd") + public AjaxResult updatePwd(@RequestBody PasswordBO bo) { + String unionId = SecurityUtils.getUnionId(); + IoTUser iotUser = iIotUserService.selectUserByUnionId(unionId); + Matcher matcher = IoTConstant.pattern.matcher(bo.getNewPassword()); + if (!matcher.matches()) { + throw new IoTException("密码中必须包含字母、数字、特殊字符,至少8个字符,最多30个字符"); + } + if (!SecurityUtils.matchesPassword(bo.getOldPassword(), iotUser.getPassword())) { + throw new IoTException("旧密码错误"); + } + iotUser.setPassword(SecurityUtils.encryptPassword(bo.getNewPassword())); + iotUser.setUpdateBy(unionId); + iotUser.setUpdateDate(new Date()); + iIotUserService.updateUserById(iotUser); + return AjaxResult.success(); + } + + /** 修改手机号 */ + @RequestMapping("/updateMobile/{mobile}") + public AjaxResult updatePwd(@PathVariable("mobile") String mobile) { + String unionId = SecurityUtils.getUnionId(); + IoTUser iotUser = iIotUserService.selectUserByUnionId(unionId); + IoTUser user = iIotUserService.selectUserByMobile(mobile); + if (Objects.nonNull(user)) { + throw new IoTException("手机号码已存在,请勿重复添加"); + } else { + iotUser.setMobile(mobile); + iIotUserService.updateUserById(iotUser); + return AjaxResult.success(); + } + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysRoleController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysRoleController.java new file mode 100644 index 0000000..2cefbde --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysRoleController.java @@ -0,0 +1,255 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.web; + +import cn.hutool.core.util.ObjectUtil; +import cn.universal.admin.common.annotation.Log; +import cn.universal.admin.common.enums.BusinessType; +import cn.universal.admin.common.utils.ExcelUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.admin.system.service.ISysRoleService; +import cn.universal.common.constant.IoTUserConstants; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.admin.SysRole; +import cn.universal.persistence.entity.admin.SysUserRole; +import cn.universal.persistence.entity.bo.IoTUserBO; +import cn.universal.persistence.page.TableDataInfo; +import cn.universal.persistence.query.AjaxResult; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 角色信息 @Author ruoyi */ +@RestController +@RequestMapping("admin/system/role") +public class SysRoleController extends BaseController { + + @Autowired private ISysRoleService roleService; + + @Autowired private IIotUserService userService; + + @GetMapping("/list") + public TableDataInfo list(SysRole role) { + if (!userService.selectUserByUnionId(SecurityUtils.getUnionId()).isAdmin()) { + role.setCreateBy(SecurityUtils.getUnionId()); + } + startPage(); + return getDataTable(roleService.selectPageRoleList(role)); + } + + @PostMapping("/export") + public void export(HttpServletResponse response, SysRole role) { + if (!userService.selectUserByUnionId(SecurityUtils.getUnionId()).isAdmin()) { + role.setCreateBy(SecurityUtils.getUnionId()); + } + List list = roleService.selectRoleList(role); + ExcelUtil util = new ExcelUtil(SysRole.class); + util.exportExcel(response, list, "角色数据"); + } + + /** 根据角色编号获取详细信息 */ + @GetMapping(value = "/{roleId}") + public AjaxResult getInfo(@PathVariable Long roleId) { + SysRole sysRole = roleService.selectRoleById(roleId); + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin() + && ObjectUtil.isNotEmpty(sysRole) + && !sysRole.getCreateBy().equals(iotUser.getUnionId())) { + throw new IoTException("你无权操作"); + } + return AjaxResult.success(sysRole); + } + + /** 新增角色 */ + @PostMapping + @Log(title = "新增角色", businessType = BusinessType.INSERT) + public AjaxResult add(@Validated @RequestBody SysRole role) { + String unionId = SecurityUtils.getUnionId(); + IoTUser parentUser = userService.selectUserByUnionId(unionId); + if (!parentUser.isAdmin()) { + throw new IoTException("你无权操作"); + } + if (IoTUserConstants.NOT_UNIQUE.equals(roleService.checkRoleNameUnique(role))) { + return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } else if (IoTUserConstants.NOT_UNIQUE.equals(roleService.checkRoleKeyUnique(role))) { + return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setCreateBy(SecurityUtils.getUnionId()); + role.setCreateTime(new Date()); + return toAjax(roleService.insertRole(role)); + } + + /** 修改保存角色 */ + @PutMapping + @Log(title = "修改保存角色", businessType = BusinessType.UPDATE) + public AjaxResult edit(@Validated @RequestBody SysRole role) { + SysRole sysRole = roleService.selectRoleById(role.getRoleId()); + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin() + && ObjectUtil.isNotEmpty(sysRole) + && !sysRole.getCreateBy().equals(iotUser.getUnionId())) { + throw new IoTException("你无权操作"); + } + roleService.checkRoleAllowed(role); + if (IoTUserConstants.NOT_UNIQUE.equals(roleService.checkRoleNameUnique(role))) { + return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } else if (IoTUserConstants.NOT_UNIQUE.equals(roleService.checkRoleKeyUnique(role))) { + return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setUpdateBy(SecurityUtils.getUnionId()); + + if (roleService.updateRole(role) > 0) { + // 更新缓存用户权限 + // LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + // if (Validator.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin()) { + // loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser())); + // + // loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName())); + // tokenService.setLoginUser(loginUser); + // } + return success(); + } + return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员"); + } + + /** 修改保存数据权限 */ + // @PreAuthorize("@ss.hasPermi('system:role:edit')") + //// @Log(title = "角色管理", businessType = BusinessType.UPDATE) + // @PutMapping("/dataScope") + // public AjaxResult dataScope(@RequestBody SysRole role) { + // roleService.checkRoleAllowed(role); + // return AjaxResult.toAjax(roleService.authDataScope(role)); + // } + + /** 状态修改 */ + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysRole role) { + SysRole sysRole = roleService.selectRoleById(role.getRoleId()); + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin() + && ObjectUtil.isNotEmpty(sysRole) + && !sysRole.getCreateBy().equals(iotUser.getUnionId())) { + throw new IoTException("你无权操作"); + } + roleService.checkRoleAllowed(role); + role.setUpdateBy(SecurityUtils.getUnionId()); + return toAjax(roleService.updateRoleStatus(role)); + } + + /** 删除角色 */ + @DeleteMapping("/{roleIds}") + @Log(title = "删除角色", businessType = BusinessType.DELETE) + public AjaxResult remove(@PathVariable Long[] roleIds) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + Arrays.stream(roleIds) + .forEach( + id -> { + SysRole sysRole = roleService.selectRoleById(id); + if (!iotUser.isAdmin() + && ObjectUtil.isNotEmpty(sysRole) + && !sysRole.getCreateBy().equals(iotUser.getUnionId())) { + throw new IoTException("你无权操作"); + } + }); + + return toAjax(roleService.deleteRoleByIds(roleIds)); + } + + /** 获取角色选择框列表 */ + + // @GetMapping("/optionselect") + // public AjaxResult optionselect() { + // return AjaxResult.success(roleService.selectRoleAll()); + // } + + /** 查询已分配用户角色列表 */ + @GetMapping("/authUser/allocatedList") + public TableDataInfo allocatedList(IoTUserBO iotUserBo) { + SysRole sysRole = roleService.selectRoleById(iotUserBo.getRoleId()); + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin() + && ObjectUtil.isNotEmpty(sysRole) + && !sysRole.getCreateBy().equals(iotUser.getUnionId())) { + throw new IoTException("你无权操作"); + } + startPage(); + return getDataTable(userService.selectAllocatedList(iotUserBo)); + } + + /** 查询未分配用户角色列表 */ + @GetMapping("/authUser/unallocatedList") + public TableDataInfo unallocatedList(IoTUserBO user) { + SysRole sysRole = roleService.selectRoleById(user.getRoleId()); + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin() + && ObjectUtil.isNotEmpty(sysRole) + && !sysRole.getCreateBy().equals(iotUser.getUnionId())) { + throw new IoTException("你无权操作"); + } + startPage(); + user.setCreateBy(SecurityUtils.getUnionId()); + return getDataTable(userService.selectUnallocatedList(user)); + } + + /** 取消授权用户 */ + @PutMapping("/authUser/cancel") + public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole) { + SysRole sysRole = roleService.selectRoleById(userRole.getRoleId()); + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin() + && ObjectUtil.isNotEmpty(sysRole) + && !sysRole.getCreateBy().equals(iotUser.getUnionId())) { + throw new IoTException("你无权操作"); + } + return toAjax(roleService.deleteAuthUser(userRole)); + } + + /** 批量取消授权用户 */ + @PutMapping("/authUser/cancelAll") + public AjaxResult cancelAuthUserAll(Long roleId, String[] unionIds) { + SysRole sysRole = roleService.selectRoleById(roleId); + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin() + && ObjectUtil.isNotEmpty(sysRole) + && !sysRole.getCreateBy().equals(iotUser.getUnionId())) { + throw new IoTException("你无权操作"); + } + return toAjax(roleService.deleteAuthUsers(roleId, unionIds)); + } + + /** 批量选择用户授权 */ + @PutMapping("/authUser/selectAll") + public AjaxResult selectAuthUserAll(Long roleId, String[] unionIds) { + SysRole sysRole = roleService.selectRoleById(roleId); + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!iotUser.isAdmin() + && ObjectUtil.isNotEmpty(sysRole) + && !sysRole.getCreateBy().equals(iotUser.getUnionId())) { + throw new IoTException("你无权操作"); + } + return toAjax(roleService.insertAuthUsers(roleId, unionIds)); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysUserController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysUserController.java new file mode 100644 index 0000000..a38bd0c --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SysUserController.java @@ -0,0 +1,97 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.web; + +import cn.hutool.core.util.StrUtil; +import cn.universal.admin.common.utils.SecurityUtils; +import cn.universal.admin.system.permission.SysPermissionService; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.admin.system.service.ISysMenuService; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.domain.R; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.admin.SysMenu; +import cn.universal.persistence.entity.admin.vo.RouterVo; +import cn.universal.persistence.query.AjaxResult; +import io.netty.util.internal.StringUtil; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** 登录验证 @Author ruoyi */ +@RestController +@RequestMapping("admin") +public class SysUserController { + + @Resource private IIotUserService iIotUserService; + + @Resource private ISysMenuService menuService; + + @Resource private SysPermissionService permissionService; + @Resource private StringRedisTemplate stringRedisTemplate; + + /** + * 获取用户信息 + * + * @return 用户信息 + */ + @GetMapping("getInfo") + public R getInfo(HttpServletRequest request) { + final String unionId = SecurityUtils.getUnionId(); + IoTUser iotUser = iIotUserService.selectUserByUnionId(unionId); + // 更新登录时间 + // 角色集合 + Set roles = permissionService.getRolePermission(iotUser); + // 权限集合 + Set permissions = permissionService.getMenuPermission(iotUser); + Map map = new HashMap<>(); + map.put("user", iotUser); + map.put("roles", roles); + map.put("permissions", permissions); + return R.ok(map); + } + + /** + * 获取路由信息 + * + * @return 路由信息 + */ + @GetMapping("getRouters") + public AjaxResult> getRouters() { + List menus = menuService.selectMenuTreeByUnionId(SecurityUtils.getUnionId()); + return AjaxResult.success(menuService.buildMenus(menus)); + } + + @GetMapping("logout") + public AjaxResult logout(HttpServletRequest request) { + String token = StringUtil.substringAfter(request.getHeader("Authorization"), ' '); + String unionId = SecurityUtils.getUnionId(); + String key = IoTConstant.EXCLUSIVE_LOGIN + ":" + unionId; + String tokenKey = IoTConstant.EXCLUSIVE_LOGIN_TOKEN + ":" + unionId; + if (StrUtil.isNotBlank(token) && Boolean.TRUE.equals(stringRedisTemplate.hasKey(tokenKey))) { + String origin = stringRedisTemplate.opsForValue().get(tokenKey); + if (token.equals(origin)) { + stringRedisTemplate.delete(key); + stringRedisTemplate.delete(tokenKey); + } + } + return AjaxResult.success(); + } +} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SystemProtocolController.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SystemProtocolController.java new file mode 100644 index 0000000..3651477 --- /dev/null +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/web/SystemProtocolController.java @@ -0,0 +1,232 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.admin.system.web; + +import cn.universal.core.protocol.ProtocolModuleInfo; +import cn.universal.core.protocol.ProtocolModuleRegistry; +import cn.universal.core.protocol.ProtocolModuleRuntimeRegistry; +import cn.universal.core.service.IoTDownlFactory; +import cn.universal.persistence.query.AjaxResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 系统协议模块状态控制器 + * + *

提供系统中各个协议模块的启用状态查询接口,帮助前端了解当前可用的协议服务 + * + * @version 1.0 @Author Aleo + * @since 2025/1/2 + */ +@RestController +@Tag(name = "系统协议模块", description = "系统协议模块状态管理") +@RequestMapping("/admin/protocol") +@Slf4j +public class SystemProtocolController extends BaseController { + + @Resource private ProtocolModuleRegistry protocolModuleRegistry; + + @Resource private ProtocolModuleRuntimeRegistry protocolModuleRuntimeRegistry; + + /** + * 获取系统协议模块状态 + * + *

查询当前系统中所有协议模块的启用状态,包括: + * + *

    + *
  • CT-AIoT (电信物联网平台) + *
  • MQTT (消息队列遥测传输) + *
  • TCP (传输控制协议) + *
  • HTTP (超文本传输协议) + *
  • OneNet (中国移动物联网平台) + *
  • ImouLife (乐橙云) + *
+ * + * @return 协议模块状态信息 + */ + @Operation(summary = "获取协议模块状态", description = "查询当前系统中所有协议模块的启用状态") + @GetMapping("/status") + public AjaxResult getProtocolModuleStatus() { + log.info("查询协议模块状态"); + + try { + // 使用运行时注册表获取实际运行的协议模块 + Map> runtimeProtocols = new HashMap<>(); + ProtocolModuleRuntimeRegistry.getAllRuntimeProtocols() + .forEach( + protocol -> { + Map details = new HashMap<>(); + details.put("code", protocol.getCode()); + details.put("name", protocol.getName()); + details.put("description", protocol.getDescription()); + details.put("version", protocol.getVersion()); + details.put("vendor", protocol.getVendor()); + details.put("isCore", protocol.isCore()); + details.put("category", protocol.getCategory().name()); + details.put("available", true); // 运行时注册表中的都是可用的 + details.put("status", "已启用"); + runtimeProtocols.put(protocol.getCode(), details); + }); + + // 获取统计信息 + ProtocolModuleRuntimeRegistry.RuntimeStatistics stats = + ProtocolModuleRuntimeRegistry.getStatistics(); + int totalRegistered = ProtocolModuleRegistry.getAllProtocolCodes().size(); + + Map response = new HashMap<>(); + response.put("protocols", runtimeProtocols); + response.put("totalEnabled", stats.getTotalRunning()); + response.put("totalRegistered", totalRegistered); + response.put("coreModulesCount", stats.getCoreCount()); + response.put("optionalModulesCount", stats.getOptionalCount()); + response.put("categoryStats", stats.getCategoryStats()); + + return AjaxResult.success("查询成功", response); + + } catch (Exception e) { + log.error("查询协议模块状态失败", e); + return AjaxResult.error("查询协议模块状态失败: " + e.getMessage()); + } + } + + /** + * 检查指定协议模块是否可用 + * + * @param protocolCode 协议代码 (如: ctwing, mqtt, tcp, http, onenet, imoulife) + * @return 协议模块可用性信息 + */ + @Operation(summary = "检查协议模块可用性", description = "检查指定协议模块是否可用") + @GetMapping("/check") + public AjaxResult checkProtocolAvailability(String protocolCode) { + try { + if (protocolCode == null || protocolCode.trim().isEmpty()) { + return AjaxResult.error("协议代码不能为空"); + } + + // 使用运行时注册表检查协议是否可用 + boolean available = ProtocolModuleRuntimeRegistry.isProtocolRunning(protocolCode); + ProtocolModuleInfo protocolInfo = ProtocolModuleRuntimeRegistry.getProtocolInfo(protocolCode); + + Map result = new HashMap<>(); + result.put("protocolCode", protocolCode); + result.put("available", available); + result.put("status", available ? "可用" : "不可用"); + + // 如果协议可用,添加详细信息 + if (available && protocolInfo != null) { + result.put("name", protocolInfo.getName()); + result.put("description", protocolInfo.getDescription()); + result.put("version", protocolInfo.getVersion()); + result.put("vendor", protocolInfo.getVendor()); + result.put("isCore", protocolInfo.isCore()); + result.put("category", protocolInfo.getCategory().name()); + } + result.put("timestamp", System.currentTimeMillis()); + + if (available) { + log.info("[协议模块检查] 协议 {} 可用", protocolCode); + return AjaxResult.success("协议模块可用", result); + } else { + log.warn( + "[协议模块检查] 协议 {} 不可用,可用协议: {}", protocolCode, IoTDownlFactory.getEnabledDownServices()); + result.put("availableProtocols", IoTDownlFactory.getEnabledDownServices()); + return AjaxResult.success("协议模块不可用", result); + } + + } catch (Exception e) { + log.error("[协议模块检查] 检查协议 {} 失败", protocolCode, e); + return AjaxResult.error("检查协议模块失败: " + e.getMessage()); + } + } + + /** + * 获取协议模块分类统计 + * + * @return 协议模块分类统计信息 + */ + @Operation(summary = "获取协议分类统计", description = "按分类统计协议模块数量") + @GetMapping("/categories") + public AjaxResult getProtocolCategories() { + try { + Map result = new HashMap<>(); + + // 按分类统计 + Map categoryStats = new HashMap<>(); + Map> categoryModules = new HashMap<>(); + + for (ProtocolModuleInfo.ProtocolCategory category : + ProtocolModuleInfo.ProtocolCategory.values()) { + java.util.List modules = + ProtocolModuleRegistry.getModulesByCategory(category); + categoryStats.put(category.name(), modules.size()); + + java.util.List moduleNames = + modules.stream() + .map(ProtocolModuleInfo::getName) + .collect(java.util.stream.Collectors.toList()); + categoryModules.put(category.name(), moduleNames); + } + + result.put("categoryStats", categoryStats); + result.put("categoryModules", categoryModules); + + // 核心模块统计 + java.util.List coreModules = ProtocolModuleRegistry.getCoreModules(); + java.util.List optionalModules = + ProtocolModuleRegistry.getOptionalModules(); + + result.put( + "coreModules", + coreModules.stream() + .map( + m -> + Map.of( + "code", + m.getCode(), + "name", + m.getName(), + "category", + m.getCategory().name())) + .collect(java.util.stream.Collectors.toList())); + + result.put( + "optionalModules", + optionalModules.stream() + .map( + m -> + Map.of( + "code", + m.getCode(), + "name", + m.getName(), + "category", + m.getCategory().name())) + .collect(java.util.stream.Collectors.toList())); + + result.put("timestamp", System.currentTimeMillis()); + + return AjaxResult.success("查询成功", result); + + } catch (Exception e) { + log.error("[协议分类统计] 查询失败", e); + return AjaxResult.error("查询协议分类失败: " + e.getMessage()); + } + } +} diff --git a/cn-universal-admin/src/main/resources/generator.yml b/cn-universal-admin/src/main/resources/generator.yml new file mode 100644 index 0000000..01ec683 --- /dev/null +++ b/cn-universal-admin/src/main/resources/generator.yml @@ -0,0 +1,10 @@ +# 代码生成 +gen: + # 作者 + Author: ruoyi + # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool + packageName: cn.universal. + # 自动去除表前缀,默认是false + autoRemovePre: false + # 表前缀(生成类名不会包含表前缀,多个用逗号分隔) + tablePrefix: \ No newline at end of file diff --git a/cn-universal-admin/src/main/resources/mybatis/GenTableColumnMapper.xml b/cn-universal-admin/src/main/resources/mybatis/GenTableColumnMapper.xml new file mode 100644 index 0000000..e8160dd --- /dev/null +++ b/cn-universal-admin/src/main/resources/mybatis/GenTableColumnMapper.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select column_id, + table_id, + column_name, + column_comment, + column_type, + java_type, + java_field, + is_pk, + is_increment, + is_required, + is_insert, + is_edit, + is_list, + is_query, + query_type, + html_type, + dict_type, + sort, + create_by, + create_time, + update_by, + update_time + from gen_table_column + + + + + + + + insert into gen_table_column ( + table_id, + column_name, + column_comment, + column_type, + java_type, + java_field, + is_pk, + is_increment, + is_required, + is_insert, + is_edit, + is_list, + is_query, + query_type, + html_type, + dict_type, + sort, + create_by, + create_time + )values( + #{tableId}, + #{columnName}, + #{columnComment}, + #{columnType}, + #{javaType}, + #{javaField}, + #{isPk}, + #{isIncrement}, + #{isRequired}, + #{isInsert}, + #{isEdit}, + #{isList}, + #{isQuery}, + #{queryType}, + #{htmlType}, + #{dictType}, + #{sort}, + #{createBy}, + sysdate() + ) + + + + update gen_table_column + + column_comment = #{columnComment}, + java_type = #{javaType}, + java_field = #{javaField}, + is_insert = #{isInsert}, + is_edit = #{isEdit}, + is_list = #{isList}, + is_query = #{isQuery}, + is_required = #{isRequired}, + query_type = #{queryType}, + html_type = #{htmlType}, + dict_type = #{dictType}, + sort = #{sort}, + update_by = #{updateBy}, + update_time = sysdate() + + where column_id = #{columnId} + + + + delete from gen_table_column where table_id in + + #{tableId} + + + + + delete from gen_table_column where column_id in + + #{item.columnId} + + + + \ No newline at end of file diff --git a/cn-universal-admin/src/main/resources/mybatis/GenTableMapper.xml b/cn-universal-admin/src/main/resources/mybatis/GenTableMapper.xml new file mode 100644 index 0000000..d931e0c --- /dev/null +++ b/cn-universal-admin/src/main/resources/mybatis/GenTableMapper.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select table_id, + table_name, + table_comment, + sub_table_name, + sub_table_fk_name, + class_name, + tpl_category, + package_name, + module_name, + business_name, + function_name, + function_Author, + gen_type, + gen_path, + options, + create_by, + create_time, + update_by, + update_time, + remark + from gen_table + + + + + + + + + + + + + + + + + + insert into gen_table ( + table_name, + table_comment, + class_name, + tpl_category, + package_name, + module_name, + business_name, + function_name, + function_Author, + gen_type, + gen_path, + remark, + create_by, + create_time + )values( + #{tableName}, + #{tableComment}, + #{className}, + #{tplCategory}, + #{packageName}, + #{moduleName}, + #{businessName}, + #{functionName}, + #{functionAuthor}, + #{genType}, + #{genPath}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update gen_table + + table_name = #{tableName}, + table_comment = #{tableComment}, + sub_table_name = #{subTableName}, + sub_table_fk_name = #{subTableFkName}, + class_name = #{className}, + function_Author = + #{functionAuthor}, + + gen_type = #{genType}, + gen_path = #{genPath}, + tpl_category = #{tplCategory}, + package_name = #{packageName}, + module_name = #{moduleName}, + business_name = #{businessName}, + function_name = #{functionName}, + options = #{options}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where table_id = #{tableId} + + + + delete from gen_table where table_id in + + #{tableId} + + + + \ No newline at end of file diff --git a/cn-universal-admin/src/main/resources/vm/java/controller.java.vm b/cn-universal-admin/src/main/resources/vm/java/controller.java.vm new file mode 100644 index 0000000..0fadc1f --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/java/controller.java.vm @@ -0,0 +1,106 @@ +package ${basePackage}.${moduleName}.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import cn.universal.admin.system.web.BaseController; +import cn.universal.persistence.query.AjaxResult; +import com.zebra.common.utils.poi.ExcelUtil; + #if($table.crud) + import cn.universal.persistence.page.TableDataInfo; + #elseif($table.tree) + import com.zebra.common.core.domain.Ztree; + #end +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.Api; + +/** + * ${functionName}Controller + * + * @Author ${Author} + * @date ${datetime} + */ +@RestController +@Api(value = "${functionName}管理", tags = {"${functionName}管理"}) +@RequestMapping("/admin/${moduleName}/${businessName}") +public class ${ClassName}Controller extends BaseController { + + @Autowired + private I${ClassName}Service ${className}Service; + +/** + * 查询${functionName}列表 + */ +@ApiOperation("查询${functionName}列表") +@GetMapping("/list") + #if($table.crud || $table.sub) + public TableDataInfo list(${ClassName} ${className}) { + startPage(); + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return getDataTable(list); + } + #elseif($table.tree) + public AjaxResult list(${ClassName} ${className}) { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return AjaxResult.success(list); + } + #end + + /** + * 导出${functionName}列表 + */ + @ApiOperation("导出${functionName}列表") + @PostMapping("/export") + public void export(HttpServletResponse response, ${ClassName} ${className}) { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}. class); + util.exportExcel(response, list, "${functionName}数据"); + } + + /** + * 获取${functionName}详细信息 + */ + @ApiOperation("获取${functionName}详细信息") + @GetMapping(value = "/{${pkColumn.javaField}}") + public AjaxResult<${ClassName}> getInfo( + @PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) { + return AjaxResult.success(${className}Service.select${ClassName}ById(${pkColumn.javaField})); + } + + /** + * 新增${functionName} + */ + @ApiOperation("新增${functionName}") + @PostMapping + public AjaxResult add(@RequestBody ${ClassName} ${className}) { + ${className}Service.insert${ClassName}(${className}); + return AjaxResult.success(); + } + + /** + * 修改${functionName} + */ + @ApiOperation("修改${functionName}") + @PutMapping + public AjaxResult edit(@RequestBody ${ClassName} ${className}) { + ${className}Service.update${ClassName}(${className}); + return AjaxResult.success(); + } + + /** + * 删除${functionName} + */ + @ApiOperation("删除${functionName}") + @DeleteMapping("/{${pkColumn.javaField}s}") + public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) { + ${className}Service.delete${ClassName}ByIds(${pkColumn.javaField}s); + return AjaxResult.success(); + } +} diff --git a/cn-universal-admin/src/main/resources/vm/java/domain.java.vm b/cn-universal-admin/src/main/resources/vm/java/domain.java.vm new file mode 100644 index 0000000..65aca14 --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/java/domain.java.vm @@ -0,0 +1,63 @@ +package ${packageName}.domain; + +import javax.persistence.Table; +import javax.persistence.Id; +import javax.persistence.Column; + + +import lombok.Data; +import com.zebra.common.annotation.Excel; +#if($table.crud) +import com.zebra.common.core.domain.BussinessEntity; +#elseif($table.tree) +#end +#foreach ($import in $importList) +import ${import}; +#end + +/** + * ${tableName}表 ${ClassName} + * + * @Author ${Author} + * @date ${datetime} + */ + #if($table.crud) + #set($Entity="BaseEntity") + #elseif($table.tree) + #set($Entity="TreeEntity") + #end + @Table(name = "${tableName}") + @Data + public class ${ClassName} extends + + BaseEntity { + private static final long serialVersionUID = 1L; + + #foreach ($column in $columns) + #if(!$column.superColumn) + /** $column.columnComment */ + #if($column.list) + #set($parentheseIndex=$column.columnComment.indexOf("(")) + #if($parentheseIndex != -1) + #set($comment=$column.columnComment.substring(0, $parentheseIndex)) + #else + #set($comment=$column.columnComment) + #end + #if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + #elseif($column.javaType == 'Date') + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") + #else + @Excel(name = "${comment}") + #end + #end + #if($column.isPk==1) + @Id + #else + @Column(name = "${column.columnName}") + #end + private $column.javaType $column.javaField; + + #end + #end + } diff --git a/cn-universal-admin/src/main/resources/vm/java/mapper.java.vm b/cn-universal-admin/src/main/resources/vm/java/mapper.java.vm new file mode 100644 index 0000000..ec1b9dc --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/java/mapper.java.vm @@ -0,0 +1,62 @@ +ackage ${packageName}.mapper; + +import ${packageName}.domain.${ClassName}; +import java.util.List; +import cn.universal.persistence.common.BaseMapper; + +/** + * ${functionName}Mapper接口 + * + * @Author ${Author} + * @date ${datetime} + */ +public interface ${ClassName}Mapper extends BaseMapper<${ClassName}> { + + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return ${functionName} + */ + ${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + int update${ClassName}(${ClassName} ${className}); + + /** + * 删除${functionName} + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return 结果 + */ + int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据ID + * @return 结果 + */ + int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s); +} \ No newline at end of file diff --git a/cn-universal-admin/src/main/resources/vm/java/service.java.vm b/cn-universal-admin/src/main/resources/vm/java/service.java.vm new file mode 100644 index 0000000..4018130 --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/java/service.java.vm @@ -0,0 +1,73 @@ +package ${packageName}.service; + +import ${packageName}.domain.${ClassName}; +import java.util.List; +#if($table.tree) +import com.zebra.common.core.domain.Ztree; +#end + +/** + * ${functionName}Service接口 + * + * @Author ${Author} + * @date ${datetime} + */ +public interface I${ClassName}Service { + + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return ${functionName} + */ + ${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + int update${ClassName}(${ClassName} ${className}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaType}[] ${pkColumn.javaField}s 需要删除的数据ID + * @return 结果 + */ + int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return 结果 + */ + int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField}); + #if($table.tree) + + /** + * 查询${functionName}树列表 + * + * @return 所有${functionName}信息 + */ + List select${ClassName}Tree(); + #end +} diff --git a/cn-universal-admin/src/main/resources/vm/java/serviceImpl.java.vm b/cn-universal-admin/src/main/resources/vm/java/serviceImpl.java.vm new file mode 100644 index 0000000..11807e0 --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/java/serviceImpl.java.vm @@ -0,0 +1,140 @@ +package ${packageName}.service.impl; + +import cn.universal.admin.common.utils.SecurityUtils; +import java.util.Date; +import java.util.List; + #if($table.tree) + import java.util.ArrayList; + import com.zebra.common.core.domain.Ztree; + #end +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import ${packageName}.mapper.${ClassName}Mapper; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; + +/** + * ${functionName}Service业务层处理 + * + * @Author ${Author} + * @date ${datetime} + */ +@Service +public class ${ClassName}ServiceImpl implements I${ClassName}Service { + + @Resource + private ${ClassName}Mapper ${className}Mapper; + + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return ${functionName} + */ + @Override + public ${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField}) { + return ${className}Mapper.select${ClassName}ById(${pkColumn.javaField}); + } + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName} + */ + @Override + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) { + return ${className}Mapper.select${ClassName}List(${className}); + } + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + @Override + public int insert${ClassName}(${ClassName} ${className}) { + #foreach ($column in $columns) + #if($column.javaField == 'createTime') + ${className}.setCreateTime(new Date()); + #end + #if($column.javaField == 'updateTime') + ${className}.setUpdateTime(new Date()); + #end + #if($column.javaField == 'updateBy') + ${className}.setUpdateBy(SecurityUtils.getUnionId()); + #end + #end + return ${className}Mapper.insert${ClassName}(${className}); + } + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + @Override + public int update${ClassName}(${ClassName} ${className}) { + #foreach ($column in $columns) + #if($column.javaField == 'updateTime') + ${className}.setUpdateTime(new Date()); + #end + #if($column.javaField == 'updateBy') + ${className}.setUpdateBy(SecurityUtils.getUnionId()); + #end + #end + return ${className}Mapper.update${ClassName}(${className}); + } + + /** + * 删除${functionName}对象 + * + * @param ${pkColumn.javaType}[] ${pkColumn.javaField}s 需要删除的数据ID + * @return 结果 + */ + @Override + public int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s) { + return ${className}Mapper.delete${ClassName}ByIds(${pkColumn.javaType}[]${pkColumn.javaField}s); + } + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return 结果 + */ + @Override + public int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField}) { + return ${className}Mapper.delete${ClassName}ById(${pkColumn.javaField}); + } + #if($table.tree) + + /** + * 查询${functionName}树列表 + * + * @return 所有${functionName}信息 + */ + @Override + public List select${ClassName}Tree() { + List<${ClassName}> ${className}List = ${className}Mapper.select${ClassName}List( + new ${ClassName}()); + List ztrees = new ArrayList(); + for (${ClassName} ${className} :${className}List) + { + Ztree ztree = new Ztree(); + #set($TreeCode=$treeCode.substring(0,1).toUpperCase() + ${treeCode.substring(1)}) + #set($TreeParentCode=$treeParentCode.substring(0,1).toUpperCase() + ${treeParentCode.substring(1)}) + #set($TreeName=$treeName.substring(0,1).toUpperCase() + ${treeName.substring(1)}) + ztree.setId(${className}.get${TreeCode}()); + ztree.setpId(${className}.get${TreeParentCode}()); + ztree.setName(${className}.get${TreeName}()); + ztree.setTitle(${className}.get${TreeName}()); + ztrees.add(ztree); + } + return ztrees; + } + #end +} diff --git a/cn-universal-admin/src/main/resources/vm/java/sub-domain.java.vm b/cn-universal-admin/src/main/resources/vm/java/sub-domain.java.vm new file mode 100644 index 0000000..05e0ab9 --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/java/sub-domain.java.vm @@ -0,0 +1,45 @@ +package ${packageName}.domain; + + #foreach ($import in $subImportList) + import ${import}; + #end +import lombok.Data; +import com.ruoyi.common.annotation.Excel; + +/** + * ${subTable.functionName}对象 ${subTableName} + * + * @Author ${Author} + * @date ${datetime} + */ +@Data +public class ${subClassName} extends + + BaseEntity { + private static final long serialVersionUID = 1L; + + #foreach ($column in $subTable.columns) + #if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ + #if($column.list) + #set($parentheseIndex=$column.columnComment.indexOf("(")) + #if($parentheseIndex != -1) + #set($comment=$column.columnComment.substring(0, $parentheseIndex)) + #else + #set($comment=$column.columnComment) + #end + #if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + #elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") + #else + @Excel(name = "${comment}") + #end + #end + private $column.javaType $column.javaField; + + #end + #end + + } diff --git a/cn-universal-admin/src/main/resources/vm/js/api.js.vm b/cn-universal-admin/src/main/resources/vm/js/api.js.vm new file mode 100644 index 0000000..f8d7525 --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/js/api.js.vm @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询${functionName}列表 +export function list${BusinessName}(query) { + return request({ + url: '/admin/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/admin/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + +// 新增${functionName} +export function add${BusinessName}(data) { + return request({ + url: '/admin/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName}(data) { + return request({ + url: '/admin/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/admin/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} diff --git a/cn-universal-admin/src/main/resources/vm/sql/sql.vm b/cn-universal-admin/src/main/resources/vm/sql/sql.vm new file mode 100644 index 0000000..0575583 --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/sql/sql.vm @@ -0,0 +1,22 @@ +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}', '${parentMenuId}', '1', '${businessName}', '${moduleName}/${businessName}/index', 1, 0, 'C', '0', '0', '${permissionPrefix}:list', '#', 'admin', sysdate(), '', null, '${functionName}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'admin', sysdate(), '', null, ''); \ No newline at end of file diff --git a/cn-universal-admin/src/main/resources/vm/vue/index-tree.vue.vm b/cn-universal-admin/src/main/resources/vm/vue/index-tree.vue.vm new file mode 100644 index 0000000..d24143f --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/vue/index-tree.vue.vm @@ -0,0 +1,553 @@ + + + diff --git a/cn-universal-admin/src/main/resources/vm/vue/index.vue.vm b/cn-universal-admin/src/main/resources/vm/vue/index.vue.vm new file mode 100644 index 0000000..bfd7337 --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/vue/index.vue.vm @@ -0,0 +1,748 @@ + + + diff --git a/cn-universal-admin/src/main/resources/vm/vue/v3/index-tree.vue.vm b/cn-universal-admin/src/main/resources/vm/vue/v3/index-tree.vue.vm new file mode 100644 index 0000000..f3cea35 --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/vue/v3/index-tree.vue.vm @@ -0,0 +1,538 @@ + + + diff --git a/cn-universal-admin/src/main/resources/vm/vue/v3/index.vue.vm b/cn-universal-admin/src/main/resources/vm/vue/v3/index.vue.vm new file mode 100644 index 0000000..26e7a0b --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/vue/v3/index.vue.vm @@ -0,0 +1,742 @@ + + + diff --git a/cn-universal-admin/src/main/resources/vm/xml/mapper.xml.vm b/cn-universal-admin/src/main/resources/vm/xml/mapper.xml.vm new file mode 100644 index 0000000..03b3ead --- /dev/null +++ b/cn-universal-admin/src/main/resources/vm/xml/mapper.xml.vm @@ -0,0 +1,139 @@ + + + + + + #foreach ($column in $columns) + + #end + #if($table.tree) + + #end + + + + select#foreach($column in $columns) $column.columnName#if($velocityCount != $columns.size()),#end#end + from ${tableName} + + + + + + + + insert into ${tableName} + + #foreach($column in $columns) + #if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + $column.columnName, + + #end + #end + + + #foreach($column in $columns) + #if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + #{$column.javaField}, + + #end + #end + + + + + update ${tableName} + + #foreach($column in $columns) + #if($column.columnName != $pkColumn.columnName) + $column.columnName = + #{$column.javaField}, + + #end + #end + + where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete + from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} in + + #{${pkColumn.javaField}} + + + + \ No newline at end of file diff --git a/cn-universal-framework/cn-universal-common/pom.xml b/cn-universal-framework/cn-universal-common/pom.xml new file mode 100644 index 0000000..766e526 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/pom.xml @@ -0,0 +1,55 @@ + + + cn-universal-common + 4.0.0 + + cn-universal-framework + cn.universal.iot + 1.5-SNAPSHOT + + + + + cn.hutool + hutool-all + + + org.projectlombok + lombok + + + org.slf4j + slf4j-api + + + com.fasterxml.jackson.core + jackson-annotations + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-web + + + org.apache.commons + commons-lang3 + + + + org.apache.poi + poi-ooxml + 4.1.2 + + + + commons-fileupload + commons-fileupload + + + \ No newline at end of file diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/annotation/Excel.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/annotation/Excel.java new file mode 100644 index 0000000..ebc3ee6 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/annotation/Excel.java @@ -0,0 +1,138 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.annotation; + +import cn.universal.common.poi.ExcelHandlerAdapter; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.BigDecimal; + +/** 自定义导出Excel数据注解 @Author ruoyi */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Excel { + + /** 导出时在excel中排序 */ + public int sort() default Integer.MAX_VALUE; + + /** 导出到Excel中的名字. */ + public String name() default ""; + + /** 日期格式, 如: yyyy-MM-dd */ + public String dateFormat() default ""; + + /** 如果是字典类型,请设置字典的type值 (如: sys_user_sex) */ + public String dictType() default ""; + + /** 读取内容转表达式 (如: 0=男,1=女,2=未知) */ + public String readConverterExp() default ""; + + /** 分隔符,读取字符串组内容 */ + public String separator() default ","; + + /** BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化) */ + public int scale() default -1; + + /** BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN */ + public int roundingMode() default BigDecimal.ROUND_HALF_EVEN; + + /** 导出类型(0数字 1字符串) */ + public ColumnType cellType() default ColumnType.STRING; + + /** 导出时在excel中每个列的高度 单位为字符 */ + public double height() default 14; + + /** 导出时在excel中每个列的宽 单位为字符 */ + public double width() default 16; + + /** 文字后缀,如% 90 变成90% */ + public String suffix() default ""; + + /** 当值为空时,字段的默认值 */ + public String defaultValue() default ""; + + /** 提示信息 */ + public String prompt() default ""; + + /** 设置只能选择不能输入的列内容. */ + public String[] combo() default {}; + + /** 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写. */ + public boolean isExport() default true; + + /** 另一个类中的属性名称,支持多级获取,以小数点隔开 */ + public String targetAttr() default ""; + + /** 是否自动统计数据,在最后追加一行统计数据总和 */ + public boolean isStatistics() default false; + + /** 导出字段对齐方式(0:默认;1:靠左;2:居中;3:靠右) */ + public Align align() default Align.AUTO; + + /** 自定义数据处理器 */ + public Class handler() default ExcelHandlerAdapter.class; + + /** 自定义数据处理器参数 */ + public String[] args() default {}; + + public enum Align { + AUTO(0), + LEFT(1), + CENTER(2), + RIGHT(3); + private final int value; + + Align(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + } + + /** 字段类型(0:导出导入;1:仅导出;2:仅导入) */ + Type type() default Type.ALL; + + public enum Type { + ALL(0), + EXPORT(1), + IMPORT(2); + private final int value; + + Type(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + } + + public enum ColumnType { + NUMERIC(0), + STRING(1), + IMAGE(2); + private final int value; + + ColumnType(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/annotation/Excels.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/annotation/Excels.java new file mode 100644 index 0000000..fee6b17 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/annotation/Excels.java @@ -0,0 +1,26 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Excel注解集 @Author ruoyi */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Excels { + + public Excel[] value(); +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/config/InstanceIdProvider.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/config/InstanceIdProvider.java new file mode 100644 index 0000000..2c99f24 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/config/InstanceIdProvider.java @@ -0,0 +1,325 @@ +package cn.universal.common.config; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.Resource; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.core.env.Environment; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 实例ID提供者 用于生成唯一的应用实例标识 支持心跳机制和TTL管理,确保实例状态可追踪 + * + * @version 1.0 @Author Aleo + * @since 2025/1/27 + */ +@Component +@Slf4j +public class InstanceIdProvider { + + @Value("${server.instance.id:platform}") + private String configInstanceId; + + @Value("${node.offline.threshold.minutes:2}") + private int offlineThresholdMinutes; + + @Resource private ServerProperties serverProperties; + + @Autowired private Environment environment; + + @Autowired(required = false) + private StringRedisTemplate redisTemplate; + + private String instanceId; + private static final String HEARTBEAT_KEY_PREFIX = "instance:heartbeat:"; + private static final String INSTANCE_INFO_KEY_PREFIX = "instance:info:"; + private static final int HEARTBEAT_TTL_MINUTES = 10; // 心跳TTL时间 + + public InstanceIdProvider() { + // 构造函数中不生成实例ID,延迟到依赖注入完成后 + } + + /** 初始化实例ID 在依赖注入完成后调用 */ + @jakarta.annotation.PostConstruct + public void init() { + this.instanceId = generateInstanceId(); + log.info("实例ID初始化完成: {}", instanceId); + } + + /** + * 生成实例ID 格式: hostname-ip-port-stableId 使用基于机器特征的稳定ID,确保同一台机器每次启动的实例ID一致 + * + * @return 实例ID + */ + private String generateInstanceId() { + try { + String hostname = InetAddress.getLocalHost().getHostName(); + String ip = InetAddress.getLocalHost().getHostAddress(); + String port = environment != null ? environment.getProperty("server.port", "8080") : "8080"; + + // 使用配置的实例ID或生成基于机器特征的稳定ID + String stableId; + if (StrUtil.isNotBlank(configInstanceId)) { + stableId = configInstanceId; + } else { + // 基于机器特征生成稳定ID:hostname + ip + port 的hash值 + String machineSignature = hostname + "-" + ip + "-" + port; + int hash = Math.abs(machineSignature.hashCode()); + stableId = String.format("%08x", hash & 0xFFFFFFFF); // 8位十六进制 + } + + return String.format("%s-%s-%s-%s", hostname, ip, port, stableId); + } catch (UnknownHostException e) { + log.warn("无法获取主机信息,使用默认实例ID", e); + String fallbackId = StrUtil.isNotBlank(configInstanceId) ? configInstanceId : "default"; + return "univ-" + fallbackId; + } + } + + /** + * 获取当前实例ID + * + * @return 实例ID + */ + public String getInstanceId() { + if (instanceId == null) { + // 如果实例ID还未初始化,立即生成 + this.instanceId = generateInstanceId(); + log.info("实例ID延迟初始化完成: {}", instanceId); + } + return instanceId; + } + + /** + * 检查消息是否来自当前实例 + * + * @param sourceId 消息源ID + * @return true表示来自当前实例 + */ + public boolean isOwnMessage(String sourceId) { + return getInstanceId().equals(sourceId); + } + + /** + * 获取简化的实例标识(用于日志等场景) + * + * @return 简化实例标识 + */ + public String getSimpleInstanceId() { + String id = getInstanceId(); + return id.substring(0, Math.min(id.length(), 20)) + "..."; + } + + /** 更新实例心跳 每15秒执行一次,保持实例活跃状态 */ + @Scheduled(fixedRate = 15 * 1000) // 15秒(从30秒改为15秒) + public void updateHeartbeat() { + if (redisTemplate == null) { + return; + } + + try { + String heartbeatKey = HEARTBEAT_KEY_PREFIX + getInstanceId(); + String infoKey = INSTANCE_INFO_KEY_PREFIX + getInstanceId(); + + long currentTime = System.currentTimeMillis(); + + // 更新心跳时间戳 + redisTemplate + .opsForValue() + .set(heartbeatKey, String.valueOf(currentTime), HEARTBEAT_TTL_MINUTES, TimeUnit.MINUTES); + + // 更新实例信息 + String instanceInfo = + String.format( + "hostname:%s,ip:%s,port:%s,startTime:%d", + InetAddress.getLocalHost().getHostName(), + InetAddress.getLocalHost().getHostAddress(), + environment != null ? environment.getProperty("server.port", "8080") : "8080", + currentTime); + + redisTemplate + .opsForValue() + .set(infoKey, instanceInfo, HEARTBEAT_TTL_MINUTES, TimeUnit.MINUTES); + + log.debug("实例心跳已更新: instanceId={}, time={}", getInstanceId(), currentTime); + } catch (Exception e) { + log.warn("更新实例心跳失败: instanceId={}, error={}", getInstanceId(), e.getMessage()); + } + } + + /** + * 检查实例是否活跃 使用可配置的时间差额判断,而不是依赖TTL时间 + * + * @param targetInstanceId 目标实例ID + * @return true表示实例活跃 + */ + public boolean isInstanceActive(String targetInstanceId) { + if (redisTemplate == null || StrUtil.isBlank(targetInstanceId)) { + return false; + } + + try { + String heartbeatKey = HEARTBEAT_KEY_PREFIX + targetInstanceId; + String lastHeartbeat = redisTemplate.opsForValue().get(heartbeatKey); + + if (lastHeartbeat == null) { + log.debug("实例无心跳记录: instanceId={}", targetInstanceId); + return false; + } + + // 检查心跳时间是否在合理范围内 + long lastTime = Long.parseLong(lastHeartbeat); + long currentTime = System.currentTimeMillis(); + long timeDiff = currentTime - lastTime; + + // 使用可配置的离线阈值,而不是依赖TTL时间 + long offlineThreshold = offlineThresholdMinutes * 60 * 1000; + boolean isActive = timeDiff < offlineThreshold; + + log.debug( + "实例活跃性检查: instanceId={}, lastHeartbeat={}, timeDiff={}ms, threshold={}ms, isActive={}", + targetInstanceId, + lastTime, + timeDiff, + offlineThreshold, + isActive); + + return isActive; + } catch (Exception e) { + log.warn("检查实例活跃性失败: instanceId={}, error={}", targetInstanceId, e.getMessage()); + return false; + } + } + + /** + * 获取实例信息 + * + * @param targetInstanceId 目标实例ID + * @return 实例信息 + */ + public String getInstanceInfo(String targetInstanceId) { + if (redisTemplate == null || StrUtil.isBlank(targetInstanceId)) { + return null; + } + + try { + String infoKey = INSTANCE_INFO_KEY_PREFIX + targetInstanceId; + return redisTemplate.opsForValue().get(infoKey); + } catch (Exception e) { + log.warn("获取实例信息失败: instanceId={}, error={}", targetInstanceId, e.getMessage()); + return null; + } + } + + /** + * 清理实例心跳数据 + * + * @param targetInstanceId 目标实例ID + */ + public void cleanupInstanceHeartbeat(String targetInstanceId) { + if (redisTemplate == null || StrUtil.isBlank(targetInstanceId)) { + return; + } + + try { + String heartbeatKey = HEARTBEAT_KEY_PREFIX + targetInstanceId; + String infoKey = INSTANCE_INFO_KEY_PREFIX + targetInstanceId; + + redisTemplate.delete(heartbeatKey); + redisTemplate.delete(infoKey); + + log.info("实例心跳数据已清理: instanceId={}", targetInstanceId); + } catch (Exception e) { + log.warn("清理实例心跳数据失败: instanceId={}, error={}", targetInstanceId, e.getMessage()); + } + } + + /** + * 获取所有活跃实例ID + * + * @return 活跃实例ID列表 + */ + public java.util.Set getActiveInstanceIds() { + if (redisTemplate == null) { + return new java.util.HashSet<>(); + } + + try { + java.util.Set activeInstances = new java.util.HashSet<>(); + + // 使用SCAN命令替代KEYS命令,兼容禁用了KEYS的Redis服务器 + String pattern = HEARTBEAT_KEY_PREFIX + "*"; + java.util.Set heartbeatKeys = scanKeys(pattern); + + for (String heartbeatKey : heartbeatKeys) { + String instanceId = heartbeatKey.substring(HEARTBEAT_KEY_PREFIX.length()); + if (isInstanceActive(instanceId)) { + activeInstances.add(instanceId); + } + } + + log.debug("获取活跃实例列表: count={}, instances={}", activeInstances.size(), activeInstances); + return activeInstances; + } catch (Exception e) { + log.warn("获取活跃实例列表失败: error={}", e.getMessage()); + return new java.util.HashSet<>(); + } + } + + /** + * 使用SCAN命令扫描匹配的键 + * + * @param pattern 匹配模式 + * @return 匹配的键集合 + */ + private java.util.Set scanKeys(String pattern) { + java.util.Set keys = new java.util.HashSet<>(); + + try { + // 使用SCAN命令替代KEYS命令 + redisTemplate.execute( + (org.springframework.data.redis.core.RedisCallback>) + connection -> { + org.springframework.data.redis.core.Cursor cursor = + connection.scan( + org.springframework.data.redis.core.ScanOptions.scanOptions() + .match(pattern) + .count(100) + .build()); + + while (cursor.hasNext()) { + byte[] keyBytes = cursor.next(); + String key = new String(keyBytes, java.nio.charset.StandardCharsets.UTF_8); + keys.add(key); + } + + return keys; + }); + + log.debug("SCAN命令扫描完成: pattern={}, foundKeys={}", pattern, keys.size()); + + } catch (Exception e) { + log.warn("SCAN命令执行失败: pattern={}, error={}", pattern, e.getMessage()); + + // 如果SCAN也失败,尝试使用KEYS命令(作为备选方案) + try { + java.util.Set keysResult = redisTemplate.keys(pattern); + if (keysResult != null) { + keys.addAll(keysResult); + log.debug("使用KEYS命令作为备选方案: pattern={}, foundKeys={}", pattern, keys.size()); + } + } catch (Exception keysException) { + log.error("KEYS命令也失败: pattern={}, error={}", pattern, keysException.getMessage()); + } + } + + return keys; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/config/RedisKeyGenerate.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/config/RedisKeyGenerate.java new file mode 100644 index 0000000..b68b27b --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/config/RedisKeyGenerate.java @@ -0,0 +1,39 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.MD5; +import cn.hutool.json.JSONUtil; +import java.lang.reflect.Method; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.stereotype.Component; + +@Component("redisKeyGenerate") +public class RedisKeyGenerate implements KeyGenerator { + + @Override + public Object generate(Object target, Method method, Object... params) { + // 生成参数哈希值 + String paramsHash = generateParamsHash(params); + return target.getClass().getSimpleName() + "::" + paramsHash; + } + + private String generateParamsHash(Object... params) { + if (params == null || params.length == 0) { + return StrUtil.EMPTY; + } + // 使用MD5或SHA256生成短哈希 + return MD5.create().digestHex16(JSONUtil.toJsonStr(params)); + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/config/UnivUPloadConfig.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/config/UnivUPloadConfig.java new file mode 100644 index 0000000..0d4bbbe --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/config/UnivUPloadConfig.java @@ -0,0 +1,56 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.config; + +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** 读取项目相关配置 @Author ruoyi */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@Component +@ConfigurationProperties(prefix = "univ") +public class UnivUPloadConfig { + + /** 上传路径 */ + @Getter private static String profile; + + public void setProfile(String profile) { + UnivUPloadConfig.profile = profile; + } + + /** 获取导入上传路径 */ + public static String getImportPath() { + return profile + "/import"; + } + + /** 获取头像上传路径 */ + public static String getAvatarPath() { + return profile + "/avatar"; + } + + /** 获取下载路径 */ + public static String getDownloadPath() { + return profile + "/download/"; + } + + /** 获取上传路径 */ + public static String getUploadPath() { + return profile + "/upload"; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/constant/Constants.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/constant/Constants.java new file mode 100644 index 0000000..3e60519 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/constant/Constants.java @@ -0,0 +1,98 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.constant; + +/** 通用常量信息 @Author ruoyi */ +public class Constants { + + /** UTF-8 字符集 */ + public static final String UTF8 = "UTF-8"; + + /** GBK 字符集 */ + public static final String GBK = "GBK"; + + /** http请求 */ + public static final String HTTP = "http://"; + + /** https请求 */ + public static final String HTTPS = "https://"; + + /** 通用成功标识 */ + public static final String SUCCESS = "0"; + + /** 通用失败标识 */ + public static final String FAIL = "1"; + + /** 登录成功 */ + public static final String LOGIN_SUCCESS = "Success"; + + /** 注销 */ + public static final String LOGOUT = "Logout"; + + /** 登录失败 */ + public static final String LOGIN_FAIL = "Error"; + + /** 验证码 redis key */ + public static final String CAPTCHA_CODE_KEY = "captcha_codes:"; + + /** 登录用户 redis key */ + public static final String LOGIN_TOKEN_KEY = "login_tokens:"; + + /** 防重提交 redis key */ + public static final String REPEAT_SUBMIT_KEY = "repeat_submit:"; + + /** 验证码有效期(分钟) */ + public static final Integer CAPTCHA_EXPIRATION = 2; + + /** 令牌 */ + public static final String TOKEN = "token"; + + /** 令牌前缀 */ + public static final String TOKEN_PREFIX = "Bearer "; + + /** 令牌前缀 */ + public static final String LOGIN_USER_KEY = "login_user_key"; + + /** 用户ID */ + public static final String JWT_USERID = "userid"; + + /** 用户名称 */ + public static final String JWT_USERNAME = "sub"; + + /** 用户头像 */ + public static final String JWT_AVATAR = "avatar"; + + /** 创建时间 */ + public static final String JWT_CREATED = "created"; + + /** 用户权限 */ + public static final String JWT_AUTHORITIES = "Authorities"; + + /** 参数管理 cache key */ + public static final String SYS_CONFIG_KEY = "sys_config:"; + + /** 字典管理 cache key */ + public static final String SYS_DICT_KEY = "sys_dict:"; + + /** 资源映射路径 前缀 */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** RMI 远程方法调用 */ + public static final String LOOKUP_RMI = "rmi://"; + + /** 资源映射路径 前缀 */ + public static final String REDIS_LOCK_KEY = "redis_lock:"; + + /** 根节点id */ + public static final String ROOT_LEAF_ID = "0"; +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/constant/IoTConstant.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/constant/IoTConstant.java new file mode 100644 index 0000000..ec4c8e4 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/constant/IoTConstant.java @@ -0,0 +1,503 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.constant; + +import cn.hutool.core.util.ObjectUtil; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** IoT常量 */ +public interface IoTConstant { + + String CURRENT_INSTANCE_ID = "instanceId"; + public final String CERT_DEFAULT_KEY = "default-tcp"; + public final String HTTP_UP_BLACK_LIST = "httpUpBlackList"; + public final String FENCE_DELAY_SIGN = "fenceDelaySign"; + public final String FENCE_TRIGGER_SIGN = "fenceTriggerSign"; + + public final String EXCLUSIVE_FIRST_LOGIN = "exclusiveFirstLogin"; + public final String EXCLUSIVE_LOGIN = "exclusiveLogin"; + public final String EXCLUSIVE_LOGIN_TOKEN = "exclusiveLoginToken"; + + public final String TCP_STANDARD_PRODUCT_KEY = "standardTcpProduct"; + public final String TCP_FUNCTION = "tcpFunction"; + public final String MSG_ID = "msgId"; + public final String PRE_STORE_FUNCTION = "preStoreFunction"; + + public final String LOG_META_PROPERTY_DELETE_SIGN = "logMetaPropertyDeleteSign"; + public final String LOG_META_SHARD_PROPERTY_DELETE_SIGN = "logMetaShardPropertyDeleteSign"; + + public final String LOG_META_EVENT_DELETE_SIGN = "logMetaEventDeleteSign"; + public final String LOG_META_SHARD_EVENT_DELETE_SIGN = "logMetaShardEventDeleteSign"; + + String MAGIC_REDIS_SIGN = "magicRedisSign:"; + // Redis Key 常量 + final String DEVICE_ROUTES_KEY = "tcp:device:routes"; + final String INSTANCE_DEVICES_KEY_PREFIX = "tcp:instance:devices:"; + + /** tcp设备指令下行的mqtt消息标识 */ + public final String TCP_DOWN_MQTT_TOPIC_SIGN = "tcpDownMqttSign"; + + public final String TCP_ROUTE_PREFIX = "tcp:route:"; + public final String TCP_DEVICE_DEL_SIGN = "tcpDeviceDelSign"; + public final String TCP_SERVER_CODEC_ERROR_PREFIX = "tcpCodecErr:"; + public final String TCP_SERVER_IP_BLACK_LIST_PREFIX = "tcpServerIpBlackListPrefix:"; + public final String TCP_ERROR_MONITOR = "tcpErrorMonitor"; + + public final String HIK_TOKEN_APPLICATION = "hikTokenApplication"; + public final String DH_TOKEN_APPLICATION = "dhTokenApplication"; + + public final Pattern pattern = + Pattern.compile("^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9]).{8,30}$"); + + /** REDIS KEY */ + public final String REDIS_STORE_COMMAND = "storeCommand"; + + public final String REDIS_REPLY_COMMAND = "replyCommand"; + + /** 是否是定位设备 */ + String IS_GPS_PRODUCT = "isGps"; + + /** 不存在时是否新增 */ + String ALLOW_INSERT = "allowInsert"; + + /** + * customized + * + *

设备公共字段,下发可能要用到的,deivceId,imei,meterNo + */ + public final String DEVICE_SHADOW_CUSTOMIZED_PROPERTY = "device_common_property"; + + public final String DEVICE_SHADOW_DESIRED_PROPERTY = "device_desired_property"; + + /** 默认离线阈值1440分钟(24小时) */ + public final int DEFAULT_OFFLINE_THRESHOLD_VALUE = 24 * 60; + + public final String DEFAULT_OFFLINE_THRESHOLD = "offlineThreshold"; + + /** 日志最大存储时间,单位天 */ + public final int DEFAULT_LOG_MAX_STORAGE_TIME = 100; + + public final String DEFAULT_LOG_MAX_STORAGE = "logMaxStorage"; + public final Integer NORMAL = 0; + public final Integer UN_NORMAL = 1; + + /*** + * 设备订阅最大数量 + */ + public final int MAX_DEV_MSG_SUBSCRIBE_NUM = 5; + + /** http请求超时时间 */ + public final Integer HTTP_TIME_OUT = 8000; + + public final String HTTP_HEADER_APP = "cn-universal-APP"; + public final String HTTP_HEADER_AUTH = "univ-AUTH"; + public final String HTTP_HEADER_TIME = "univ-TIME"; + + /** 不解析,直接透传 */ + public final String DATA_PASS_THROUGH = "passThrough"; + + /** 中台上行带上原始串 */ + public final String REQUIRE_PAYLOAD = "requirePayload"; + + /** iot_device_log_metadata单属性最大存储10条 */ + public final Integer maxStorage = 10; + + /** 超时120s */ + public final Integer HTTP_AUTH_TIMEOUT = 120; + + /** UTC时区+8 */ + public final String HTTP_UTC_8 = "+8"; + + public static final String TRACE_ID = "traceId"; + + static final String defaultMetadata = + """ + {"tags":[],"events":[{"id":"online","name":"上线","valueType":{"type":"string"}},{"id":"offline","name":"下线","valueType":{"type":"string"}}],"functions":[],"properties":[]}"""; + + enum ProtocolModule { + ctaiot, + ezviz, + onenet, + lechen, + tcp, + sniTcp + } + + /** 非设备真实上报事件 */ + Set DevNotReallyReportEvent = + Stream.of("create", "offline", "enable", "disable", "delete", "update") + .collect(Collectors.toCollection(HashSet::new)); + + /** 标签类型 */ + enum DevLifeCycle { + /** 创建 */ + create("设备创建"), + /** 上线 */ + online("设备上线"), + + /** 离线 */ + offline("设备离线"), + /** 上下线 */ + onOffline("设备上下线"), + + /** 启用 */ + enable("设备启用"), + + /** 离线 */ + disable("设备停用"), + + /** 删除 */ + delete("设备删除"), + /** 修改 */ + update("设备修改"); + + private String value; + + DevLifeCycle(String value) { + this.value = value; + } + + @JsonCreator + public static DevLifeCycle find(String value) { + return Stream.of(values()) + .filter(e -> e.getValue().equalsIgnoreCase(value)) + .findFirst() + .orElse(null); + } + + @JsonValue + public String getValue() { + return this.value; + } + } + + /** 上下线枚举,保持物模型一致,使用小写 */ + enum DeviceStatus { + online(true), + offline(false); + + private boolean code; + + private DeviceStatus(boolean code) { + this.code = code; + } + + public boolean getCode() { + return code; + } + } + + /** 下行指令 */ + enum DownCmd { + DEV_ADD, + DEV_ADDS, + DEV_FUNCTION, + DEV_DEL, + DEV_UPDATE, + DEV_MONITOR_TURN, + MANUAL_CAPTURE, + DEV_CONTROLLING, + DEV_MONITOR_PLAY, + + DEV_ELECTRIC_QUANTITY, + DEV_DOOR_KEYS, + DEV_OPENDOOR_RECORD, + DEV_GENERATE_SNAPKEY, + DEV_DELETE_DOORKEY, + DEV_SNAPKEY_LIST, + DEV_WAKE_UP, + DEV_LIVE, + DEV_LIVE_STREAMINFO, + DEV_SETTING, + DEV_PLAY_BACK, + DEV_PLAY_BACKCLOUD, + DEV_VIDEO_LOCAL, + DEV_VIDEO_CLOUD, + DEV_SNAP, + PRO_ADD, + PRO_UPDATE, + PRO_DEL, + PUBPRO_GET, + PUBPRO_ADD, + DEV_SDCARD_STATUS, + DEV_SDCARD, + RECOVER_SDCARD, + CAPTURE, + GET_DEVICE_CLOUD, + CREATR_DEVICE_RTMP, + CREATR_DEVICE_FLV, + INTERCOMBYIDS, + QUERY_DEVICE_FLV, + LIST_DEVICE_DETAILS_ID, + SET_NIGHT_VISION_MODE, + UP_GRADE_PROCESS_DEVICE, + SET_LOCAL_RECORD_STREAM, + QUERY_LOCAL_RECORD_STREAM, + SET_LOCAL_RECORD_PLAN_RULES, + GUERY_LOCAL_RECORD_PLAN, + GET_CLOUD_RECORDS, + GET_DOWN_LOAD_LIST, + UN_USED_CLOUD_LIST, + QUERY_CLOUD_RECORD_CALL_NUM, + SET_ALL_STORAGE_STRATEGY, + DEVICE_CLOUD_LIST, + UN_BIND_DEVICE_CLOUD, + DOWNLOAD_RECORD, + OPEN_CLOUD_RECORD, + SET_STORAGE_STRATEGY, + FRAME_REVERSE_STATUS, + SOUND_VOLUME_SIZE_GET, + CAMERA_STATUS, + CAMERA_SNAPSHOT, + CONTROL_DEVICE_WIFI, + WIFI_AROUND, + UPGRADE_DEVICE, + DEVICE_VERSIONLIST, + DEVICE_CLOUD, + ENABLE_CONFIG, + REVERSE_STATUS, + RESTART_DEVICE, + SOUND_VOLUME_SIZE, + ALARM_MESSAGE, + DEV_MONITOR_CHECK_ONLINE; + + private DownCmd() {} + + @JsonCreator + public static DownCmd find(String value) { + return Stream.of(values()) + .filter(e -> e.getValue().equalsIgnoreCase(value)) + .findFirst() + .orElse(null); + } + + @JsonValue + public String getValue() { + return super.toString().toLowerCase(); + } + } + + /** 设备订阅 */ + public static enum DeviceSubscribe { + PRODUCT, + + DEVICE; + + private DeviceSubscribe() {} + + @JsonCreator + public static DeviceSubscribe find(String value) { + return Stream.of(values()) + .filter(e -> e.getValue().equalsIgnoreCase(value)) + .findFirst() + .orElse(null); + } + + @JsonValue + public String getValue() { + return super.toString().toLowerCase(); + } + } + + /** 设备节点,网关,直连,网关子设备 */ + enum DeviceNode { + /** 直连 */ + DEVICE, + /** 网关 */ + GATEWAY, + /** 网关子设备 */ + GATEWAY_SUB_DEVICE; + + private DeviceNode() {} + + @JsonCreator + public static DeviceNode find(String value) { + return Stream.of(values()) + .filter(e -> e.getValue().equalsIgnoreCase(value)) + .findFirst() + .orElse(null); + } + + @JsonValue + public String getValue() { + return super.toString().toLowerCase(); + } + } + + /** 消息类型 */ + enum MessageType { + /** 所有 */ + ALL, + /** 属性数据上报 */ + PROPERTIES, + /** 事件 */ + EVENT, + /** 指令响应 */ + REPLY, + /** 微信用户信息 */ + WXOPENID, + /** 服务调用 */ + FUNCTIONS; + + private MessageType() {} + + @JsonCreator + public static MessageType find(String value) { + return Stream.of(values()) + .filter(e -> e.getValue().equalsIgnoreCase(value)) + .findFirst() + .orElse(MessageType.PROPERTIES); + } + + public static boolean devReallyReport(MessageType messageType) { + return MessageType.EVENT.equals(messageType) + || MessageType.PROPERTIES.equals(messageType) + || MessageType.REPLY.equals(messageType); + } + + @JsonValue + public String getValue() { + return super.toString().toLowerCase(); + } + } + + /** 事件类型 */ + public static enum EventType { + ; + + private EventType() {} + + @JsonCreator + public static EventType find(String value) { + return Stream.of(values()) + .filter(e -> e.getValue().equalsIgnoreCase(value)) + .findFirst() + .orElse(null); + } + + @JsonValue + public String getValue() { + return super.toString().toLowerCase(); + } + } + + public static enum ERROR_CODE { + /** Lora终端不存在 */ + DATA_NOT_FIND(700, "数据不存在"), + DEV_NOT_FIND(700, "设备不存在"), + DEV_NOT_FOR_YOU(701, "设备没有操作权限!"), + DEV_ADD_DEVICE_ID_EXIST(702, "deivceId已存在"), + DEV_DEL_DEVICE_NO_ID_EXIST(702, "deivceId不存在"), + DEV_DEL_ERROR(703, "设备删除错误"), + DEV_UPDATE_ERROR(704, "设备修改错误"), + DEV_UPDATE_DEVICE_NO_ID_EXIST(705, "deviceId不存在"), + DEV_CONFIG_DEVICE_NO_ID_EXIST(706, "deviceId不存在"), + DEV_CONFIG_DEVICE_PARA_NULL(707, "参数为空"), + DEV_CONFIG_DEVICE_PARA_MISS(765, "缺少参数"), + DEV_CONFIG_DEVICE_LEVEL_ERROR(708, "level不合法"), + DEV_CONFIG_DEVICE_STATE_ERROR(709, "设备未激活"), + DEV_CONFIG_DEVICE_PARA_ERROR(710, "参数校验失败"), + DEV_CONFIG_DEVICE_MASTERKEY_ERROR(711, "MasterKey不匹配"), + DEV_CONFIG_DEVICE_MESSAGE_ERROR(712, "指令应为偶数"), + DEV_CONFIG_DEVICE_NULL(713, "下发指令不能为空"), + DEV_CONFIG_PAYLOAD_NULL(714, "下发报文(payload)不能为空"), + DEV_CONFIG_ERROR(715, "设备配置错误"), + DEV_DOWN_ADD_ERROR(717, "指令 cmd 配置错误(指令 cmd 不能为空)"), + DEV_PARA_RANGE_ERROR(721, "指令下发内容与服务模型参数范围不匹配"), + DEV_DOWN_CMD_ERROR(715, "下发指令错误"), + DEV_CONFIG_DEVICE_PARA_FAIL(716, "参数解析失败"), + DEV_B_SERVICE_EXIST(720, "B端设备不能添加到C端"), + DEV_DOWN_NOT_SUPPORT(721, "设备不支持功能下发"), + DEV_DOWN_FAILE(722, "设备下发失败"), + DEV_DOWN_NO_MQTT_TOPIC(723, "mqtt接入,产品`端云配置`未配置指令下行主题项"), + DEV_METADATA_NOT_FIND(750, "物模型不存在"), + DEV_METADATA_FUNCTION_NOT_FIND(751, "物模型定义功能不存在"), + DEV_ADD_ERROR(799, "设备添加错误,请检查参数"), + DEV_WAIT_ADD(754, "存在等待回调设备,请勿重复操作"), + HTTP_TIME_OUT(788, "请求超时"), + WVP_DOWNLOAD_LIMIT(789, "同时下载数量到达上限"), + WVP_THIRD_APPLICATION_NOT_EXIST(790, "wvp第三方平台不存在"); + + // DEV_ADD_LZ_MODEL_NOT_EXIST(701, "Lora终端不存在"); + + private Integer code; + private String name; + + private ERROR_CODE(Integer code, String name) { + this.code = code; + this.name = name; + } + + public Integer getCode() { + return code; + } + + public String getName() { + return name; + } + } + + public static enum TcpFlushType { + start, + reload, + close + } + + public static enum ProductFlushType { + script, + server, + client, + delete + } + + enum NetwotkType { + MQTT_CLIENT("MQTT_CLIENT", "mqtt客户端"), + TCP_CLIENT("TCP_CLIENT", "tcp客户端"), + HTTP_CLIENT("HTTP_CLIENT", "http客户端"), + TCP_SERVER("TCP_SERVER", "tcp服务端"), + WEB_SOCKET_CLIENT("WEB_SOCKET_CLIENT", "socket客户端"); + private String type; + private String name; + + private NetwotkType(String type, String name) { + this.type = type; + this.name = name; + } + + public String getType() { + return this.type; + } + + public String getName() { + return this.name; + } + + public static String findType(String type) { + NetwotkType t = + Stream.of(values()) + .filter(netwotkType -> netwotkType.getType().equals(type)) + .findFirst() + .orElse(null); + if (ObjectUtil.isNull(t)) { + return "该网络类型不存在"; + } + return t.getName(); + } + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/constant/IoTUserConstants.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/constant/IoTUserConstants.java new file mode 100644 index 0000000..c3aa991 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/constant/IoTUserConstants.java @@ -0,0 +1,73 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.constant; + +/** 用户常量信息 @Author ruoyi */ +public class IoTUserConstants { + + /** 平台内系统用户的唯一标志 */ + public static final String SYS_USER = "SYS_USER"; + + /** 正常状态 */ + public static final String NORMAL = "0"; + + /** 异常状态 */ + public static final String EXCEPTION = "1"; + + /** 用户封禁状态 */ + public static final String USER_DISABLE = "1"; + + /** 角色封禁状态 */ + public static final String ROLE_DISABLE = "1"; + + /** 部门正常状态 */ + public static final String DEPT_NORMAL = "0"; + + /** 部门停用状态 */ + public static final String DEPT_DISABLE = "1"; + + /** 字典正常状态 */ + public static final String DICT_NORMAL = "0"; + + /** 是否为系统默认(是) */ + public static final String YES = "Y"; + + /** 是否菜单外链(是) */ + public static final String YES_FRAME = "0"; + + /** 是否菜单外链(否) */ + public static final String NO_FRAME = "1"; + + /** 菜单类型(目录) */ + public static final String TYPE_DIR = "M"; + + /** 菜单类型(菜单) */ + public static final String TYPE_MENU = "C"; + + /** 菜单类型(按钮) */ + public static final String TYPE_BUTTON = "F"; + + /** Layout组件标识 */ + public static final String LAYOUT = "Layout"; + + /** ParentView组件标识 */ + public static final String PARENT_VIEW = "ParentView"; + + /** InnerLink组件标识 */ + public static final String INNER_LINK = "InnerLink"; + + /** 校验返回结果码 */ + public static final String UNIQUE = "0"; + + public static final String NOT_UNIQUE = "1"; +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/constant/NetworkTypeConstants.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/constant/NetworkTypeConstants.java new file mode 100644 index 0000000..02a872a --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/constant/NetworkTypeConstants.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ +package cn.universal.common.constant; + +/** + * 网络类型常量类 用于替换硬编码的网络类型字符串 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +public final class NetworkTypeConstants { + + private NetworkTypeConstants() { + // 私有构造函数,防止实例化 + } + + // TCP相关 + public static final String TCP_CLIENT = "TCP_CLIENT"; + public static final String TCP_SERVER = "TCP_SERVER"; + + // MQTT相关 + public static final String MQTT_CLIENT = "MQTT_CLIENT"; + public static final String MQTT_SERVER = "MQTT_SERVER"; + + // HTTP相关 + public static final String HTTP_CLIENT = "HTTP_CLIENT"; + public static final String HTTP_SERVER = "HTTP_SERVER"; + + // WebSocket相关 + public static final String WEB_SOCKET_CLIENT = "WEB_SOCKET_CLIENT"; + public static final String WEB_SOCKET_SERVER = "WEB_SOCKET_SERVER"; + + // UDP相关 + public static final String UDP = "UDP"; + + // CoAP相关 + public static final String COAP_CLIENT = "COAP_CLIENT"; + public static final String COAP_SERVER = "COAP_SERVER"; +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/domain/BaseEntity.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/domain/BaseEntity.java new file mode 100644 index 0000000..5a054c8 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/domain/BaseEntity.java @@ -0,0 +1,57 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import lombok.Data; + +/** Entity基类 @Author ruoyi */ +@Data +public class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 搜索值 */ + private String searchValue; + + /** 创建者 */ + private String createBy; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新者 */ + private String updateBy; + + /** 更新时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 备注 */ + private String remark; + + /** 请求参数 */ + private Map params; + + public Map getParams() { + if (params == null) { + params = new HashMap<>(); + } + return params; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/domain/R.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/domain/R.java new file mode 100644 index 0000000..d159641 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/domain/R.java @@ -0,0 +1,157 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.domain; + +import cn.universal.common.constant.IoTConstant; +import org.slf4j.MDC; + +/** + * @Author Aleo + */ +public class R { + + private static final long serialVersionUID = 1L; + private Integer code; + private String msg; + private T data; + private String requestId; + + public static final Integer SUCCESS = 0; + public static final Integer ERROR = 500; + public static final String ERROR_MSG = "error"; + public static final String SUCCESS_MSG = "success"; + + private R(int code, String msg) { + this.requestId = MDC.get(IoTConstant.TRACE_ID); + this.code = code; + this.msg = msg; + } + + public R() { + this.requestId = MDC.get(IoTConstant.TRACE_ID); + } + + private R(int code, String msg, T data) { + this.requestId = MDC.get(IoTConstant.TRACE_ID); + this.code = code; + this.msg = msg; + this.data = data; + } + + public static R ok() { + return ok(SUCCESS_MSG); + } + + public static R ok(T data) { + return ok(SUCCESS_MSG, data); + } + + public static R ok(String msg) { + return new R<>(SUCCESS, msg, null); + } + + public static R ok(String msg, T data) { + return new R<>(SUCCESS, msg, data); + } + + public static R ok(int code, String msg, T data) { + return new R<>(code, msg, data); + } + + public static R error() { + return error(ERROR_MSG); + } + + public static R error(String msg) { + return new R<>(ERROR, msg, null); + } + + public static R error(String msg, T data) { + return new R<>(ERROR, msg, data); + } + + public static R error(int code, String msg, T data) { + return new R<>(code, msg, data); + } + + public static R error(int code, String msg) { + return new R<>(code, msg, null); + } + + public static R toAjax(int rows) { + if (rows > 0) { + return ok(); + } else { + return error(); + } + } + + /** + * 判断当前状态是否为成功 + * + * @return true表示成功,false表示失败 + */ + public boolean isSuccess() { + return SUCCESS.equals(this.code); + } + + /** + * 判断当前状态是否为失败 + * + * @return true表示失败,false表示成功 + */ + public boolean isError() { + return !isSuccess(); + } + + /** + * 判断当前状态是否为失败(与isError()方法相同,提供更直观的命名) + * + * @return true表示失败,false表示成功 + */ + public boolean isFailed() { + return isError(); + } + + public Integer getCode() { + return code; + } + + public String getMsg() { + return msg; + } + + public T getData() { + return data; + } + + public void setCode(Integer code) { + this.code = code; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public void setData(T data) { + this.data = data; + } + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/enums/NetworkType.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/enums/NetworkType.java new file mode 100644 index 0000000..4d7e871 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/enums/NetworkType.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 AleoXin 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: AleoXin + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ +package cn.universal.common.enums; + +public enum NetworkType { + TCP_CLIENT("TCP_CLIENT", "TCP客户端"), + TCP_SERVER("TCP_SERVER", "TCP服务"), + // MQTT + MQTT_CLIENT("MQTT_CLIENT", "MQTT客户端"), + MQTT_SERVER("MQTT_SERVER", "MQTT服务"), + + HTTP_CLIENT("HTTP_CLIENT", "HTTP客户端"), + HTTP_SERVER("HTTP_SERVER", "HTTP服务"), + + WEB_SOCKET_CLIENT("WEB_SOCKET_CLIENT", "WebSocket客户端"), + WEB_SOCKET_SERVER("WEB_SOCKET_SERVER", "WebSocket服务"), + + UDP("UDP", "UDP"), + + COAP_CLIENT("COAP_CLIENT", "CoAP客户端"), + COAP_SERVER("COAP_SERVER", "CoAP服务"); + + private final String id; + private final String description; + + NetworkType(String id, String description) { + this.id = id; + this.description = description; + } + + public String getId() { + return id; + } + + public String getDescription() { + return description; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/DelayEventProcessor.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/DelayEventProcessor.java new file mode 100644 index 0000000..ee22908 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/DelayEventProcessor.java @@ -0,0 +1,93 @@ +package cn.universal.common.event; + +import cn.hutool.json.JSONUtil; +import cn.universal.common.event.processer.EventPublisher; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** 延迟事件处理器 定时扫描并处理延迟事件 */ +@Slf4j +@Component +public class DelayEventProcessor { + + @Autowired private StringRedisTemplate redisTemplate; + + @Autowired private EventPublisher eventPublisher; + + /** 每10秒扫描一次延迟事件 */ + @Scheduled(fixedRate = 10000) + public void processDelayEvents() { + try { + // 使用SCAN命令替代KEYS命令,避免Redis环境限制 + Set delayKeys = scanDelayKeys(); + if (delayKeys == null || delayKeys.isEmpty()) { + return; + } + + for (String delayKey : delayKeys) { + String eventData = redisTemplate.opsForValue().get(delayKey); + if (eventData != null) { + try { + EventMessage eventMessage = JSONUtil.toBean(eventData, EventMessage.class); + + // 检查是否到了执行时间 + long currentTime = System.currentTimeMillis(); + long eventTime = eventMessage.getTimestamp() + (eventMessage.getDelaySeconds() * 1000); + + if (currentTime >= eventTime) { + // 发布事件 + eventPublisher.publishEvent(eventMessage.getEventType(), eventMessage.getData()); + + // 删除延迟键 + redisTemplate.delete(delayKey); + + log.debug( + "[延迟事件处理] 事件已发布: eventId={}, topic={}", + eventMessage.getEventId(), + eventMessage.getEventType()); + } + } catch (Exception e) { + log.error("[延迟事件处理] 处理延迟事件失败: key={}, data={}", delayKey, eventData, e); + // 删除无效的延迟键 + redisTemplate.delete(delayKey); + } + } + } + } catch (Exception e) { + log.error("[延迟事件处理] 扫描延迟事件失败", e); + } + } + + /** + * 使用SCAN命令扫描延迟事件键 + * + * @return 延迟事件键集合 + */ + private Set scanDelayKeys() { + Set delayKeys = new java.util.HashSet<>(); + String pattern = "delay:*"; + long cursor = 0; + + do { + org.springframework.data.redis.core.ScanOptions options = + org.springframework.data.redis.core.ScanOptions.scanOptions() + .match(pattern) + .count(100) + .build(); + + org.springframework.data.redis.core.Cursor cursorResult = redisTemplate.scan(options); + + while (cursorResult.hasNext()) { + delayKeys.add(cursorResult.next()); + } + + cursor = cursorResult.getCursorId(); + } while (cursor != 0); + + return delayKeys; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/EventConfig.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/EventConfig.java new file mode 100644 index 0000000..71a722a --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/EventConfig.java @@ -0,0 +1,26 @@ +package cn.universal.common.event; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +/** + * 事件系统配置 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +@Configuration +public class EventConfig { + + /** 配置Redis消息监听器容器 */ + @Bean + public RedisMessageListenerContainer redisMessageListenerContainer( + RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + return container; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/EventMessage.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/EventMessage.java new file mode 100644 index 0000000..318c642 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/EventMessage.java @@ -0,0 +1,36 @@ +package cn.universal.common.event; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** 事件消息 */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EventMessage { + + /** 事件类型 */ + private String eventType; + + /** 事件数据 */ + private Object data; + + /** 节点ID */ + private String nodeId; + + /** 时间戳 */ + private Long timestamp; + + /** 创建时间 */ + private LocalDateTime createTime; + + /** 延迟秒数 */ + private Long delaySeconds; + + /** 事件ID */ + private String eventId; +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/EventProcessorFactory.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/EventProcessorFactory.java new file mode 100644 index 0000000..19c716e --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/EventProcessorFactory.java @@ -0,0 +1,84 @@ +package cn.universal.common.event; + +import cn.universal.common.event.processer.FenceEventProcessor; +import cn.universal.common.event.processer.ProductConfigProcessor; +import cn.universal.common.event.processer.TcpDownProcessor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 事件处理器工厂类 统一管理所有Redis事件处理器 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +@Slf4j +@Component +public class EventProcessorFactory { + + // 电子围栏事件处理器 + @Autowired(required = false) + private FenceEventProcessor fenceEventProcessor; + + // TCP下行指令处理器 + @Autowired(required = false) + private TcpDownProcessor tcpDownProcessor; + + // 产品配置处理器 + @Autowired(required = false) + private ProductConfigProcessor productConfigProcessor; + + /** 处理电子围栏事件 */ + public void handleFenceEvent(EventMessage message) { + if (fenceEventProcessor != null) { + try { + fenceEventProcessor.handleFenceEvent(message); + } catch (Exception e) { + log.error("[事件处理器] 电子围栏事件处理失败", e); + } + } else { + log.warn("[事件处理器] FenceEventProcessor未找到,跳过电子围栏事件处理"); + } + } + + /** 处理TCP下行指令事件 */ + public void handleTcpDown(EventMessage message) { + if (tcpDownProcessor != null) { + try { + tcpDownProcessor.handleTcpDownEvent(message); + } catch (Exception e) { + log.error("[事件处理器] TCP下行指令处理失败", e); + } + } else { + log.warn("[事件处理器] TcpDownProcessor未找到,跳过TCP下行指令处理"); + } + } + + /** 处理产品配置更新事件 */ + public void handleProductConfigUpdated(EventMessage message) { + if (productConfigProcessor != null) { + try { + productConfigProcessor.handleProductConfigUpdated(message); + } catch (Exception e) { + log.error("[事件处理器] 产品配置更新处理失败", e); + } + } else { + log.warn("[事件处理器] ProductConfigProcessor未找到,跳过产品配置更新处理"); + } + } + + /** 处理产品配置更新事件 */ + public void handleProtocolUpdated(EventMessage message) { + if (productConfigProcessor != null) { + try { + productConfigProcessor.handleProtocolUpdated(message); + } catch (Exception e) { + log.error("[事件处理器] 产品协议更新处理失败", e); + } + } else { + log.warn("[事件处理器] ProductConfigProcessor未找到,产品协议更新处理失败"); + } + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/EventTopics.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/EventTopics.java new file mode 100644 index 0000000..75f6a24 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/EventTopics.java @@ -0,0 +1,59 @@ +package cn.universal.common.event; + +/** + * 事件主题管理类 集中管理所有Redis事件主题 + * + *

使用示例: + * + *

// 发布事件 eventPublisher.publishEvent(EventTopics.PROTOCOL_UPDATED, eventData); + * eventPublisher.publishEvent(EventTopics.getTcpCommandTopic(instanceId), command); + * + *

// 订阅事件(在RedisEventSubscriber中) container.addMessageListener( new MessageListenerAdapter(this, + * "handleProtocolUpdated"), new ChannelTopic(EventTopics.PROTOCOL_UPDATED)); + * + *

// 模式匹配订阅 container.addMessageListener( new MessageListenerAdapter(this, "handleTcpCommand"), + * new PatternTopic(EventTopics.TCP_COMMAND)); + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +public class EventTopics { + + // ==================== 协议相关事件 ==================== + + /** 协议更新事件 用于通知集群内其他实例协议配置已更新 */ + public static final String PROTOCOL_UPDATED = "protocol:updated"; + + // ==================== 电子围栏相关事件 ==================== + + /** 电子围栏事件 用于处理电子围栏触发事件 */ + public static final String FENCE_EVENT = "fence:event"; + + // ==================== TCP相关事件 ==================== + + /** TCP下行指令事件(模式匹配) 用于处理TCP下行指令 */ + public static final String TCP_DOWN = "tcp:down:*"; + + // ==================== 产品配置相关事件 ==================== + + /** 产品配置更新事件 用于通知产品配置变更 */ + public static final String PRODUCT_CONFIG_UPDATED = "product:config:updated"; + + // ==================== 测试相关事件 ==================== + + /** 测试TCP事件 用于测试TCP相关功能 */ + public static final String TEST_TCP_REL = "test:tcp:rel"; + + // ==================== 工具方法 ==================== + + /** + * 获取TCP下行指令主题(带实例ID) + * + * @param instanceId 实例ID + * @return 完整的TCP下行指令主题 + */ + public static String getTcpDownTopic(String instanceId) { + return "tcp:down:" + instanceId; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/ProtocolUpdatedEvent.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/ProtocolUpdatedEvent.java new file mode 100644 index 0000000..63a9db1 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/ProtocolUpdatedEvent.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +/** + * 协议更新事件 + * + *

当协议配置更新时触发,用于通知相关组件清理缓存 + * + * @version 1.0 @Author Aleo + * @since 2025/01/20 + */ +@Getter +public class ProtocolUpdatedEvent extends ApplicationEvent { + + private final String productKey; + private final String protocolType; + + /** + * 构造函数 + * + * @param source 事件源 + * @param productKey 产品Key + * @param protocolType 协议类型 + */ + public ProtocolUpdatedEvent(Object source, String productKey, String protocolType) { + super(source); + this.productKey = productKey; + this.protocolType = protocolType; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/README.md b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/README.md new file mode 100644 index 0000000..49fc411 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/README.md @@ -0,0 +1,108 @@ +# Redis事件处理系统 + +## 架构图 + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ EventPublisher│ │ Redis Pub/Sub │ │RedisEventSubscriber│ +│ │───▶│ │───▶│ │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ + ▼ + ┌──────────────────┐ + │EventProcessorFactory│ + │ │ + └──────────────────┘ + │ + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ProductFlushProc │ │ TcpDownProcessor│ │FenceDelayProcessor│ + │ │ │ │ │ │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │NettyAndCodec │ │TcpDownRedis │ │FenceDelayRedis │ + │RedisHandler │ │Handler │ │Handler │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## 事件流程 + +### 1. 事件发布 + +- `EventPublisher.publishEvent()` 发布事件到Redis +- 事件包含:事件类型、数据、节点ID、时间戳等 + +### 2. 事件订阅 + +- `RedisEventSubscriber` 监听Redis事件 +- 使用 `EventTopics` 统一管理事件主题 +- 支持模式匹配订阅(如 `tcp:command:*`) + +### 3. 事件分发 + +- `RedisEventSubscriber` 接收事件后调用 `EventProcessorFactory` +- `EventProcessorFactory` 根据事件类型分发到对应的处理器 + +### 4. 事件处理 + +- 具体的处理器实现业务逻辑 +- 处理器可以注入其他服务来处理具体业务 + +## 事件主题 + +| 事件类型 | 主题 | 处理器 | 说明 | +|---------|--------------------------|-----------------------------|------------| +| 协议更新 | `protocol:updated` | - | 协议配置更新 | +| 电子围栏 | `fence:event` | - | 电子围栏触发 | +| 电子围栏延迟 | `fence:delay` | `FenceDelayRedisHandler` | 电子围栏延迟处理 | +| TCP指令 | `tcp:command:*` | - | TCP指令转发 | +| TCP下行指令 | `tcp:down:*` | `TcpDownRedisHandler` | TCP下行指令处理 | +| TCP推送 | `tcp:push:*` | - | TCP主动推送 | +| 产品配置更新 | `product:config:updated` | - | 产品配置变更 | +| 产品刷新 | `product:flush` | `NettyAndCodecRedisHandler` | 产品刷新和编解码重载 | +| 测试TCP | `test:tcp:rel` | - | 测试TCP功能 | + +## 使用示例 + +### 发布事件 + +```java +// 发布协议更新事件 +eventPublisher.publishEvent(EventTopics.PROTOCOL_UPDATED, eventData); + +// 发布TCP指令事件(带实例ID) +eventPublisher.publishEvent(EventTopics.getTcpCommandTopic(instanceId), command); +``` + +### 订阅事件 + +```java +// 在RedisEventSubscriber中订阅 +container.addMessageListener( + new MessageListenerAdapter(this, "handleProtocolUpdated"), + new ChannelTopic(EventTopics.PROTOCOL_UPDATED)); +``` + +### 处理事件 + +```java +// 实现处理器接口 +@Component +public class MyHandler implements ProductFlushProcessor { + @Override + public void handleProductFlushEvent(Object message) { + // 处理产品刷新逻辑 + } +} +``` + +## 优势 + +1. **解耦**: 事件发布者和订阅者完全解耦 +2. **扩展性**: 新增事件类型只需实现对应接口 +3. **统一管理**: 所有事件主题通过 `EventTopics` 统一管理 +4. **容错性**: 处理器不存在时不会影响其他事件处理 +5. **集群支持**: 支持多实例集群部署,自动过滤自己发送的事件 \ No newline at end of file diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/RedisEventPublisher.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/RedisEventPublisher.java new file mode 100644 index 0000000..ee88a3a --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/RedisEventPublisher.java @@ -0,0 +1,120 @@ +package cn.universal.common.event; + +import cn.hutool.json.JSONUtil; +import cn.universal.common.config.InstanceIdProvider; +import cn.universal.common.event.processer.EventPublisher; +import java.time.LocalDateTime; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +/** + * Redis事件发布器实现 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +@Slf4j +@Component +public class RedisEventPublisher implements EventPublisher { + + @Autowired private StringRedisTemplate redisTemplate; + + @Autowired private InstanceIdProvider instanceIdProvider; + + @Override + public void publishEvent(String topic, Object event) { + try { + EventMessage eventMessage = + EventMessage.builder() + .eventType(topic) + .data(event) + .nodeId(instanceIdProvider.getInstanceId()) + .timestamp(System.currentTimeMillis()) + .createTime(LocalDateTime.now()) + .eventId(UUID.randomUUID().toString()) + .build(); + + String message = JSONUtil.toJsonStr(eventMessage); + redisTemplate.convertAndSend(topic, message); + + log.debug("[Redis事件发布] 发布事件到主题: {}, 消息: {}", topic, message); + } catch (Exception e) { + log.error("[Redis事件发布] 发布事件失败: topic={}, event={}", topic, event, e); + } + } + + /** + * 发布延迟事件 + * + * @param topic 主题 + * @param event 事件数据 + * @param delaySeconds 延迟秒数 + */ + public void publishEvent(String topic, Object event, long delaySeconds) { + try { + EventMessage eventMessage = + EventMessage.builder() + .eventType(topic) + .data(event) + .nodeId(instanceIdProvider.getInstanceId()) + .timestamp(System.currentTimeMillis()) + .createTime(LocalDateTime.now()) + .delaySeconds(delaySeconds) + .eventId(UUID.randomUUID().toString()) + .build(); + + String message = JSONUtil.toJsonStr(eventMessage); + + if (delaySeconds > 0) { + // 延迟事件:存储到Redis,由定时任务处理 + String delayKey = "delay:" + eventMessage.getEventId(); + redisTemplate.opsForValue().set(delayKey, message, delaySeconds, TimeUnit.SECONDS); + log.debug( + "[Redis事件发布] 延迟事件已存储: topic={}, delaySeconds={}, eventId={}", + topic, + delaySeconds, + eventMessage.getEventId()); + } else { + // 立即发布 + redisTemplate.convertAndSend(topic, message); + log.debug("[Redis事件发布] 事件已发布: topic={}, eventId={}", topic, eventMessage.getEventId()); + } + } catch (Exception e) { + log.error("[Redis事件发布] 发布事件失败: topic={}, event={}", topic, event, e); + } + } + + @Override + public void publishEvent(String topic, Object event, String excludeNodeId) { + try { + EventMessage eventMessage = + EventMessage.builder() + .eventType(topic) + .data(event) + .nodeId(instanceIdProvider.getInstanceId()) + .timestamp(System.currentTimeMillis()) + .createTime(LocalDateTime.now()) + .eventId(UUID.randomUUID().toString()) + .build(); + // 在消息中标记排除的节点 + eventMessage.setNodeId(excludeNodeId); + + String message = JSONUtil.toJsonStr(eventMessage); + redisTemplate.convertAndSend(topic, message); + + log.debug( + "[Redis事件发布] 发布事件(排除节点): topic={}, excludeNode={}, message={}", + topic, + excludeNodeId, + message); + } catch (Exception e) { + log.error( + "[Redis事件发布] 发布事件失败: topic={}, event={}, excludeNode={}", topic, event, excludeNodeId, e); + } + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/RedisEventSubscriber.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/RedisEventSubscriber.java new file mode 100644 index 0000000..91b577b --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/RedisEventSubscriber.java @@ -0,0 +1,162 @@ +package cn.universal.common.event; + +import cn.hutool.json.JSONUtil; +import cn.universal.common.config.InstanceIdProvider; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; +import org.springframework.stereotype.Component; + +/** Redis事件订阅者 处理集群间的消息通信 */ +@Slf4j +@Component +public class RedisEventSubscriber { + + @Resource private StringRedisTemplate stringRedisTemplate; + + @Resource private InstanceIdProvider instanceIdProvider; + + @Autowired private RedisMessageListenerContainer container; + + @Autowired private EventProcessorFactory eventProcessorFactory; + + // 定义一个通用的带异常处理的适配器创建方法 + private MessageListenerAdapter createAdapter(Object delegate, String methodName) { + MessageListenerAdapter adapter = + new MessageListenerAdapter(delegate) { + @Override + public void onMessage(Message message, byte[] pattern) { + try { + // 直接提取消息内容,只传递String参数 + String messageContent = new String(message.getBody()); + EventMessage eventMessage = JSONUtil.toBean(messageContent, EventMessage.class); + log.info( + "[Redis事件] 收到消息: channel={}, content={}", + new String(message.getChannel()), + messageContent); + try { + delegate + .getClass() + .getMethod(methodName, EventMessage.class) + .invoke(delegate, eventMessage); + } catch (Exception e) { + log.error("调用处理方法[{}]失败", methodName, e); + } + } catch (Exception e) { + log.error( + "适配器方法[{}]处理消息失败,channel={}, pattern={}", + methodName, + new String(message.getChannel()), + pattern != null ? new String(pattern) : "null", + e); + } + } + }; + // 显式设置字符串转换器,确保消息体被转为String + return adapter; + } + + @PostConstruct + public void setupListeners() { + log.info("当前实例类型: {}", this.getClass().getName()); + + // 电子围栏事件 + MessageListenerAdapter fenceAdapter = createAdapter(this, "handleFenceEvent"); + fenceAdapter.setDefaultListenerMethod("handleFenceEvent"); + container.addMessageListener(fenceAdapter, new ChannelTopic(EventTopics.FENCE_EVENT)); + + // TCP下行指令事件(使用Pattern订阅) + MessageListenerAdapter tcpDownAdapter = createAdapter(this, "handleTcpDownEvent"); + tcpDownAdapter.setDefaultListenerMethod("handleTcpDownEvent"); + container.addMessageListener(tcpDownAdapter, new PatternTopic(EventTopics.TCP_DOWN)); + + // 产品配置更新事件 + MessageListenerAdapter productConfigAdapter = createAdapter(this, "handleProductConfigUpdated"); + productConfigAdapter.setDefaultListenerMethod("handleProductConfigUpdated"); + container.addMessageListener( + productConfigAdapter, new ChannelTopic(EventTopics.PRODUCT_CONFIG_UPDATED)); + + // 协议配置更新事件 + MessageListenerAdapter protocolAdapter = createAdapter(this, "handleProtocolUpdated"); + protocolAdapter.setDefaultListenerMethod("handleProtocolUpdated"); + container.addMessageListener(protocolAdapter, new ChannelTopic(EventTopics.PROTOCOL_UPDATED)); + log.info("[Redis事件订阅] 已启动事件监听器"); + } + + /** 处理电子围栏事件 */ + public void handleFenceEvent(EventMessage message) { + if (isOwnMessage(message)) { + return; + } + log.info("[Redis事件] 收到电子围栏事件: {}", message); + + try { + // 使用事件处理器工厂处理电子围栏事件 + eventProcessorFactory.handleFenceEvent(message); + } catch (Exception e) { + log.error("[Redis事件] 处理电子围栏事件失败", e); + } + } + + /** 处理TCP下行指令事件 */ + public void handleTcpDownEvent(EventMessage message) { + if (isOwnMessage(message)) { + return; + } + log.info("[Redis事件] 收到TCP下行指令事件: {}", message); + + try { + // 使用事件处理器工厂处理TCP下行指令 + eventProcessorFactory.handleTcpDown(message); + } catch (Exception e) { + log.error("[Redis事件] 处理TCP下行指令事件失败", e); + } + } + + /** 处理产品配置更新事件 */ + public void handleProductConfigUpdated(EventMessage message) { + if (isOwnMessage(message)) { + return; + } + log.info("[Redis事件] 收到产品配置更新事件: {}", message); + + try { + // 使用事件处理器工厂处理产品配置更新 + eventProcessorFactory.handleProductConfigUpdated(message); + } catch (Exception e) { + log.error("[Redis事件] 处理产品配置更新失败", e); + } + } + + /** 处理协议配置更新事件 */ + public void handleProtocolUpdated(EventMessage message) { + if (isOwnMessage(message)) { + return; + } + log.info("[Redis事件] 收到产品配置更新事件: {}", message); + + try { + // 使用事件处理器工厂处理产品配置更新 + eventProcessorFactory.handleProtocolUpdated(message); + } catch (Exception e) { + log.error("[Redis事件] 处理产品配置更新失败", e); + } + } + + /** 判断是否是自己发送的消息 */ + private boolean isOwnMessage(EventMessage message) { + try { + String nodeId = message.getNodeId(); + return instanceIdProvider.getInstanceId().equals(nodeId); + } catch (Exception e) { + return false; + } + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/processer/EventPublisher.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/processer/EventPublisher.java new file mode 100644 index 0000000..c3f5ff4 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/processer/EventPublisher.java @@ -0,0 +1,37 @@ +package cn.universal.common.event.processer; + +/** + * 事件发布器接口 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +public interface EventPublisher { + + /** + * 发布事件 + * + * @param topic 事件主题 + * @param event 事件对象 + */ + void publishEvent(String topic, Object event); + + /** + * 发布延迟事件 + * + * @param topic 事件主题 + * @param event 事件对象 + * @param delaySeconds 延迟秒数 + */ + void publishEvent(String topic, Object event, long delaySeconds); + + /** + * 发布事件(带节点过滤) + * + * @param topic 事件主题 + * @param event 事件对象 + * @param excludeNodeId 排除的节点ID + */ + void publishEvent(String topic, Object event, String excludeNodeId); +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/processer/FenceEventProcessor.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/processer/FenceEventProcessor.java new file mode 100644 index 0000000..661a87e --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/processer/FenceEventProcessor.java @@ -0,0 +1,20 @@ +package cn.universal.common.event.processer; + +import cn.universal.common.event.EventMessage; + +/** + * 电子围栏事件处理器接口 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +public interface FenceEventProcessor { + + /** + * 处理电子围栏事件 + * + * @param message 事件消息字符串 + */ + void handleFenceEvent(EventMessage message); +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/processer/ProductConfigProcessor.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/processer/ProductConfigProcessor.java new file mode 100644 index 0000000..a422393 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/processer/ProductConfigProcessor.java @@ -0,0 +1,22 @@ +package cn.universal.common.event.processer; + +import cn.universal.common.event.EventMessage; + +/** + * 产品配置处理器接口 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +public interface ProductConfigProcessor { + + /** + * 处理产品配置更新事件 + * + * @param message 事件消息字符串 + */ + void handleProductConfigUpdated(EventMessage message); + + void handleProtocolUpdated(EventMessage message); +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/processer/TcpDownProcessor.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/processer/TcpDownProcessor.java new file mode 100644 index 0000000..b31c1da --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/event/processer/TcpDownProcessor.java @@ -0,0 +1,20 @@ +package cn.universal.common.event.processer; + +import cn.universal.common.event.EventMessage; + +/** + * TCP下行指令处理器接口 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +public interface TcpDownProcessor { + + /** + * 处理TCP下行指令事件 + * + * @param message 事件消息字符串 + */ + void handleTcpDownEvent(EventMessage message); +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/BaseException.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/BaseException.java new file mode 100644 index 0000000..404e24c --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/BaseException.java @@ -0,0 +1,75 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.exception; + +/** 基础异常 */ +public class BaseException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** 所属模块 */ + private String module; + + /** 错误码 */ + private String code; + + /** 错误码对应的参数 */ + private Object[] args; + + /** 错误消息 */ + private String defaultMessage; + + public BaseException(String module, String code, Object[] args, String defaultMessage) { + this.module = module; + this.code = code; + this.args = args; + this.defaultMessage = defaultMessage; + } + + public BaseException(String module, String code, Object[] args) { + this(module, code, args, null); + } + + public BaseException(String module, String defaultMessage) { + this(module, null, null, defaultMessage); + } + + public BaseException(String code, Object[] args) { + this(null, code, args, null); + } + + public BaseException(String defaultMessage) { + this(null, null, null, defaultMessage); + } + + @Override + public String getMessage() { + return defaultMessage; + } + + public String getModule() { + return module; + } + + public String getCode() { + return code; + } + + public Object[] getArgs() { + return args; + } + + public String getDefaultMessage() { + return defaultMessage; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/CodeKeyException.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/CodeKeyException.java new file mode 100644 index 0000000..f22194e --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/CodeKeyException.java @@ -0,0 +1,25 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.exception; + +/** + * 参数未能找到异常 @Author Aleo + * + * @since 2023/11/23 9:36 + */ +public class CodeKeyException extends RuntimeException { + + public CodeKeyException(String message) { + super(message); + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/CodecException.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/CodecException.java new file mode 100644 index 0000000..110516a --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/CodecException.java @@ -0,0 +1,60 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.exception; + +/** 编解码错误 */ +public class CodecException extends Exception { + + private static final long serialVersionUID = 1L; + + private String msg; + private int code = 758; + + public CodecException(String msg) { + super(msg); + this.msg = msg; + } + + public CodecException(String msg, Throwable e) { + super(msg, e); + this.msg = msg; + } + + public CodecException(String msg, int code) { + super(msg); + this.msg = msg; + this.code = code; + } + + public CodecException(String msg, int code, Throwable e) { + super(msg, e); + this.msg = msg; + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/FileException.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/FileException.java new file mode 100644 index 0000000..7ef7d85 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/FileException.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.exception; + +/** 文件信息异常类 @Author ruoyi */ +public class FileException extends BaseException { + + private static final long serialVersionUID = 1L; + + public FileException(String code, Object[] args) { + super("file", code, args, null); + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/FileNameLengthLimitExceededException.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/FileNameLengthLimitExceededException.java new file mode 100644 index 0000000..aede585 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/FileNameLengthLimitExceededException.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.exception; + +/** 文件名称超长限制异常类 @Author ruoyi */ +public class FileNameLengthLimitExceededException extends FileException { + + private static final long serialVersionUID = 1L; + + public FileNameLengthLimitExceededException(int defaultFileNameLength) { + super("upload.filename.exceed.length", new Object[] {defaultFileNameLength}); + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/FileSizeLimitExceededException.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/FileSizeLimitExceededException.java new file mode 100644 index 0000000..18c6c7b --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/FileSizeLimitExceededException.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.exception; + +/** 文件名大小限制异常类 @Author ruoyi */ +public class FileSizeLimitExceededException extends FileException { + + private static final long serialVersionUID = 1L; + + public FileSizeLimitExceededException(long defaultMaxSize) { + super("upload.exceed.maxSize", new Object[] {defaultMaxSize}); + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/InvalidExtensionException.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/InvalidExtensionException.java new file mode 100644 index 0000000..bd95ce1 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/InvalidExtensionException.java @@ -0,0 +1,92 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.exception; + +import java.util.Arrays; +import org.apache.commons.fileupload.FileUploadException; + +/** 文件上传 误异常类 @Author ruoyi */ +public class InvalidExtensionException extends FileUploadException { + + private static final long serialVersionUID = 1L; + + private String[] allowedExtension; + private String extension; + private String filename; + + public InvalidExtensionException(String[] allowedExtension, String extension, String filename) { + super( + "filename : [" + + filename + + "], extension : [" + + extension + + "], allowed extension : [" + + Arrays.toString(allowedExtension) + + "]"); + this.allowedExtension = allowedExtension; + this.extension = extension; + this.filename = filename; + } + + public String[] getAllowedExtension() { + return allowedExtension; + } + + public String getExtension() { + return extension; + } + + public String getFilename() { + return filename; + } + + public static class InvalidImageExtensionException extends InvalidExtensionException { + + private static final long serialVersionUID = 1L; + + public InvalidImageExtensionException( + String[] allowedExtension, String extension, String filename) { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidFlashExtensionException extends InvalidExtensionException { + + private static final long serialVersionUID = 1L; + + public InvalidFlashExtensionException( + String[] allowedExtension, String extension, String filename) { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidMediaExtensionException extends InvalidExtensionException { + + private static final long serialVersionUID = 1L; + + public InvalidMediaExtensionException( + String[] allowedExtension, String extension, String filename) { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidVideoExtensionException extends InvalidExtensionException { + + private static final long serialVersionUID = 1L; + + public InvalidVideoExtensionException( + String[] allowedExtension, String extension, String filename) { + super(allowedExtension, extension, filename); + } + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/IoTErrorCode.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/IoTErrorCode.java new file mode 100644 index 0000000..775de4e --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/IoTErrorCode.java @@ -0,0 +1,80 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.exception; + +/** IoT设备错误码 */ +public enum IoTErrorCode { + /** 设备 */ + DEV_NOT_FIND(700, "设备不存在"), + DEV_NOT_FOR_YOU(701, "设备没有操作权限!"), + DEV_ADD_DEVICE_ID_EXIST(702, "deivceId已存在"), + DEV_DEL_DEVICE_NO_ID_EXIST(702, "deivceId已存在"), + DEV_DEL_ERROR(703, "设备删除错误"), + DEV_UPDATE_ERROR(704, "设备修改错误"), + DEV_UPDATE_DEVICE_NO_ID_EXIST(705, "deviceId不存在"), + DEV_CONFIG_DEVICE_NO_ID_EXIST(706, "deviceId不存在"), + DEV_CONFIG_DEVICE_PARA_NULL(707, "参数为空"), + DEV_CONFIG_DEVICE_LEVEL_ERROR(708, "level不合法"), + DEV_CONFIG_DEVICE_STATE_ERROR(709, "设备未激活"), + DEV_CONFIG_DEVICE_PARA_ERROR(710, "参数校验失败"), + DEV_CONFIG_DEVICE_MASTERKEY_ERROR(711, "MasterKey不匹配"), + DEV_CONFIG_DEVICE_MESSAGE_ERROR(712, "指令应为偶数"), + DEV_CONFIG_DEVICE_NULL(713, "下发指令不能为空"), + DEV_CONFIG_PAYLOAD_NULL(714, "下发报文(payload)不能为空"), + DEV_CONFIG_ERROR(715, "设备配置错误"), + DEV_DOWN_ADD_ERROR(717, "指令 cmd 配置错误(指令 cmd 不能为空)"), + DEV_PARA_RANGE_ERROR(721, "指令下发内容与服务模型参数范围不匹配"), + DEV_DOWN_CMD_ERROR(715, "下发指令错误"), + DEV_CONFIG_DEVICE_PARA_FAIL(716, "参数解析失败"), + DEV_B_SERVICE_EXIST(720, "B端设备不能添加到C端"), + DEV_DOWN_NOT_SUPPORT(721, "设备不支持功能下发"), + DEV_DOWN_FAILURE(722, "设备下发失败"), + DEV_DOWN_NOT_ONLINE(723, "设备与平台连接已断开,指令已暂存3天(同指令以最新为准),重连后自动发送"), + DEV_SUBSCRIBE_REPEAT_ERROR(730, "超过最大允许订阅数"), + DEV_MASTER_NOT_EXIST_ERROR(731, "主设备不存在"), + DEV_ADD_DEVICE_ERROR(732, "设备序列号不合法"), + + DEV_METADATA_NOT_FIND(750, "物模型不存在"), + /** 编解码 */ + CODEC_ERROR(758, "编解码异常"), + DEV_METADATA_FUNCTION_NOT_FIND(751, "物模型定义功能不存在"), + + DEV_ADD_ERROR(799, "设备添加错误,请检查参数"), + + /** 参数校验 */ + DATA_CAN_NOT_NULL(600, "数据不能为空"), + + /** 应用 */ + APPLICATION_BIND_FAILURE(800, "绑定应用失败"), + + APPLICATION_NOT_FOR_YOU(801, "应用没有操作权限!"), + /** 产品 */ + PRODUCT_NOT_FOR_YOU(901, "您没有权限操作此产品!"), + PRODUCT_NOT_EXIST(902, "产品不存在"); + + private Integer code; + private String name; + + private IoTErrorCode(Integer code, String name) { + this.code = code; + this.name = name; + } + + public Integer getCode() { + return code; + } + + public String getName() { + return name; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/IoTException.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/IoTException.java new file mode 100644 index 0000000..cb39a1d --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/exception/IoTException.java @@ -0,0 +1,60 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.exception; + +/** IoT 异常,默认错误500 */ +public class IoTException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private String msg; + private int code = 500; + + public IoTException(String msg) { + super(msg); + this.msg = msg; + } + + public IoTException(String msg, Throwable e) { + super(msg, e); + this.msg = msg; + } + + public IoTException(String msg, int code) { + super(msg); + this.msg = msg; + this.code = code; + } + + public IoTException(String msg, int code, Throwable e) { + super(msg, e); + this.msg = msg; + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/package-info.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/package-info.java new file mode 100644 index 0000000..d970937 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/package-info.java @@ -0,0 +1,11 @@ +/* + * + * Copyright (c) 2025, IoT-universal. All Rights Reserved. + * + * @Description: 本文件由 AleoXin 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: AleoXin + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ package cn.universal.common; diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/poi/ExcelHandlerAdapter.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/poi/ExcelHandlerAdapter.java new file mode 100644 index 0000000..2d7a2be --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/poi/ExcelHandlerAdapter.java @@ -0,0 +1,26 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.poi; + +/** Excel数据格式处理适配器 @Author ruoyi */ +public interface ExcelHandlerAdapter { + + /** + * 格式化 + * + * @param value 单元格数据值 + * @param args excel注解args参数组 + * @return 处理后的值 + */ + Object format(Object value, String[] args); +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/poi/ExcelTemplate.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/poi/ExcelTemplate.java new file mode 100644 index 0000000..c500424 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/poi/ExcelTemplate.java @@ -0,0 +1,193 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.poi; + +import cn.universal.common.exception.BaseException; +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Excel相关处理工具类 */ +public class ExcelTemplate { + + private static final Logger log = LoggerFactory.getLogger(ExcelTemplate.class); + + /** 工作薄对象 */ + private Workbook wb; + + /** 工作表对象 */ + private Sheet sheet; + + private CellStyle headerStyle; + private CellStyle hiddenHeaderStyle; + + private Map headers; + private String excelName; + + private List demoRow; + + public ExcelTemplate(Map headers, List demoRow, String excelName) { + this.createWorkbook(); + this.headers = headers; + this.excelName = excelName; + this.demoRow = demoRow; + } + + /** 创建一个工作簿 */ + public void createWorkbook() { + this.wb = new SXSSFWorkbook(500); + } + + public void init() { + + try { + createSheet(); + + // 产生一行 + Row row = sheet.createRow(0); + + int column = 0; + row.setHeight((short) 0); + // 写入各个字段的列头名称 + for (String header : headers.keySet()) { + this.createNormalCell(header, row, column++); + } + + column = 0; + row = sheet.createRow(1); + // 写入各个字段的列头名称 + for (String header : headers.values()) { + this.createCell(header, row, column++); + } + + column = 0; + row = sheet.createRow(2); + // 写入各个字段的列头名称 + for (String rowValue : demoRow) { + this.createNormalCell(rowValue, row, column++); + } + + } catch (Exception e) { + log.error("导出Excel异常", e); + throw new BaseException("导出Excel失败,请联系网站管理员!"); + } + } + + public void setXSSFPrompt( + Sheet sheet, + String promptTitle, + String promptContent, + int firstRow, + int endRow, + int firstCol, + int endCol) { + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = helper.createCustomConstraint("DD1"); + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + DataValidation dataValidation = helper.createValidation(constraint, regions); + dataValidation.createPromptBox(promptTitle, promptContent); + dataValidation.setShowPromptBox(true); + sheet.addValidationData(dataValidation); + } + + /** 创建单元格 */ + public Cell createNormalCell(String header, Row row, int column) { + // 创建列 + Cell cell = row.createCell(column); + // 写入列信息 + cell.setCellValue(header); + cell.setCellStyle(hiddenHeaderStyle); + return cell; + } + + /** 创建单元格 */ + public Cell createCell(String header, Row row, int column) { + // 创建列 + Cell cell = row.createCell(column); + // 写入列信息 + cell.setCellValue(header); + cell.setCellStyle(headerStyle); + return cell; + } + + /** 创建工作表 */ + public void createSheet() { + this.sheet = wb.createSheet(); + createHeaderStyles(wb); + wb.setSheetName(0, "1"); + } + + private void createHeaderStyles(Workbook wb) { + headerStyle = wb.createCellStyle(); + headerStyle.setVerticalAlignment(VerticalAlignment.CENTER); + headerStyle.setBorderRight(BorderStyle.THIN); + headerStyle.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + headerStyle.setBorderLeft(BorderStyle.THIN); + headerStyle.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + headerStyle.setBorderTop(BorderStyle.THIN); + headerStyle.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + headerStyle.setBorderBottom(BorderStyle.THIN); + headerStyle.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + headerStyle.setAlignment(HorizontalAlignment.CENTER); + headerStyle.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex()); + headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + Font headerFont = wb.createFont(); + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short) 10); + headerFont.setBold(true); + headerFont.setColor(IndexedColors.WHITE.getIndex()); + headerStyle.setFont(headerFont); + + hiddenHeaderStyle = wb.createCellStyle(); + hiddenHeaderStyle.setVerticalAlignment(VerticalAlignment.CENTER); + hiddenHeaderStyle.setAlignment(HorizontalAlignment.CENTER); + headerFont = wb.createFont(); + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short) 10); + headerFont.setColor(IndexedColors.BLACK.getIndex()); + hiddenHeaderStyle.setFont(headerFont); + } + + public void exportExcel(HttpServletResponse response) { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(); + try { + wb.write(response.getOutputStream()); + } catch (Exception e) { + log.error("导出Excel异常", e); + } finally { + IOUtils.closeQuietly(wb); + } + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/AESOperator.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/AESOperator.java new file mode 100644 index 0000000..9ab9c03 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/AESOperator.java @@ -0,0 +1,86 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import java.util.Base64; +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import lombok.extern.slf4j.Slf4j; + +/** AES CBC加密 */ +@Slf4j +public class AESOperator { + /* + * 加密用的Key 可以用26个字母和数字组成 此处使用AES-128-CBC加密模式,key需要为16位。 + */ + // private String VECTOR = "!WFNZFU_{H%M(S|a"; + // private String KEY = "1234567812345678"; + // private String VECTOR = "1234567812345678"; + + private AESOperator() {} + + public static AESOperator getInstance() { + return Nested.instance; + } + + // 于内部静态类只会被加载一次,故该实现方式时线程安全的! + static class Nested { + + private static AESOperator instance = new AESOperator(); + } + + /** + * 加密 + * + * @param content 加密消息体 + * @param appId APPID + * @param appSecret APPSECRET + */ + public String encrypt(String content, String appId, String appSecret) { + try { + SecretKeySpec skeySpec = new SecretKeySpec(appSecret.getBytes("UTF-8"), "AES"); + IvParameterSpec iv = new IvParameterSpec(appId.getBytes()); // 使用CBC模式,需要一个向量iv,可增加加密算法的强度 + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv); + byte[] encrypted = cipher.doFinal(content.getBytes("UTF-8")); + return Base64.getEncoder().encodeToString(encrypted); // 此处使用BASE64做转码。 + } catch (Exception e) { + log.error("aes加密失败"); + } + return ""; + } + + /** + * 解密 + * + * @param content 解密消息体 + * @param appId APPID + * @param appSecret APPSECRET + */ + public String decrypt(String content, String appId, String appSecret) { + try { + SecretKeySpec skeySpec = new SecretKeySpec(appSecret.getBytes("UTF-8"), "AES"); + IvParameterSpec iv = new IvParameterSpec(appId.getBytes()); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv); + byte[] encrypted1 = Base64.getDecoder().decode(content); // 先用base64解密 + byte[] original = cipher.doFinal(encrypted1); + String originalString = new String(original, "UTF-8"); + return originalString; + } catch (Exception ex) { + log.error("aes加密失败", ex); + return null; + } + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/AESUtil.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/AESUtil.java new file mode 100644 index 0000000..84a1d68 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/AESUtil.java @@ -0,0 +1,131 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import cn.hutool.core.codec.Base64; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * @ClassName: AESUtil @Description 参数对称加密 @Author Administrator + * + * @since 2025/4/1 20:34 + */ +public class AESUtil { + + // 参数分别代表 算法名称/加密模式/数据填充方式 + private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding"; + private static final String AES = "AES"; + + /** + * AES加密 + * + * @param data 加密串 + * @param aesKey + * @return + * @throws Exception + */ + public static String encrypt(String data, String aesKey) { + try { + KeyGenerator kg = KeyGenerator.getInstance(AES); + kg.init(128); // 秘钥长度 + Cipher cipher = Cipher.getInstance(ALGORITHMSTR); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey.getBytes(), AES)); + byte[] b = cipher.doFinal(data.getBytes("UTF-8")); + // 采用base64算法进行转码,避免出现中文乱码 + return Base64.encode(b); + } catch (Exception e) { + throw new RuntimeException("加密失败"); + } + } + + /** + * 解密 + * + * @param data 解密的字符串 + * @param aesKey 解密的key值 + * @return + * @throws Exception + */ + public static String decrypt(String data, String aesKey) { + try { + KeyGenerator kgen = KeyGenerator.getInstance(AES); + kgen.init(128); + Cipher cipher = Cipher.getInstance(ALGORITHMSTR); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey.getBytes(), AES)); + // 采用base64算法进行转码,避免出现中文乱码 + byte[] encryptBytes = Base64.decode(data); + byte[] decryptBytes = cipher.doFinal(encryptBytes); + return new String(decryptBytes); + } catch (Exception e) { + throw new RuntimeException("业务参数解密失败"); + } + } + + /** 空格转+号 */ + public static String SpaceTurnPlus(String str) { + char[] chars = str.toCharArray(); + StringBuffer returnStr = new StringBuffer(); + for (char s : chars) { + if (s == ' ') { + returnStr = returnStr.append("+"); + } else { + returnStr = returnStr.append(s); + } + } + return returnStr.toString(); + } + + public static void main(String[] args) throws Exception {} + + // 加密 + public static String Encrypt(String content, String key) throws Exception { + byte[] raw = key.getBytes("utf-8"); + SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // "算法/模式/补码方式" + // 使用CBC模式,需要一个向量iv,可增加加密算法的强度 + IvParameterSpec ips = new IvParameterSpec(key.substring(16).getBytes()); + cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ips); + byte[] encrypted = cipher.doFinal(content.getBytes()); + return cn.hutool.core.codec.Base64.encode(encrypted); + } + + // 解密 + public static String Decrypt(String content, String key) { + try { + byte[] raw = key.getBytes("utf-8"); + SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + IvParameterSpec ips = new IvParameterSpec(key.substring(16).getBytes()); + cipher.init(Cipher.DECRYPT_MODE, skeySpec, ips); + // 采用base64算法进行转码,避免出现中文乱码 + byte[] encryptBytes = Base64.decode(content); + byte[] decryptBytes = cipher.doFinal(encryptBytes); + return new String(decryptBytes); + // byte[] encrypted1 = new BASE64Decoder().decodeBuffer(content); + // try { + // byte[] original = cipher.doFinal(encrypted1); + // String originalString = new String(original); + // return originalString; + // } catch (Exception e) { + // System.out.println(e.toString()); + // return null; + // } + } catch (Exception ex) { + System.out.println(ex.toString()); + return null; + } + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/AuthUtil.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/AuthUtil.java new file mode 100644 index 0000000..038b40f --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/AuthUtil.java @@ -0,0 +1,69 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import cn.hutool.crypto.digest.MD5; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Base64; +import java.util.Locale; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import lombok.extern.log4j.Log4j2; + +/** + * @Author 🐤 Aleo + * + * @email ✉ Aleo@outlook.com + * @since ⏰ 2020/1/7 + */ +@Log4j2 +public class AuthUtil { + + private static DateTimeFormatter z8Formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH).withZone(ZoneId.of("+8")); + + private static DateTimeFormatter secretDateFormatter = + DateTimeFormatter.ofPattern("E, dd MMM yyyy HH:mm:ss O", Locale.ENGLISH) + .withZone(ZoneId.of("+0")); + + public static String formatZ8DateTime(ZonedDateTime zonedDateTime) { + return z8Formatter.format(zonedDateTime); + } + + public static TemporalAccessor parseZ8DateTime(String z8DateTime) { + return z8Formatter.parse(z8DateTime); + } + + public static String hashingMsg(String z8DateTime, String appSecret, String msg) { + String utcTime = secretDateFormatter.format(z8Formatter.parse(z8DateTime)); + String secret = MD5.create().digestHex(utcTime + appSecret); + byte[] sha256 = hmacSha256(msg, secret); + return Base64.getEncoder().encodeToString(sha256); + } + + public static byte[] hmacSha256(String message, String secret) { + byte[] hash = new byte[0]; + try { + Mac sha256Hmac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), "HmacSHA256"); + sha256Hmac.init(secret_key); + hash = sha256Hmac.doFinal(message.getBytes()); + } catch (Exception e) { + log.info("encrypt hmac sha256 error:{}", e.getMessage()); + } + return hash; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/BleLockCryptoNet.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/BleLockCryptoNet.java new file mode 100644 index 0000000..4eb1f45 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/BleLockCryptoNet.java @@ -0,0 +1,796 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +public class BleLockCryptoNet { + + public static int ENCRPYT_TYPE_NO = 0; + public static int ENCRPYT_TYPE_DEFAULT = 1; + public static int ENCRPYT_TYPE_DYNAMIC = 2; + public static int ENCRPYT_TYPE_REPLAY = 3; + + private static final byte[] staticKey = { + 48, 101, 54, 100, 100, 54, 99, 98, 98, 57, 56, 57, 97, 98, 54, 48 + }; + private static final byte[] rePlayKey = { + 101, 99, 101, 53, 97, 100, 98, 53, 100, 100, 100, 49, 101, 53, 102, 57 + }; + + private int encryptType = ENCRPYT_TYPE_REPLAY + 1; + private String sn; + private String verify; + private int utc = 0; + + private final int SN_LEN = 20; + private final int ID_CODE_LEN = 8; + + private static int gindex = 1; + private static int gdayOld = 0; + + private static final int Nk = 4; + private static final int Nb = 4; + private static final int KEYLEN = 16; + private static final int Nr = 10; + private byte[] RoundKey = new byte[176]; + private byte[] Key = new byte[16]; + private byte[][] state = new byte[4][4]; + + private final byte[] tmpeKeyStaticCode = { + 48, 101, 54, 100, 100, 54, 99, 98, 98, 57, 56, 57, 97, 98, 54, 48 + }; + + private static short crc_ta[] = { + 0, 4129, 8258, 12387, 16516, 20645, 24774, 28903, -32504, -28375, -24246, -20117, -15988, + -11859, -7730, -3601, 4657, 528, 12915, 8786, 21173, 17044, 29431, 25302, -27847, -31976, + -19589, -23718, -11331, -15460, -3073, -7202, 9314, 13379, 1056, 5121, 25830, 29895, 17572, + 21637, -23190, -19125, -31448, -27383, -6674, -2609, -14932, -10867, 13907, 9842, 5649, 1584, + 30423, 26358, 22165, 18100, -18597, -22662, -26855, -30920, -2081, -6146, -10339, -14404, 18628, + 22757, 26758, 30887, 2112, 6241, 10242, 14371, -13876, -9747, -5746, -1617, -30392, -26263, + -22262, -18133, 23285, 19156, 31415, 27286, 6769, 2640, 14899, 10770, -9219, -13348, -1089, + -5218, -25735, -29864, -17605, -21734, 27814, 31879, 19684, 23749, 11298, 15363, 3168, 7233, + -4690, -625, -12820, -8755, -21206, -17141, -29336, -25271, 32407, 28342, 24277, 20212, 15891, + 11826, 7761, 3696, -97, -4162, -8227, -12292, -16613, -20678, -24743, -28808, -28280, -32343, + -20022, -24085, -12020, -16083, -3762, -7825, 4224, 161, 12482, 8419, 20484, 16421, 28742, + 24679, -31815, -27752, -23557, -19494, -15555, -11492, -7297, -3234, 689, 4752, 8947, 13010, + 16949, 21012, 25207, 29270, -18966, -23093, -27224, -31351, -2706, -6833, -10964, -15091, 13538, + 9411, 5280, 1153, 29798, 25671, 21540, 17413, -22565, -18438, -30823, -26696, -6305, -2178, + -14563, -10436, 9939, 14066, 1681, 5808, 26199, 30326, 17941, 22068, -9908, -13971, -1778, + -5841, -26168, -30231, -18038, -22101, 22596, 18533, 30726, 26663, 6336, 2273, 14466, 10403, + -13443, -9380, -5313, -1250, -29703, -25640, -21573, -17510, 19061, 23124, 27191, 31254, 2801, + 6864, 10931, 14994, -722, -4849, -8852, -12979, -16982, -21109, -25112, -29239, 31782, 27655, + 23652, 19525, 15522, 11395, 7392, 3265, -4321, -194, -12451, -8324, -20581, -16454, -28711, + -24584, 28183, 32310, 20053, 24180, 11923, 16050, 3793, 7920, + }; + + private static BleLockCryptoNet bleLockCryptoNet = new BleLockCryptoNet(); + + public static BleLockCryptoNet getInstance() { + bleLockCryptoNet.setSn(null); + bleLockCryptoNet.setUtc(0); + bleLockCryptoNet.setVerify(null); + return bleLockCryptoNet; + } + + private byte[] aesEncrypt(byte[] data, byte[] key) { + return aes128EcbEncrypt(data, key); + } + + private byte[] aesDecrypt(byte[] data, byte[] key) { + return aes128EcbDecrypt(data, key); + } + + private static final byte sbox[] = { + 99, 124, 119, 123, -14, 107, 111, -59, 48, 1, 103, 43, -2, -41, -85, 118, + -54, -126, -55, 125, -6, 89, 71, -16, -83, -44, -94, -81, -100, -92, 114, -64, + -73, -3, -109, 38, 54, 63, -9, -52, 52, -91, -27, -15, 113, -40, 49, 21, + 4, -57, 35, -61, 24, -106, 5, -102, 7, 18, -128, -30, -21, 39, -78, 117, + 9, -125, 44, 26, 27, 110, 90, -96, 82, 59, -42, -77, 41, -29, 47, -124, + 83, -47, 0, -19, 32, -4, -79, 91, 106, -53, -66, 57, 74, 76, 88, -49, + -48, -17, -86, -5, 67, 77, 51, -123, 69, -7, 2, 127, 80, 60, -97, -88, + 81, -93, 64, -113, -110, -99, 56, -11, -68, -74, -38, 33, 16, -1, -13, -46, + -51, 12, 19, -20, 95, -105, 68, 23, -60, -89, 126, 61, 100, 93, 25, 115, + 96, -127, 79, -36, 34, 42, -112, -120, 70, -18, -72, 20, -34, 94, 11, -37, + -32, 50, 58, 10, 73, 6, 36, 92, -62, -45, -84, 98, -111, -107, -28, 121, + -25, -56, 55, 109, -115, -43, 78, -87, 108, 86, -12, -22, 101, 122, -82, 8, + -70, 120, 37, 46, 28, -90, -76, -58, -24, -35, 116, 31, 75, -67, -117, -118, + 112, 62, -75, 102, 72, 3, -10, 14, 97, 53, 87, -71, -122, -63, 29, -98, + -31, -8, -104, 17, 105, -39, -114, -108, -101, 30, -121, -23, -50, 85, 40, -33, + -116, -95, -119, 13, -65, -26, 66, 104, 65, -103, 45, 15, -80, 84, -69, 22 + }; + + private static final byte rsbox[] = { + 82, 9, 106, -43, 48, 54, -91, 56, -65, 64, -93, -98, -127, -13, -41, -5, + 124, -29, 57, -126, -101, 47, -1, -121, 52, -114, 67, 68, -60, -34, -23, -53, + 84, 123, -108, 50, -90, -62, 35, 61, -18, 76, -107, 11, 66, -6, -61, 78, + 8, 46, -95, 102, 40, -39, 36, -78, 118, 91, -94, 73, 109, -117, -47, 37, + 114, -8, -10, 100, -122, 104, -104, 22, -44, -92, 92, -52, 93, 101, -74, -110, + 108, 112, 72, 80, -3, -19, -71, -38, 94, 21, 70, 87, -89, -115, -99, -124, + -112, -40, -85, 0, -116, -68, -45, 10, -9, -28, 88, 5, -72, -77, 69, 6, + -48, 44, 30, -113, -54, 63, 15, 2, -63, -81, -67, 3, 1, 19, -118, 107, + 58, -111, 17, 65, 79, 103, -36, -22, -105, -14, -49, -50, -16, -76, -26, 115, + -106, -84, 116, 34, -25, -83, 53, -123, -30, -7, 55, -24, 28, 117, -33, 110, + 71, -15, 26, 113, 29, 41, -59, -119, 111, -73, 98, 14, -86, 24, -66, 27, + -4, 86, 62, 75, -58, -46, 121, 32, -102, -37, -64, -2, 120, -51, 90, -12, + 31, -35, -88, 51, -120, 7, -57, 49, -79, 18, 16, 89, 39, -128, -20, 95, + 96, 81, 127, -87, 25, -75, 74, 13, 45, -27, 122, -97, -109, -55, -100, -17, + -96, -32, 59, 77, -82, 42, -11, -80, -56, -21, -69, 60, -125, 83, -103, 97, + 23, 43, 4, 126, -70, 119, -42, 38, -31, 105, 20, 99, 85, 33, 12, 125 + }; + + private static final byte Rcon[] = { + -115, 1, 2, 4, 8, 16, 32, 64, -128, 27, 54, 108, -40, -85, 77, -102, + 47, 94, -68, 99, -58, -105, 53, 106, -44, -77, 125, -6, -17, -59, -111, 57, + 114, -28, -45, -67, 97, -62, -97, 37, 74, -108, 51, 102, -52, -125, 29, 58, + 116, -24, -53, -115, 1, 2, 4, 8, 16, 32, 64, -128, 27, 54, 108, -40, + -85, 77, -102, 47, 94, -68, 99, -58, -105, 53, 106, -44, -77, 125, -6, -17, + -59, -111, 57, 114, -28, -45, -67, 97, -62, -97, 37, 74, -108, 51, 102, -52, + -125, 29, 58, 116, -24, -53, -115, 1, 2, 4, 8, 16, 32, 64, -128, 27, + 54, 108, -40, -85, 77, -102, 47, 94, -68, 99, -58, -105, 53, 106, -44, -77, + 125, -6, -17, -59, -111, 57, 114, -28, -45, -67, 97, -62, -97, 37, 74, -108, + 51, 102, -52, -125, 29, 58, 116, -24, -53, -115, 1, 2, 4, 8, 16, 32, + 64, -128, 27, 54, 108, -40, -85, 77, -102, 47, 94, -68, 99, -58, -105, 53, + 106, -44, -77, 125, -6, -17, -59, -111, 57, 114, -28, -45, -67, 97, -62, -97, + 37, 74, -108, 51, 102, -52, -125, 29, 58, 116, -24, -53, -115, 1, 2, 4, + 8, 16, 32, 64, -128, 27, 54, 108, -40, -85, 77, -102, 47, 94, -68, 99, + -58, -105, 53, 106, -44, -77, 125, -6, -17, -59, -111, 57, 114, -28, -45, -67, + 97, -62, -97, 37, 74, -108, 51, 102, -52, -125, 29, 58, 116, -24, -53 + }; + + private byte getSBoxValue(char num) { + return this.sbox[num]; + } + + private byte getSBoxInvert(char num) { + return this.rsbox[num]; + } + + private void KeyExpansion() { + int i, j, k; + char[] tempa = new char[4]; + + for (i = 0; i < Nk; ++i) { + RoundKey[(i * 4) + 0] = Key[(i * 4) + 0]; + RoundKey[(i * 4) + 1] = Key[(i * 4) + 1]; + RoundKey[(i * 4) + 2] = Key[(i * 4) + 2]; + RoundKey[(i * 4) + 3] = Key[(i * 4) + 3]; + } + + for (; (i < (Nb * (Nr + 1))); ++i) { + for (j = 0; j < 4; ++j) { + tempa[j] = (char) (RoundKey[(i - 1) * 4 + j] & 0x00ff); + } + if (i % Nk == 0) { + + { + k = tempa[0]; + tempa[0] = tempa[1]; + tempa[1] = tempa[2]; + tempa[2] = tempa[3]; + tempa[3] = (char) (k & 0x00ff); + } + + { + tempa[0] = (char) getSBoxValue(tempa[0]); + tempa[1] = (char) getSBoxValue(tempa[1]); + tempa[2] = (char) getSBoxValue(tempa[2]); + tempa[3] = (char) getSBoxValue(tempa[3]); + } + + tempa[0] = (char) ((tempa[0] ^ Rcon[i / Nk]) & 0x00ff); + } else if (Nk > 6 && i % Nk == 4) { + { + tempa[0] = (char) (getSBoxValue(tempa[0]) & 0x00ff); + tempa[1] = (char) (getSBoxValue(tempa[1]) & 0x00ff); + tempa[2] = (char) (getSBoxValue(tempa[2]) & 0x00ff); + tempa[3] = (char) (getSBoxValue(tempa[3]) & 0x00ff); + } + } + RoundKey[i * 4 + 0] = (byte) (RoundKey[(i - Nk) * 4 + 0] ^ tempa[0]); + RoundKey[i * 4 + 1] = (byte) (RoundKey[(i - Nk) * 4 + 1] ^ tempa[1]); + RoundKey[i * 4 + 2] = (byte) (RoundKey[(i - Nk) * 4 + 2] ^ tempa[2]); + RoundKey[i * 4 + 3] = (byte) (RoundKey[(i - Nk) * 4 + 3] ^ tempa[3]); + } + } + + private void AddRoundKey(char round) { + char i, j; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + state[i][j] ^= RoundKey[round * Nb * 4 + i * Nb + j]; + } + } + } + + private void SubBytes() { + char i, j; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + state[j][i] = getSBoxValue((char) (state[j][i] & 0x00ff)); + } + } + } + + private void ShiftRows() { + byte temp; + + temp = state[0][1]; + state[0][1] = state[1][1]; + state[1][1] = state[2][1]; + state[2][1] = state[3][1]; + state[3][1] = temp; + + temp = state[0][2]; + state[0][2] = state[2][2]; + state[2][2] = temp; + + temp = state[1][2]; + state[1][2] = state[3][2]; + state[3][2] = temp; + + temp = state[0][3]; + state[0][3] = state[3][3]; + state[3][3] = state[2][3]; + state[2][3] = state[1][3]; + state[1][3] = temp; + } + + private byte xtime(byte x) { + return (byte) ((x << 1) ^ (((x >> 7) & 1) * 0x1b)); + } + + private void MixColumns() { + byte i; + byte Tmp, Tm, t; + for (i = 0; i < 4; ++i) { + t = state[i][0]; + Tmp = (byte) (state[i][0] ^ state[i][1] ^ state[i][2] ^ state[i][3]); + Tm = (byte) (state[i][0] ^ state[i][1]); + Tm = xtime(Tm); + state[i][0] ^= Tm ^ Tmp; + Tm = (byte) (state[i][1] ^ state[i][2]); + Tm = xtime(Tm); + state[i][1] ^= Tm ^ Tmp; + Tm = (byte) (state[i][2] ^ state[i][3]); + Tm = xtime(Tm); + state[i][2] ^= Tm ^ Tmp; + Tm = (byte) (state[i][3] ^ t); + Tm = xtime(Tm); + state[i][3] ^= Tm ^ Tmp; + } + } + + private byte Multiply(byte x, byte y) { + return (byte) + (((y & 1) * x) + ^ ((y >> 1 & 1) * xtime(x)) + ^ ((y >> 2 & 1) * xtime(xtime(x))) + ^ ((y >> 3 & 1) * xtime(xtime(xtime(x)))) + ^ ((y >> 4 & 1) * xtime(xtime(xtime(xtime(x)))))); + } + + private void InvMixColumns() { + byte i; + byte a, b, c, d; + for (i = 0; i < 4; ++i) { + a = state[i][0]; + b = state[i][1]; + c = state[i][2]; + d = state[i][3]; + + state[i][0] = + (byte) + (Multiply(a, (byte) 0x0e) + ^ Multiply(b, (byte) 0x0b) + ^ Multiply(c, (byte) 0x0d) + ^ Multiply(d, (byte) 0x09)); + state[i][1] = + (byte) + (Multiply(a, (byte) 0x09) + ^ Multiply(b, (byte) 0x0e) + ^ Multiply(c, (byte) 0x0b) + ^ Multiply(d, (byte) 0x0d)); + state[i][2] = + (byte) + (Multiply(a, (byte) 0x0d) + ^ Multiply(b, (byte) 0x09) + ^ Multiply(c, (byte) 0x0e) + ^ Multiply(d, (byte) 0x0b)); + state[i][3] = + (byte) + (Multiply(a, (byte) 0x0b) + ^ Multiply(b, (byte) 0x0d) + ^ Multiply(c, (byte) 0x09) + ^ Multiply(d, (byte) 0x0e)); + } + } + + private void InvSubBytes() { + char i, j; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + state[j][i] = getSBoxInvert((char) (state[j][i] & 0x00ff)); + } + } + } + + private void InvShiftRows() { + byte temp; + + temp = state[3][1]; + state[3][1] = state[2][1]; + state[2][1] = state[1][1]; + state[1][1] = state[0][1]; + state[0][1] = temp; + + temp = state[0][2]; + state[0][2] = state[2][2]; + state[2][2] = temp; + + temp = state[1][2]; + state[1][2] = state[3][2]; + state[3][2] = temp; + + temp = state[0][3]; + state[0][3] = state[1][3]; + state[1][3] = state[2][3]; + state[2][3] = state[3][3]; + state[3][3] = temp; + } + + private void Cipher() { + char round = 0; + + AddRoundKey((char) 0); + + for (round = 1; round < Nr; ++round) { + SubBytes(); + ShiftRows(); + MixColumns(); + AddRoundKey(round); + } + + SubBytes(); + ShiftRows(); + AddRoundKey((char) Nr); + } + + private void InvCipher() { + char round = 0; + + AddRoundKey((char) Nr); + + for (round = Nr - 1; round > 0; round--) { + InvShiftRows(); + InvSubBytes(); + AddRoundKey(round); + InvMixColumns(); + } + + InvShiftRows(); + InvSubBytes(); + AddRoundKey((char) 0); + } + + private void BlockCopy(byte[] output, byte[] input) { + char i; + for (i = 0; i < KEYLEN; ++i) { + output[i] = input[i]; + } + } + + private byte[] aes128EcbEncrypt(byte[] input, byte[] key) { + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + state[i][j] = input[i * 4 + j]; + } + } + byte[] encryptData = new byte[16]; + + BlockCopy(this.Key, key); + KeyExpansion(); + Cipher(); + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + encryptData[i * 4 + j] = state[i][j]; + } + } + + return encryptData; + } + + private byte[] aes128EcbDecrypt(byte[] input, byte[] key) { + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + state[i][j] = input[i * 4 + j]; + } + } + BlockCopy(Key, key); + KeyExpansion(); + InvCipher(); + byte[] decryptData = new byte[16]; + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + decryptData[i * 4 + j] = state[i][j]; + } + } + + return decryptData; + } + + private byte[] createReplay(int utc) { + int i; + byte[] ret = new byte[4]; + for (i = 0; i < 4; i++) { + ret[i] = (byte) (rePlayKey[i] ^ (byte) (utc >> (i * 8))); + } + return ret; + } + + private int getUtcFromReplay(byte[] rePlayData) { + + return (rePlayData[0] ^ rePlayKey[0]) & 0x000000ff + | ((rePlayData[1] ^ rePlayKey[1]) << 8) & 0x0000ff00 + | ((rePlayData[2] ^ rePlayKey[2]) << 16) & 0x00ff0000 + | ((rePlayData[3] ^ rePlayKey[3]) << 24) & 0xff000000; + } + + private byte[] createDynamicKey(int utc, String sn, String idCode) { + int i; + byte[] snByte = sn.getBytes(); + byte[] idCodeByte = idCode.getBytes(); + byte[] key = new byte[16]; + for (i = 0; i < this.ID_CODE_LEN; i++) { + key[i] = (byte) (snByte[i + 8] ^ idCodeByte[i]); + } + for (; i < this.ID_CODE_LEN + 4; i++) { + key[i] = snByte[i + 8]; + } + for (; i < 16; i++) { + key[i] = (byte) ((utc >> (((i - 12) * 8)))); + } + return key; + } + + private byte[] lockEncrypt(byte[] data) throws Exception { + + byte[] headEncrypt = new byte[5]; + int headEncryptLen; + byte[] encryptKey; + + headEncrypt[0] = (byte) data.length; + if (encryptType == ENCRPYT_TYPE_NO) { + return data; + } else if (encryptType == ENCRPYT_TYPE_DEFAULT) { + encryptKey = new byte[16]; + System.arraycopy(staticKey, 0, encryptKey, 0, 16); + headEncryptLen = 1; + } else if (encryptType == ENCRPYT_TYPE_DYNAMIC) { + if (sn.length() != SN_LEN) { + throw new Exception("error sn length"); + } + if (verify.length() != ID_CODE_LEN) { + throw new Exception("error id code length"); + } + System.arraycopy(createReplay(utc), 0, headEncrypt, 1, 4); + encryptKey = createDynamicKey(utc, sn, verify); + headEncryptLen = 5; + + } else if (encryptType == ENCRPYT_TYPE_REPLAY) { + byte[] ret = new byte[5 + data.length]; + ret[0] = headEncrypt[0]; + System.arraycopy(createReplay(utc), 0, ret, 1, 4); + System.arraycopy(data, 0, ret, 5, data.length); + return ret; + + } else { + throw new Exception("error encrypt type"); + } + byte[] encryptData = new byte[(data.length + 15) / 16 * 16]; + byte[] ret = new byte[headEncryptLen + encryptData.length]; + byte[] temp = new byte[16]; + System.arraycopy(data, 0, encryptData, 0, data.length); + System.arraycopy(headEncrypt, 0, ret, 0, headEncryptLen); + for (int i = 0; i < encryptData.length; ) { + System.arraycopy(encryptData, i, temp, 0, 16); + System.arraycopy(aesEncrypt(temp, encryptKey), 0, ret, i + headEncryptLen, 16); + i += 16; + } + return ret; + } + + private byte[] lockDecrypt(byte[] data) throws Exception { + int headLen; + int decryptLen; + byte[] decryptKey; + + decryptLen = data[0]; + if (encryptType == ENCRPYT_TYPE_NO) { + return data; + } else if (encryptType == ENCRPYT_TYPE_DEFAULT) { + if (1 != data.length % 16) { + throw new Exception("error decrypt length"); + } + decryptLen = data.length - 1; + + decryptKey = new byte[16]; + System.arraycopy(staticKey, 0, decryptKey, 0, 16); + headLen = 1; + + } else if (encryptType == ENCRPYT_TYPE_DYNAMIC) { + if (5 != data.length % 16) { + throw new Exception("error decrypt length"); + } + decryptLen = data.length - 5; + + if (verify.length() != ID_CODE_LEN) { + throw new Exception("error id code length"); + } + if (sn.length() != SN_LEN) { + throw new Exception("error sn length"); + } + byte[] rePlayData = new byte[4]; + System.arraycopy(data, 1, rePlayData, 0, 4); + int encryptUtc = getUtcFromReplay(rePlayData); + if (Math.abs(encryptUtc - utc) > 10) { + throw new Exception("error utc"); + } + decryptKey = createDynamicKey(encryptUtc, sn, verify); + headLen = 5; + + } else if (encryptType == ENCRPYT_TYPE_REPLAY) { + if (5 + data[0] != data.length) { + throw new Exception("error decrypt length"); + } + + byte[] rePlayData = new byte[4]; + System.arraycopy(data, 1, rePlayData, 0, 4); + int encryptUtc = getUtcFromReplay(rePlayData); + if (Math.abs(encryptUtc - utc) > 10) { + throw new Exception("error utc"); + } + byte[] ret = new byte[data.length - 5]; + System.arraycopy(data, 5, ret, 0, data.length - 5); + return ret; + + } else { + throw new Exception("error encrypt type"); + } + byte[] decryptData = new byte[decryptLen]; + byte[] temp = new byte[16]; + + for (int i = 0; i < decryptLen; ) { + System.arraycopy(data, i + headLen, temp, 0, 16); + System.arraycopy(aesDecrypt(temp, decryptKey), 0, decryptData, i, 16); + i += 16; + } + return decryptData; + } + + private byte[] lockRfidEncrypt(byte[] id) throws Exception { + if (id.length > 16) { + throw new Exception("error id length"); + } + byte[] tempData = new byte[16]; + System.arraycopy(id, 0, tempData, 0, id.length); + byte[] encryptData = aesEncrypt(tempData, staticKey); + byte[] ret = new byte[6]; + System.arraycopy(encryptData, 10, ret, 0, 6); + return ret; + } + + private int lockRCR16(byte[] data) { + short u16crc; + char u8da; + int temp; + u16crc = 0; + for (int i = 0; i < data.length; i++) { + temp = u16crc & 0x0000ffff; + u8da = (char) (temp / 256); + u16crc <<= 8; + u16crc ^= crc_ta[(u8da ^ data[i]) & 0x000000ff]; + } + + temp = (~u16crc) & 0x0000ffff; + return temp; + } + + private long lockRand(int seed) { + int next = seed; + int result; + + next *= 1103515245; + next += 12345; + result = (next / 65536) % 2048; + + next *= 1103515245; + next += 12345; + result <<= 10; + result ^= (next / 65536) % 1024; + + next *= 1103515245; + next += 12345; + result <<= 10; + long temp = next & 0x00000000ffffffffL; + temp = ((temp / 65536) % 1024) ^ result; + return temp; + } + + private String lockFillPasswdPrefix(byte[] cryptedStr, int iCryLen) { + int j = 1; + int seed = 0; + byte[] passwdTbl = "0123456789".getBytes(); + + for (int i = 0; i < iCryLen; i++) { + if (100000 == j) { + j = 1; + } + seed += (cryptedStr[i]) * j; + j = j * 10; + } + long randRet = lockRand(seed); + + int passwdTblLen = passwdTbl.length; + byte[] ret = new byte[2]; + ret[0] = passwdTbl[(int) (randRet % passwdTblLen)]; + randRet /= passwdTblLen; + ret[1] = passwdTbl[(int) (randRet % passwdTblLen)]; + return String.format("%c%c", ret[0], ret[1]); + } + + private String lockFillPasswdPostfix(byte[] cryptedStr, int iCryLen) { + int crc; + crc = lockRCR16(cryptedStr); + + String str = String.format("%04x", crc); + byte[] strByte = str.getBytes(); + for (int i = 0; i < 4; i++) { + if (strByte[i] >= 'a' && strByte[i] <= 'z') { + strByte[i] = (byte) (strByte[i] - 'a' + '0'); + } + } + + return String.format("%c%c%c%c", strByte[0], strByte[1], strByte[2], strByte[3]); + } + + private String lockCreateOneTimePasswdForSlock(int utc, String sn) throws Exception { + long day = (utc / (24 * 60 * 60)); + long timeCal; + + if (gdayOld != day) { + gindex = 1; + } + timeCal = day * 24 * 60 * 60 + gindex * 60 * 60 + gindex * 29 + gindex; + String str = String.format("%08x", timeCal) + sn; + String pre = lockFillPasswdPrefix(str.getBytes(), 28); + String pos = lockFillPasswdPostfix(str.getBytes(), 28); + return pre + pos; + } + + private String lockCreateOneTimePasswd(int index, int utc, String sn) throws Exception { + long day = (utc / (24 * 60 * 60)); + int tempIndex = index + 1; + long timeCal = day * 24 * 60 * 60 + tempIndex * 60 * 60 + tempIndex * 29 + tempIndex; + String str = String.format("%08x", timeCal) + sn; + String pre = lockFillPasswdPrefix(str.getBytes(), 28); + String pos = lockFillPasswdPostfix(str.getBytes(), 28); + return pre + pos; + } + + private String lockCreateTempKey(int effectiveTime, String sn, int utc, String verify) + throws Exception { + + byte[] aesKey = new byte[16]; + byte[] snByte = sn.getBytes(); + byte[] idCodeByte = verify.getBytes(); + String retString; + int utcHours = (utc / 3600); /* utc小时数 */ + if (effectiveTime > 24 || effectiveTime < 1) { + throw new Exception("error sn effectiveTime"); + } + byte utcLastSixBit = (byte) (utcHours & 0x0000003f); + System.out.print("utcLastSixBit=" + utcLastSixBit); + System.out.print(" \n"); + + // effectiveTime +=50; /* 去掉该条件,java无无符号数据类型,数据太大可能会导致数据溢出出错 */ + int i; + for (i = 0; i < 8; i++) { + aesKey[i] = (byte) (snByte[i + 8] ^ idCodeByte[i]); + } + + for (; i < 8 + 4; i++) { + aesKey[i] = (byte) (snByte[i + 8] ^ (utcHours >> (24 - ((i - 8) * 8)))); + } + + for (; i < 8 + 4 + 4; i++) { + aesKey[i] = (byte) (idCodeByte[i - 12] ^ (utcHours >> (24 - ((i - 12) * 8)))); + } + + byte[] staticCode = new byte[16]; + System.arraycopy(tmpeKeyStaticCode, 0, staticCode, 0, 16); + staticCode[14] += utcLastSixBit; + staticCode[15] += (byte) effectiveTime; + System.out.format("staticCode[15] = %x", staticCode[15]); + System.out.print(" \n"); + + byte[] encrypt = aes128EcbEncrypt(staticCode, aesKey); + for (int ii = 0; ii < encrypt.length; ii++) { + System.out.format("%x", encrypt[ii]); + System.out.print(" "); + } + System.out.print(" \n"); + + long lastEncrypt_12 = (encrypt[12] & 0x00000000000000ff); + long lastEncrypt_13 = (encrypt[13] & 0x00000000000000ff); + long lastEncrypt_14 = (encrypt[14] & 0x00000000000000ff); + long lastEncrypt_15 = (encrypt[15] & 0x00000000000000ff); + long lastEncrypt = + lastEncrypt_12 << 24 + | lastEncrypt_13 << 16 + | lastEncrypt_14 << 8 + | lastEncrypt_15; // 先单独与并用long变量保存,否则会把数值变成负数,然后移位进行异或; + // long lastEncrypt = ( encrypt[12] & 0x00000000000000ff) <<24 | ( encrypt[13] & + // 0x00000000000000ff) <<16 | ( + // encrypt[14] & 0x00000000000000ff) <<8 | ( encrypt[15] & 0x00000000000000ff); + + System.out.format("lastEncrypt= %x", lastEncrypt); + System.out.print(" \n"); + + lastEncrypt = lastEncrypt % 100000000; /* 保证转成整数字符串最多只有8位*/ + + retString = String.format("%02d%02d%08d", utcLastSixBit, effectiveTime, lastEncrypt); + return retString; + } + + public byte[] encrypt(byte[] data) throws Exception { + return lockEncrypt(data); + } + + public byte[] decrypt(byte[] data) throws Exception { + return lockDecrypt(data); + } + + public byte[] rfidEncrypt(byte[] id) throws Exception { + return lockRfidEncrypt(id); + } + + public String createOneTimePasswdForSlock(int utc, String sn) throws Exception { + return lockCreateOneTimePasswdForSlock(utc, sn); + } + + public String createOneTimePasswd(int index, int utc, String sn) throws Exception { + return lockCreateOneTimePasswd(index, utc, sn); + } + + public String createTempKey(int effectiveTime, String sn, int utc, String verify) + throws Exception { + return lockCreateTempKey(effectiveTime, sn, utc, verify); + } + + public void setSn(String sn) { + this.sn = sn; + } + + public void setVerify(String verify) { + this.verify = verify; + } + + public void setEncryptType(int encryptType) { + this.encryptType = encryptType; + } + + public void setUtc(int utc) { + this.utc = utc; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/CertInfoUtil.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/CertInfoUtil.java new file mode 100644 index 0000000..1d0c27b --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/CertInfoUtil.java @@ -0,0 +1,111 @@ +package cn.universal.common.utils; + +import cn.hutool.json.JSONObject; +import java.io.ByteArrayInputStream; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CertInfoUtil { + + public static JSONObject extractCertInfo(String certContent) { + List certs = parseCertificateChain(certContent); + X509Certificate cert = certs.get(0); + JSONObject info = new JSONObject(); + info.set("subject", cert.getSubjectX500Principal().getName()); + info.set("issuer", cert.getIssuerX500Principal().getName()); + info.set("notBefore", cert.getNotBefore()); + info.set("notAfter", cert.getNotAfter()); + info.set("serialNumber", cert.getSerialNumber().toString(16)); + info.set("signatureAlgorithm", cert.getSigAlgName()); + info.set("version", cert.getVersion()); + info.set("publicKeyAlgorithm", cert.getPublicKey().getAlgorithm()); + info.set("chainLength", certs.size()); + // 解析SAN + try { + Collection> sans = cert.getSubjectAlternativeNames(); + if (sans != null) { + List sanList = new ArrayList<>(); + for (List san : sans) { + sanList.add(san.get(1).toString()); + } + info.set("subjectAlternativeNames", sanList); + } + } catch (Exception ignore) { + } + // KeyUsage + boolean[] keyUsage = cert.getKeyUsage(); + if (keyUsage != null) { + info.set("keyUsage", Arrays.toString(keyUsage)); + } + // ExtendedKeyUsage + try { + List extKeyUsage = cert.getExtendedKeyUsage(); + if (extKeyUsage != null) { + info.set("extendedKeyUsage", extKeyUsage); + } + } catch (Exception ignore) { + } + return info; + } + + /** 自动提取证书支持的所有域名(SAN、CN、通配符) */ + public static List parseDomainsFromCert(String certContent) { + List domains = new ArrayList<>(); + List certs = parseCertificateChain(certContent); + if (certs.isEmpty()) { + return domains; + } + X509Certificate cert = certs.get(0); + // 1. 解析SAN + try { + Collection> sans = cert.getSubjectAlternativeNames(); + if (sans != null) { + for (List san : sans) { + if (san.size() > 1 && san.get(1) != null) { + domains.add(san.get(1).toString()); + } + } + } + } catch (Exception ignore) { + } + // 2. 解析CN + try { + String subject = cert.getSubjectX500Principal().getName(); + for (String part : subject.split(",")) { + if (part.trim().startsWith("CN=")) { + String cn = part.trim().substring(3); + if (!domains.contains(cn)) { + domains.add(cn); + } + } + } + } catch (Exception ignore) { + } + return domains; + } + + public static List parseCertificateChain(String pem) { + List certs = new ArrayList<>(); + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + try (ByteArrayInputStream bis = new ByteArrayInputStream(pem.getBytes())) { + while (bis.available() > 0) { + Certificate cert = cf.generateCertificate(bis); + if (cert instanceof X509Certificate) { + certs.add((X509Certificate) cert); + } + } + } + } catch (Exception e) { + log.warn("证书校验失败"); + } + return certs; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/DateUtils.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/DateUtils.java new file mode 100644 index 0000000..3396a8f --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/DateUtils.java @@ -0,0 +1,134 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import java.lang.management.ManagementFactory; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import org.apache.commons.lang3.time.DateFormatUtils; + +/** 时间工具类 @Author ruoyi */ +public class DateUtils extends org.apache.commons.lang3.time.DateUtils { + + public static String YYYY = "yyyy"; + + public static String YYYY_MM = "yyyy-MM"; + + public static String YYYY_MM_DD = "yyyy-MM-dd"; + + public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss"; + + public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; + + private static String[] parsePatterns = { + "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", + "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", + "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM" + }; + + /** + * 获取当前Date型日期 + * + * @return Date() 当前日期 + */ + public static Date getNowDate() { + return new Date(); + } + + /** + * 获取当前日期, 默认格式为yyyy-MM-dd + * + * @return String + */ + public static String getDate() { + return dateTimeNow(YYYY_MM_DD); + } + + public static final String getTime() { + return dateTimeNow(YYYY_MM_DD_HH_MM_SS); + } + + public static final String dateTimeNow() { + return dateTimeNow(YYYYMMDDHHMMSS); + } + + public static final String dateTimeNow(final String format) { + return parseDateToStr(format, new Date()); + } + + public static final String dateTime(final Date date) { + return parseDateToStr(YYYY_MM_DD, date); + } + + public static final String parseDateToStr(final String format, final Date date) { + return new SimpleDateFormat(format).format(date); + } + + public static final Date dateTime(final String format, final String ts) { + try { + return new SimpleDateFormat(format).parse(ts); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + /** 日期路径 即年/月/日 如2018/08/08 */ + public static final String datePath() { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyy/MM/dd"); + } + + /** 日期路径 即年/月/日 如20180808 */ + public static final String dateTime() { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyyMMdd"); + } + + /** 日期型字符串转化为日期 格式 */ + public static Date parseDate(Object str) { + if (str == null) { + return null; + } + try { + return DateUtils.parseDate(str.toString(), parsePatterns); + } catch (ParseException e) { + return null; + } + } + + /** 获取服务器启动时间 */ + public static Date getServerStartDate() { + long time = ManagementFactory.getRuntimeMXBean().getStartTime(); + return new Date(time); + } + + /** 计算两个时间差 */ + public static String getDatePoor(Date endDate, Date nowDate) { + long nd = 1000 * 24 * 60 * 60; + long nh = 1000 * 60 * 60; + long nm = 1000 * 60; + // long ns = 1000; + // 获得两个时间的毫秒时间差异 + long diff = endDate.getTime() - nowDate.getTime(); + // 计算差多少天 + long day = diff / nd; + // 计算差多少小时 + long hour = diff % nd / nh; + // 计算差多少分钟 + long min = diff % nd % nh / nm; + // 计算差多少秒//输出结果 + // long sec = diff % nd % nh % nm / ns; + return day + "天" + hour + "小时" + min + "分钟"; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/DelayedTaskUtil.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/DelayedTaskUtil.java new file mode 100644 index 0000000..8b4428e --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/DelayedTaskUtil.java @@ -0,0 +1,164 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import static cn.hutool.script.ScriptUtil.eval; + +import java.lang.reflect.Method; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import javax.script.ScriptContext; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +/** + * 延迟任务工具类 - 使用Spring配置的虚拟线程 @Author Aleo + * + * @since 2019年07月24日 上午09:49 + */ +@Slf4j +@Component +public class DelayedTaskUtil { + + @Autowired + @Qualifier("virtualScheduledExecutor") + private ScheduledExecutorService scheduledExecutor; + + // 使用Spring配置的虚拟线程执行器 + @Autowired + @Qualifier("virtualThreadExecutor") + private ExecutorService scriptExecutor; + + /** + * 添加延迟任务 + * + * @param command 要执行的任务 + * @param delay 延迟时间 + * @param unit 时间单位 + */ + public void putTask(Runnable command, long delay, TimeUnit unit) { + log.info( + "[CORE][定时任务] 线程: {}, 执行延迟任务, 时间: {}, 延迟: {} {}", + Thread.currentThread().getName(), + LocalDateTime.now(), + delay, + unit.name().toLowerCase()); + + // 使用Spring配置的虚拟线程调度器 + scheduledExecutor.schedule(command, delay, unit); + } + + /** + * 添加固定频率的重复任务 + * + * @param command 要执行的任务 + * @param initialDelay 初始延迟时间 + * @param period 重复周期 + * @param unit 时间单位 + */ + public void putFixedRateTask(Runnable command, long initialDelay, long period, TimeUnit unit) { + log.info( + "[CORE][定时任务] 线程: {}, 执行固定频率任务, 时间: {}, 初始延迟: {}, 周期: {} {}", + Thread.currentThread().getName(), + LocalDateTime.now(), + initialDelay, + period, + unit.name().toLowerCase()); + + scheduledExecutor.scheduleAtFixedRate(command, initialDelay, period, unit); + } + + /** + * 添加固定延迟的重复任务 + * + * @param command 要执行的任务 + * @param initialDelay 初始延迟时间 + * @param delay 重复延迟 + * @param unit 时间单位 + */ + public void putFixedDelayTask(Runnable command, long initialDelay, long delay, TimeUnit unit) { + log.info( + "[CORE][定时任务] 线程: {}, 执行固定延迟任务, 时间: {}, 初始延迟: {}, 延迟: {} {}", + Thread.currentThread().getName(), + LocalDateTime.now(), + initialDelay, + delay, + unit.name().toLowerCase()); + + scheduledExecutor.scheduleWithFixedDelay(command, initialDelay, delay, unit); + } + + // 使用Record优化方法信息 + public record ScriptMethodInfo(String name, Class returnType, List> parameters) { + + public static ScriptMethodInfo from(Method method) { + return new ScriptMethodInfo( + method.getName(), method.getReturnType(), Arrays.asList(method.getParameterTypes())); + } + } + + // 异步执行脚本 + public CompletableFuture executeAsync(String script, ScriptContext context) { + return CompletableFuture.supplyAsync( + () -> { + try { + return eval(script, context); + } catch (Exception e) { + log.error("[CORE][定时任务] 脚本执行失败: {}", script, e); + throw new RuntimeException("脚本执行失败", e); + } + }, + scriptExecutor); + } + + // 异步执行脚本(带超时) + public CompletableFuture executeAsyncWithTimeout( + String script, ScriptContext context, long timeout, TimeUnit unit) { + CompletableFuture future = executeAsync(script, context); + + return future + .orTimeout(timeout, unit) + .exceptionally( + throwable -> { + if (throwable instanceof TimeoutException) { + log.error("[CORE][定时任务] 脚本执行超时: {}", script); + throw new RuntimeException("脚本执行超时", throwable); + } + throw new RuntimeException("脚本执行失败", throwable); + }); + } + + // 批量异步执行脚本 + public List> executeBatchAsync( + List scripts, ScriptContext context) { + return scripts.stream() + .map(script -> executeAsync(script, context)) + .collect(Collectors.toList()); + } + + // 等待所有脚本执行完成 + public List executeBatchAndWait(List scripts, ScriptContext context) { + List> futures = executeBatchAsync(scripts, context); + + return futures.stream().map(CompletableFuture::join).collect(Collectors.toList()); + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/ErrorUtil.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/ErrorUtil.java new file mode 100644 index 0000000..afad95d --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/ErrorUtil.java @@ -0,0 +1,34 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** 捕获报错日志处理工具类 */ +public class ErrorUtil { + + /** Exception出错的栈信息转成字符串 用于打印到日志中 */ + public static String errorInfoToString(Throwable e) { + // try-with-resource语法糖 处理机制 + try (StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw)) { + e.printStackTrace(pw); + pw.flush(); + sw.flush(); + return sw.toString(); + } catch (Exception ignored) { + throw new RuntimeException(ignored.getMessage(), ignored); + } + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/FileTypeUtils.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/FileTypeUtils.java new file mode 100644 index 0000000..82bc622 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/FileTypeUtils.java @@ -0,0 +1,79 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import java.io.File; +import org.apache.commons.lang3.StringUtils; + +/** 文件类型工具类 @Author ruoyi */ +public class FileTypeUtils { + + /** + * 获取文件类型 + * + *

例如: ruoyi.txt, 返回: txt + * + * @param file 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(File file) { + if (null == file) { + return StringUtils.EMPTY; + } + return getFileType(file.getName()); + } + + /** + * 获取文件类型 + * + *

例如: ruoyi.txt, 返回: txt + * + * @param fileName 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(String fileName) { + int separatorIndex = fileName.lastIndexOf("."); + if (separatorIndex < 0) { + return ""; + } + return fileName.substring(separatorIndex + 1).toLowerCase(); + } + + /** + * 获取文件类型 + * + * @param photoByte 文件字节码 + * @return 后缀(不含".") + */ + public static String getFileExtendName(byte[] photoByte) { + String strFileExtendName = "JPG"; + if ((photoByte[0] == 71) + && (photoByte[1] == 73) + && (photoByte[2] == 70) + && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) + && (photoByte[5] == 97)) { + strFileExtendName = "GIF"; + } else if ((photoByte[6] == 74) + && (photoByte[7] == 70) + && (photoByte[8] == 73) + && (photoByte[9] == 70)) { + strFileExtendName = "JPG"; + } else if ((photoByte[0] == 66) && (photoByte[1] == 77)) { + strFileExtendName = "BMP"; + } else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) { + strFileExtendName = "PNG"; + } + return strFileExtendName; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/FileUploadUtils.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/FileUploadUtils.java new file mode 100644 index 0000000..3f6d44e --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/FileUploadUtils.java @@ -0,0 +1,200 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import cn.universal.common.config.UnivUPloadConfig; +import cn.universal.common.constant.Constants; +import cn.universal.common.exception.FileNameLengthLimitExceededException; +import cn.universal.common.exception.FileSizeLimitExceededException; +import cn.universal.common.exception.InvalidExtensionException; +import java.io.File; +import java.io.IOException; +import org.springframework.web.multipart.MultipartFile; + +/** 文件上传工具类 @Author ruoyi */ +public class FileUploadUtils { + + /** 默认大小 50M */ + public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024; + + /** 默认的文件名最大长度 100 */ + public static final int DEFAULT_FILE_NAME_LENGTH = 100; + + /** 默认上传的地址 */ + private static String defaultBaseDir = UnivUPloadConfig.getProfile(); + + public static void setDefaultBaseDir(String defaultBaseDir) { + FileUploadUtils.defaultBaseDir = defaultBaseDir; + } + + public static String getDefaultBaseDir() { + return defaultBaseDir; + } + + /** + * 以默认配置进行文件上传 + * + * @param file 上传的文件 + * @return 文件名称 + * @throws Exception + */ + public static final String upload(MultipartFile file) throws IOException { + try { + return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } catch (Exception e) { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 根据文件路径上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @return 文件名称 + * @throws IOException + */ + public static final String upload(String baseDir, MultipartFile file) throws IOException { + try { + return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } catch (Exception e) { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 文件上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @param allowedExtension 上传文件类型 + * @return 返回上传成功的文件名 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws FileNameLengthLimitExceededException 文件名太长 + * @throws IOException 比如读写文件出错时 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, + IOException, + FileNameLengthLimitExceededException, + InvalidExtensionException { + int fileNamelength = file.getOriginalFilename().length(); + if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) { + throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH); + } + + assertAllowed(file, allowedExtension); + + String fileName = extractFilename(file); + + File desc = getAbsoluteFile(baseDir, fileName); + file.transferTo(desc); + String pathFileName = getPathFileName(baseDir, fileName); + return pathFileName; + } + + /** 编码文件名 */ + public static final String extractFilename(MultipartFile file) { + String fileName = file.getOriginalFilename(); + String extension = getExtension(file); + fileName = DateUtils.datePath() + "/" + IdUtil.fastUUID() + "." + extension; + return fileName; + } + + public static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException { + File desc = new File(uploadDir + File.separator + fileName); + + if (!desc.exists()) { + if (!desc.getParentFile().exists()) { + desc.getParentFile().mkdirs(); + } + } + return desc; + } + + public static final String getPathFileName(String uploadDir, String fileName) throws IOException { + int dirLastIndex = UnivUPloadConfig.getProfile().length() + 1; + String currentDir = StringUtils.substring(uploadDir, dirLastIndex); + String pathFileName = Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName; + return pathFileName; + } + + /** + * 文件大小校验 + * + * @param file 上传的文件 + * @return + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws InvalidExtensionException + */ + public static final void assertAllowed(MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, InvalidExtensionException { + long size = file.getSize(); + if (DEFAULT_MAX_SIZE != -1 && size > DEFAULT_MAX_SIZE) { + throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024); + } + + String fileName = file.getOriginalFilename(); + String extension = getExtension(file); + if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) { + if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) { + throw new InvalidExtensionException.InvalidImageExtensionException( + allowedExtension, extension, fileName); + } else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) { + throw new InvalidExtensionException.InvalidFlashExtensionException( + allowedExtension, extension, fileName); + } else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) { + throw new InvalidExtensionException.InvalidMediaExtensionException( + allowedExtension, extension, fileName); + } else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION) { + throw new InvalidExtensionException.InvalidVideoExtensionException( + allowedExtension, extension, fileName); + } else { + throw new InvalidExtensionException(allowedExtension, extension, fileName); + } + } + } + + /** + * 判断MIME类型是否是允许的MIME类型 + * + * @param extension + * @param allowedExtension + * @return + */ + public static final boolean isAllowedExtension(String extension, String[] allowedExtension) { + for (String str : allowedExtension) { + if (str.equalsIgnoreCase(extension)) { + return true; + } + } + return false; + } + + /** + * 获取文件名的后缀 + * + * @param file 表单文件 + * @return 后缀名 + */ + public static final String getExtension(MultipartFile file) { + String extension = FileUtil.extName(file.getOriginalFilename()); + if (StringUtils.isEmpty(extension)) { + extension = MimeTypeUtils.getExtension(file.getContentType()); + } + return extension; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/FileUtils.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/FileUtils.java new file mode 100644 index 0000000..09f5ba1 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/FileUtils.java @@ -0,0 +1,239 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.IdUtil; +import cn.universal.common.config.UnivUPloadConfig; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import org.apache.commons.lang3.ArrayUtils; + +/** 文件处理工具类 @Author ruoyi */ +public class FileUtils { + + public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; + + /** + * 输出指定文件的byte数组 + * + * @param filePath 文件路径 + * @param os 输出流 + * @return + */ + public static void writeBytes(String filePath, OutputStream os) throws IOException { + FileInputStream fis = null; + try { + File file = new File(filePath); + if (!file.exists()) { + throw new FileNotFoundException(filePath); + } + fis = new FileInputStream(file); + byte[] b = new byte[1024]; + int length; + while ((length = fis.read(b)) > 0) { + os.write(b, 0, length); + } + } catch (IOException e) { + throw e; + } finally { + IoUtil.close(os); + IoUtil.close(fis); + } + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @return 目标文件 + * @throws IOException IO异常 + */ + public static String writeImportBytes(byte[] data) throws IOException { + return writeBytes(data, UnivUPloadConfig.getImportPath()); + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @param uploadDir 目标文件 + * @return 目标文件 + * @throws IOException IO异常 + */ + public static String writeBytes(byte[] data, String uploadDir) throws IOException { + FileOutputStream fos = null; + String pathName = ""; + try { + String extension = getFileExtendName(data); + pathName = DateUtils.datePath() + "/" + IdUtil.fastUUID() + "." + extension; + File file = FileUploadUtils.getAbsoluteFile(uploadDir, pathName); + fos = new FileOutputStream(file); + fos.write(data); + } finally { + IoUtil.close(fos); + } + return FileUploadUtils.getPathFileName(uploadDir, pathName); + } + + /** + * 删除文件 + * + * @param filePath 文件 + * @return + */ + public static boolean deleteFile(String filePath) { + boolean flag = false; + File file = new File(filePath); + // 路径为文件且不为空则进行删除 + if (file.isFile() && file.exists()) { + file.delete(); + flag = true; + } + return flag; + } + + /** + * 文件名称验证 + * + * @param filename 文件名称 + * @return true 正常 false 非法 + */ + public static boolean isValidFilename(String filename) { + return filename.matches(FILENAME_PATTERN); + } + + /** + * 检查文件是否可下载 + * + * @param resource 需要下载的文件 + * @return true 正常 false 非法 + */ + public static boolean checkAllowDownload(String resource) { + // 禁止目录上跳级别 + if (StringUtils.contains(resource, "..")) { + return false; + } + + // 检查允许下载的文件规则 + if (ArrayUtils.contains( + MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) { + return true; + } + + // 不在允许下载的文件规则 + return false; + } + + /** + * 下载文件名重新编码 + * + * @param request 请求对象 + * @param fileName 文件名 + * @return 编码后的文件名 + */ + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) + throws UnsupportedEncodingException { + final String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) { + // IE浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + filename = filename.replace("+", " "); + } else if (agent.contains("Firefox")) { + // 火狐浏览器 + filename = new String(fileName.getBytes(), "ISO8859-1"); + } else if (agent.contains("Chrome")) { + // google浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } else { + // 其它浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + return filename; + } + + /** + * 下载文件名重新编码 + * + * @param response 响应对象 + * @param realFileName 真实文件名 + * @return + */ + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) + throws UnsupportedEncodingException { + String percentEncodedFileName = percentEncode(realFileName); + + StringBuilder contentDispositionValue = new StringBuilder(); + contentDispositionValue + .append("attachment; filename=") + .append(percentEncodedFileName) + .append(";") + .append("filename*=") + .append("utf-8''") + .append(percentEncodedFileName); + + response.addHeader("Access-Control-Allow-Origin", "*"); + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename"); + response.setHeader("Content-disposition", contentDispositionValue.toString()); + response.setHeader("download-filename", percentEncodedFileName); + } + + /** + * 百分号编码工具方法 + * + * @param s 需要百分号编码的字符串 + * @return 百分号编码后的字符串 + */ + public static String percentEncode(String s) throws UnsupportedEncodingException { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + return encode.replaceAll("\\+", "%20"); + } + + /** + * 获取图像后缀 + * + * @param photoByte 图像数据 + * @return 后缀名 + */ + public static String getFileExtendName(byte[] photoByte) { + String strFileExtendName = "jpg"; + if ((photoByte[0] == 71) + && (photoByte[1] == 73) + && (photoByte[2] == 70) + && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) + && (photoByte[5] == 97)) { + strFileExtendName = "gif"; + } else if ((photoByte[6] == 74) + && (photoByte[7] == 70) + && (photoByte[8] == 73) + && (photoByte[9] == 70)) { + strFileExtendName = "jpg"; + } else if ((photoByte[0] == 66) && (photoByte[1] == 77)) { + strFileExtendName = "bmp"; + } else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) { + strFileExtendName = "png"; + } + return strFileExtendName; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/IPUtil.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/IPUtil.java new file mode 100644 index 0000000..911fbb6 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/IPUtil.java @@ -0,0 +1,98 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import java.util.Set; + +public class IPUtil { + + /** + * 根据IP,及可用Ip列表来判断ip是否包含在白名单之中 + * + * @param ip + * @param ipList + * @return + */ + public static boolean checkIPWhitelist(String ip, Set ipList) { + if (ipList.isEmpty() || ipList.contains(ip)) { + return true; + } + for (String allow : ipList) { + if (allow.indexOf("-") > -1) { // 处理 类似 192.168.0.0-192.168.2.1 + String[] tempAllow = allow.split("-"); + String[] from = tempAllow[0].split("\\."); + String[] end = tempAllow[1].split("\\."); + String[] tag = ip.split("\\."); + boolean check = true; + for (int i = 0; i < 4; i++) { // 对IP从左到右进行逐段匹配 + int s = Integer.valueOf(from[i]); + int t = Integer.valueOf(tag[i]); + int e = Integer.valueOf(end[i]); + if (!(s <= t && t <= e)) { + check = false; + break; + } + } + if (check) { + return true; + } + } else if (allow.contains("/")) { // 处理 网段 xxx.xxx.xxx./24 + int splitIndex = allow.indexOf("/"); + // 取出子网段 + String ipSegment = allow.substring(0, splitIndex); // 192.168.3.0 + // 子网数 + String netmask = allow.substring(splitIndex + 1); // 24 + // ip 转二进制 + long ipLong = ipToLong(ip); + // 子网二进制 + long maskLong = (2L << 32 - 1) - (2L << Integer.valueOf(32 - Integer.valueOf(netmask)) - 1); + // ip与和子网相与 得到 网络地址 + String calcSegment = longToIP(ipLong & maskLong); + // 如果计算得出网络地址和库中网络地址相同 则合法 + if (ipSegment.equals(calcSegment)) { + return true; + } + } + } + return false; + } + + private static long ipToLong(String strIP) { + long[] ip = new long[4]; + // 先找到IP地址字符串中.的位置 + int position1 = strIP.indexOf("."); + int position2 = strIP.indexOf(".", position1 + 1); + int position3 = strIP.indexOf(".", position2 + 1); + // 将每个.之间的字符串转换成整型 + ip[0] = Long.parseLong(strIP.substring(0, position1)); + ip[1] = Long.parseLong(strIP.substring(position1 + 1, position2)); + ip[2] = Long.parseLong(strIP.substring(position2 + 1, position3)); + ip[3] = Long.parseLong(strIP.substring(position3 + 1)); + return (ip[0] << 24) + (ip[1] << 16) + (ip[2] << 8) + ip[3]; + } + + // 将10进制整数形式转换成127.0.0.1形式的IP地址 + private static String longToIP(long longIP) { + StringBuilder sb = new StringBuilder(""); + // 直接右移24位 + sb.append(String.valueOf(longIP >>> 24)); + sb.append("."); + // 将高8位置0,然后右移16位 + sb.append(String.valueOf((longIP & 0x00FFFFFF) >>> 16)); + sb.append("."); + sb.append(String.valueOf((longIP & 0x0000FFFF) >>> 8)); + sb.append("."); + sb.append(String.valueOf(longIP & 0x000000FF)); + return sb.toString(); + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/IPUtils.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/IPUtils.java new file mode 100644 index 0000000..2bce778 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/IPUtils.java @@ -0,0 +1,83 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.InetAddress; +import java.net.UnknownHostException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class IPUtils { + + private static Logger logger = LoggerFactory.getLogger(IPUtils.class); + private static final String IP_UTILS_FLAG = ","; + private static final String UNKNOWN = "unknown"; + private static final String LOCALHOST_IP = "0:0:0:0:0:0:0:1"; + private static final String LOCALHOST_IP1 = "127.0.0.1"; + + /** + * 获取IP地址 + * + *

使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + // 以下两个获取在k8s中,将真实的客户端IP,放到了x-Original-Forwarded-For。而将WAF的回源地址放到了 x-Forwarded-For了。 + ip = request.getHeader("X-Original-Forwarded-For"); + if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Forwarded-For"); + } + // 获取nginx等代理的ip + if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("x-forwarded-for"); + } + if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (StringUtils.isEmpty(ip) || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + // 兼容k8s集群获取ip + if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) { + // 根据网卡取本机配置的IP + InetAddress iNet = null; + try { + iNet = InetAddress.getLocalHost(); + } catch (UnknownHostException e) { + logger.error("getClientIp error: {}", e); + } + ip = iNet.getHostAddress(); + } + } + } catch (Exception e) { + logger.error("IPUtils ERROR ", e); + } + // 使用代理,则获取第一个IP地址 + if (!StringUtils.isEmpty(ip) && ip.indexOf(IP_UTILS_FLAG) > 0) { + ip = ip.substring(0, ip.indexOf(IP_UTILS_FLAG)); + } + + return ip; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/IotMapUtil.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/IotMapUtil.java new file mode 100644 index 0000000..97198e9 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/IotMapUtil.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import java.util.Map; +import java.util.Map.Entry; + +/** + * @version 1.0 @Author Aleo + * @since 2023/10/12 + */ +public class IotMapUtil { + + public static String sortJoinValue(Map map) { + return sortJoinValue(map, ":", true); + } + + public static String sortJoinValue( + Map map, String keyValueSeparator, boolean isIgnoreNull) { + map = MapUtil.sort(map); + final StringBuilder strBuilder = StrUtil.builder(); + if (MapUtil.isNotEmpty(map)) { + for (Entry entry : map.entrySet()) { + if (false == isIgnoreNull || entry.getKey() != null && entry.getValue() != null) { + strBuilder.append(Convert.toStr(entry.getValue())).append(keyValueSeparator); + } + } + } + if (StrUtil.isNotBlank(strBuilder)) { + return strBuilder.substring(0, strBuilder.length() - 1); + } + return strBuilder.toString(); + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/IotStrUtil.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/IotStrUtil.java new file mode 100644 index 0000000..ab790ea --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/IotStrUtil.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import cn.hutool.core.util.StrUtil; +import java.util.regex.Pattern; + +/** + * 字符串扩展工具类 + * + * @version 1.0 @Author Aleo + * @since 2025/8/23 22:10 + */ +public class IotStrUtil { + + /** + * 检验是否被Base64 + * + * @param str + * @return + */ + public static boolean isBase64(String str) { + if (StrUtil.isBlank(str)) { + return false; + } + String base64Pattern = + "^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$"; + boolean matches = Pattern.matches(base64Pattern, str); + if (matches) { + return true; + } + return false; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/MimeTypeUtils.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/MimeTypeUtils.java new file mode 100644 index 0000000..ec0740c --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/MimeTypeUtils.java @@ -0,0 +1,84 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +/** 媒体类型工具类 @Author ruoyi */ +public class MimeTypeUtils { + + public static final String IMAGE_PNG = "image/png"; + + public static final String IMAGE_JPG = "image/jpg"; + + public static final String IMAGE_JPEG = "image/jpeg"; + + public static final String IMAGE_BMP = "image/bmp"; + + public static final String IMAGE_GIF = "image/gif"; + + public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"}; + + public static final String[] FLASH_EXTENSION = {"swf", "flv"}; + + public static final String[] MEDIA_EXTENSION = { + "swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", "asf", "rm", "rmvb" + }; + + public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"}; + + public static final String[] DEFAULT_ALLOWED_EXTENSION = { + // 图片 + "bmp", + "gif", + "jpg", + "jpeg", + "png", + // word excel powerpoint + "doc", + "docx", + "xls", + "xlsx", + "ppt", + "pptx", + "html", + "htm", + "txt", + // 压缩文件 + "rar", + "zip", + "gz", + "bz2", + // 视频格式 + "mp4", + "avi", + "rmvb", + // pdf + "pdf" + }; + + public static String getExtension(String prefix) { + switch (prefix) { + case IMAGE_PNG: + return "png"; + case IMAGE_JPG: + return "jpg"; + case IMAGE_JPEG: + return "jpeg"; + case IMAGE_BMP: + return "bmp"; + case IMAGE_GIF: + return "gif"; + default: + return ""; + } + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/RSAUtils.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/RSAUtils.java new file mode 100644 index 0000000..8e97df7 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/RSAUtils.java @@ -0,0 +1,111 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import cn.hutool.core.codec.Base64; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.HashMap; +import java.util.Map; +import javax.crypto.Cipher; + +public class RSAUtils { + + /** 密钥长度 于原文长度对应 以及越长速度越慢 */ + private static final int KEY_SIZE = 1024; + + /** 用于封装随机产生的公钥与私钥 */ + public static Map keyMap = new HashMap(); + + /** 随机生成密钥对 */ + public static void genKeyPair() throws Exception { + // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象 + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + // 初始化密钥对生成器 + keyPairGen.initialize(KEY_SIZE, new SecureRandom()); + // 生成一个密钥对,保存在keyPair中 + KeyPair keyPair = keyPairGen.generateKeyPair(); + // 得到私钥 + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + // 得到公钥 + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + String publicKeyString = encryptBASE64(publicKey.getEncoded()); + // 得到私钥字符串 + String privateKeyString = encryptBASE64(privateKey.getEncoded()); + // 将公钥和私钥保存到Map + // 0表示公钥 + keyMap.put(0, publicKeyString); + // 1表示私钥 + keyMap.put(1, privateKeyString); + } + + // 编码返回字符串 + public static String encryptBASE64(byte[] key) throws Exception { + return Base64.encode(key); + } + + // 解码返回byte + public static byte[] decryptBASE64(String key) throws Exception { + return Base64.decode(key); + } + + /** + * RSA公钥加密 + * + * @param str 加密字符串 + * @param publicKey 公钥 + * @return 密文 + * @throws Exception 加密过程中的异常信息 + */ + public static String encrypt(String str, String publicKey) throws Exception { + // base64编码的公钥 + byte[] decoded = decryptBASE64(publicKey); + RSAPublicKey pubKey = + (RSAPublicKey) + KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded)); + // RSA加密 + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, pubKey); + String outStr = encryptBASE64(cipher.doFinal(str.getBytes("UTF-8"))); + return outStr; + } + + /** + * RSA私钥解密 + * + * @param str 加密字符串 + * @param privateKey 私钥 + * @return 明文 + * @throws Exception 解密过程中的异常信息 + */ + public static String decrypt(String str, String privateKey) throws Exception { + // 64位解码加密后的字符串 + byte[] inputByte = decryptBASE64(str); + // base64编码的私钥 + byte[] decoded = decryptBASE64(privateKey); + RSAPrivateKey priKey = + (RSAPrivateKey) + KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded)); + // RSA解密 + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, priKey); + String outStr = new String(cipher.doFinal(inputByte)); + return outStr; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/ReflectUtils.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/ReflectUtils.java new file mode 100644 index 0000000..c6620b8 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/ReflectUtils.java @@ -0,0 +1,54 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import java.lang.reflect.Method; +import java.util.List; + +/** 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数. @Author Lion Li */ +@SuppressWarnings("rawtypes") +public class ReflectUtils extends ReflectUtil { + + private static final String SETTER_PREFIX = "set"; + + private static final String GETTER_PREFIX = "get"; + + /** 调用Getter方法. 支持多级,如:对象名.对象名.方法 */ + @SuppressWarnings("unchecked") + public static E invokeGetter(Object obj, String propertyName) { + Object object = obj; + for (String name : StrUtil.split(propertyName, ".")) { + String getterMethodName = GETTER_PREFIX + StrUtil.upperFirst(name); + object = invoke(object, getterMethodName); + } + return (E) object; + } + + /** 调用Setter方法, 仅匹配方法名。 支持多级,如:对象名.对象名.方法 */ + public static void invokeSetter(Object obj, String propertyName, E value) { + Object object = obj; + List names = StrUtil.split(propertyName, "."); + for (int i = 0; i < names.size(); i++) { + if (i < names.size() - 1) { + String getterMethodName = GETTER_PREFIX + StrUtil.upperFirst(names.get(i)); + object = invoke(object, getterMethodName); + } else { + String setterMethodName = SETTER_PREFIX + StrUtil.upperFirst(names.get(i)); + Method method = getMethodByName(object.getClass(), setterMethodName); + invoke(object, method, value); + } + } + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/ServletUtils.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/ServletUtils.java new file mode 100644 index 0000000..03ffdca --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/ServletUtils.java @@ -0,0 +1,124 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import cn.hutool.core.convert.Convert; +import cn.hutool.extra.servlet.JakartaServletUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import java.io.IOException; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** 客户端工具类 @Author ruoyi */ +public class ServletUtils extends JakartaServletUtil { + + /** 获取String参数 */ + public static String getParameter(String name) { + return getRequest().getParameter(name); + } + + /** 获取String参数 */ + public static String getParameter(String name, String defaultValue) { + return Convert.toStr(getRequest().getParameter(name), defaultValue); + } + + /** 获取Integer参数 */ + public static Integer getParameterToInt(String name) { + return Convert.toInt(getRequest().getParameter(name)); + } + + /** 获取Integer参数 */ + public static Integer getParameterToInt(String name, Integer defaultValue) { + return Convert.toInt(getRequest().getParameter(name), defaultValue); + } + + /** 获取Boolean参数 */ + public static Boolean getParameterToBool(String name) { + return Convert.toBool(getRequest().getParameter(name)); + } + + /** 获取Boolean参数 */ + public static Boolean getParameterToBool(String name, Boolean defaultValue) { + return Convert.toBool(getRequest().getParameter(name), defaultValue); + } + + /** 获取request */ + public static HttpServletRequest getRequest() { + return getRequestAttributes().getRequest(); + } + + /** 获取response */ + public static HttpServletResponse getResponse() { + return getRequestAttributes().getResponse(); + } + + /** 获取session */ + public static HttpSession getSession() { + return getRequest().getSession(); + } + + public static ServletRequestAttributes getRequestAttributes() { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + return (ServletRequestAttributes) attributes; + } + + /** + * 将字符串渲染到客户端 + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + * @return null + */ + public static String renderString(HttpServletResponse response, String string) { + try { + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().print(string); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** 是否是Ajax异步请求 */ + public static boolean isAjaxRequest(HttpServletRequest request) { + String accept = request.getHeader("accept"); + if (accept != null && accept.indexOf("application/json") != -1) { + return true; + } + + String xRequestedWith = request.getHeader("X-Requested-With"); + if (xRequestedWith != null && xRequestedWith.indexOf("XMLHttpRequest") != -1) { + return true; + } + + String uri = request.getRequestURI(); + if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) { + return true; + } + + String ajax = request.getParameter("__ajax"); + if (StringUtils.inStringIgnoreCase(ajax, "json", "xml")) { + return true; + } + return false; + } + + public static String getClientIP() { + return IPUtils.getIpAddr(getRequest()); + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/SpringUtils.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/SpringUtils.java new file mode 100644 index 0000000..47a6d6b --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/SpringUtils.java @@ -0,0 +1,98 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.stereotype.Component; + +@Component +public final class SpringUtils implements BeanFactoryPostProcessor { + + /** Spring应用上下文环境 */ + private static ConfigurableListableBeanFactory beanFactory; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + SpringUtils.beanFactory = beanFactory; + } + + /** + * 获取对象 + * + * @param name + * @return Object 一个以所给名字注册的bean的实例 + * @throws org.springframework.beans.BeansException + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) throws BeansException { + return (T) beanFactory.getBean(name); + } + + /** + * 获取类型为requiredType的对象 + * + * @param clz + * @return + * @throws org.springframework.beans.BeansException + */ + public static T getBean(Class clz) throws BeansException { + T result = (T) beanFactory.getBean(clz); + return result; + } + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + * + * @param name + * @return boolean + */ + public static boolean containsBean(String name) { + return beanFactory.containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + * + * @param name + * @return boolean + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return beanFactory.isSingleton(name); + } + + /** + * @param name + * @return Class 注册对象的类型 + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + */ + public static Class getType(String name) throws NoSuchBeanDefinitionException { + return beanFactory.getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + * + * @param name + * @return + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return beanFactory.getAliases(name); + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/StringUtils.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/StringUtils.java new file mode 100644 index 0000000..920dfe6 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/StringUtils.java @@ -0,0 +1,460 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import cn.hutool.core.text.StrFormatter; +import cn.universal.common.constant.Constants; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.util.AntPathMatcher; + +/** 字符串工具类 @Author ruoyi */ +public class StringUtils extends org.apache.commons.lang3.StringUtils { + + /** 空字符串 */ + private static final String NULLSTR = ""; + + /** 下划线 */ + private static final char SEPARATOR = '_'; + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) { + return value != null ? value : defaultValue; + } + + /** + * * 判断一个Collection是否为空, 包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Collection coll) { + return isNull(coll) || coll.isEmpty(); + } + + /** + * * 判断一个Collection是否非空,包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Collection coll) { + return !isEmpty(coll); + } + + /** + * * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 * @return true:为空 false:非空 + */ + public static boolean isEmpty(Object[] objects) { + return isNull(objects) || (objects.length == 0); + } + + /** + * * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Object[] objects) { + return !isEmpty(objects); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Map map) { + return isNull(map) || map.isEmpty(); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + + /** + * * 判断一个字符串是否为空串 + * + * @param str String + * @return true:为空 false:非空 + */ + public static boolean isEmpty(String str) { + return isNull(str) || NULLSTR.equals(str.trim()); + } + + /** + * * 判断一个字符串是否为非空串 + * + * @param str String + * @return true:非空串 false:空串 + */ + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * * 判断一个对象是否为空 + * + * @param object Object + * @return true:为空 false:非空 + */ + public static boolean isNull(Object object) { + return object == null; + } + + /** + * * 判断一个对象是否非空 + * + * @param object Object + * @return true:非空 false:空 + */ + public static boolean isNotNull(Object object) { + return !isNull(object); + } + + /** + * * 判断一个对象是否是数组类型(Java基本型别的数组) + * + * @param object 对象 + * @return true:是数组 false:不是数组 + */ + public static boolean isArray(Object object) { + return isNotNull(object) && object.getClass().isArray(); + } + + /** 去空格 */ + public static String trim(String str) { + return (str == null ? "" : str.trim()); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) { + if (str == null) { + return NULLSTR; + } + + if (start < 0) { + start = str.length() + start; + } + + if (start < 0) { + start = 0; + } + if (start > str.length()) { + return NULLSTR; + } + + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) { + if (str == null) { + return NULLSTR; + } + + if (end < 0) { + end = str.length() + end; + } + if (start < 0) { + start = str.length() + start; + } + + if (end > str.length()) { + end = str.length(); + } + + if (start > end) { + return NULLSTR; + } + + if (start < 0) { + start = 0; + } + if (end < 0) { + end = 0; + } + + return str.substring(start, end); + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(String template, Object... params) { + if (isEmpty(params) || isEmpty(template)) { + return template; + } + return StrFormatter.format(template, params); + } + + /** + * 是否为http(s)://开头 + * + * @param link 链接 + * @return 结果 + */ + public static boolean ishttp(String link) { + return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS); + } + + /** + * 字符串转set + * + * @param str 字符串 + * @param sep 分隔符 + * @return set集合 + */ + public static final Set str2Set(String str, String sep) { + return new HashSet(str2List(str, sep, true, false)); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @param filterBlank 过滤纯空白 + * @param trim 去掉首尾空白 + * @return list集合 + */ + public static final List str2List( + String str, String sep, boolean filterBlank, boolean trim) { + List list = new ArrayList(); + if (StringUtils.isEmpty(str)) { + return list; + } + + // 过滤空白字符串 + if (filterBlank && StringUtils.isBlank(str)) { + return list; + } + String[] split = str.split(sep); + for (String string : split) { + if (filterBlank && StringUtils.isBlank(string)) { + continue; + } + if (trim) { + string = string.trim(); + } + list.add(string); + } + + return list; + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写 + * + * @param cs 指定字符串 + * @param searchCharSequences 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + */ + public static boolean containsAnyIgnoreCase( + CharSequence cs, CharSequence... searchCharSequences) { + if (StringUtils.isEmpty(cs) || isEmpty(searchCharSequences)) { + return false; + } + for (CharSequence testStr : searchCharSequences) { + if (StringUtils.containsIgnoreCase(cs, testStr)) { + return true; + } + } + return false; + } + + /** 驼峰转下划线命名 */ + public static String toUnderScoreCase(String str) { + if (str == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean curreCharIsUpperCase = true; + // 下一字符是否大写 + boolean nexteCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (i > 0) { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } else { + preCharIsUpperCase = false; + } + + curreCharIsUpperCase = Character.isUpperCase(c); + + if (i < (str.length() - 1)) { + nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + + if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) { + sb.append(SEPARATOR); + } else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) { + if (str != null && strs != null) { + for (String s : strs) { + if (str.equalsIgnoreCase(trim(s))) { + return true; + } + } + } + return false; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) { + // 没必要转换 + return ""; + } else if (!name.contains("_")) { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + String[] camels = name.split("_"); + for (String camel : camels) { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** 驼峰式命名法 例如:user_name->userName */ + public static String toCamelCase(String s) { + if (s == null) { + return null; + } + s = s.toLowerCase(); + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (c == SEPARATOR) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, List strs) { + if (isEmpty(str) || isEmpty(strs)) { + return false; + } + for (String pattern : strs) { + if (isMatch(pattern, str)) { + return true; + } + } + return false; + } + + /** + * 判断url是否与规则配置: ? 表示单个字符; * 表示一层路径内的任意字符串,不可跨层级; ** 表示任意层路径; + * + * @param pattern 匹配规则 + * @param url 需要匹配的url + * @return + */ + public static boolean isMatch(String pattern, String url) { + AntPathMatcher matcher = new AntPathMatcher(); + return matcher.match(pattern, url); + } + + @SuppressWarnings("unchecked") + public static T cast(Object obj) { + return (T) obj; + } +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/ThreadLocalCache.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/ThreadLocalCache.java new file mode 100644 index 0000000..966ecd4 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/ThreadLocalCache.java @@ -0,0 +1,21 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import org.springframework.stereotype.Component; + +@Component +public class ThreadLocalCache { + + public static ThreadLocal localExtDeviceId = new ThreadLocal(); +} diff --git a/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/UrlUtils.java b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/UrlUtils.java new file mode 100644 index 0000000..225fe90 --- /dev/null +++ b/cn-universal-framework/cn-universal-common/src/main/java/cn/universal/common/utils/UrlUtils.java @@ -0,0 +1,85 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.common.utils; + +import cn.hutool.core.util.StrUtil; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Created by terry on 2017/8/1. */ +public class UrlUtils { + + private static final String RE_TOP_DOMAIN = + "(com\\.cn|net\\.cn|gov\\.cn|org\\.nz|org\\" + ".cn|com|net|org|gov|cc|biz|info|cn|co|me)"; + + // 一级域名提取 + private static final String RE_TOP_1 = "(\\w*\\.?){1}\\." + RE_TOP_DOMAIN; + + // 二级域名提取 + private static final String RE_TOP_2 = "(\\w*\\.?){2}\\." + RE_TOP_DOMAIN; + + // 三级域名提取 + private static final String RE_TOP_3 = "(\\w*\\.?){3}\\." + RE_TOP_DOMAIN; + + private static final Pattern PATTEN_IP = + Pattern.compile("((http://)|(https://))?((\\d+\\.){3}(\\d+))"); + private static final Pattern PATTEN_TOP1 = Pattern.compile(RE_TOP_1); + private static final Pattern PATTEN_TOP2 = Pattern.compile(RE_TOP_2); + private static final Pattern PATTEN_TOP3 = Pattern.compile(RE_TOP_3); + + public static String getDomain(String url, int level) { + Matcher matcher = PATTEN_IP.matcher(url); + if (matcher.find()) { + return matcher.group(4); + } + + switch (level) { + case 1: + matcher = PATTEN_TOP1.matcher(url); + break; + case 2: + matcher = PATTEN_TOP2.matcher(url); + break; + case 3: + matcher = PATTEN_TOP3.matcher(url); + break; + default: + return ""; + } + if (matcher.find()) { + return matcher.group(0); + } + return ""; + } + + public static void main(String[] args) { + String[] urls = { + "yunfan.tcp.univiot.cn", + "http://meiwen.me/src/index.html", + "http://1000chi.com/game/index.html", + "http://see.xidian.edu.cn/cpp/html/1429.html", + "https://docs.python.org/2/howto/regex.html", + "https://www.google.com.hk/search?client=aff-cs-360chromium&hs=TSj&q=url%E8%A7%A3%E6%9E%90%E5%9F%9F%E5%90" + + "%8Dre&oq=url%E8%A7%A3%E6%9E%90%E5%9F%9F%E5%90%8Dre&gs_l=serp.3...74418.86867.0.87673.28.25.2.0.0.0.541" + + ".2454.2-6j0j1j1.8.0....0...1c.1j4.53.serp..26.2.547.IuHTj4uoyHg", + "file:///D:/code/echarts-2.0.3/doc/example/tooltip.html", + "http://api.mongodb.org/python/current/faq.html#is-pymongo-thread-safe", + "https://pypi.python.org/pypi/publicsuffix/", + "http://127.0.0.1:8000", + }; + + System.out.println(StrUtil.subBefore("yunfan.tcp.univiot.cn", ".", false)); + } + + public static void test() {} +} diff --git a/cn-universal-framework/cn-universal-config/pom.xml b/cn-universal-framework/cn-universal-config/pom.xml new file mode 100644 index 0000000..1aa8065 --- /dev/null +++ b/cn-universal-framework/cn-universal-config/pom.xml @@ -0,0 +1,24 @@ + + + cn-universal-config + 4.0.0 + + cn-universal-framework + cn.universal.iot + 1.5-SNAPSHOT + + + + + cn.universal.iot + cn-universal-common + ${project.version} + + + org.springframework.boot + spring-boot-starter + + + \ No newline at end of file diff --git a/cn-universal-framework/cn-universal-core/pom.xml b/cn-universal-framework/cn-universal-core/pom.xml new file mode 100644 index 0000000..7be647a --- /dev/null +++ b/cn-universal-framework/cn-universal-core/pom.xml @@ -0,0 +1,91 @@ + + + cn-universal-core + + + + org.apache.maven.plugins + maven-compiler-plugin + + 21 + 21 + + + + + 4.0.0 + + cn-universal-framework + cn.universal.iot + 1.5-SNAPSHOT + + + + + org.apache.commons + commons-lang3 + + + + org.aspectj + aspectjweaver + + + cn.universal.iot + cn-universal-common + ${project.version} + + + org.springframework.boot + spring-boot-starter-web + + + + io.lettuce + lettuce-core + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.graalvm.js + js + 23.0.0 + + + org.graalvm.js + js-scriptengine + 23.0.0 + + + + + org.springframework.security + spring-security-crypto + + + + + + + + + + org.projectlombok + lombok + true + + + + + org.slf4j + slf4j-api + + + \ No newline at end of file diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicResourceLoader.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicResourceLoader.java new file mode 100644 index 0000000..dc44352 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicResourceLoader.java @@ -0,0 +1,206 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine; + +import cn.universal.core.engine.exception.ResourceNotFoundException; +import cn.universal.core.engine.functions.DynamicModuleImport; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** 资源加载器 */ +public class MagicResourceLoader { + + /** 保存自动导入的包路径 */ + private static final Set PACKAGES = new HashSet<>(); + + /** 函数加载器 */ + private static final List> FUNCTION_LOADERS = + new ArrayList<>(); + + /** JSR223 脚本函数加载器 */ + private static final List, String, Object>>> + SCRIPT_LANGUAGE_LOADERS = new ArrayList<>(); + + /** 保存已注册的模块 */ + private static final Map MODULES = new ConcurrentHashMap<>(); + + /** 默认的类加载器 */ + private static Function classLoader = + (className) -> { + try { + return Class.forName(className); + } catch (Exception e) { + return null; + } + }; + + static { + // 默认导入 java.util.* 、java.lang.* + addPackage("java.util.*"); + addPackage("java.lang.*"); + } + + /** 获取已注册的模块信息,此方法主要用于代码提示 */ + public static Map getModules() { + return MODULES.entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + entry -> { + ScriptClass scriptClass; + if (entry.getValue() instanceof Class) { + Class clazz = (Class) entry.getValue(); + scriptClass = MagicScriptEngine.getScriptClassFromClass(clazz); + } else if (entry.getValue() instanceof DynamicModuleImport) { + DynamicModuleImport dmi = (DynamicModuleImport) entry.getValue(); + scriptClass = MagicScriptEngine.getScriptClassFromClass(dmi.getTargetClass()); + } else { + scriptClass = + MagicScriptEngine.getScriptClassFromClass(entry.getValue().getClass()); + } + scriptClass.setModule(true); + return scriptClass; + })); + } + + /** 添加函数加载器 */ + public static void addFunctionLoader( + BiFunction functionLoader) { + FUNCTION_LOADERS.add(functionLoader); + } + + /** 设置类加载器 */ + public static void setClassLoader(Function classLoader) { + MagicResourceLoader.classLoader = classLoader; + } + + /** + * 添加模块 + * + * @param moduleName 模块名称 + * @param target 模块,可以是对象实例,也可以是Class类型的,此时只能使用类中的静态方法 + */ + public static void addModule(String moduleName, Object target) { + MODULES.put(moduleName, target); + } + + /** + * 加载模块 + * + * @param moduleName 模块名称 + */ + public static Object loadModule(MagicScriptContext context, String moduleName) { + return loadModule(moduleName); + } + + /** + * 加载模块 + * + * @param moduleName 模块名称 + */ + public static Object loadModule(String moduleName) { + Object module = MODULES.get(moduleName); + if (module == null) { + throw new ResourceNotFoundException("找不到模块:" + moduleName); + } + return module; + } + + /** + * 加载类 + * + * @param className 类全限定名 + */ + public static Object loadClass(MagicScriptContext context, String className) { + return classLoader.apply(className); + } + + /** 获取可用的模块列表 */ + public static Set getModuleNames() { + return MODULES.keySet(); + } + + /** + * 添加自动导包 + * + * @param prefix 包前缀,如java.lang.*, 不支持 java.lang.**.* + */ + public static void addPackage(String prefix) { + PACKAGES.add(prefix.replace("*", "")); + } + + /** + * 加载类 + * + * @param simpleName 类缩写,如HashMap、ArrayList + */ + public static Class findClass(String simpleName) { + for (String prefix : PACKAGES) { + try { + return Class.forName(prefix + simpleName); + } catch (Exception ignored) { + } + } + return null; + } + + /** 添加JSR223 脚本函数加载器 */ + public static void addScriptLanguageLoader( + Function, String, Object>> loader) { + SCRIPT_LANGUAGE_LOADERS.add(loader); + } + + /** + * 加载脚本函数加载器 + * + * @param name 脚本名称 + */ + public static BiFunction, String, Object> loadScriptLanguage(String name) { + for (Function, String, Object>> languageLoader : + SCRIPT_LANGUAGE_LOADERS) { + try { + BiFunction, String, Object> function = languageLoader.apply(name); + if (function != null) { + return function; + } + } catch (Exception ignored) { + } + } + throw new ResourceNotFoundException("找不到语言:" + name); + } + + /** + * 加载函数加载器 + * + * @param name 函数名称 + */ + public static Object loadFunction(MagicScriptContext context, String name) { + for (BiFunction loader : FUNCTION_LOADERS) { + try { + Object value = loader.apply(context, name); + if (value != null) { + return value; + } + } catch (Exception ignored) { + } + } + throw new ResourceNotFoundException("找不到函数:" + name); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScript.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScript.java new file mode 100644 index 0000000..996910d --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScript.java @@ -0,0 +1,216 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine; + +import cn.universal.core.engine.compile.CompileCache; +import cn.universal.core.engine.compile.MagicScriptCompileException; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.exception.MagicExitException; +import cn.universal.core.engine.exception.MagicScriptException; +import cn.universal.core.engine.functions.DynamicModuleImport; +import cn.universal.core.engine.parsing.Parser; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.VarIndex; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Node; +import cn.universal.core.engine.parsing.ast.statement.Import; +import cn.universal.core.engine.parsing.ast.statement.Return; +import cn.universal.core.engine.parsing.ast.statement.VariableAccess; +import cn.universal.core.engine.runtime.MagicScriptClassLoader; +import cn.universal.core.engine.runtime.MagicScriptRuntime; +import cn.universal.core.engine.runtime.MagicScriptVariableAccessRuntime; +import java.lang.reflect.Constructor; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.script.Bindings; +import javax.script.CompiledScript; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; + +public class MagicScript extends CompiledScript { + + public static final String CONTEXT_ROOT = "ROOT"; + + public static final String DEBUG_MARK = "!# DEBUG\r\n"; + + /** 所有语句 */ + private final List nodes; + + private final ScriptEngine scriptEngine; + + /** 存放所有变量定义 */ + private final Set varIndices; + + /** 编译后的类 */ + private MagicScriptVariableAccessRuntime accessRuntime; + + /** 构造函数 */ + private Constructor constructor; + + private final boolean debug; + + private List spans; + + private String[] varNames; + + private static CompileCache compileCache; + + private MagicScript( + List nodes, Set varIndices, ScriptEngine scriptEngine, boolean debug) { + this.nodes = nodes; + this.varIndices = varIndices; + this.scriptEngine = scriptEngine; + this.debug = debug; + } + + public static void setCompileCache(int capacity) { + compileCache = new CompileCache(capacity); + } + + /** 创建MagicScript */ + public static MagicScript create(String source, ScriptEngine scriptEngine) { + return create(false, source, scriptEngine); + } + + /** 创建MagicScript */ + public static MagicScript create(boolean expression, String source, ScriptEngine scriptEngine) { + if (compileCache == null) { + compileCache = new CompileCache(500); + } + return compileCache.get( + source, + () -> { + Parser parser = new Parser(); + boolean debug = source.startsWith(DEBUG_MARK); + String script = debug ? source.substring(DEBUG_MARK.length()) : source; + List nodes = parser.parse(expression ? "return " + script : script); + Set varIndices = parser.getVarIndices(); + return new MagicScript(nodes, varIndices, scriptEngine, debug); + }); + } + + public Object execute(MagicScriptContext context) { + MagicScriptRuntime runtime = null; + try { + MagicScriptEngine.getDefaultImports() + .forEach( + (name, value) -> { + if (value instanceof DynamicModuleImport) { + context.set(name, ((DynamicModuleImport) value).getDynamicModule(context)); + } else { + context.set(name, value); + } + }); + runtime = compile(); + return runtime.execute(context); + } catch (MagicExitException e) { + return e.getExitValue(); + } catch (MagicScriptCompileException e) { + throw e; + } catch (Throwable t) { + MagicScriptError.transfer(runtime, t); + } + return null; + } + + /** 编译 */ + public MagicScriptRuntime compile() throws MagicScriptCompileException { + if (this.accessRuntime != null) { + return this.accessRuntime; + } + if (nodes.size() == 1 && nodes.get(0) instanceof Return) { + Return returnNode = (Return) nodes.get(0); + if (returnNode.getReturnValue() instanceof VariableAccess) { + return this.accessRuntime = + new MagicScriptVariableAccessRuntime( + ((VariableAccess) returnNode.getReturnValue()).getVarIndex().getName()); + } + } + try { + MagicScriptCompiler compiler = new MagicScriptCompiler(this.varIndices, this.debug); + nodes.forEach(node -> node.visitMethod(compiler)); + // 如果只是一个表达式 + if (nodes.size() == 1 && nodes.get(0) instanceof Expression) { + Node node = nodes.get(0); + compiler.loadVars(); + compiler.compile(new Return(node.getSpan(), node)); + } else { + // 根据是否有 import "xxx.xx.xx.*" 来分组 + Map> nodeMap = + nodes.stream() + .collect( + Collectors.partitioningBy( + it -> it instanceof Import && ((Import) it).isImportPackage())); + // 编译需要的方法 + compiler.compile(nodeMap.get(Boolean.TRUE)); // 先编译 import "xxx.xxx.x.*" + // 加载变量信息 + compiler.loadVars(); + // 编译其它语句 + compiler.compile(nodeMap.get(Boolean.FALSE)); + } + Class clazz = + new MagicScriptClassLoader(Thread.currentThread().getContextClassLoader()) + .load(compiler.getClassName(), compiler.bytecode()); + this.constructor = clazz.getConstructor(); + // 设置变量名字 + this.varNames = varIndices.stream().map(VarIndex::getName).toArray(String[]::new); + // 设置所有Span + this.spans = compiler.getSpans(); + return buildRuntime(); + } catch (MagicScriptException mse) { + throw new MagicScriptCompileException(mse.getSimpleMessage(), mse); + } catch (MagicScriptCompileException e) { + throw e; + } catch (Exception e) { + throw new MagicScriptCompileException(e); + } + } + + private MagicScriptRuntime buildRuntime() { + try { + MagicScriptRuntime target = constructor.newInstance(); + // 设置变量名字 + target.setVarNames(this.varNames); + // 设置所有Span + target.setSpans(this.spans); + return target; + } catch (Exception e) { + throw new MagicScriptCompileException(e); + } + } + + @Override + public Object eval(ScriptContext context) { + Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE); + if (bindings.containsKey(CONTEXT_ROOT)) { + Object root = bindings.get(CONTEXT_ROOT); + if (root instanceof MagicScriptContext) { + MagicScriptContext rootContext = (MagicScriptContext) root; + return execute(rootContext); + } else { + throw new MagicScriptException("参数不正确!"); + } + } + MagicScriptContext magicScriptContext = new MagicScriptContext(); + magicScriptContext.putMapIntoContext(context.getBindings(ScriptContext.GLOBAL_SCOPE)); + magicScriptContext.putMapIntoContext(context.getBindings(ScriptContext.ENGINE_SCOPE)); + return execute(magicScriptContext); + } + + @Override + public ScriptEngine getEngine() { + return scriptEngine; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptContext.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptContext.java new file mode 100644 index 0000000..e809eca --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptContext.java @@ -0,0 +1,183 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine; + +import cn.universal.core.engine.exception.MagicScriptException; +import cn.universal.core.engine.runtime.MagicScriptRuntime; +import cn.universal.core.engine.runtime.RuntimeContext; +import cn.universal.core.engine.runtime.Variables; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** 脚本环境上下文 编译后的类每个方法的第一个参数都是本类。 此类主要用于辅助读写变量以及设置/读取/ */ +public class MagicScriptContext { + + /** 保存手动设置的环境变量 */ + private final Map rootVariables = new LinkedHashMap<>(); + + /** 代码执行时,存放 import "xx.xx.xx.*" 的包 */ + private final List importPackages = new ArrayList<>(); + + private MagicScriptRuntime runtime; + + private Variables variables; + + private String scriptName; + + public MagicScriptContext() {} + + public MagicScriptContext(Map variables) { + putMapIntoContext(variables); + } + + public String getScriptName() { + return scriptName; + } + + public void setScriptName(String scriptName) { + this.scriptName = scriptName; + } + + /** + * 获取当前作用域内的String变量值 + * + * @param name 变量名称 + * @return 变量值 + */ + public String getString(String name) { + return Objects.toString(get(name), null); + } + + /** + * 添加 .* 的导包 + * + * @param packageName 包名 如 java.text. + */ + public void addImport(String packageName) { + importPackages.add(packageName); + } + + public Class getImportClass(String simpleClassName) { + for (int i = importPackages.size() - 1; i >= 0; i--) { + try { + return Class.forName(importPackages.get(i) + simpleClassName); + } catch (ClassNotFoundException ignored) { + } + } + return null; + } + + /** + * 获取当前作用域内的变量值 + * + * @param name 变量名称 + * @return 变量值 + */ + public Object get(String name) { + return rootVariables.get(name); + } + + /** + * 设置环境变量 + * + * @param name 变量名 + * @param value 变量值 + */ + public MagicScriptContext set(String name, Object value) { + rootVariables.put(name, value); + return this; + } + + /** + * 创建变量 + * + * @param runtime 脚本实例 + * @param size 数组大小(变量个数) + */ + public Variables createVariables(MagicScriptRuntime runtime, int size) { + this.runtime = runtime; + return this.variables = new Variables(size); + } + + public Variables getVariables() { + return variables; + } + + /** + * 从当前上下文中动态执行脚本 + * + * @param runtimeContext + * @param script 脚本内容 + */ + public Object eval(RuntimeContext runtimeContext, String script) { + Map varMap = + new LinkedHashMap<>(runtimeContext.getScriptContext().getRootVariables()); + varMap.putAll(runtimeContext.getVariables().getVariables(runtimeContext.getScriptContext())); + return eval(script, varMap); + } + + /** + * 从当前上下文中动态执行脚本 + * + * @param script 脚本内容 + * @param varMap 变量信息 + */ + public Object eval(String script, Map varMap) { + try { + MagicScript magicScript = MagicScript.create(true, script, null); + MagicScriptRuntime runtime = magicScript.compile(); + MagicScriptContext context = new MagicScriptContext(varMap); + context.setScriptName(this.getScriptName()); + return runtime.execute(context); + } catch (Exception e) { + Throwable throwable = MagicScriptError.unwrap(e); + if (throwable instanceof MagicScriptException) { + throw new RuntimeException(((MagicScriptException) throwable).getSimpleMessage()); + } + throw new RuntimeException(throwable); + } + } + + public String[] getVarNames() { + return runtime.getVarNames(); + } + + /** 获取调用时传入的变量信息 */ + public Map getRootVariables() { + return rootVariables; + } + + /** 批量设置环境变量 */ + public void putMapIntoContext(Map map) { + if (map != null && !map.isEmpty()) { + rootVariables.putAll(map); + } + } + + /** + * 从环境中获取值,此方法给编译后的类专用。 + * + * @param name 变量名 + */ + public Object getEnvironmentValue(String name) { + Object value = get(name); + value = value == null ? getImportClass(name) : value; + return value == null ? MagicResourceLoader.findClass(name) : value; + } + + public void pause(int startRow, int startCol, int endRow, int endCol, Variables variables) + throws InterruptedException {} +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptDebugContext.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptDebugContext.java new file mode 100644 index 0000000..a4255f9 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptDebugContext.java @@ -0,0 +1,115 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine; + +import cn.universal.core.engine.runtime.Variables; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class MagicScriptDebugContext extends MagicScriptContext { + + private final BlockingQueue producer = new LinkedBlockingQueue<>(); + private final BlockingQueue consumer = new LinkedBlockingQueue<>(); + public List breakpoints; + private String id = UUID.randomUUID().toString().replace("-", ""); + private Consumer> callback; + + private int[] line; + + private int timeout = 60; + + private boolean stepInto = false; + + public MagicScriptDebugContext(List breakpoints) { + this.breakpoints = breakpoints; + } + + public void setCallback(Consumer> callback) { + this.callback = callback; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public void setBreakpoints(List breakpoints) { + this.breakpoints = breakpoints; + } + + @Override + public synchronized void pause( + int startRow, int startCol, int endRow, int endCol, Variables variables) + throws InterruptedException { + if (stepInto || breakpoints.contains(startRow)) { + this.line = new int[] {startRow, startCol, endRow, endCol}; + consumer.offer(this.id); + Map varMap = new LinkedHashMap<>(getRootVariables()); + varMap.putAll(variables.getVariables(this)); + callback.accept(getDebugInfo(varMap)); + producer.poll(timeout, TimeUnit.SECONDS); + } + } + + public void await() throws InterruptedException { + consumer.take(); + } + + public void singal() throws InterruptedException { + producer.offer(this.id); + await(); + } + + public void setStepInto(boolean stepInto) { + this.stepInto = stepInto; + } + + private Map getDebugInfo(Map variables) { + List> varList = new ArrayList<>(); + Set> entries = variables.entrySet(); + for (Map.Entry entry : entries) { + Object value = entry.getValue(); + Map variable = new HashMap<>(); + variable.put("name", entry.getKey()); + if (value != null) { + variable.put("value", value); + variable.put("type", value.getClass()); + } else { + variable.put("value", "null"); + } + varList.add(variable); + } + Collections.reverse(varList); + Map info = new HashMap<>(); + info.put("variables", varList); + info.put("range", line); + return info; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptEngine.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptEngine.java new file mode 100644 index 0000000..762675f --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptEngine.java @@ -0,0 +1,269 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine; + +import cn.universal.core.engine.ScriptClass.ScriptAttribute; +import cn.universal.core.engine.ScriptClass.ScriptMethod; +import cn.universal.core.engine.reflection.JavaReflection; +import java.beans.Transient; +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import javax.script.AbstractScriptEngine; +import javax.script.Bindings; +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptException; +import javax.script.SimpleBindings; +import javax.script.SimpleScriptContext; + +public class MagicScriptEngine extends AbstractScriptEngine implements ScriptEngine, Compilable { + + private static final Map DEFAULT_IMPORTS = new ConcurrentHashMap<>(); + + private static Map classMap = null; + + private final MagicScriptEngineFactory magicScriptEngineFactory; + + public MagicScriptEngine(MagicScriptEngineFactory magicScriptEngineFactory) { + this.magicScriptEngineFactory = magicScriptEngineFactory; + } + + public static void addScriptClass(Class clazz) { + if (classMap == null) { + getScriptClassMap(); + } + getScriptClass(clazz) + .forEach(scriptClass -> classMap.put(scriptClass.getClassName(), scriptClass)); + } + + public static synchronized Map getScriptClassMap() { + if (classMap == null) { + classMap = new HashMap<>(); + Arrays.asList( + String.class, + Object.class, + Date.class, + Integer.class, + Double.class, + Float.class, + Long.class, + List.class, + Short.class, + Byte.class, + Boolean.class, + BigDecimal.class) + .forEach( + clazz -> + getScriptClass(clazz) + .forEach( + scriptClass -> classMap.put(scriptClass.getClassName(), scriptClass))); + } + return classMap; + } + + public static List getFunctions() { + return JavaReflection.getFunctions().stream() + .map(it -> new ScriptMethod(it.getExecutable())) + .collect(Collectors.toList()); + } + + public static Map getExtensionScriptClass() { + Map, List>> extensionMap = JavaReflection.getExtensionMap(); + Map classMap = new HashMap<>(); + for (Map.Entry, List>> entry : extensionMap.entrySet()) { + ScriptClass clazz = classMap.get(entry.getKey().getName()); + if (clazz == null) { + clazz = new ScriptClass(); + classMap.put(entry.getKey().getName(), clazz); + } + for (Class extensionClass : entry.getValue()) { + for (ScriptMethod method : getMethod(extensionClass)) { + clazz.addMethod(method); + } + } + } + return classMap; + } + + public static ScriptClass getScriptClassFromClass(Class clazz) { + Class superClass = clazz.getSuperclass(); + ScriptClass scriptClass = new ScriptClass(); + scriptClass.setClassName(clazz.getName()); + scriptClass.setSuperClass(superClass != null ? superClass.getName() : null); + appendMethod(clazz, scriptClass); + return scriptClass; + } + + public static List getScriptClass(Class clazz) { + List classList = new ArrayList<>(); + Class superClass; + do { + superClass = clazz.getSuperclass(); + ScriptClass scriptClass = new ScriptClass(); + scriptClass.setClassName(clazz.getName()); + scriptClass.setSuperClass(superClass != null ? superClass.getName() : null); + if (clazz.isEnum()) { + scriptClass.setEnums(clazz.getEnumConstants()); + } else { + appendAttributes(clazz, scriptClass); + } + appendMethod(clazz, scriptClass); + classList.add(scriptClass); + Class[] interfaces = clazz.getInterfaces(); + List interfaceList = new ArrayList<>(); + for (Class interfaceClazz : interfaces) { + classList.addAll(getScriptClass(interfaceClazz)); + interfaceList.add(interfaceClazz.getName()); + } + scriptClass.setInterfaces(interfaceList); + clazz = superClass; + } while (superClass != null && superClass != Object.class && superClass != Class.class); + return classList; + } + + private static void appendMethod(Class clazz, ScriptClass scriptClass) { + getMethod(clazz) + .forEach( + method -> { + scriptClass.addMethod(method); + String methodName = method.getName(); + if (method.getParameters().isEmpty() + && ((methodName.startsWith("get") && methodName.length() > 3) + || (methodName.startsWith("is") && methodName.length() > 2))) { + String attributeName = method.getName().substring(3); + attributeName = + attributeName.substring(0, 1).toLowerCase() + attributeName.substring(1); + if (!"class".equalsIgnoreCase(attributeName)) { + scriptClass.addAttribute( + new ScriptAttribute(method.getReturnType(), attributeName)); + } + } + }); + } + + public static Set getScriptClass(String className) { + try { + return new LinkedHashSet<>(getScriptClass(Class.forName(className))); + } catch (ClassNotFoundException e) { + return Collections.emptySet(); + } + } + + private static List getMethod(Class clazz) { + List methods = new ArrayList<>(); + try { + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method declaredMethod : declaredMethods) { + if (!Modifier.isVolatile(declaredMethod.getModifiers())) { + if (Modifier.isPublic(declaredMethod.getModifiers()) + && declaredMethod.getAnnotation(Transient.class) == null) { + if (Modifier.isPublic(declaredMethod.getModifiers())) { + methods.add(new ScriptMethod(declaredMethod)); + } + } + } + } + } catch (Exception ignored) { + } + return methods; + } + + private static void appendAttributes(Class clazz, ScriptClass target) { + try { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isPublic(field.getModifiers()) + && field.getAnnotation(Transient.class) == null) { + target.addAttribute(new ScriptAttribute(field.getType().getName(), field.getName())); + } + } + } catch (Exception ignored) { + } + } + + public static void addDefaultImport(String name, Object target) { + DEFAULT_IMPORTS.put(name, target); + } + + public static Map getDefaultImports() { + return DEFAULT_IMPORTS; + } + + public static Object execute(MagicScript magicScript, MagicScriptContext context) { + SimpleScriptContext simpleScriptContext = new SimpleScriptContext(); + simpleScriptContext.setAttribute(MagicScript.CONTEXT_ROOT, context, ScriptContext.ENGINE_SCOPE); + return magicScript.eval(simpleScriptContext); + } + + @Override + public Object eval(String script, ScriptContext context) throws ScriptException { + return compile(script).eval(context); + } + + @Override + public Object eval(Reader reader, ScriptContext context) throws ScriptException { + return compile(reader).eval(context); + } + + @Override + public Bindings createBindings() { + return new SimpleBindings(); + } + + @Override + public ScriptEngineFactory getFactory() { + return magicScriptEngineFactory; + } + + @Override + public CompiledScript compile(String script) { + return MagicScript.create(script, this); + } + + @Override + public CompiledScript compile(Reader script) { + return compile(readString(script)); + } + + private String readString(Reader reader) { + StringBuilder builder = new StringBuilder(); + char[] buf = new char[1024]; + int len; + try { + while ((len = reader.read(buf, 0, buf.length)) != -1) { + builder.append(buf, 0, len); + } + } catch (IOException ignored) { + + } + return builder.toString(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptEngineFactory.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptEngineFactory.java new file mode 100644 index 0000000..c77039c --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptEngineFactory.java @@ -0,0 +1,95 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; + +public class MagicScriptEngineFactory implements ScriptEngineFactory { + + @Override + public String getEngineName() { + return "MagicScript"; + } + + @Override + public String getEngineVersion() { + return MagicScriptEngineFactory.class.getPackage().getImplementationVersion(); + } + + @Override + public List getExtensions() { + return Collections.singletonList("ms"); + } + + @Override + public List getMimeTypes() { + return Collections.singletonList("application/magic-engine"); + } + + @Override + public List getNames() { + return Arrays.asList("MagicScript", "magic-engine"); + } + + @Override + public String getLanguageName() { + return "MagicScript"; + } + + @Override + public String getLanguageVersion() { + return Objects.toString( + MagicScriptEngineFactory.class.getPackage().getImplementationVersion(), "unknow"); + } + + @Override + public Object getParameter(String key) { + if (ScriptEngine.ENGINE.equals(key)) { + return getEngineName(); + } else if (ScriptEngine.ENGINE_VERSION.equals(key)) { + return getEngineVersion(); + } else if (ScriptEngine.LANGUAGE_VERSION.equals(key)) { + return getLanguageVersion(); + } else if (ScriptEngine.LANGUAGE.equals(key)) { + return getLanguageName(); + } else if ("THREADING".equals(key)) { + return null; + } + throw new IllegalArgumentException("Invalid key:" + key); + } + + @Override + public String getMethodCallSyntax(String obj, String m, String... args) { + throw new UnsupportedOperationException(); + } + + @Override + public String getOutputStatement(String toDisplay) { + throw new UnsupportedOperationException(); + } + + @Override + public String getProgram(String... statements) { + throw new UnsupportedOperationException(); + } + + @Override + public ScriptEngine getScriptEngine() { + return new MagicScriptEngine(this); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptError.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptError.java new file mode 100644 index 0000000..b5db800 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/MagicScriptError.java @@ -0,0 +1,129 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine; + +import cn.universal.core.engine.exception.MagicExitException; +import cn.universal.core.engine.exception.MagicScriptException; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.TokenStream; +import cn.universal.core.engine.runtime.MagicScriptRuntime; +import java.util.ArrayList; +import java.util.List; + +/** All errors reported by the library go through the static functions of this class. */ +public class MagicScriptError { + + /** + * Create an error message based on the provided message and stream, highlighting the line on + * which the error happened. If the stream has more tokens, the next token will be highlighted. + * Otherwise the end of the source of the stream will be highlighted. + * + *

Throws a {@link RuntimeException} + */ + public static void error(String message, TokenStream stream) { + if (stream.hasMore()) { + error(message, stream.consume().getSpan()); + } else { + error(message, stream.getPrev().getSpan()); + } + } + + /** + * Create an error message based on the provided message and location, highlighting the location + * in the line on which the error happened. Throws a {@link MagicScriptException} + */ + public static void error(String message, Span location, Throwable cause) { + cause = unwrap(cause); + if (cause instanceof MagicExitException) { + throw (MagicExitException) cause; + } + if (cause instanceof MagicScriptException) { + MagicScriptException mse = ((MagicScriptException) cause); + if (mse.getLocation() == null) { + error(message, location, cause.getCause()); + return; + } + throw mse; + } + String errorMessage = message; + if (location != null) { + Span.Line line = location.getLine(); + errorMessage += " at Row:"; + errorMessage += line.getLineNumber() + "~" + line.getEndLineNumber() + ",Col:"; + errorMessage += line.getStartCol() + "~" + line.getEndCol() + "\n\n"; + errorMessage += line.getText(); + errorMessage += "\n"; + int errorStart = location.getStart() - line.getStart(); + int errorEnd = errorStart + location.getText().length() - 1; + for (int i = 0, n = line.getText().length(); i < n; i++) { + boolean useTab = line.getText().charAt(i) == '\t'; + errorMessage += i >= errorStart && i <= errorEnd ? "^" : useTab ? "\t" : " "; + } + } + if (cause == null) { + throw new MagicScriptException(errorMessage, message, location); + } else { + throw new MagicScriptException(errorMessage, message, cause, location); + } + } + + /** + * Create an error message based on the provided message and location, highlighting the location + * in the line on which the error happened. Throws a {@link MagicScriptException} + */ + public static void error(String message, Span location) { + error(message, location, null); + } + + public static Throwable unwrap(Throwable root) { + Throwable parent = root; + while (parent != null) { + if (parent instanceof MagicScriptException) { + root = parent; + } + parent = parent.getCause(); + } + return root; + } + + public static void transfer(MagicScriptRuntime runtime, Throwable t) { + StackTraceElement[] elements = t.getStackTrace(); + Throwable cause = t; + while (cause.getCause() != null) { + cause = cause.getCause(); + } + Span span = null; + if (runtime != null) { + List elementList = new ArrayList<>(); + String className = runtime.getClass().getName(); + for (StackTraceElement element : elements) { + if (element.getLineNumber() > -1 && element.getClassName().equals(className)) { + Span currentSpan = runtime.getSpan(element.getLineNumber()); + elementList.add( + new StackTraceElement( + element.getClassName(), + element.getMethodName(), + element.getFileName(), + currentSpan.getLine().getLineNumber())); + if (span == null) { + span = currentSpan; + } + } else { + elementList.add(element); + } + } + cause.setStackTrace(elementList.toArray(new StackTraceElement[0])); + } + MagicScriptError.error(t.getMessage(), span, cause); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/ScriptClass.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/ScriptClass.java new file mode 100644 index 0000000..d46e5a1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/ScriptClass.java @@ -0,0 +1,295 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine; + +import cn.universal.core.engine.annotation.Comment; +import cn.universal.core.engine.runtime.RuntimeContext; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public class ScriptClass { + + private String className; + + private Set methods = new HashSet<>(); + + private Set attributes = new LinkedHashSet<>(); + + private String superClass; + + private List interfaces = new ArrayList<>(); + + private Object[] enums; + + private boolean module; + + public Object[] getEnums() { + return enums; + } + + public void setEnums(Object[] enums) { + this.enums = enums; + } + + public void addAttribute(ScriptAttribute attribute) { + this.attributes.add(attribute); + } + + public void addMethod(ScriptMethod method) { + this.methods.add(method); + } + + public void addInterface(String interfaceName) { + this.interfaces.add(interfaceName); + } + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public Set getMethods() { + return methods; + } + + public void setMethods(Set methods) { + this.methods = methods; + } + + public String getSuperClass() { + return superClass; + } + + public void setSuperClass(String superClass) { + this.superClass = superClass; + } + + public List getInterfaces() { + return interfaces; + } + + public void setInterfaces(List interfaces) { + this.interfaces = interfaces; + } + + public Set getAttributes() { + return attributes; + } + + public void setAttributes(Set attributes) { + this.attributes = attributes; + } + + public boolean isModule() { + return module; + } + + public void setModule(boolean module) { + this.module = module; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ScriptClass)) { + return false; + } + ScriptClass that = (ScriptClass) o; + return Objects.equals(className, that.className); + } + + @Override + public int hashCode() { + return Objects.hash(className); + } + + public static class ScriptAttribute { + + private final String type; + + private final String name; + + public ScriptAttribute(String type, String name) { + this.type = type; + this.name = name; + } + + public String getType() { + return type; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ScriptAttribute)) { + return false; + } + ScriptAttribute that = (ScriptAttribute) o; + return Objects.equals(type, that.type) && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(type, name); + } + } + + public static class ScriptMethod { + + private final String name; + + private final String returnType; + + private final String comment; + + private final boolean origin; + + private final boolean deprecated; + + private final List parameters = new ArrayList<>(); + + public ScriptMethod(Method method) { + this.name = method.getName(); + Comment methodComment = method.getAnnotation(Comment.class); + comment = methodComment == null ? null : methodComment.value(); + deprecated = method.getAnnotation(Deprecated.class) != null; + origin = methodComment != null && methodComment.origin(); + Class returnType = method.getReturnType(); + this.returnType = returnType.isArray() ? returnType.getSimpleName() : returnType.getName(); + Parameter[] parameters = method.getParameters(); + if (parameters != null) { + for (Parameter parameter : parameters) { + if (!RuntimeContext.class.isAssignableFrom(parameter.getType())) { + this.parameters.add(new ScriptMethodParameter(parameter)); + } + } + } + } + + public String getComment() { + return comment; + } + + public String getName() { + return name; + } + + public boolean isOrigin() { + return origin; + } + + public String getReturnType() { + return returnType; + } + + public List getParameters() { + return parameters; + } + + public boolean isDeprecated() { + return deprecated; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ScriptMethod that = (ScriptMethod) o; + return Objects.equals(name, that.name) && Objects.equals(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(name, parameters); + } + } + + public static class ScriptMethodParameter { + + private String name; + + private final String type; + + private final boolean varArgs; + + private String comment; + + public ScriptMethodParameter(Parameter parameter) { + Class type = parameter.getType(); + this.varArgs = parameter.isVarArgs(); + Comment parameterComment = parameter.getAnnotation(Comment.class); + this.name = parameter.getName(); + if (parameterComment != null) { + this.comment = parameterComment.value(); + if (!parameterComment.name().isEmpty()) { + this.name = parameterComment.name(); + } + } + this.type = type.isArray() ? type.getSimpleName() : type.getName(); + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public boolean isVarArgs() { + return varArgs; + } + + public String getComment() { + return comment; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ScriptMethodParameter that = (ScriptMethodParameter) o; + return Objects.equals(name, that.name) && Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + return Objects.hash(name, type); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/annotation/Comment.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/annotation/Comment.java new file mode 100644 index 0000000..37932b7 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/annotation/Comment.java @@ -0,0 +1,33 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** 方法、字段、参数注释 */ +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Comment { + + String value(); + + String name() default ""; + + /** 是否返回原类型 */ + boolean origin() default false; +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/annotation/Function.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/annotation/Function.java new file mode 100644 index 0000000..ea30ec8 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/annotation/Function.java @@ -0,0 +1,25 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** 标识是个函数 */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Function {} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/AnnotationVisitor.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/AnnotationVisitor.java new file mode 100644 index 0000000..dc221a9 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/AnnotationVisitor.java @@ -0,0 +1,139 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A visitor to visit a Java annotation. The methods of this class must be called in the following + * order: ( {@code visit} | {@code visitEnum} | {@code visitAnnotation} | {@code visitArray} )* + * {@code visitEnd}. @Author Eric Bruneton @Author Eugene Kuleshov + */ +public abstract class AnnotationVisitor { + + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + protected final int api; + + /** + * The annotation visitor to which this visitor must delegate method calls. May be {@literal + * null}. + */ + protected AnnotationVisitor av; + + /** + * Constructs a new {@link AnnotationVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + public AnnotationVisitor(final int api) { + this(api, null); + } + + /** + * Constructs a new {@link AnnotationVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + * @param annotationVisitor the annotation visitor to which this visitor must delegate method + * calls. May be {@literal null}. + */ + public AnnotationVisitor(final int api, final AnnotationVisitor annotationVisitor) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + if (api == Opcodes.ASM10_EXPERIMENTAL) { + Constants.checkAsmExperimental(this); + } + this.api = api; + this.av = annotationVisitor; + } + + /** + * Visits a primitive value of the annotation. + * + * @param name the value name. + * @param value the actual value, whose type must be {@link Byte}, {@link Boolean}, {@link + * Character}, {@link Short}, {@link Integer} , {@link Long}, {@link Float}, {@link Double}, + * {@link String} or {@link Type} of {@link Type#OBJECT} or {@link Type#ARRAY} sort. This + * value can also be an array of byte, boolean, short, char, int, long, float or double values + * (this is equivalent to using {@link #visitArray} and visiting each array element in turn, + * but is more convenient). + */ + public void visit(final String name, final Object value) { + if (av != null) { + av.visit(name, value); + } + } + + /** + * Visits an enumeration value of the annotation. + * + * @param name the value name. + * @param descriptor the class descriptor of the enumeration class. + * @param value the actual enumeration value. + */ + public void visitEnum(final String name, final String descriptor, final String value) { + if (av != null) { + av.visitEnum(name, descriptor, value); + } + } + + /** + * Visits a nested annotation value of the annotation. + * + * @param name the value name. + * @param descriptor the class descriptor of the nested annotation class. + * @return a visitor to visit the actual nested annotation value, or {@literal null} if this + * visitor is not interested in visiting this nested annotation. The nested annotation + * value must be fully visited before calling other methods on this annotation visitor. + */ + public AnnotationVisitor visitAnnotation(final String name, final String descriptor) { + if (av != null) { + return av.visitAnnotation(name, descriptor); + } + return null; + } + + /** + * Visits an array value of the annotation. Note that arrays of primitive values (such as byte, + * boolean, short, char, int, long, float or double) can be passed as value to {@link #visit + * visit}. This is what {@link ClassReader} does for non empty arrays of primitive values. + * + * @param name the value name. + * @return a visitor to visit the actual array value elements, or {@literal null} if this visitor + * is not interested in visiting these values. The 'name' parameters passed to the methods of + * this visitor are ignored. All the array values must be visited before calling other + * methods on this annotation visitor. + */ + public AnnotationVisitor visitArray(final String name) { + if (av != null) { + return av.visitArray(name); + } + return null; + } + + /** Visits the end of the annotation. */ + public void visitEnd() { + if (av != null) { + av.visitEnd(); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/AnnotationWriter.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/AnnotationWriter.java new file mode 100644 index 0000000..8d6c7f6 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/AnnotationWriter.java @@ -0,0 +1,537 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * An {@link AnnotationVisitor} that generates a corresponding 'annotation' or 'type_annotation' + * structure, as defined in the Java Virtual Machine Specification (JVMS). AnnotationWriter + * instances can be chained in a doubly linked list, from which Runtime[In]Visible[Type]Annotations + * attributes can be generated with the {@link #putAnnotations} method. Similarly, arrays of such + * lists can be used to generate Runtime[In]VisibleParameterAnnotations attributes. @Author Eric + * Bruneton @Author Eugene Kuleshov + * + * @see JVMS + * 4.7.16 + * @see JVMS + * 4.7.20 + */ +final class AnnotationWriter extends AnnotationVisitor { + + /** Where the constants used in this AnnotationWriter must be stored. */ + private final SymbolTable symbolTable; + + /** + * Whether values are named or not. AnnotationWriter instances used for annotation default and + * annotation arrays use unnamed values (i.e. they generate an 'element_value' structure for each + * value, instead of an element_name_index followed by an element_value). + */ + private final boolean useNamedValues; + + /** + * The 'annotation' or 'type_annotation' JVMS structure corresponding to the annotation values + * visited so far. All the fields of these structures, except the last one - the + * element_value_pairs array, must be set before this ByteVector is passed to the constructor + * (num_element_value_pairs can be set to 0, it is reset to the correct value in {@link + * #visitEnd()}). The element_value_pairs array is filled incrementally in the various visit() + * methods. + * + *

Note: as an exception to the above rules, for AnnotationDefault attributes (which contain a + * single element_value by definition), this ByteVector is initially empty when passed to the + * constructor, and {@link #numElementValuePairsOffset} is set to -1. + */ + private final ByteVector annotation; + + /** + * The offset in {@link #annotation} where {@link #numElementValuePairs} must be stored (or -1 for + * the case of AnnotationDefault attributes). + */ + private final int numElementValuePairsOffset; + + /** The number of element value pairs visited so far. */ + private int numElementValuePairs; + + /** + * The previous AnnotationWriter. This field is used to store the list of annotations of a + * Runtime[In]Visible[Type]Annotations attribute. It is unused for nested or array annotations + * (annotation values of annotation type), or for AnnotationDefault attributes. + */ + private final AnnotationWriter previousAnnotation; + + /** + * The next AnnotationWriter. This field is used to store the list of annotations of a + * Runtime[In]Visible[Type]Annotations attribute. It is unused for nested or array annotations + * (annotation values of annotation type), or for AnnotationDefault attributes. + */ + private AnnotationWriter nextAnnotation; + + // ----------------------------------------------------------------------------------------------- + // Constructors and factories + // ----------------------------------------------------------------------------------------------- + + /** + * Constructs a new {@link AnnotationWriter}. + * + * @param symbolTable where the constants used in this AnnotationWriter must be stored. + * @param useNamedValues whether values are named or not. AnnotationDefault and annotation arrays + * use unnamed values. + * @param annotation where the 'annotation' or 'type_annotation' JVMS structure corresponding to + * the visited content must be stored. This ByteVector must already contain all the fields of + * the structure except the last one (the element_value_pairs array). + * @param previousAnnotation the previously visited annotation of the + * Runtime[In]Visible[Type]Annotations attribute to which this annotation belongs, or + * {@literal null} in other cases (e.g. nested or array annotations). + */ + AnnotationWriter( + final SymbolTable symbolTable, + final boolean useNamedValues, + final ByteVector annotation, + final AnnotationWriter previousAnnotation) { + super(/* latest api = */ Opcodes.ASM9); + this.symbolTable = symbolTable; + this.useNamedValues = useNamedValues; + this.annotation = annotation; + // By hypothesis, num_element_value_pairs is stored in the last unsigned short of 'annotation'. + this.numElementValuePairsOffset = annotation.length == 0 ? -1 : annotation.length - 2; + this.previousAnnotation = previousAnnotation; + if (previousAnnotation != null) { + previousAnnotation.nextAnnotation = this; + } + } + + /** + * Creates a new {@link AnnotationWriter} using named values. + * + * @param symbolTable where the constants used in this AnnotationWriter must be stored. + * @param descriptor the class descriptor of the annotation class. + * @param previousAnnotation the previously visited annotation of the + * Runtime[In]Visible[Type]Annotations attribute to which this annotation belongs, or + * {@literal null} in other cases (e.g. nested or array annotations). + * @return a new {@link AnnotationWriter} for the given annotation descriptor. + */ + static AnnotationWriter create( + final SymbolTable symbolTable, + final String descriptor, + final AnnotationWriter previousAnnotation) { + // Create a ByteVector to hold an 'annotation' JVMS structure. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.16. + ByteVector annotation = new ByteVector(); + // Write type_index and reserve space for num_element_value_pairs. + annotation.putShort(symbolTable.addConstantUtf8(descriptor)).putShort(0); + return new AnnotationWriter( + symbolTable, /* useNamedValues= */ true, annotation, previousAnnotation); + } + + /** + * Creates a new {@link AnnotationWriter} using named values. + * + * @param symbolTable where the constants used in this AnnotationWriter must be stored. + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#CLASS_TYPE_PARAMETER}, {@link + * TypeReference#CLASS_TYPE_PARAMETER_BOUND} or {@link TypeReference#CLASS_EXTENDS}. See + * {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param previousAnnotation the previously visited annotation of the + * Runtime[In]Visible[Type]Annotations attribute to which this annotation belongs, or + * {@literal null} in other cases (e.g. nested or array annotations). + * @return a new {@link AnnotationWriter} for the given type annotation reference and descriptor. + */ + static AnnotationWriter create( + final SymbolTable symbolTable, + final int typeRef, + final TypePath typePath, + final String descriptor, + final AnnotationWriter previousAnnotation) { + // Create a ByteVector to hold a 'type_annotation' JVMS structure. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.20. + ByteVector typeAnnotation = new ByteVector(); + // Write target_type, target_info, and target_path. + TypeReference.putTarget(typeRef, typeAnnotation); + TypePath.put(typePath, typeAnnotation); + // Write type_index and reserve space for num_element_value_pairs. + typeAnnotation.putShort(symbolTable.addConstantUtf8(descriptor)).putShort(0); + return new AnnotationWriter( + symbolTable, /* useNamedValues= */ true, typeAnnotation, previousAnnotation); + } + + // ----------------------------------------------------------------------------------------------- + // Implementation of the AnnotationVisitor abstract class + // ----------------------------------------------------------------------------------------------- + + @Override + public void visit(final String name, final Object value) { + // Case of an element_value with a const_value_index, class_info_index or array_index field. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.16.1. + ++numElementValuePairs; + if (useNamedValues) { + annotation.putShort(symbolTable.addConstantUtf8(name)); + } + if (value instanceof String) { + annotation.put12('s', symbolTable.addConstantUtf8((String) value)); + } else if (value instanceof Byte) { + annotation.put12('B', symbolTable.addConstantInteger(((Byte) value).byteValue()).index); + } else if (value instanceof Boolean) { + int booleanValue = ((Boolean) value).booleanValue() ? 1 : 0; + annotation.put12('Z', symbolTable.addConstantInteger(booleanValue).index); + } else if (value instanceof Character) { + annotation.put12('C', symbolTable.addConstantInteger(((Character) value).charValue()).index); + } else if (value instanceof Short) { + annotation.put12('S', symbolTable.addConstantInteger(((Short) value).shortValue()).index); + } else if (value instanceof Type) { + annotation.put12('c', symbolTable.addConstantUtf8(((Type) value).getDescriptor())); + } else if (value instanceof byte[]) { + byte[] byteArray = (byte[]) value; + annotation.put12('[', byteArray.length); + for (byte byteValue : byteArray) { + annotation.put12('B', symbolTable.addConstantInteger(byteValue).index); + } + } else if (value instanceof boolean[]) { + boolean[] booleanArray = (boolean[]) value; + annotation.put12('[', booleanArray.length); + for (boolean booleanValue : booleanArray) { + annotation.put12('Z', symbolTable.addConstantInteger(booleanValue ? 1 : 0).index); + } + } else if (value instanceof short[]) { + short[] shortArray = (short[]) value; + annotation.put12('[', shortArray.length); + for (short shortValue : shortArray) { + annotation.put12('S', symbolTable.addConstantInteger(shortValue).index); + } + } else if (value instanceof char[]) { + char[] charArray = (char[]) value; + annotation.put12('[', charArray.length); + for (char charValue : charArray) { + annotation.put12('C', symbolTable.addConstantInteger(charValue).index); + } + } else if (value instanceof int[]) { + int[] intArray = (int[]) value; + annotation.put12('[', intArray.length); + for (int intValue : intArray) { + annotation.put12('I', symbolTable.addConstantInteger(intValue).index); + } + } else if (value instanceof long[]) { + long[] longArray = (long[]) value; + annotation.put12('[', longArray.length); + for (long longValue : longArray) { + annotation.put12('J', symbolTable.addConstantLong(longValue).index); + } + } else if (value instanceof float[]) { + float[] floatArray = (float[]) value; + annotation.put12('[', floatArray.length); + for (float floatValue : floatArray) { + annotation.put12('F', symbolTable.addConstantFloat(floatValue).index); + } + } else if (value instanceof double[]) { + double[] doubleArray = (double[]) value; + annotation.put12('[', doubleArray.length); + for (double doubleValue : doubleArray) { + annotation.put12('D', symbolTable.addConstantDouble(doubleValue).index); + } + } else { + Symbol symbol = symbolTable.addConstant(value); + annotation.put12(".s.IFJDCS".charAt(symbol.tag), symbol.index); + } + } + + @Override + public void visitEnum(final String name, final String descriptor, final String value) { + // Case of an element_value with an enum_const_value field. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.16.1. + ++numElementValuePairs; + if (useNamedValues) { + annotation.putShort(symbolTable.addConstantUtf8(name)); + } + annotation + .put12('e', symbolTable.addConstantUtf8(descriptor)) + .putShort(symbolTable.addConstantUtf8(value)); + } + + @Override + public AnnotationVisitor visitAnnotation(final String name, final String descriptor) { + // Case of an element_value with an annotation_value field. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.16.1. + ++numElementValuePairs; + if (useNamedValues) { + annotation.putShort(symbolTable.addConstantUtf8(name)); + } + // Write tag and type_index, and reserve 2 bytes for num_element_value_pairs. + annotation.put12('@', symbolTable.addConstantUtf8(descriptor)).putShort(0); + return new AnnotationWriter(symbolTable, /* useNamedValues= */ true, annotation, null); + } + + @Override + public AnnotationVisitor visitArray(final String name) { + // Case of an element_value with an array_value field. + // https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.16.1 + ++numElementValuePairs; + if (useNamedValues) { + annotation.putShort(symbolTable.addConstantUtf8(name)); + } + // Write tag, and reserve 2 bytes for num_values. Here we take advantage of the fact that the + // end of an element_value of array type is similar to the end of an 'annotation' structure: an + // unsigned short num_values followed by num_values element_value, versus an unsigned short + // num_element_value_pairs, followed by num_element_value_pairs { element_name_index, + // element_value } tuples. This allows us to use an AnnotationWriter with unnamed values to + // visit the array elements. Its num_element_value_pairs will correspond to the number of array + // elements and will be stored in what is in fact num_values. + annotation.put12('[', 0); + return new AnnotationWriter(symbolTable, /* useNamedValues= */ false, annotation, null); + } + + @Override + public void visitEnd() { + if (numElementValuePairsOffset != -1) { + byte[] data = annotation.data; + data[numElementValuePairsOffset] = (byte) (numElementValuePairs >>> 8); + data[numElementValuePairsOffset + 1] = (byte) numElementValuePairs; + } + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the size of a Runtime[In]Visible[Type]Annotations attribute containing this annotation + * and all its predecessors (see {@link #previousAnnotation}. Also adds the attribute name + * to the constant pool of the class (if not null). + * + * @param attributeName one of "Runtime[In]Visible[Type]Annotations", or {@literal null}. + * @return the size in bytes of a Runtime[In]Visible[Type]Annotations attribute containing this + * annotation and all its predecessors. This includes the size of the attribute_name_index and + * attribute_length fields. + */ + int computeAnnotationsSize(final String attributeName) { + if (attributeName != null) { + symbolTable.addConstantUtf8(attributeName); + } + // The attribute_name_index, attribute_length and num_annotations fields use 8 bytes. + int attributeSize = 8; + AnnotationWriter annotationWriter = this; + while (annotationWriter != null) { + attributeSize += annotationWriter.annotation.length; + annotationWriter = annotationWriter.previousAnnotation; + } + return attributeSize; + } + + /** + * Returns the size of the Runtime[In]Visible[Type]Annotations attributes containing the given + * annotations and all their predecessors (see {@link #previousAnnotation}. Also adds the + * attribute names to the constant pool of the class (if not null). + * + * @param lastRuntimeVisibleAnnotation The last runtime visible annotation of a field, method or + * class. The previous ones can be accessed with the {@link #previousAnnotation} field. May be + * {@literal null}. + * @param lastRuntimeInvisibleAnnotation The last runtime invisible annotation of this a field, + * method or class. The previous ones can be accessed with the {@link #previousAnnotation} + * field. May be {@literal null}. + * @param lastRuntimeVisibleTypeAnnotation The last runtime visible type annotation of this a + * field, method or class. The previous ones can be accessed with the {@link + * #previousAnnotation} field. May be {@literal null}. + * @param lastRuntimeInvisibleTypeAnnotation The last runtime invisible type annotation of a + * field, method or class field. The previous ones can be accessed with the {@link + * #previousAnnotation} field. May be {@literal null}. + * @return the size in bytes of a Runtime[In]Visible[Type]Annotations attribute containing the + * given annotations and all their predecessors. This includes the size of the + * attribute_name_index and attribute_length fields. + */ + static int computeAnnotationsSize( + final AnnotationWriter lastRuntimeVisibleAnnotation, + final AnnotationWriter lastRuntimeInvisibleAnnotation, + final AnnotationWriter lastRuntimeVisibleTypeAnnotation, + final AnnotationWriter lastRuntimeInvisibleTypeAnnotation) { + int size = 0; + if (lastRuntimeVisibleAnnotation != null) { + size += + lastRuntimeVisibleAnnotation.computeAnnotationsSize( + Constants.RUNTIME_VISIBLE_ANNOTATIONS); + } + if (lastRuntimeInvisibleAnnotation != null) { + size += + lastRuntimeInvisibleAnnotation.computeAnnotationsSize( + Constants.RUNTIME_INVISIBLE_ANNOTATIONS); + } + if (lastRuntimeVisibleTypeAnnotation != null) { + size += + lastRuntimeVisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + } + if (lastRuntimeInvisibleTypeAnnotation != null) { + size += + lastRuntimeInvisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS); + } + return size; + } + + /** + * Puts a Runtime[In]Visible[Type]Annotations attribute containing this annotations and all its + * predecessors (see {@link #previousAnnotation} in the given ByteVector. Annotations are + * put in the same order they have been visited. + * + * @param attributeNameIndex the constant pool index of the attribute name (one of + * "Runtime[In]Visible[Type]Annotations"). + * @param output where the attribute must be put. + */ + void putAnnotations(final int attributeNameIndex, final ByteVector output) { + int attributeLength = 2; // For num_annotations. + int numAnnotations = 0; + AnnotationWriter annotationWriter = this; + AnnotationWriter firstAnnotation = null; + while (annotationWriter != null) { + // In case the user forgot to call visitEnd(). + annotationWriter.visitEnd(); + attributeLength += annotationWriter.annotation.length; + numAnnotations++; + firstAnnotation = annotationWriter; + annotationWriter = annotationWriter.previousAnnotation; + } + output.putShort(attributeNameIndex); + output.putInt(attributeLength); + output.putShort(numAnnotations); + annotationWriter = firstAnnotation; + while (annotationWriter != null) { + output.putByteArray(annotationWriter.annotation.data, 0, annotationWriter.annotation.length); + annotationWriter = annotationWriter.nextAnnotation; + } + } + + /** + * Puts the Runtime[In]Visible[Type]Annotations attributes containing the given annotations and + * all their predecessors (see {@link #previousAnnotation} in the given ByteVector. + * Annotations are put in the same order they have been visited. + * + * @param symbolTable where the constants used in the AnnotationWriter instances are stored. + * @param lastRuntimeVisibleAnnotation The last runtime visible annotation of a field, method or + * class. The previous ones can be accessed with the {@link #previousAnnotation} field. May be + * {@literal null}. + * @param lastRuntimeInvisibleAnnotation The last runtime invisible annotation of this a field, + * method or class. The previous ones can be accessed with the {@link #previousAnnotation} + * field. May be {@literal null}. + * @param lastRuntimeVisibleTypeAnnotation The last runtime visible type annotation of this a + * field, method or class. The previous ones can be accessed with the {@link + * #previousAnnotation} field. May be {@literal null}. + * @param lastRuntimeInvisibleTypeAnnotation The last runtime invisible type annotation of a + * field, method or class field. The previous ones can be accessed with the {@link + * #previousAnnotation} field. May be {@literal null}. + * @param output where the attributes must be put. + */ + static void putAnnotations( + final SymbolTable symbolTable, + final AnnotationWriter lastRuntimeVisibleAnnotation, + final AnnotationWriter lastRuntimeInvisibleAnnotation, + final AnnotationWriter lastRuntimeVisibleTypeAnnotation, + final AnnotationWriter lastRuntimeInvisibleTypeAnnotation, + final ByteVector output) { + if (lastRuntimeVisibleAnnotation != null) { + lastRuntimeVisibleAnnotation.putAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_VISIBLE_ANNOTATIONS), output); + } + if (lastRuntimeInvisibleAnnotation != null) { + lastRuntimeInvisibleAnnotation.putAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_INVISIBLE_ANNOTATIONS), output); + } + if (lastRuntimeVisibleTypeAnnotation != null) { + lastRuntimeVisibleTypeAnnotation.putAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS), output); + } + if (lastRuntimeInvisibleTypeAnnotation != null) { + lastRuntimeInvisibleTypeAnnotation.putAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS), output); + } + } + + /** + * Returns the size of a Runtime[In]VisibleParameterAnnotations attribute containing all the + * annotation lists from the given AnnotationWriter sub-array. Also adds the attribute name to the + * constant pool of the class. + * + * @param attributeName one of "Runtime[In]VisibleParameterAnnotations". + * @param annotationWriters an array of AnnotationWriter lists (designated by their last + * element). + * @param annotableParameterCount the number of elements in annotationWriters to take into account + * (elements [0..annotableParameterCount[ are taken into account). + * @return the size in bytes of a Runtime[In]VisibleParameterAnnotations attribute corresponding + * to the given sub-array of AnnotationWriter lists. This includes the size of the + * attribute_name_index and attribute_length fields. + */ + static int computeParameterAnnotationsSize( + final String attributeName, + final AnnotationWriter[] annotationWriters, + final int annotableParameterCount) { + // Note: attributeName is added to the constant pool by the call to computeAnnotationsSize + // below. This assumes that there is at least one non-null element in the annotationWriters + // sub-array (which is ensured by the lazy instantiation of this array in MethodWriter). + // The attribute_name_index, attribute_length and num_parameters fields use 7 bytes, and each + // element of the parameter_annotations array uses 2 bytes for its num_annotations field. + int attributeSize = 7 + 2 * annotableParameterCount; + for (int i = 0; i < annotableParameterCount; ++i) { + AnnotationWriter annotationWriter = annotationWriters[i]; + attributeSize += + annotationWriter == null ? 0 : annotationWriter.computeAnnotationsSize(attributeName) - 8; + } + return attributeSize; + } + + /** + * Puts a Runtime[In]VisibleParameterAnnotations attribute containing all the annotation lists + * from the given AnnotationWriter sub-array in the given ByteVector. + * + * @param attributeNameIndex constant pool index of the attribute name (one of + * Runtime[In]VisibleParameterAnnotations). + * @param annotationWriters an array of AnnotationWriter lists (designated by their last + * element). + * @param annotableParameterCount the number of elements in annotationWriters to put (elements + * [0..annotableParameterCount[ are put). + * @param output where the attribute must be put. + */ + static void putParameterAnnotations( + final int attributeNameIndex, + final AnnotationWriter[] annotationWriters, + final int annotableParameterCount, + final ByteVector output) { + // The num_parameters field uses 1 byte, and each element of the parameter_annotations array + // uses 2 bytes for its num_annotations field. + int attributeLength = 1 + 2 * annotableParameterCount; + for (int i = 0; i < annotableParameterCount; ++i) { + AnnotationWriter annotationWriter = annotationWriters[i]; + attributeLength += + annotationWriter == null ? 0 : annotationWriter.computeAnnotationsSize(null) - 8; + } + output.putShort(attributeNameIndex); + output.putInt(attributeLength); + output.putByte(annotableParameterCount); + for (int i = 0; i < annotableParameterCount; ++i) { + AnnotationWriter annotationWriter = annotationWriters[i]; + AnnotationWriter firstAnnotation = null; + int numAnnotations = 0; + while (annotationWriter != null) { + // In case user the forgot to call visitEnd(). + annotationWriter.visitEnd(); + numAnnotations++; + firstAnnotation = annotationWriter; + annotationWriter = annotationWriter.previousAnnotation; + } + output.putShort(numAnnotations); + annotationWriter = firstAnnotation; + while (annotationWriter != null) { + output.putByteArray( + annotationWriter.annotation.data, 0, annotationWriter.annotation.length); + annotationWriter = annotationWriter.nextAnnotation; + } + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Attribute.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Attribute.java new file mode 100644 index 0000000..4828c41 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Attribute.java @@ -0,0 +1,375 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A non standard class, field, method or Code attribute, as defined in the Java Virtual Machine + * Specification (JVMS). @Author Eric Bruneton @Author Eugene Kuleshov + * + * @see JVMS + * 4.7 + * @see JVMS + * 4.7.3 + */ +public class Attribute { + + /** The type of this attribute, also called its name in the JVMS. */ + public final String type; + + /** + * The raw content of this attribute, only used for unknown attributes (see {@link #isUnknown()}). + * The 6 header bytes of the attribute (attribute_name_index and attribute_length) are not + * included. + */ + private byte[] content; + + /** + * The next attribute in this attribute list (Attribute instances can be linked via this field to + * store a list of class, field, method or Code attributes). May be {@literal null}. + */ + Attribute nextAttribute; + + /** + * Constructs a new empty attribute. + * + * @param type the type of the attribute. + */ + protected Attribute(final String type) { + this.type = type; + } + + /** + * Returns {@literal true} if this type of attribute is unknown. This means that the attribute + * content can't be parsed to extract constant pool references, labels, etc. Instead, the + * attribute content is read as an opaque byte array, and written back as is. This can lead to + * invalid attributes, if the content actually contains constant pool references, labels, or other + * symbolic references that need to be updated when there are changes to the constant pool, the + * method bytecode, etc. The default implementation of this method always returns {@literal true}. + * + * @return {@literal true} if this type of attribute is unknown. + */ + public boolean isUnknown() { + return true; + } + + /** + * Returns {@literal true} if this type of attribute is a Code attribute. + * + * @return {@literal true} if this type of attribute is a Code attribute. + */ + public boolean isCodeAttribute() { + return false; + } + + /** + * Returns the labels corresponding to this attribute. + * + * @return the labels corresponding to this attribute, or {@literal null} if this attribute is not + * a Code attribute that contains labels. + */ + protected Label[] getLabels() { + return new Label[0]; + } + + /** + * Reads a {@link #type} attribute. This method must return a new {@link Attribute} object, + * of type {@link #type}, corresponding to the 'length' bytes starting at 'offset', in the given + * ClassReader. + * + * @param classReader the class that contains the attribute to be read. + * @param offset index of the first byte of the attribute's content in {@link ClassReader}. The 6 + * attribute header bytes (attribute_name_index and attribute_length) are not taken into + * account here. + * @param length the length of the attribute's content (excluding the 6 attribute header bytes). + * @param charBuffer the buffer to be used to call the ClassReader methods requiring a + * 'charBuffer' parameter. + * @param codeAttributeOffset index of the first byte of content of the enclosing Code attribute + * in {@link ClassReader}, or -1 if the attribute to be read is not a Code attribute. The 6 + * attribute header bytes (attribute_name_index and attribute_length) are not taken into + * account here. + * @param labels the labels of the method's code, or {@literal null} if the attribute to be read + * is not a Code attribute. + * @return a new {@link Attribute} object corresponding to the specified bytes. + */ + protected Attribute read( + final ClassReader classReader, + final int offset, + final int length, + final char[] charBuffer, + final int codeAttributeOffset, + final Label[] labels) { + Attribute attribute = new Attribute(type); + attribute.content = new byte[length]; + System.arraycopy(classReader.classFileBuffer, offset, attribute.content, 0, length); + return attribute; + } + + /** + * Returns the byte array form of the content of this attribute. The 6 header bytes + * (attribute_name_index and attribute_length) must not be added in the returned + * ByteVector. + * + * @param classWriter the class to which this attribute must be added. This parameter can be used + * to add the items that corresponds to this attribute to the constant pool of this class. + * @param code the bytecode of the method corresponding to this Code attribute, or {@literal null} + * if this attribute is not a Code attribute. Corresponds to the 'code' field of the Code + * attribute. + * @param codeLength the length of the bytecode of the method corresponding to this code + * attribute, or 0 if this attribute is not a Code attribute. Corresponds to the 'code_length' + * field of the Code attribute. + * @param maxStack the maximum stack size of the method corresponding to this Code attribute, or + * -1 if this attribute is not a Code attribute. + * @param maxLocals the maximum number of local variables of the method corresponding to this code + * attribute, or -1 if this attribute is not a Code attribute. + * @return the byte array form of this attribute. + */ + protected ByteVector write( + final ClassWriter classWriter, + final byte[] code, + final int codeLength, + final int maxStack, + final int maxLocals) { + return new ByteVector(content); + } + + /** + * Returns the number of attributes of the attribute list that begins with this attribute. + * + * @return the number of attributes of the attribute list that begins with this attribute. + */ + final int getAttributeCount() { + int count = 0; + Attribute attribute = this; + while (attribute != null) { + count += 1; + attribute = attribute.nextAttribute; + } + return count; + } + + /** + * Returns the total size in bytes of all the attributes in the attribute list that begins with + * this attribute. This size includes the 6 header bytes (attribute_name_index and + * attribute_length) per attribute. Also adds the attribute type names to the constant pool. + * + * @param symbolTable where the constants used in the attributes must be stored. + * @return the size of all the attributes in this attribute list. This size includes the size of + * the attribute headers. + */ + final int computeAttributesSize(final SymbolTable symbolTable) { + final byte[] code = null; + final int codeLength = 0; + final int maxStack = -1; + final int maxLocals = -1; + return computeAttributesSize(symbolTable, code, codeLength, maxStack, maxLocals); + } + + /** + * Returns the total size in bytes of all the attributes in the attribute list that begins with + * this attribute. This size includes the 6 header bytes (attribute_name_index and + * attribute_length) per attribute. Also adds the attribute type names to the constant pool. + * + * @param symbolTable where the constants used in the attributes must be stored. + * @param code the bytecode of the method corresponding to these Code attributes, or {@literal + * null} if they are not Code attributes. Corresponds to the 'code' field of the Code + * attribute. + * @param codeLength the length of the bytecode of the method corresponding to these code + * attributes, or 0 if they are not Code attributes. Corresponds to the 'code_length' field of + * the Code attribute. + * @param maxStack the maximum stack size of the method corresponding to these Code attributes, or + * -1 if they are not Code attributes. + * @param maxLocals the maximum number of local variables of the method corresponding to these + * Code attributes, or -1 if they are not Code attribute. + * @return the size of all the attributes in this attribute list. This size includes the size of + * the attribute headers. + */ + final int computeAttributesSize( + final SymbolTable symbolTable, + final byte[] code, + final int codeLength, + final int maxStack, + final int maxLocals) { + final ClassWriter classWriter = symbolTable.classWriter; + int size = 0; + Attribute attribute = this; + while (attribute != null) { + symbolTable.addConstantUtf8(attribute.type); + size += 6 + attribute.write(classWriter, code, codeLength, maxStack, maxLocals).length; + attribute = attribute.nextAttribute; + } + return size; + } + + /** + * Returns the total size in bytes of all the attributes that correspond to the given field, + * method or class access flags and signature. This size includes the 6 header bytes + * (attribute_name_index and attribute_length) per attribute. Also adds the attribute type names + * to the constant pool. + * + * @param symbolTable where the constants used in the attributes must be stored. + * @param accessFlags some field, method or class access flags. + * @param signatureIndex the constant pool index of a field, method of class signature. + * @return the size of all the attributes in bytes. This size includes the size of the attribute + * headers. + */ + static int computeAttributesSize( + final SymbolTable symbolTable, final int accessFlags, final int signatureIndex) { + int size = 0; + // Before Java 1.5, synthetic fields are represented with a Synthetic attribute. + if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0 + && symbolTable.getMajorVersion() < Opcodes.V1_5) { + // Synthetic attributes always use 6 bytes. + symbolTable.addConstantUtf8(Constants.SYNTHETIC); + size += 6; + } + if (signatureIndex != 0) { + // Signature attributes always use 8 bytes. + symbolTable.addConstantUtf8(Constants.SIGNATURE); + size += 8; + } + // ACC_DEPRECATED is ASM specific, the ClassFile format uses a Deprecated attribute instead. + if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) { + // Deprecated attributes always use 6 bytes. + symbolTable.addConstantUtf8(Constants.DEPRECATED); + size += 6; + } + return size; + } + + /** + * Puts all the attributes of the attribute list that begins with this attribute, in the given + * byte vector. This includes the 6 header bytes (attribute_name_index and attribute_length) per + * attribute. + * + * @param symbolTable where the constants used in the attributes must be stored. + * @param output where the attributes must be written. + */ + final void putAttributes(final SymbolTable symbolTable, final ByteVector output) { + final byte[] code = null; + final int codeLength = 0; + final int maxStack = -1; + final int maxLocals = -1; + putAttributes(symbolTable, code, codeLength, maxStack, maxLocals, output); + } + + /** + * Puts all the attributes of the attribute list that begins with this attribute, in the given + * byte vector. This includes the 6 header bytes (attribute_name_index and attribute_length) per + * attribute. + * + * @param symbolTable where the constants used in the attributes must be stored. + * @param code the bytecode of the method corresponding to these Code attributes, or {@literal + * null} if they are not Code attributes. Corresponds to the 'code' field of the Code + * attribute. + * @param codeLength the length of the bytecode of the method corresponding to these code + * attributes, or 0 if they are not Code attributes. Corresponds to the 'code_length' field of + * the Code attribute. + * @param maxStack the maximum stack size of the method corresponding to these Code attributes, or + * -1 if they are not Code attributes. + * @param maxLocals the maximum number of local variables of the method corresponding to these + * Code attributes, or -1 if they are not Code attribute. + * @param output where the attributes must be written. + */ + final void putAttributes( + final SymbolTable symbolTable, + final byte[] code, + final int codeLength, + final int maxStack, + final int maxLocals, + final ByteVector output) { + final ClassWriter classWriter = symbolTable.classWriter; + Attribute attribute = this; + while (attribute != null) { + ByteVector attributeContent = + attribute.write(classWriter, code, codeLength, maxStack, maxLocals); + // Put attribute_name_index and attribute_length. + output.putShort(symbolTable.addConstantUtf8(attribute.type)).putInt(attributeContent.length); + output.putByteArray(attributeContent.data, 0, attributeContent.length); + attribute = attribute.nextAttribute; + } + } + + /** + * Puts all the attributes that correspond to the given field, method or class access flags and + * signature, in the given byte vector. This includes the 6 header bytes (attribute_name_index and + * attribute_length) per attribute. + * + * @param symbolTable where the constants used in the attributes must be stored. + * @param accessFlags some field, method or class access flags. + * @param signatureIndex the constant pool index of a field, method of class signature. + * @param output where the attributes must be written. + */ + static void putAttributes( + final SymbolTable symbolTable, + final int accessFlags, + final int signatureIndex, + final ByteVector output) { + // Before Java 1.5, synthetic fields are represented with a Synthetic attribute. + if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0 + && symbolTable.getMajorVersion() < Opcodes.V1_5) { + output.putShort(symbolTable.addConstantUtf8(Constants.SYNTHETIC)).putInt(0); + } + if (signatureIndex != 0) { + output + .putShort(symbolTable.addConstantUtf8(Constants.SIGNATURE)) + .putInt(2) + .putShort(signatureIndex); + } + if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) { + output.putShort(symbolTable.addConstantUtf8(Constants.DEPRECATED)).putInt(0); + } + } + + /** A set of attribute prototypes (attributes with the same type are considered equal). */ + static final class Set { + + private static final int SIZE_INCREMENT = 6; + + private int size; + private Attribute[] data = new Attribute[SIZE_INCREMENT]; + + void addAttributes(final Attribute attributeList) { + Attribute attribute = attributeList; + while (attribute != null) { + if (!contains(attribute)) { + add(attribute); + } + attribute = attribute.nextAttribute; + } + } + + Attribute[] toArray() { + Attribute[] result = new Attribute[size]; + System.arraycopy(data, 0, result, 0, size); + return result; + } + + private boolean contains(final Attribute attribute) { + for (int i = 0; i < size; ++i) { + if (data[i].type.equals(attribute.type)) { + return true; + } + } + return false; + } + + private void add(final Attribute attribute) { + if (size >= data.length) { + Attribute[] newData = new Attribute[data.length + SIZE_INCREMENT]; + System.arraycopy(data, 0, newData, 0, size); + data = newData; + } + data[size++] = attribute; + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ByteVector.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ByteVector.java new file mode 100644 index 0000000..d61b4ee --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ByteVector.java @@ -0,0 +1,344 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A dynamically extensible vector of bytes. This class is roughly equivalent to a DataOutputStream + * on top of a ByteArrayOutputStream, but is more efficient. @Author Eric Bruneton + */ +public class ByteVector { + + /** The content of this vector. Only the first {@link #length} bytes contain real data. */ + byte[] data; + + /** The actual number of bytes in this vector. */ + int length; + + /** Constructs a new {@link ByteVector} with a default initial capacity. */ + public ByteVector() { + data = new byte[64]; + } + + /** + * Constructs a new {@link ByteVector} with the given initial capacity. + * + * @param initialCapacity the initial capacity of the byte vector to be constructed. + */ + public ByteVector(final int initialCapacity) { + data = new byte[initialCapacity]; + } + + /** + * Constructs a new {@link ByteVector} from the given initial data. + * + * @param data the initial data of the new byte vector. + */ + ByteVector(final byte[] data) { + this.data = data; + this.length = data.length; + } + + /** + * Puts a byte into this byte vector. The byte vector is automatically enlarged if necessary. + * + * @param byteValue a byte. + * @return this byte vector. + */ + public ByteVector putByte(final int byteValue) { + int currentLength = length; + if (currentLength + 1 > data.length) { + enlarge(1); + } + data[currentLength++] = (byte) byteValue; + length = currentLength; + return this; + } + + /** + * Puts two bytes into this byte vector. The byte vector is automatically enlarged if necessary. + * + * @param byteValue1 a byte. + * @param byteValue2 another byte. + * @return this byte vector. + */ + final ByteVector put11(final int byteValue1, final int byteValue2) { + int currentLength = length; + if (currentLength + 2 > data.length) { + enlarge(2); + } + byte[] currentData = data; + currentData[currentLength++] = (byte) byteValue1; + currentData[currentLength++] = (byte) byteValue2; + length = currentLength; + return this; + } + + /** + * Puts a short into this byte vector. The byte vector is automatically enlarged if necessary. + * + * @param shortValue a short. + * @return this byte vector. + */ + public ByteVector putShort(final int shortValue) { + int currentLength = length; + if (currentLength + 2 > data.length) { + enlarge(2); + } + byte[] currentData = data; + currentData[currentLength++] = (byte) (shortValue >>> 8); + currentData[currentLength++] = (byte) shortValue; + length = currentLength; + return this; + } + + /** + * Puts a byte and a short into this byte vector. The byte vector is automatically enlarged if + * necessary. + * + * @param byteValue a byte. + * @param shortValue a short. + * @return this byte vector. + */ + final ByteVector put12(final int byteValue, final int shortValue) { + int currentLength = length; + if (currentLength + 3 > data.length) { + enlarge(3); + } + byte[] currentData = data; + currentData[currentLength++] = (byte) byteValue; + currentData[currentLength++] = (byte) (shortValue >>> 8); + currentData[currentLength++] = (byte) shortValue; + length = currentLength; + return this; + } + + /** + * Puts two bytes and a short into this byte vector. The byte vector is automatically enlarged if + * necessary. + * + * @param byteValue1 a byte. + * @param byteValue2 another byte. + * @param shortValue a short. + * @return this byte vector. + */ + final ByteVector put112(final int byteValue1, final int byteValue2, final int shortValue) { + int currentLength = length; + if (currentLength + 4 > data.length) { + enlarge(4); + } + byte[] currentData = data; + currentData[currentLength++] = (byte) byteValue1; + currentData[currentLength++] = (byte) byteValue2; + currentData[currentLength++] = (byte) (shortValue >>> 8); + currentData[currentLength++] = (byte) shortValue; + length = currentLength; + return this; + } + + /** + * Puts an int into this byte vector. The byte vector is automatically enlarged if necessary. + * + * @param intValue an int. + * @return this byte vector. + */ + public ByteVector putInt(final int intValue) { + int currentLength = length; + if (currentLength + 4 > data.length) { + enlarge(4); + } + byte[] currentData = data; + currentData[currentLength++] = (byte) (intValue >>> 24); + currentData[currentLength++] = (byte) (intValue >>> 16); + currentData[currentLength++] = (byte) (intValue >>> 8); + currentData[currentLength++] = (byte) intValue; + length = currentLength; + return this; + } + + /** + * Puts one byte and two shorts into this byte vector. The byte vector is automatically enlarged + * if necessary. + * + * @param byteValue a byte. + * @param shortValue1 a short. + * @param shortValue2 another short. + * @return this byte vector. + */ + final ByteVector put122(final int byteValue, final int shortValue1, final int shortValue2) { + int currentLength = length; + if (currentLength + 5 > data.length) { + enlarge(5); + } + byte[] currentData = data; + currentData[currentLength++] = (byte) byteValue; + currentData[currentLength++] = (byte) (shortValue1 >>> 8); + currentData[currentLength++] = (byte) shortValue1; + currentData[currentLength++] = (byte) (shortValue2 >>> 8); + currentData[currentLength++] = (byte) shortValue2; + length = currentLength; + return this; + } + + /** + * Puts a long into this byte vector. The byte vector is automatically enlarged if necessary. + * + * @param longValue a long. + * @return this byte vector. + */ + public ByteVector putLong(final long longValue) { + int currentLength = length; + if (currentLength + 8 > data.length) { + enlarge(8); + } + byte[] currentData = data; + int intValue = (int) (longValue >>> 32); + currentData[currentLength++] = (byte) (intValue >>> 24); + currentData[currentLength++] = (byte) (intValue >>> 16); + currentData[currentLength++] = (byte) (intValue >>> 8); + currentData[currentLength++] = (byte) intValue; + intValue = (int) longValue; + currentData[currentLength++] = (byte) (intValue >>> 24); + currentData[currentLength++] = (byte) (intValue >>> 16); + currentData[currentLength++] = (byte) (intValue >>> 8); + currentData[currentLength++] = (byte) intValue; + length = currentLength; + return this; + } + + /** + * Puts an UTF8 string into this byte vector. The byte vector is automatically enlarged if + * necessary. + * + * @param stringValue a String whose UTF8 encoded length must be less than 65536. + * @return this byte vector. + */ + // DontCheck(AbbreviationAsWordInName): can't be renamed (for backward binary compatibility). + public ByteVector putUTF8(final String stringValue) { + int charLength = stringValue.length(); + if (charLength > 65535) { + throw new IllegalArgumentException("UTF8 string too large"); + } + int currentLength = length; + if (currentLength + 2 + charLength > data.length) { + enlarge(2 + charLength); + } + byte[] currentData = data; + // Optimistic algorithm: instead of computing the byte length and then serializing the string + // (which requires two loops), we assume the byte length is equal to char length (which is the + // most frequent case), and we start serializing the string right away. During the + // serialization, if we find that this assumption is wrong, we continue with the general method. + currentData[currentLength++] = (byte) (charLength >>> 8); + currentData[currentLength++] = (byte) charLength; + for (int i = 0; i < charLength; ++i) { + char charValue = stringValue.charAt(i); + if (charValue >= '\u0001' && charValue <= '\u007F') { + currentData[currentLength++] = (byte) charValue; + } else { + length = currentLength; + return encodeUtf8(stringValue, i, 65535); + } + } + length = currentLength; + return this; + } + + /** + * Puts an UTF8 string into this byte vector. The byte vector is automatically enlarged if + * necessary. The string length is encoded in two bytes before the encoded characters, if there is + * space for that (i.e. if this.length - offset - 2 >= 0). + * + * @param stringValue the String to encode. + * @param offset the index of the first character to encode. The previous characters are supposed + * to have already been encoded, using only one byte per character. + * @param maxByteLength the maximum byte length of the encoded string, including the already + * encoded characters. + * @return this byte vector. + */ + final ByteVector encodeUtf8(final String stringValue, final int offset, final int maxByteLength) { + int charLength = stringValue.length(); + int byteLength = offset; + for (int i = offset; i < charLength; ++i) { + char charValue = stringValue.charAt(i); + if (charValue >= 0x0001 && charValue <= 0x007F) { + byteLength++; + } else if (charValue <= 0x07FF) { + byteLength += 2; + } else { + byteLength += 3; + } + } + if (byteLength > maxByteLength) { + throw new IllegalArgumentException("UTF8 string too large"); + } + // Compute where 'byteLength' must be stored in 'data', and store it at this location. + int byteLengthOffset = length - offset - 2; + if (byteLengthOffset >= 0) { + data[byteLengthOffset] = (byte) (byteLength >>> 8); + data[byteLengthOffset + 1] = (byte) byteLength; + } + if (length + byteLength - offset > data.length) { + enlarge(byteLength - offset); + } + int currentLength = length; + for (int i = offset; i < charLength; ++i) { + char charValue = stringValue.charAt(i); + if (charValue >= 0x0001 && charValue <= 0x007F) { + data[currentLength++] = (byte) charValue; + } else if (charValue <= 0x07FF) { + data[currentLength++] = (byte) (0xC0 | charValue >> 6 & 0x1F); + data[currentLength++] = (byte) (0x80 | charValue & 0x3F); + } else { + data[currentLength++] = (byte) (0xE0 | charValue >> 12 & 0xF); + data[currentLength++] = (byte) (0x80 | charValue >> 6 & 0x3F); + data[currentLength++] = (byte) (0x80 | charValue & 0x3F); + } + } + length = currentLength; + return this; + } + + /** + * Puts an array of bytes into this byte vector. The byte vector is automatically enlarged if + * necessary. + * + * @param byteArrayValue an array of bytes. May be {@literal null} to put {@code byteLength} null + * bytes into this byte vector. + * @param byteOffset index of the first byte of byteArrayValue that must be copied. + * @param byteLength number of bytes of byteArrayValue that must be copied. + * @return this byte vector. + */ + public ByteVector putByteArray( + final byte[] byteArrayValue, final int byteOffset, final int byteLength) { + if (length + byteLength > data.length) { + enlarge(byteLength); + } + if (byteArrayValue != null) { + System.arraycopy(byteArrayValue, byteOffset, data, length, byteLength); + } + length += byteLength; + return this; + } + + /** + * Enlarges this byte vector so that it can receive 'size' more bytes. + * + * @param size number of additional bytes that this byte vector should be able to receive. + */ + private void enlarge(final int size) { + int doubleCapacity = 2 * data.length; + int minimalCapacity = length + size; + byte[] newData = new byte[doubleCapacity > minimalCapacity ? doubleCapacity : minimalCapacity]; + System.arraycopy(data, 0, newData, 0, length); + data = newData; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ClassReader.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ClassReader.java new file mode 100644 index 0000000..bb240c1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ClassReader.java @@ -0,0 +1,3832 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A parser to make a {@link ClassVisitor} visit a ClassFile structure, as defined in the Java + * Virtual Machine Specification (JVMS). This class parses the ClassFile content and calls the + * appropriate visit methods of a given {@link ClassVisitor} for each field, method and bytecode + * instruction encountered. @Author Eric Bruneton @Author Eugene Kuleshov + * + * @see JVMS 4 + */ +public class ClassReader { + + /** + * A flag to skip the Code attributes. If this flag is set the Code attributes are neither parsed + * nor visited. + */ + public static final int SKIP_CODE = 1; + + /** + * A flag to skip the SourceFile, SourceDebugExtension, LocalVariableTable, + * LocalVariableTypeTable, LineNumberTable and MethodParameters attributes. If this flag is set + * these attributes are neither parsed nor visited (i.e. {@link ClassVisitor#visitSource}, {@link + * MethodVisitor#visitLocalVariable}, {@link MethodVisitor#visitLineNumber} and {@link + * MethodVisitor#visitParameter} are not called). + */ + public static final int SKIP_DEBUG = 2; + + /** + * A flag to skip the StackMap and StackMapTable attributes. If this flag is set these attributes + * are neither parsed nor visited (i.e. {@link MethodVisitor#visitFrame} is not called). This flag + * is useful when the {@link ClassWriter#COMPUTE_FRAMES} option is used: it avoids visiting frames + * that will be ignored and recomputed from scratch. + */ + public static final int SKIP_FRAMES = 4; + + /** + * A flag to expand the stack map frames. By default stack map frames are visited in their + * original format (i.e. "expanded" for classes whose version is less than V1_6, and "compressed" + * for the other classes). If this flag is set, stack map frames are always visited in expanded + * format (this option adds a decompression/compression step in ClassReader and ClassWriter which + * degrades performance quite a lot). + */ + public static final int EXPAND_FRAMES = 8; + + /** + * A flag to expand the ASM specific instructions into an equivalent sequence of standard bytecode + * instructions. When resolving a forward jump it may happen that the signed 2 bytes offset + * reserved for it is not sufficient to store the bytecode offset. In this case the jump + * instruction is replaced with a temporary ASM specific instruction using an unsigned 2 bytes + * offset (see {@link Label#resolve}). This internal flag is used to re-read classes containing + * such instructions, in order to replace them with standard instructions. In addition, when this + * flag is used, goto_w and jsr_w are not converted into goto and jsr, to make sure that + * infinite loops where a goto_w is replaced with a goto in ClassReader and converted back to a + * goto_w in ClassWriter cannot occur. + */ + static final int EXPAND_ASM_INSNS = 256; + + /** The maximum size of array to allocate. */ + private static final int MAX_BUFFER_SIZE = 1024 * 1024; + + /** The size of the temporary byte array used to read class input streams chunk by chunk. */ + private static final int INPUT_STREAM_DATA_CHUNK_SIZE = 4096; + + /** + * A byte array containing the JVMS ClassFile structure to be parsed. + * + * @deprecated Use {@link #readByte(int)} and the other read methods instead. This field will + * eventually be deleted. + */ + @Deprecated + // DontCheck(MemberName): can't be renamed (for backward binary compatibility). + public final byte[] b; + + /** The offset in bytes of the ClassFile's access_flags field. */ + public final int header; + + /** + * A byte array containing the JVMS ClassFile structure to be parsed. The content of this array + * must not be modified. This field is intended for {@link Attribute} sub classes, and is normally + * not needed by class visitors. + * + *

NOTE: the ClassFile structure can start at any offset within this array, i.e. it does not + * necessarily start at offset 0. Use {@link #getItem} and {@link #header} to get correct + * ClassFile element offsets within this byte array. + */ + final byte[] classFileBuffer; + + /** + * The offset in bytes, in {@link #classFileBuffer}, of each cp_info entry of the ClassFile's + * constant_pool array, plus one. In other words, the offset of constant pool entry i is + * given by cpInfoOffsets[i] - 1, i.e. its cp_info's tag field is given by b[cpInfoOffsets[i] - + * 1]. + */ + private final int[] cpInfoOffsets; + + /** + * The String objects corresponding to the CONSTANT_Utf8 constant pool items. This cache avoids + * multiple parsing of a given CONSTANT_Utf8 constant pool item. + */ + private final String[] constantUtf8Values; + + /** + * The ConstantDynamic objects corresponding to the CONSTANT_Dynamic constant pool items. This + * cache avoids multiple parsing of a given CONSTANT_Dynamic constant pool item. + */ + private final ConstantDynamic[] constantDynamicValues; + + /** + * The start offsets in {@link #classFileBuffer} of each element of the bootstrap_methods array + * (in the BootstrapMethods attribute). + * + * @see JVMS + * 4.7.23 + */ + private final int[] bootstrapMethodOffsets; + + /** + * A conservative estimate of the maximum length of the strings contained in the constant pool of + * the class. + */ + private final int maxStringLength; + + // ----------------------------------------------------------------------------------------------- + // Constructors + // ----------------------------------------------------------------------------------------------- + + /** + * Constructs a new {@link ClassReader} object. + * + * @param classFile the JVMS ClassFile structure to be read. + */ + public ClassReader(final byte[] classFile) { + this(classFile, 0, classFile.length); + } + + /** + * Constructs a new {@link ClassReader} object. + * + * @param classFileBuffer a byte array containing the JVMS ClassFile structure to be read. + * @param classFileOffset the offset in byteBuffer of the first byte of the ClassFile to be read. + * @param classFileLength the length in bytes of the ClassFile to be read. + */ + public ClassReader( + final byte[] classFileBuffer, + final int classFileOffset, + final int classFileLength) { // NOPMD(UnusedFormalParameter) used for backward compatibility. + this(classFileBuffer, classFileOffset, /* checkClassVersion= */ true); + } + + /** + * Constructs a new {@link ClassReader} object. This internal constructor must not be exposed + * as a public API. + * + * @param classFileBuffer a byte array containing the JVMS ClassFile structure to be read. + * @param classFileOffset the offset in byteBuffer of the first byte of the ClassFile to be read. + * @param checkClassVersion whether to check the class version or not. + */ + ClassReader( + final byte[] classFileBuffer, final int classFileOffset, final boolean checkClassVersion) { + this.classFileBuffer = classFileBuffer; + this.b = classFileBuffer; + // Check the class' major_version. This field is after the magic and minor_version fields, which + // use 4 and 2 bytes respectively. + if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V18) { + throw new IllegalArgumentException( + "Unsupported class file major version " + readShort(classFileOffset + 6)); + } + // Create the constant pool arrays. The constant_pool_count field is after the magic, + // minor_version and major_version fields, which use 4, 2 and 2 bytes respectively. + int constantPoolCount = readUnsignedShort(classFileOffset + 8); + cpInfoOffsets = new int[constantPoolCount]; + constantUtf8Values = new String[constantPoolCount]; + // Compute the offset of each constant pool entry, as well as a conservative estimate of the + // maximum length of the constant pool strings. The first constant pool entry is after the + // magic, minor_version, major_version and constant_pool_count fields, which use 4, 2, 2 and 2 + // bytes respectively. + int currentCpInfoIndex = 1; + int currentCpInfoOffset = classFileOffset + 10; + int currentMaxStringLength = 0; + boolean hasBootstrapMethods = false; + boolean hasConstantDynamic = false; + // The offset of the other entries depend on the total size of all the previous entries. + while (currentCpInfoIndex < constantPoolCount) { + cpInfoOffsets[currentCpInfoIndex++] = currentCpInfoOffset + 1; + int cpInfoSize; + switch (classFileBuffer[currentCpInfoOffset]) { + case Symbol.CONSTANT_FIELDREF_TAG: + case Symbol.CONSTANT_METHODREF_TAG: + case Symbol.CONSTANT_INTERFACE_METHODREF_TAG: + case Symbol.CONSTANT_INTEGER_TAG: + case Symbol.CONSTANT_FLOAT_TAG: + case Symbol.CONSTANT_NAME_AND_TYPE_TAG: + cpInfoSize = 5; + break; + case Symbol.CONSTANT_DYNAMIC_TAG: + cpInfoSize = 5; + hasBootstrapMethods = true; + hasConstantDynamic = true; + break; + case Symbol.CONSTANT_INVOKE_DYNAMIC_TAG: + cpInfoSize = 5; + hasBootstrapMethods = true; + break; + case Symbol.CONSTANT_LONG_TAG: + case Symbol.CONSTANT_DOUBLE_TAG: + cpInfoSize = 9; + currentCpInfoIndex++; + break; + case Symbol.CONSTANT_UTF8_TAG: + cpInfoSize = 3 + readUnsignedShort(currentCpInfoOffset + 1); + if (cpInfoSize > currentMaxStringLength) { + // The size in bytes of this CONSTANT_Utf8 structure provides a conservative estimate + // of the length in characters of the corresponding string, and is much cheaper to + // compute than this exact length. + currentMaxStringLength = cpInfoSize; + } + break; + case Symbol.CONSTANT_METHOD_HANDLE_TAG: + cpInfoSize = 4; + break; + case Symbol.CONSTANT_CLASS_TAG: + case Symbol.CONSTANT_STRING_TAG: + case Symbol.CONSTANT_METHOD_TYPE_TAG: + case Symbol.CONSTANT_PACKAGE_TAG: + case Symbol.CONSTANT_MODULE_TAG: + cpInfoSize = 3; + break; + default: + throw new IllegalArgumentException(); + } + currentCpInfoOffset += cpInfoSize; + } + maxStringLength = currentMaxStringLength; + // The Classfile's access_flags field is just after the last constant pool entry. + header = currentCpInfoOffset; + + // Allocate the cache of ConstantDynamic values, if there is at least one. + constantDynamicValues = hasConstantDynamic ? new ConstantDynamic[constantPoolCount] : null; + + // Read the BootstrapMethods attribute, if any (only get the offset of each method). + bootstrapMethodOffsets = + hasBootstrapMethods ? readBootstrapMethodsAttribute(currentMaxStringLength) : null; + } + + /** + * Constructs a new {@link ClassReader} object. + * + * @param inputStream an input stream of the JVMS ClassFile structure to be read. This input + * stream must contain nothing more than the ClassFile structure itself. It is read from its + * current position to its end. + * @throws IOException if a problem occurs during reading. + */ + public ClassReader(final InputStream inputStream) throws IOException { + this(readStream(inputStream, false)); + } + + /** + * Constructs a new {@link ClassReader} object. + * + * @param className the fully qualified name of the class to be read. The ClassFile structure is + * retrieved with the current class loader's {@link ClassLoader#getSystemResourceAsStream}. + * @throws IOException if an exception occurs during reading. + */ + public ClassReader(final String className) throws IOException { + this( + readStream( + ClassLoader.getSystemResourceAsStream(className.replace('.', '/') + ".class"), true)); + } + + /** + * Reads the given input stream and returns its content as a byte array. + * + * @param inputStream an input stream. + * @param close true to close the input stream after reading. + * @return the content of the given input stream. + * @throws IOException if a problem occurs during reading. + */ + private static byte[] readStream(final InputStream inputStream, final boolean close) + throws IOException { + if (inputStream == null) { + throw new IOException("Class not found"); + } + int bufferSize = calculateBufferSize(inputStream); + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + byte[] data = new byte[bufferSize]; + int bytesRead; + int readCount = 0; + while ((bytesRead = inputStream.read(data, 0, bufferSize)) != -1) { + outputStream.write(data, 0, bytesRead); + readCount++; + } + outputStream.flush(); + if (readCount == 1) { + return data; + } + return outputStream.toByteArray(); + } finally { + if (close) { + inputStream.close(); + } + } + } + + private static int calculateBufferSize(final InputStream inputStream) throws IOException { + int expectedLength = inputStream.available(); + /* + * Some implementations can return 0 while holding available data + * (e.g. new FileInputStream("/proc/a_file")) + * Also in some pathological cases a very small number might be returned, + * and in this case we use default size + */ + if (expectedLength < 256) { + return INPUT_STREAM_DATA_CHUNK_SIZE; + } + return Math.min(expectedLength, MAX_BUFFER_SIZE); + } + + // ----------------------------------------------------------------------------------------------- + // Accessors + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the class's access flags (see {@link Opcodes}). This value may not reflect Deprecated + * and Synthetic flags when bytecode is before 1.5 and those flags are represented by attributes. + * + * @return the class access flags. + * @see ClassVisitor#visit(int, int, String, String, String, String[]) + */ + public int getAccess() { + return readUnsignedShort(header); + } + + /** + * Returns the internal name of the class (see {@link Type#getInternalName()}). + * + * @return the internal class name. + * @see ClassVisitor#visit(int, int, String, String, String, String[]) + */ + public String getClassName() { + // this_class is just after the access_flags field (using 2 bytes). + return readClass(header + 2, new char[maxStringLength]); + } + + /** + * Returns the internal of name of the super class (see {@link Type#getInternalName()}). For + * interfaces, the super class is {@link Object}. + * + * @return the internal name of the super class, or {@literal null} for {@link Object} class. + * @see ClassVisitor#visit(int, int, String, String, String, String[]) + */ + public String getSuperName() { + // super_class is after the access_flags and this_class fields (2 bytes each). + return readClass(header + 4, new char[maxStringLength]); + } + + /** + * Returns the internal names of the implemented interfaces (see {@link Type#getInternalName()}). + * + * @return the internal names of the directly implemented interfaces. Inherited implemented + * interfaces are not returned. + * @see ClassVisitor#visit(int, int, String, String, String, String[]) + */ + public String[] getInterfaces() { + // interfaces_count is after the access_flags, this_class and super_class fields (2 bytes each). + int currentOffset = header + 6; + int interfacesCount = readUnsignedShort(currentOffset); + String[] interfaces = new String[interfacesCount]; + if (interfacesCount > 0) { + char[] charBuffer = new char[maxStringLength]; + for (int i = 0; i < interfacesCount; ++i) { + currentOffset += 2; + interfaces[i] = readClass(currentOffset, charBuffer); + } + } + return interfaces; + } + + // ----------------------------------------------------------------------------------------------- + // Public methods + // ----------------------------------------------------------------------------------------------- + + /** + * Makes the given visitor visit the JVMS ClassFile structure passed to the constructor of this + * {@link ClassReader}. + * + * @param classVisitor the visitor that must visit this class. + * @param parsingOptions the options to use to parse this class. One or more of {@link + * #SKIP_CODE}, {@link #SKIP_DEBUG}, {@link #SKIP_FRAMES} or {@link #EXPAND_FRAMES}. + */ + public void accept(final ClassVisitor classVisitor, final int parsingOptions) { + accept(classVisitor, new Attribute[0], parsingOptions); + } + + /** + * Makes the given visitor visit the JVMS ClassFile structure passed to the constructor of this + * {@link ClassReader}. + * + * @param classVisitor the visitor that must visit this class. + * @param attributePrototypes prototypes of the attributes that must be parsed during the visit of + * the class. Any attribute whose type is not equal to the type of one the prototypes will not + * be parsed: its byte array value will be passed unchanged to the ClassWriter. This may + * corrupt it if this value contains references to the constant pool, or has syntactic or + * semantic links with a class element that has been transformed by a class adapter between + * the reader and the writer. + * @param parsingOptions the options to use to parse this class. One or more of {@link + * #SKIP_CODE}, {@link #SKIP_DEBUG}, {@link #SKIP_FRAMES} or {@link #EXPAND_FRAMES}. + */ + public void accept( + final ClassVisitor classVisitor, + final Attribute[] attributePrototypes, + final int parsingOptions) { + Context context = new Context(); + context.attributePrototypes = attributePrototypes; + context.parsingOptions = parsingOptions; + context.charBuffer = new char[maxStringLength]; + + // Read the access_flags, this_class, super_class, interface_count and interfaces fields. + char[] charBuffer = context.charBuffer; + int currentOffset = header; + int accessFlags = readUnsignedShort(currentOffset); + String thisClass = readClass(currentOffset + 2, charBuffer); + String superClass = readClass(currentOffset + 4, charBuffer); + String[] interfaces = new String[readUnsignedShort(currentOffset + 6)]; + currentOffset += 8; + for (int i = 0; i < interfaces.length; ++i) { + interfaces[i] = readClass(currentOffset, charBuffer); + currentOffset += 2; + } + + // Read the class attributes (the variables are ordered as in Section 4.7 of the JVMS). + // Attribute offsets exclude the attribute_name_index and attribute_length fields. + // - The offset of the InnerClasses attribute, or 0. + int innerClassesOffset = 0; + // - The offset of the EnclosingMethod attribute, or 0. + int enclosingMethodOffset = 0; + // - The string corresponding to the Signature attribute, or null. + String signature = null; + // - The string corresponding to the SourceFile attribute, or null. + String sourceFile = null; + // - The string corresponding to the SourceDebugExtension attribute, or null. + String sourceDebugExtension = null; + // - The offset of the RuntimeVisibleAnnotations attribute, or 0. + int runtimeVisibleAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleAnnotations attribute, or 0. + int runtimeInvisibleAnnotationsOffset = 0; + // - The offset of the RuntimeVisibleTypeAnnotations attribute, or 0. + int runtimeVisibleTypeAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleTypeAnnotations attribute, or 0. + int runtimeInvisibleTypeAnnotationsOffset = 0; + // - The offset of the Module attribute, or 0. + int moduleOffset = 0; + // - The offset of the ModulePackages attribute, or 0. + int modulePackagesOffset = 0; + // - The string corresponding to the ModuleMainClass attribute, or null. + String moduleMainClass = null; + // - The string corresponding to the NestHost attribute, or null. + String nestHostClass = null; + // - The offset of the NestMembers attribute, or 0. + int nestMembersOffset = 0; + // - The offset of the PermittedSubclasses attribute, or 0 + int permittedSubclassesOffset = 0; + // - The offset of the Record attribute, or 0. + int recordOffset = 0; + // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field). + // This list in the reverse order or their order in the ClassFile structure. + Attribute attributes = null; + + int currentAttributeOffset = getFirstAttributeOffset(); + for (int i = readUnsignedShort(currentAttributeOffset - 2); i > 0; --i) { + // Read the attribute_info's attribute_name and attribute_length fields. + String attributeName = readUTF8(currentAttributeOffset, charBuffer); + int attributeLength = readInt(currentAttributeOffset + 2); + currentAttributeOffset += 6; + // The tests are sorted in decreasing frequency order (based on frequencies observed on + // typical classes). + if (Constants.SOURCE_FILE.equals(attributeName)) { + sourceFile = readUTF8(currentAttributeOffset, charBuffer); + } else if (Constants.INNER_CLASSES.equals(attributeName)) { + innerClassesOffset = currentAttributeOffset; + } else if (Constants.ENCLOSING_METHOD.equals(attributeName)) { + enclosingMethodOffset = currentAttributeOffset; + } else if (Constants.NEST_HOST.equals(attributeName)) { + nestHostClass = readClass(currentAttributeOffset, charBuffer); + } else if (Constants.NEST_MEMBERS.equals(attributeName)) { + nestMembersOffset = currentAttributeOffset; + } else if (Constants.PERMITTED_SUBCLASSES.equals(attributeName)) { + permittedSubclassesOffset = currentAttributeOffset; + } else if (Constants.SIGNATURE.equals(attributeName)) { + signature = readUTF8(currentAttributeOffset, charBuffer); + } else if (Constants.RUNTIME_VISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleAnnotationsOffset = currentAttributeOffset; + } else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleTypeAnnotationsOffset = currentAttributeOffset; + } else if (Constants.DEPRECATED.equals(attributeName)) { + accessFlags |= Opcodes.ACC_DEPRECATED; + } else if (Constants.SYNTHETIC.equals(attributeName)) { + accessFlags |= Opcodes.ACC_SYNTHETIC; + } else if (Constants.SOURCE_DEBUG_EXTENSION.equals(attributeName)) { + if (attributeLength > classFileBuffer.length - currentAttributeOffset) { + throw new IllegalArgumentException(); + } + sourceDebugExtension = + readUtf(currentAttributeOffset, attributeLength, new char[attributeLength]); + } else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleAnnotationsOffset = currentAttributeOffset; + } else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleTypeAnnotationsOffset = currentAttributeOffset; + } else if (Constants.RECORD.equals(attributeName)) { + recordOffset = currentAttributeOffset; + accessFlags |= Opcodes.ACC_RECORD; + } else if (Constants.MODULE.equals(attributeName)) { + moduleOffset = currentAttributeOffset; + } else if (Constants.MODULE_MAIN_CLASS.equals(attributeName)) { + moduleMainClass = readClass(currentAttributeOffset, charBuffer); + } else if (Constants.MODULE_PACKAGES.equals(attributeName)) { + modulePackagesOffset = currentAttributeOffset; + } else if (!Constants.BOOTSTRAP_METHODS.equals(attributeName)) { + // The BootstrapMethods attribute is read in the constructor. + Attribute attribute = + readAttribute( + attributePrototypes, + attributeName, + currentAttributeOffset, + attributeLength, + charBuffer, + -1, + null); + attribute.nextAttribute = attributes; + attributes = attribute; + } + currentAttributeOffset += attributeLength; + } + + // Visit the class declaration. The minor_version and major_version fields start 6 bytes before + // the first constant pool entry, which itself starts at cpInfoOffsets[1] - 1 (by definition). + classVisitor.visit( + readInt(cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces); + + // Visit the SourceFile and SourceDebugExtenstion attributes. + if ((parsingOptions & SKIP_DEBUG) == 0 + && (sourceFile != null || sourceDebugExtension != null)) { + classVisitor.visitSource(sourceFile, sourceDebugExtension); + } + + // Visit the Module, ModulePackages and ModuleMainClass attributes. + if (moduleOffset != 0) { + readModuleAttributes( + classVisitor, context, moduleOffset, modulePackagesOffset, moduleMainClass); + } + + // Visit the NestHost attribute. + if (nestHostClass != null) { + classVisitor.visitNestHost(nestHostClass); + } + + // Visit the EnclosingMethod attribute. + if (enclosingMethodOffset != 0) { + String className = readClass(enclosingMethodOffset, charBuffer); + int methodIndex = readUnsignedShort(enclosingMethodOffset + 2); + String name = methodIndex == 0 ? null : readUTF8(cpInfoOffsets[methodIndex], charBuffer); + String type = methodIndex == 0 ? null : readUTF8(cpInfoOffsets[methodIndex] + 2, charBuffer); + classVisitor.visitOuterClass(className, name, type); + } + + // Visit the RuntimeVisibleAnnotations attribute. + if (runtimeVisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + classVisitor.visitAnnotation(annotationDescriptor, /* visible= */ true), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleAnnotations attribute. + if (runtimeInvisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + classVisitor.visitAnnotation(annotationDescriptor, /* visible= */ false), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeVisibleTypeAnnotations attribute. + if (runtimeVisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + classVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible= */ true), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleTypeAnnotations attribute. + if (runtimeInvisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + classVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible= */ false), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the non standard attributes. + while (attributes != null) { + // Copy and reset the nextAttribute field so that it can also be used in ClassWriter. + Attribute nextAttribute = attributes.nextAttribute; + attributes.nextAttribute = null; + classVisitor.visitAttribute(attributes); + attributes = nextAttribute; + } + + // Visit the NestedMembers attribute. + if (nestMembersOffset != 0) { + int numberOfNestMembers = readUnsignedShort(nestMembersOffset); + int currentNestMemberOffset = nestMembersOffset + 2; + while (numberOfNestMembers-- > 0) { + classVisitor.visitNestMember(readClass(currentNestMemberOffset, charBuffer)); + currentNestMemberOffset += 2; + } + } + + // Visit the PermittedSubclasses attribute. + if (permittedSubclassesOffset != 0) { + int numberOfPermittedSubclasses = readUnsignedShort(permittedSubclassesOffset); + int currentPermittedSubclassesOffset = permittedSubclassesOffset + 2; + while (numberOfPermittedSubclasses-- > 0) { + classVisitor.visitPermittedSubclass( + readClass(currentPermittedSubclassesOffset, charBuffer)); + currentPermittedSubclassesOffset += 2; + } + } + + // Visit the InnerClasses attribute. + if (innerClassesOffset != 0) { + int numberOfClasses = readUnsignedShort(innerClassesOffset); + int currentClassesOffset = innerClassesOffset + 2; + while (numberOfClasses-- > 0) { + classVisitor.visitInnerClass( + readClass(currentClassesOffset, charBuffer), + readClass(currentClassesOffset + 2, charBuffer), + readUTF8(currentClassesOffset + 4, charBuffer), + readUnsignedShort(currentClassesOffset + 6)); + currentClassesOffset += 8; + } + } + + // Visit Record components. + if (recordOffset != 0) { + int recordComponentsCount = readUnsignedShort(recordOffset); + recordOffset += 2; + while (recordComponentsCount-- > 0) { + recordOffset = readRecordComponent(classVisitor, context, recordOffset); + } + } + + // Visit the fields and methods. + int fieldsCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (fieldsCount-- > 0) { + currentOffset = readField(classVisitor, context, currentOffset); + } + int methodsCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (methodsCount-- > 0) { + currentOffset = readMethod(classVisitor, context, currentOffset); + } + + // Visit the end of the class. + classVisitor.visitEnd(); + } + + // ---------------------------------------------------------------------------------------------- + // Methods to parse modules, fields and methods + // ---------------------------------------------------------------------------------------------- + + /** + * Reads the Module, ModulePackages and ModuleMainClass attributes and visit them. + * + * @param classVisitor the current class visitor + * @param context information about the class being parsed. + * @param moduleOffset the offset of the Module attribute (excluding the attribute_info's + * attribute_name_index and attribute_length fields). + * @param modulePackagesOffset the offset of the ModulePackages attribute (excluding the + * attribute_info's attribute_name_index and attribute_length fields), or 0. + * @param moduleMainClass the string corresponding to the ModuleMainClass attribute, or {@literal + * null}. + */ + private void readModuleAttributes( + final ClassVisitor classVisitor, + final Context context, + final int moduleOffset, + final int modulePackagesOffset, + final String moduleMainClass) { + char[] buffer = context.charBuffer; + + // Read the module_name_index, module_flags and module_version_index fields and visit them. + int currentOffset = moduleOffset; + String moduleName = readModule(currentOffset, buffer); + int moduleFlags = readUnsignedShort(currentOffset + 2); + String moduleVersion = readUTF8(currentOffset + 4, buffer); + currentOffset += 6; + ModuleVisitor moduleVisitor = classVisitor.visitModule(moduleName, moduleFlags, moduleVersion); + if (moduleVisitor == null) { + return; + } + + // Visit the ModuleMainClass attribute. + if (moduleMainClass != null) { + moduleVisitor.visitMainClass(moduleMainClass); + } + + // Visit the ModulePackages attribute. + if (modulePackagesOffset != 0) { + int packageCount = readUnsignedShort(modulePackagesOffset); + int currentPackageOffset = modulePackagesOffset + 2; + while (packageCount-- > 0) { + moduleVisitor.visitPackage(readPackage(currentPackageOffset, buffer)); + currentPackageOffset += 2; + } + } + + // Read the 'requires_count' and 'requires' fields. + int requiresCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (requiresCount-- > 0) { + // Read the requires_index, requires_flags and requires_version fields and visit them. + String requires = readModule(currentOffset, buffer); + int requiresFlags = readUnsignedShort(currentOffset + 2); + String requiresVersion = readUTF8(currentOffset + 4, buffer); + currentOffset += 6; + moduleVisitor.visitRequire(requires, requiresFlags, requiresVersion); + } + + // Read the 'exports_count' and 'exports' fields. + int exportsCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (exportsCount-- > 0) { + // Read the exports_index, exports_flags, exports_to_count and exports_to_index fields + // and visit them. + String exports = readPackage(currentOffset, buffer); + int exportsFlags = readUnsignedShort(currentOffset + 2); + int exportsToCount = readUnsignedShort(currentOffset + 4); + currentOffset += 6; + String[] exportsTo = null; + if (exportsToCount != 0) { + exportsTo = new String[exportsToCount]; + for (int i = 0; i < exportsToCount; ++i) { + exportsTo[i] = readModule(currentOffset, buffer); + currentOffset += 2; + } + } + moduleVisitor.visitExport(exports, exportsFlags, exportsTo); + } + + // Reads the 'opens_count' and 'opens' fields. + int opensCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (opensCount-- > 0) { + // Read the opens_index, opens_flags, opens_to_count and opens_to_index fields and visit them. + String opens = readPackage(currentOffset, buffer); + int opensFlags = readUnsignedShort(currentOffset + 2); + int opensToCount = readUnsignedShort(currentOffset + 4); + currentOffset += 6; + String[] opensTo = null; + if (opensToCount != 0) { + opensTo = new String[opensToCount]; + for (int i = 0; i < opensToCount; ++i) { + opensTo[i] = readModule(currentOffset, buffer); + currentOffset += 2; + } + } + moduleVisitor.visitOpen(opens, opensFlags, opensTo); + } + + // Read the 'uses_count' and 'uses' fields. + int usesCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (usesCount-- > 0) { + moduleVisitor.visitUse(readClass(currentOffset, buffer)); + currentOffset += 2; + } + + // Read the 'provides_count' and 'provides' fields. + int providesCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (providesCount-- > 0) { + // Read the provides_index, provides_with_count and provides_with_index fields and visit them. + String provides = readClass(currentOffset, buffer); + int providesWithCount = readUnsignedShort(currentOffset + 2); + currentOffset += 4; + String[] providesWith = new String[providesWithCount]; + for (int i = 0; i < providesWithCount; ++i) { + providesWith[i] = readClass(currentOffset, buffer); + currentOffset += 2; + } + moduleVisitor.visitProvide(provides, providesWith); + } + + // Visit the end of the module attributes. + moduleVisitor.visitEnd(); + } + + /** + * Reads a record component and visit it. + * + * @param classVisitor the current class visitor + * @param context information about the class being parsed. + * @param recordComponentOffset the offset of the current record component. + * @return the offset of the first byte following the record component. + */ + private int readRecordComponent( + final ClassVisitor classVisitor, final Context context, final int recordComponentOffset) { + char[] charBuffer = context.charBuffer; + + int currentOffset = recordComponentOffset; + String name = readUTF8(currentOffset, charBuffer); + String descriptor = readUTF8(currentOffset + 2, charBuffer); + currentOffset += 4; + + // Read the record component attributes (the variables are ordered as in Section 4.7 of the + // JVMS). + + // Attribute offsets exclude the attribute_name_index and attribute_length fields. + // - The string corresponding to the Signature attribute, or null. + String signature = null; + // - The offset of the RuntimeVisibleAnnotations attribute, or 0. + int runtimeVisibleAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleAnnotations attribute, or 0. + int runtimeInvisibleAnnotationsOffset = 0; + // - The offset of the RuntimeVisibleTypeAnnotations attribute, or 0. + int runtimeVisibleTypeAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleTypeAnnotations attribute, or 0. + int runtimeInvisibleTypeAnnotationsOffset = 0; + // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field). + // This list in the reverse order or their order in the ClassFile structure. + Attribute attributes = null; + + int attributesCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (attributesCount-- > 0) { + // Read the attribute_info's attribute_name and attribute_length fields. + String attributeName = readUTF8(currentOffset, charBuffer); + int attributeLength = readInt(currentOffset + 2); + currentOffset += 6; + // The tests are sorted in decreasing frequency order (based on frequencies observed on + // typical classes). + if (Constants.SIGNATURE.equals(attributeName)) { + signature = readUTF8(currentOffset, charBuffer); + } else if (Constants.RUNTIME_VISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleTypeAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleTypeAnnotationsOffset = currentOffset; + } else { + Attribute attribute = + readAttribute( + context.attributePrototypes, + attributeName, + currentOffset, + attributeLength, + charBuffer, + -1, + null); + attribute.nextAttribute = attributes; + attributes = attribute; + } + currentOffset += attributeLength; + } + + RecordComponentVisitor recordComponentVisitor = + classVisitor.visitRecordComponent(name, descriptor, signature); + if (recordComponentVisitor == null) { + return currentOffset; + } + + // Visit the RuntimeVisibleAnnotations attribute. + if (runtimeVisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + recordComponentVisitor.visitAnnotation(annotationDescriptor, /* visible= */ true), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleAnnotations attribute. + if (runtimeInvisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + recordComponentVisitor.visitAnnotation(annotationDescriptor, /* visible= */ false), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeVisibleTypeAnnotations attribute. + if (runtimeVisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + recordComponentVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible= */ true), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleTypeAnnotations attribute. + if (runtimeInvisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + recordComponentVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible= */ false), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the non standard attributes. + while (attributes != null) { + // Copy and reset the nextAttribute field so that it can also be used in FieldWriter. + Attribute nextAttribute = attributes.nextAttribute; + attributes.nextAttribute = null; + recordComponentVisitor.visitAttribute(attributes); + attributes = nextAttribute; + } + + // Visit the end of the field. + recordComponentVisitor.visitEnd(); + return currentOffset; + } + + /** + * Reads a JVMS field_info structure and makes the given visitor visit it. + * + * @param classVisitor the visitor that must visit the field. + * @param context information about the class being parsed. + * @param fieldInfoOffset the start offset of the field_info structure. + * @return the offset of the first byte following the field_info structure. + */ + private int readField( + final ClassVisitor classVisitor, final Context context, final int fieldInfoOffset) { + char[] charBuffer = context.charBuffer; + + // Read the access_flags, name_index and descriptor_index fields. + int currentOffset = fieldInfoOffset; + int accessFlags = readUnsignedShort(currentOffset); + String name = readUTF8(currentOffset + 2, charBuffer); + String descriptor = readUTF8(currentOffset + 4, charBuffer); + currentOffset += 6; + + // Read the field attributes (the variables are ordered as in Section 4.7 of the JVMS). + // Attribute offsets exclude the attribute_name_index and attribute_length fields. + // - The value corresponding to the ConstantValue attribute, or null. + Object constantValue = null; + // - The string corresponding to the Signature attribute, or null. + String signature = null; + // - The offset of the RuntimeVisibleAnnotations attribute, or 0. + int runtimeVisibleAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleAnnotations attribute, or 0. + int runtimeInvisibleAnnotationsOffset = 0; + // - The offset of the RuntimeVisibleTypeAnnotations attribute, or 0. + int runtimeVisibleTypeAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleTypeAnnotations attribute, or 0. + int runtimeInvisibleTypeAnnotationsOffset = 0; + // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field). + // This list in the reverse order or their order in the ClassFile structure. + Attribute attributes = null; + + int attributesCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (attributesCount-- > 0) { + // Read the attribute_info's attribute_name and attribute_length fields. + String attributeName = readUTF8(currentOffset, charBuffer); + int attributeLength = readInt(currentOffset + 2); + currentOffset += 6; + // The tests are sorted in decreasing frequency order (based on frequencies observed on + // typical classes). + if (Constants.CONSTANT_VALUE.equals(attributeName)) { + int constantvalueIndex = readUnsignedShort(currentOffset); + constantValue = constantvalueIndex == 0 ? null : readConst(constantvalueIndex, charBuffer); + } else if (Constants.SIGNATURE.equals(attributeName)) { + signature = readUTF8(currentOffset, charBuffer); + } else if (Constants.DEPRECATED.equals(attributeName)) { + accessFlags |= Opcodes.ACC_DEPRECATED; + } else if (Constants.SYNTHETIC.equals(attributeName)) { + accessFlags |= Opcodes.ACC_SYNTHETIC; + } else if (Constants.RUNTIME_VISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleTypeAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleTypeAnnotationsOffset = currentOffset; + } else { + Attribute attribute = + readAttribute( + context.attributePrototypes, + attributeName, + currentOffset, + attributeLength, + charBuffer, + -1, + null); + attribute.nextAttribute = attributes; + attributes = attribute; + } + currentOffset += attributeLength; + } + + // Visit the field declaration. + FieldVisitor fieldVisitor = + classVisitor.visitField(accessFlags, name, descriptor, signature, constantValue); + if (fieldVisitor == null) { + return currentOffset; + } + + // Visit the RuntimeVisibleAnnotations attribute. + if (runtimeVisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + fieldVisitor.visitAnnotation(annotationDescriptor, /* visible= */ true), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleAnnotations attribute. + if (runtimeInvisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + fieldVisitor.visitAnnotation(annotationDescriptor, /* visible= */ false), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeVisibleTypeAnnotations attribute. + if (runtimeVisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + fieldVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible= */ true), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleTypeAnnotations attribute. + if (runtimeInvisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + fieldVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible= */ false), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the non standard attributes. + while (attributes != null) { + // Copy and reset the nextAttribute field so that it can also be used in FieldWriter. + Attribute nextAttribute = attributes.nextAttribute; + attributes.nextAttribute = null; + fieldVisitor.visitAttribute(attributes); + attributes = nextAttribute; + } + + // Visit the end of the field. + fieldVisitor.visitEnd(); + return currentOffset; + } + + /** + * Reads a JVMS method_info structure and makes the given visitor visit it. + * + * @param classVisitor the visitor that must visit the method. + * @param context information about the class being parsed. + * @param methodInfoOffset the start offset of the method_info structure. + * @return the offset of the first byte following the method_info structure. + */ + private int readMethod( + final ClassVisitor classVisitor, final Context context, final int methodInfoOffset) { + char[] charBuffer = context.charBuffer; + + // Read the access_flags, name_index and descriptor_index fields. + int currentOffset = methodInfoOffset; + context.currentMethodAccessFlags = readUnsignedShort(currentOffset); + context.currentMethodName = readUTF8(currentOffset + 2, charBuffer); + context.currentMethodDescriptor = readUTF8(currentOffset + 4, charBuffer); + currentOffset += 6; + + // Read the method attributes (the variables are ordered as in Section 4.7 of the JVMS). + // Attribute offsets exclude the attribute_name_index and attribute_length fields. + // - The offset of the Code attribute, or 0. + int codeOffset = 0; + // - The offset of the Exceptions attribute, or 0. + int exceptionsOffset = 0; + // - The strings corresponding to the Exceptions attribute, or null. + String[] exceptions = null; + // - Whether the method has a Synthetic attribute. + boolean synthetic = false; + // - The constant pool index contained in the Signature attribute, or 0. + int signatureIndex = 0; + // - The offset of the RuntimeVisibleAnnotations attribute, or 0. + int runtimeVisibleAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleAnnotations attribute, or 0. + int runtimeInvisibleAnnotationsOffset = 0; + // - The offset of the RuntimeVisibleParameterAnnotations attribute, or 0. + int runtimeVisibleParameterAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleParameterAnnotations attribute, or 0. + int runtimeInvisibleParameterAnnotationsOffset = 0; + // - The offset of the RuntimeVisibleTypeAnnotations attribute, or 0. + int runtimeVisibleTypeAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleTypeAnnotations attribute, or 0. + int runtimeInvisibleTypeAnnotationsOffset = 0; + // - The offset of the AnnotationDefault attribute, or 0. + int annotationDefaultOffset = 0; + // - The offset of the MethodParameters attribute, or 0. + int methodParametersOffset = 0; + // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field). + // This list in the reverse order or their order in the ClassFile structure. + Attribute attributes = null; + + int attributesCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (attributesCount-- > 0) { + // Read the attribute_info's attribute_name and attribute_length fields. + String attributeName = readUTF8(currentOffset, charBuffer); + int attributeLength = readInt(currentOffset + 2); + currentOffset += 6; + // The tests are sorted in decreasing frequency order (based on frequencies observed on + // typical classes). + if (Constants.CODE.equals(attributeName)) { + if ((context.parsingOptions & SKIP_CODE) == 0) { + codeOffset = currentOffset; + } + } else if (Constants.EXCEPTIONS.equals(attributeName)) { + exceptionsOffset = currentOffset; + exceptions = new String[readUnsignedShort(exceptionsOffset)]; + int currentExceptionOffset = exceptionsOffset + 2; + for (int i = 0; i < exceptions.length; ++i) { + exceptions[i] = readClass(currentExceptionOffset, charBuffer); + currentExceptionOffset += 2; + } + } else if (Constants.SIGNATURE.equals(attributeName)) { + signatureIndex = readUnsignedShort(currentOffset); + } else if (Constants.DEPRECATED.equals(attributeName)) { + context.currentMethodAccessFlags |= Opcodes.ACC_DEPRECATED; + } else if (Constants.RUNTIME_VISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleTypeAnnotationsOffset = currentOffset; + } else if (Constants.ANNOTATION_DEFAULT.equals(attributeName)) { + annotationDefaultOffset = currentOffset; + } else if (Constants.SYNTHETIC.equals(attributeName)) { + synthetic = true; + context.currentMethodAccessFlags |= Opcodes.ACC_SYNTHETIC; + } else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleTypeAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleParameterAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleParameterAnnotationsOffset = currentOffset; + } else if (Constants.METHOD_PARAMETERS.equals(attributeName)) { + methodParametersOffset = currentOffset; + } else { + Attribute attribute = + readAttribute( + context.attributePrototypes, + attributeName, + currentOffset, + attributeLength, + charBuffer, + -1, + null); + attribute.nextAttribute = attributes; + attributes = attribute; + } + currentOffset += attributeLength; + } + + // Visit the method declaration. + MethodVisitor methodVisitor = + classVisitor.visitMethod( + context.currentMethodAccessFlags, + context.currentMethodName, + context.currentMethodDescriptor, + signatureIndex == 0 ? null : readUtf(signatureIndex, charBuffer), + exceptions); + if (methodVisitor == null) { + return currentOffset; + } + + // If the returned MethodVisitor is in fact a MethodWriter, it means there is no method + // adapter between the reader and the writer. In this case, it might be possible to copy + // the method attributes directly into the writer. If so, return early without visiting + // the content of these attributes. + if (methodVisitor instanceof MethodWriter) { + MethodWriter methodWriter = (MethodWriter) methodVisitor; + if (methodWriter.canCopyMethodAttributes( + this, + synthetic, + (context.currentMethodAccessFlags & Opcodes.ACC_DEPRECATED) != 0, + readUnsignedShort(methodInfoOffset + 4), + signatureIndex, + exceptionsOffset)) { + methodWriter.setMethodAttributesSource(methodInfoOffset, currentOffset - methodInfoOffset); + return currentOffset; + } + } + + // Visit the MethodParameters attribute. + if (methodParametersOffset != 0 && (context.parsingOptions & SKIP_DEBUG) == 0) { + int parametersCount = readByte(methodParametersOffset); + int currentParameterOffset = methodParametersOffset + 1; + while (parametersCount-- > 0) { + // Read the name_index and access_flags fields and visit them. + methodVisitor.visitParameter( + readUTF8(currentParameterOffset, charBuffer), + readUnsignedShort(currentParameterOffset + 2)); + currentParameterOffset += 4; + } + } + + // Visit the AnnotationDefault attribute. + if (annotationDefaultOffset != 0) { + AnnotationVisitor annotationVisitor = methodVisitor.visitAnnotationDefault(); + readElementValue(annotationVisitor, annotationDefaultOffset, null, charBuffer); + if (annotationVisitor != null) { + annotationVisitor.visitEnd(); + } + } + + // Visit the RuntimeVisibleAnnotations attribute. + if (runtimeVisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + methodVisitor.visitAnnotation(annotationDescriptor, /* visible= */ true), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleAnnotations attribute. + if (runtimeInvisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + methodVisitor.visitAnnotation(annotationDescriptor, /* visible= */ false), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeVisibleTypeAnnotations attribute. + if (runtimeVisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + methodVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible= */ true), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleTypeAnnotations attribute. + if (runtimeInvisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + methodVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible= */ false), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + } + + // Visit the RuntimeVisibleParameterAnnotations attribute. + if (runtimeVisibleParameterAnnotationsOffset != 0) { + readParameterAnnotations( + methodVisitor, context, runtimeVisibleParameterAnnotationsOffset, /* visible= */ true); + } + + // Visit the RuntimeInvisibleParameterAnnotations attribute. + if (runtimeInvisibleParameterAnnotationsOffset != 0) { + readParameterAnnotations( + methodVisitor, context, runtimeInvisibleParameterAnnotationsOffset, /* visible= */ false); + } + + // Visit the non standard attributes. + while (attributes != null) { + // Copy and reset the nextAttribute field so that it can also be used in MethodWriter. + Attribute nextAttribute = attributes.nextAttribute; + attributes.nextAttribute = null; + methodVisitor.visitAttribute(attributes); + attributes = nextAttribute; + } + + // Visit the Code attribute. + if (codeOffset != 0) { + methodVisitor.visitCode(); + readCode(methodVisitor, context, codeOffset); + } + + // Visit the end of the method. + methodVisitor.visitEnd(); + return currentOffset; + } + + // ---------------------------------------------------------------------------------------------- + // Methods to parse a Code attribute + // ---------------------------------------------------------------------------------------------- + + /** + * Reads a JVMS 'Code' attribute and makes the given visitor visit it. + * + * @param methodVisitor the visitor that must visit the Code attribute. + * @param context information about the class being parsed. + * @param codeOffset the start offset in {@link #classFileBuffer} of the Code attribute, excluding + * its attribute_name_index and attribute_length fields. + */ + private void readCode( + final MethodVisitor methodVisitor, final Context context, final int codeOffset) { + int currentOffset = codeOffset; + + // Read the max_stack, max_locals and code_length fields. + final byte[] classBuffer = classFileBuffer; + final char[] charBuffer = context.charBuffer; + final int maxStack = readUnsignedShort(currentOffset); + final int maxLocals = readUnsignedShort(currentOffset + 2); + final int codeLength = readInt(currentOffset + 4); + currentOffset += 8; + if (codeLength > classFileBuffer.length - currentOffset) { + throw new IllegalArgumentException(); + } + + // Read the bytecode 'code' array to create a label for each referenced instruction. + final int bytecodeStartOffset = currentOffset; + final int bytecodeEndOffset = currentOffset + codeLength; + final Label[] labels = context.currentMethodLabels = new Label[codeLength + 1]; + while (currentOffset < bytecodeEndOffset) { + final int bytecodeOffset = currentOffset - bytecodeStartOffset; + final int opcode = classBuffer[currentOffset] & 0xFF; + switch (opcode) { + case Opcodes.NOP: + case Opcodes.ACONST_NULL: + case Opcodes.ICONST_M1: + case Opcodes.ICONST_0: + case Opcodes.ICONST_1: + case Opcodes.ICONST_2: + case Opcodes.ICONST_3: + case Opcodes.ICONST_4: + case Opcodes.ICONST_5: + case Opcodes.LCONST_0: + case Opcodes.LCONST_1: + case Opcodes.FCONST_0: + case Opcodes.FCONST_1: + case Opcodes.FCONST_2: + case Opcodes.DCONST_0: + case Opcodes.DCONST_1: + case Opcodes.IALOAD: + case Opcodes.LALOAD: + case Opcodes.FALOAD: + case Opcodes.DALOAD: + case Opcodes.AALOAD: + case Opcodes.BALOAD: + case Opcodes.CALOAD: + case Opcodes.SALOAD: + case Opcodes.IASTORE: + case Opcodes.LASTORE: + case Opcodes.FASTORE: + case Opcodes.DASTORE: + case Opcodes.AASTORE: + case Opcodes.BASTORE: + case Opcodes.CASTORE: + case Opcodes.SASTORE: + case Opcodes.POP: + case Opcodes.POP2: + case Opcodes.DUP: + case Opcodes.DUP_X1: + case Opcodes.DUP_X2: + case Opcodes.DUP2: + case Opcodes.DUP2_X1: + case Opcodes.DUP2_X2: + case Opcodes.SWAP: + case Opcodes.IADD: + case Opcodes.LADD: + case Opcodes.FADD: + case Opcodes.DADD: + case Opcodes.ISUB: + case Opcodes.LSUB: + case Opcodes.FSUB: + case Opcodes.DSUB: + case Opcodes.IMUL: + case Opcodes.LMUL: + case Opcodes.FMUL: + case Opcodes.DMUL: + case Opcodes.IDIV: + case Opcodes.LDIV: + case Opcodes.FDIV: + case Opcodes.DDIV: + case Opcodes.IREM: + case Opcodes.LREM: + case Opcodes.FREM: + case Opcodes.DREM: + case Opcodes.INEG: + case Opcodes.LNEG: + case Opcodes.FNEG: + case Opcodes.DNEG: + case Opcodes.ISHL: + case Opcodes.LSHL: + case Opcodes.ISHR: + case Opcodes.LSHR: + case Opcodes.IUSHR: + case Opcodes.LUSHR: + case Opcodes.IAND: + case Opcodes.LAND: + case Opcodes.IOR: + case Opcodes.LOR: + case Opcodes.IXOR: + case Opcodes.LXOR: + case Opcodes.I2L: + case Opcodes.I2F: + case Opcodes.I2D: + case Opcodes.L2I: + case Opcodes.L2F: + case Opcodes.L2D: + case Opcodes.F2I: + case Opcodes.F2L: + case Opcodes.F2D: + case Opcodes.D2I: + case Opcodes.D2L: + case Opcodes.D2F: + case Opcodes.I2B: + case Opcodes.I2C: + case Opcodes.I2S: + case Opcodes.LCMP: + case Opcodes.FCMPL: + case Opcodes.FCMPG: + case Opcodes.DCMPL: + case Opcodes.DCMPG: + case Opcodes.IRETURN: + case Opcodes.LRETURN: + case Opcodes.FRETURN: + case Opcodes.DRETURN: + case Opcodes.ARETURN: + case Opcodes.RETURN: + case Opcodes.ARRAYLENGTH: + case Opcodes.ATHROW: + case Opcodes.MONITORENTER: + case Opcodes.MONITOREXIT: + case Constants.ILOAD_0: + case Constants.ILOAD_1: + case Constants.ILOAD_2: + case Constants.ILOAD_3: + case Constants.LLOAD_0: + case Constants.LLOAD_1: + case Constants.LLOAD_2: + case Constants.LLOAD_3: + case Constants.FLOAD_0: + case Constants.FLOAD_1: + case Constants.FLOAD_2: + case Constants.FLOAD_3: + case Constants.DLOAD_0: + case Constants.DLOAD_1: + case Constants.DLOAD_2: + case Constants.DLOAD_3: + case Constants.ALOAD_0: + case Constants.ALOAD_1: + case Constants.ALOAD_2: + case Constants.ALOAD_3: + case Constants.ISTORE_0: + case Constants.ISTORE_1: + case Constants.ISTORE_2: + case Constants.ISTORE_3: + case Constants.LSTORE_0: + case Constants.LSTORE_1: + case Constants.LSTORE_2: + case Constants.LSTORE_3: + case Constants.FSTORE_0: + case Constants.FSTORE_1: + case Constants.FSTORE_2: + case Constants.FSTORE_3: + case Constants.DSTORE_0: + case Constants.DSTORE_1: + case Constants.DSTORE_2: + case Constants.DSTORE_3: + case Constants.ASTORE_0: + case Constants.ASTORE_1: + case Constants.ASTORE_2: + case Constants.ASTORE_3: + currentOffset += 1; + break; + case Opcodes.IFEQ: + case Opcodes.IFNE: + case Opcodes.IFLT: + case Opcodes.IFGE: + case Opcodes.IFGT: + case Opcodes.IFLE: + case Opcodes.IF_ICMPEQ: + case Opcodes.IF_ICMPNE: + case Opcodes.IF_ICMPLT: + case Opcodes.IF_ICMPGE: + case Opcodes.IF_ICMPGT: + case Opcodes.IF_ICMPLE: + case Opcodes.IF_ACMPEQ: + case Opcodes.IF_ACMPNE: + case Opcodes.GOTO: + case Opcodes.JSR: + case Opcodes.IFNULL: + case Opcodes.IFNONNULL: + createLabel(bytecodeOffset + readShort(currentOffset + 1), labels); + currentOffset += 3; + break; + case Constants.ASM_IFEQ: + case Constants.ASM_IFNE: + case Constants.ASM_IFLT: + case Constants.ASM_IFGE: + case Constants.ASM_IFGT: + case Constants.ASM_IFLE: + case Constants.ASM_IF_ICMPEQ: + case Constants.ASM_IF_ICMPNE: + case Constants.ASM_IF_ICMPLT: + case Constants.ASM_IF_ICMPGE: + case Constants.ASM_IF_ICMPGT: + case Constants.ASM_IF_ICMPLE: + case Constants.ASM_IF_ACMPEQ: + case Constants.ASM_IF_ACMPNE: + case Constants.ASM_GOTO: + case Constants.ASM_JSR: + case Constants.ASM_IFNULL: + case Constants.ASM_IFNONNULL: + createLabel(bytecodeOffset + readUnsignedShort(currentOffset + 1), labels); + currentOffset += 3; + break; + case Constants.GOTO_W: + case Constants.JSR_W: + case Constants.ASM_GOTO_W: + createLabel(bytecodeOffset + readInt(currentOffset + 1), labels); + currentOffset += 5; + break; + case Constants.WIDE: + switch (classBuffer[currentOffset + 1] & 0xFF) { + case Opcodes.ILOAD: + case Opcodes.FLOAD: + case Opcodes.ALOAD: + case Opcodes.LLOAD: + case Opcodes.DLOAD: + case Opcodes.ISTORE: + case Opcodes.FSTORE: + case Opcodes.ASTORE: + case Opcodes.LSTORE: + case Opcodes.DSTORE: + case Opcodes.RET: + currentOffset += 4; + break; + case Opcodes.IINC: + currentOffset += 6; + break; + default: + throw new IllegalArgumentException(); + } + break; + case Opcodes.TABLESWITCH: + // Skip 0 to 3 padding bytes. + currentOffset += 4 - (bytecodeOffset & 3); + // Read the default label and the number of table entries. + createLabel(bytecodeOffset + readInt(currentOffset), labels); + int numTableEntries = readInt(currentOffset + 8) - readInt(currentOffset + 4) + 1; + currentOffset += 12; + // Read the table labels. + while (numTableEntries-- > 0) { + createLabel(bytecodeOffset + readInt(currentOffset), labels); + currentOffset += 4; + } + break; + case Opcodes.LOOKUPSWITCH: + // Skip 0 to 3 padding bytes. + currentOffset += 4 - (bytecodeOffset & 3); + // Read the default label and the number of switch cases. + createLabel(bytecodeOffset + readInt(currentOffset), labels); + int numSwitchCases = readInt(currentOffset + 4); + currentOffset += 8; + // Read the switch labels. + while (numSwitchCases-- > 0) { + createLabel(bytecodeOffset + readInt(currentOffset + 4), labels); + currentOffset += 8; + } + break; + case Opcodes.ILOAD: + case Opcodes.LLOAD: + case Opcodes.FLOAD: + case Opcodes.DLOAD: + case Opcodes.ALOAD: + case Opcodes.ISTORE: + case Opcodes.LSTORE: + case Opcodes.FSTORE: + case Opcodes.DSTORE: + case Opcodes.ASTORE: + case Opcodes.RET: + case Opcodes.BIPUSH: + case Opcodes.NEWARRAY: + case Opcodes.LDC: + currentOffset += 2; + break; + case Opcodes.SIPUSH: + case Constants.LDC_W: + case Constants.LDC2_W: + case Opcodes.GETSTATIC: + case Opcodes.PUTSTATIC: + case Opcodes.GETFIELD: + case Opcodes.PUTFIELD: + case Opcodes.INVOKEVIRTUAL: + case Opcodes.INVOKESPECIAL: + case Opcodes.INVOKESTATIC: + case Opcodes.NEW: + case Opcodes.ANEWARRAY: + case Opcodes.CHECKCAST: + case Opcodes.INSTANCEOF: + case Opcodes.IINC: + currentOffset += 3; + break; + case Opcodes.INVOKEINTERFACE: + case Opcodes.INVOKEDYNAMIC: + currentOffset += 5; + break; + case Opcodes.MULTIANEWARRAY: + currentOffset += 4; + break; + default: + throw new IllegalArgumentException(); + } + } + + // Read the 'exception_table_length' and 'exception_table' field to create a label for each + // referenced instruction, and to make methodVisitor visit the corresponding try catch blocks. + int exceptionTableLength = readUnsignedShort(currentOffset); + currentOffset += 2; + while (exceptionTableLength-- > 0) { + Label start = createLabel(readUnsignedShort(currentOffset), labels); + Label end = createLabel(readUnsignedShort(currentOffset + 2), labels); + Label handler = createLabel(readUnsignedShort(currentOffset + 4), labels); + String catchType = readUTF8(cpInfoOffsets[readUnsignedShort(currentOffset + 6)], charBuffer); + currentOffset += 8; + methodVisitor.visitTryCatchBlock(start, end, handler, catchType); + } + + // Read the Code attributes to create a label for each referenced instruction (the variables + // are ordered as in Section 4.7 of the JVMS). Attribute offsets exclude the + // attribute_name_index and attribute_length fields. + // - The offset of the current 'stack_map_frame' in the StackMap[Table] attribute, or 0. + // Initially, this is the offset of the first 'stack_map_frame' entry. Then this offset is + // updated after each stack_map_frame is read. + int stackMapFrameOffset = 0; + // - The end offset of the StackMap[Table] attribute, or 0. + int stackMapTableEndOffset = 0; + // - Whether the stack map frames are compressed (i.e. in a StackMapTable) or not. + boolean compressedFrames = true; + // - The offset of the LocalVariableTable attribute, or 0. + int localVariableTableOffset = 0; + // - The offset of the LocalVariableTypeTable attribute, or 0. + int localVariableTypeTableOffset = 0; + // - The offset of each 'type_annotation' entry in the RuntimeVisibleTypeAnnotations + // attribute, or null. + int[] visibleTypeAnnotationOffsets = null; + // - The offset of each 'type_annotation' entry in the RuntimeInvisibleTypeAnnotations + // attribute, or null. + int[] invisibleTypeAnnotationOffsets = null; + // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field). + // This list in the reverse order or their order in the ClassFile structure. + Attribute attributes = null; + + int attributesCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (attributesCount-- > 0) { + // Read the attribute_info's attribute_name and attribute_length fields. + String attributeName = readUTF8(currentOffset, charBuffer); + int attributeLength = readInt(currentOffset + 2); + currentOffset += 6; + if (Constants.LOCAL_VARIABLE_TABLE.equals(attributeName)) { + if ((context.parsingOptions & SKIP_DEBUG) == 0) { + localVariableTableOffset = currentOffset; + // Parse the attribute to find the corresponding (debug only) labels. + int currentLocalVariableTableOffset = currentOffset; + int localVariableTableLength = readUnsignedShort(currentLocalVariableTableOffset); + currentLocalVariableTableOffset += 2; + while (localVariableTableLength-- > 0) { + int startPc = readUnsignedShort(currentLocalVariableTableOffset); + createDebugLabel(startPc, labels); + int length = readUnsignedShort(currentLocalVariableTableOffset + 2); + createDebugLabel(startPc + length, labels); + // Skip the name_index, descriptor_index and index fields (2 bytes each). + currentLocalVariableTableOffset += 10; + } + } + } else if (Constants.LOCAL_VARIABLE_TYPE_TABLE.equals(attributeName)) { + localVariableTypeTableOffset = currentOffset; + // Here we do not extract the labels corresponding to the attribute content. We assume they + // are the same or a subset of those of the LocalVariableTable attribute. + } else if (Constants.LINE_NUMBER_TABLE.equals(attributeName)) { + if ((context.parsingOptions & SKIP_DEBUG) == 0) { + // Parse the attribute to find the corresponding (debug only) labels. + int currentLineNumberTableOffset = currentOffset; + int lineNumberTableLength = readUnsignedShort(currentLineNumberTableOffset); + currentLineNumberTableOffset += 2; + while (lineNumberTableLength-- > 0) { + int startPc = readUnsignedShort(currentLineNumberTableOffset); + int lineNumber = readUnsignedShort(currentLineNumberTableOffset + 2); + currentLineNumberTableOffset += 4; + createDebugLabel(startPc, labels); + labels[startPc].addLineNumber(lineNumber); + } + } + } else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + visibleTypeAnnotationOffsets = + readTypeAnnotations(methodVisitor, context, currentOffset, /* visible= */ true); + // Here we do not extract the labels corresponding to the attribute content. This would + // require a full parsing of the attribute, which would need to be repeated when parsing + // the bytecode instructions (see below). Instead, the content of the attribute is read one + // type annotation at a time (i.e. after a type annotation has been visited, the next type + // annotation is read), and the labels it contains are also extracted one annotation at a + // time. This assumes that type annotations are ordered by increasing bytecode offset. + } else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + invisibleTypeAnnotationOffsets = + readTypeAnnotations(methodVisitor, context, currentOffset, /* visible= */ false); + // Same comment as above for the RuntimeVisibleTypeAnnotations attribute. + } else if (Constants.STACK_MAP_TABLE.equals(attributeName)) { + if ((context.parsingOptions & SKIP_FRAMES) == 0) { + stackMapFrameOffset = currentOffset + 2; + stackMapTableEndOffset = currentOffset + attributeLength; + } + // Here we do not extract the labels corresponding to the attribute content. This would + // require a full parsing of the attribute, which would need to be repeated when parsing + // the bytecode instructions (see below). Instead, the content of the attribute is read one + // frame at a time (i.e. after a frame has been visited, the next frame is read), and the + // labels it contains are also extracted one frame at a time. Thanks to the ordering of + // frames, having only a "one frame lookahead" is not a problem, i.e. it is not possible to + // see an offset smaller than the offset of the current instruction and for which no Label + // exist. Except for UNINITIALIZED type offsets. We solve this by parsing the stack map + // table without a full decoding (see below). + } else if ("StackMap".equals(attributeName)) { + if ((context.parsingOptions & SKIP_FRAMES) == 0) { + stackMapFrameOffset = currentOffset + 2; + stackMapTableEndOffset = currentOffset + attributeLength; + compressedFrames = false; + } + // IMPORTANT! Here we assume that the frames are ordered, as in the StackMapTable attribute, + // although this is not guaranteed by the attribute format. This allows an incremental + // extraction of the labels corresponding to this attribute (see the comment above for the + // StackMapTable attribute). + } else { + Attribute attribute = + readAttribute( + context.attributePrototypes, + attributeName, + currentOffset, + attributeLength, + charBuffer, + codeOffset, + labels); + attribute.nextAttribute = attributes; + attributes = attribute; + } + currentOffset += attributeLength; + } + + // Initialize the context fields related to stack map frames, and generate the first + // (implicit) stack map frame, if needed. + final boolean expandFrames = (context.parsingOptions & EXPAND_FRAMES) != 0; + if (stackMapFrameOffset != 0) { + // The bytecode offset of the first explicit frame is not offset_delta + 1 but only + // offset_delta. Setting the implicit frame offset to -1 allows us to use of the + // "offset_delta + 1" rule in all cases. + context.currentFrameOffset = -1; + context.currentFrameType = 0; + context.currentFrameLocalCount = 0; + context.currentFrameLocalCountDelta = 0; + context.currentFrameLocalTypes = new Object[maxLocals]; + context.currentFrameStackCount = 0; + context.currentFrameStackTypes = new Object[maxStack]; + if (expandFrames) { + computeImplicitFrame(context); + } + // Find the labels for UNINITIALIZED frame types. Instead of decoding each element of the + // stack map table, we look for 3 consecutive bytes that "look like" an UNINITIALIZED type + // (tag ITEM_Uninitialized, offset within bytecode bounds, NEW instruction at this offset). + // We may find false positives (i.e. not real UNINITIALIZED types), but this should be rare, + // and the only consequence will be the creation of an unneeded label. This is better than + // creating a label for each NEW instruction, and faster than fully decoding the whole stack + // map table. + for (int offset = stackMapFrameOffset; offset < stackMapTableEndOffset - 2; ++offset) { + if (classBuffer[offset] == Frame.ITEM_UNINITIALIZED) { + int potentialBytecodeOffset = readUnsignedShort(offset + 1); + if (potentialBytecodeOffset >= 0 + && potentialBytecodeOffset < codeLength + && (classBuffer[bytecodeStartOffset + potentialBytecodeOffset] & 0xFF) + == Opcodes.NEW) { + createLabel(potentialBytecodeOffset, labels); + } + } + } + } + if (expandFrames && (context.parsingOptions & EXPAND_ASM_INSNS) != 0) { + // Expanding the ASM specific instructions can introduce F_INSERT frames, even if the method + // does not currently have any frame. These inserted frames must be computed by simulating the + // effect of the bytecode instructions, one by one, starting from the implicit first frame. + // For this, MethodWriter needs to know maxLocals before the first instruction is visited. To + // ensure this, we visit the implicit first frame here (passing only maxLocals - the rest is + // computed in MethodWriter). + methodVisitor.visitFrame(Opcodes.F_NEW, maxLocals, null, 0, null); + } + + // Visit the bytecode instructions. First, introduce state variables for the incremental parsing + // of the type annotations. + + // Index of the next runtime visible type annotation to read (in the + // visibleTypeAnnotationOffsets array). + int currentVisibleTypeAnnotationIndex = 0; + // The bytecode offset of the next runtime visible type annotation to read, or -1. + int currentVisibleTypeAnnotationBytecodeOffset = + getTypeAnnotationBytecodeOffset(visibleTypeAnnotationOffsets, 0); + // Index of the next runtime invisible type annotation to read (in the + // invisibleTypeAnnotationOffsets array). + int currentInvisibleTypeAnnotationIndex = 0; + // The bytecode offset of the next runtime invisible type annotation to read, or -1. + int currentInvisibleTypeAnnotationBytecodeOffset = + getTypeAnnotationBytecodeOffset(invisibleTypeAnnotationOffsets, 0); + + // Whether a F_INSERT stack map frame must be inserted before the current instruction. + boolean insertFrame = false; + + // The delta to subtract from a goto_w or jsr_w opcode to get the corresponding goto or jsr + // opcode, or 0 if goto_w and jsr_w must be left unchanged (i.e. when expanding ASM specific + // instructions). + final int wideJumpOpcodeDelta = + (context.parsingOptions & EXPAND_ASM_INSNS) == 0 ? Constants.WIDE_JUMP_OPCODE_DELTA : 0; + + currentOffset = bytecodeStartOffset; + while (currentOffset < bytecodeEndOffset) { + final int currentBytecodeOffset = currentOffset - bytecodeStartOffset; + + // Visit the label and the line number(s) for this bytecode offset, if any. + Label currentLabel = labels[currentBytecodeOffset]; + if (currentLabel != null) { + currentLabel.accept(methodVisitor, (context.parsingOptions & SKIP_DEBUG) == 0); + } + + // Visit the stack map frame for this bytecode offset, if any. + while (stackMapFrameOffset != 0 + && (context.currentFrameOffset == currentBytecodeOffset + || context.currentFrameOffset == -1)) { + // If there is a stack map frame for this offset, make methodVisitor visit it, and read the + // next stack map frame if there is one. + if (context.currentFrameOffset != -1) { + if (!compressedFrames || expandFrames) { + methodVisitor.visitFrame( + Opcodes.F_NEW, + context.currentFrameLocalCount, + context.currentFrameLocalTypes, + context.currentFrameStackCount, + context.currentFrameStackTypes); + } else { + methodVisitor.visitFrame( + context.currentFrameType, + context.currentFrameLocalCountDelta, + context.currentFrameLocalTypes, + context.currentFrameStackCount, + context.currentFrameStackTypes); + } + // Since there is already a stack map frame for this bytecode offset, there is no need to + // insert a new one. + insertFrame = false; + } + if (stackMapFrameOffset < stackMapTableEndOffset) { + stackMapFrameOffset = + readStackMapFrame(stackMapFrameOffset, compressedFrames, expandFrames, context); + } else { + stackMapFrameOffset = 0; + } + } + + // Insert a stack map frame for this bytecode offset, if requested by setting insertFrame to + // true during the previous iteration. The actual frame content is computed in MethodWriter. + if (insertFrame) { + if ((context.parsingOptions & EXPAND_FRAMES) != 0) { + methodVisitor.visitFrame(Constants.F_INSERT, 0, null, 0, null); + } + insertFrame = false; + } + + // Visit the instruction at this bytecode offset. + int opcode = classBuffer[currentOffset] & 0xFF; + switch (opcode) { + case Opcodes.NOP: + case Opcodes.ACONST_NULL: + case Opcodes.ICONST_M1: + case Opcodes.ICONST_0: + case Opcodes.ICONST_1: + case Opcodes.ICONST_2: + case Opcodes.ICONST_3: + case Opcodes.ICONST_4: + case Opcodes.ICONST_5: + case Opcodes.LCONST_0: + case Opcodes.LCONST_1: + case Opcodes.FCONST_0: + case Opcodes.FCONST_1: + case Opcodes.FCONST_2: + case Opcodes.DCONST_0: + case Opcodes.DCONST_1: + case Opcodes.IALOAD: + case Opcodes.LALOAD: + case Opcodes.FALOAD: + case Opcodes.DALOAD: + case Opcodes.AALOAD: + case Opcodes.BALOAD: + case Opcodes.CALOAD: + case Opcodes.SALOAD: + case Opcodes.IASTORE: + case Opcodes.LASTORE: + case Opcodes.FASTORE: + case Opcodes.DASTORE: + case Opcodes.AASTORE: + case Opcodes.BASTORE: + case Opcodes.CASTORE: + case Opcodes.SASTORE: + case Opcodes.POP: + case Opcodes.POP2: + case Opcodes.DUP: + case Opcodes.DUP_X1: + case Opcodes.DUP_X2: + case Opcodes.DUP2: + case Opcodes.DUP2_X1: + case Opcodes.DUP2_X2: + case Opcodes.SWAP: + case Opcodes.IADD: + case Opcodes.LADD: + case Opcodes.FADD: + case Opcodes.DADD: + case Opcodes.ISUB: + case Opcodes.LSUB: + case Opcodes.FSUB: + case Opcodes.DSUB: + case Opcodes.IMUL: + case Opcodes.LMUL: + case Opcodes.FMUL: + case Opcodes.DMUL: + case Opcodes.IDIV: + case Opcodes.LDIV: + case Opcodes.FDIV: + case Opcodes.DDIV: + case Opcodes.IREM: + case Opcodes.LREM: + case Opcodes.FREM: + case Opcodes.DREM: + case Opcodes.INEG: + case Opcodes.LNEG: + case Opcodes.FNEG: + case Opcodes.DNEG: + case Opcodes.ISHL: + case Opcodes.LSHL: + case Opcodes.ISHR: + case Opcodes.LSHR: + case Opcodes.IUSHR: + case Opcodes.LUSHR: + case Opcodes.IAND: + case Opcodes.LAND: + case Opcodes.IOR: + case Opcodes.LOR: + case Opcodes.IXOR: + case Opcodes.LXOR: + case Opcodes.I2L: + case Opcodes.I2F: + case Opcodes.I2D: + case Opcodes.L2I: + case Opcodes.L2F: + case Opcodes.L2D: + case Opcodes.F2I: + case Opcodes.F2L: + case Opcodes.F2D: + case Opcodes.D2I: + case Opcodes.D2L: + case Opcodes.D2F: + case Opcodes.I2B: + case Opcodes.I2C: + case Opcodes.I2S: + case Opcodes.LCMP: + case Opcodes.FCMPL: + case Opcodes.FCMPG: + case Opcodes.DCMPL: + case Opcodes.DCMPG: + case Opcodes.IRETURN: + case Opcodes.LRETURN: + case Opcodes.FRETURN: + case Opcodes.DRETURN: + case Opcodes.ARETURN: + case Opcodes.RETURN: + case Opcodes.ARRAYLENGTH: + case Opcodes.ATHROW: + case Opcodes.MONITORENTER: + case Opcodes.MONITOREXIT: + methodVisitor.visitInsn(opcode); + currentOffset += 1; + break; + case Constants.ILOAD_0: + case Constants.ILOAD_1: + case Constants.ILOAD_2: + case Constants.ILOAD_3: + case Constants.LLOAD_0: + case Constants.LLOAD_1: + case Constants.LLOAD_2: + case Constants.LLOAD_3: + case Constants.FLOAD_0: + case Constants.FLOAD_1: + case Constants.FLOAD_2: + case Constants.FLOAD_3: + case Constants.DLOAD_0: + case Constants.DLOAD_1: + case Constants.DLOAD_2: + case Constants.DLOAD_3: + case Constants.ALOAD_0: + case Constants.ALOAD_1: + case Constants.ALOAD_2: + case Constants.ALOAD_3: + opcode -= Constants.ILOAD_0; + methodVisitor.visitVarInsn(Opcodes.ILOAD + (opcode >> 2), opcode & 0x3); + currentOffset += 1; + break; + case Constants.ISTORE_0: + case Constants.ISTORE_1: + case Constants.ISTORE_2: + case Constants.ISTORE_3: + case Constants.LSTORE_0: + case Constants.LSTORE_1: + case Constants.LSTORE_2: + case Constants.LSTORE_3: + case Constants.FSTORE_0: + case Constants.FSTORE_1: + case Constants.FSTORE_2: + case Constants.FSTORE_3: + case Constants.DSTORE_0: + case Constants.DSTORE_1: + case Constants.DSTORE_2: + case Constants.DSTORE_3: + case Constants.ASTORE_0: + case Constants.ASTORE_1: + case Constants.ASTORE_2: + case Constants.ASTORE_3: + opcode -= Constants.ISTORE_0; + methodVisitor.visitVarInsn(Opcodes.ISTORE + (opcode >> 2), opcode & 0x3); + currentOffset += 1; + break; + case Opcodes.IFEQ: + case Opcodes.IFNE: + case Opcodes.IFLT: + case Opcodes.IFGE: + case Opcodes.IFGT: + case Opcodes.IFLE: + case Opcodes.IF_ICMPEQ: + case Opcodes.IF_ICMPNE: + case Opcodes.IF_ICMPLT: + case Opcodes.IF_ICMPGE: + case Opcodes.IF_ICMPGT: + case Opcodes.IF_ICMPLE: + case Opcodes.IF_ACMPEQ: + case Opcodes.IF_ACMPNE: + case Opcodes.GOTO: + case Opcodes.JSR: + case Opcodes.IFNULL: + case Opcodes.IFNONNULL: + methodVisitor.visitJumpInsn( + opcode, labels[currentBytecodeOffset + readShort(currentOffset + 1)]); + currentOffset += 3; + break; + case Constants.GOTO_W: + case Constants.JSR_W: + methodVisitor.visitJumpInsn( + opcode - wideJumpOpcodeDelta, + labels[currentBytecodeOffset + readInt(currentOffset + 1)]); + currentOffset += 5; + break; + case Constants.ASM_IFEQ: + case Constants.ASM_IFNE: + case Constants.ASM_IFLT: + case Constants.ASM_IFGE: + case Constants.ASM_IFGT: + case Constants.ASM_IFLE: + case Constants.ASM_IF_ICMPEQ: + case Constants.ASM_IF_ICMPNE: + case Constants.ASM_IF_ICMPLT: + case Constants.ASM_IF_ICMPGE: + case Constants.ASM_IF_ICMPGT: + case Constants.ASM_IF_ICMPLE: + case Constants.ASM_IF_ACMPEQ: + case Constants.ASM_IF_ACMPNE: + case Constants.ASM_GOTO: + case Constants.ASM_JSR: + case Constants.ASM_IFNULL: + case Constants.ASM_IFNONNULL: + { + // A forward jump with an offset > 32767. In this case we automatically replace ASM_GOTO + // with GOTO_W, ASM_JSR with JSR_W and ASM_IFxxx with IFNOTxxx GOTO_W L:..., + // where IFNOTxxx is the "opposite" opcode of ASMS_IFxxx (e.g. IFNE for ASM_IFEQ) and + // where designates the instruction just after the GOTO_W. + // First, change the ASM specific opcodes ASM_IFEQ ... ASM_JSR, ASM_IFNULL and + // ASM_IFNONNULL to IFEQ ... JSR, IFNULL and IFNONNULL. + opcode = + opcode < Constants.ASM_IFNULL + ? opcode - Constants.ASM_OPCODE_DELTA + : opcode - Constants.ASM_IFNULL_OPCODE_DELTA; + Label target = labels[currentBytecodeOffset + readUnsignedShort(currentOffset + 1)]; + if (opcode == Opcodes.GOTO || opcode == Opcodes.JSR) { + // Replace GOTO with GOTO_W and JSR with JSR_W. + methodVisitor.visitJumpInsn(opcode + Constants.WIDE_JUMP_OPCODE_DELTA, target); + } else { + // Compute the "opposite" of opcode. This can be done by flipping the least + // significant bit for IFNULL and IFNONNULL, and similarly for IFEQ ... IF_ACMPEQ + // (with a pre and post offset by 1). + opcode = opcode < Opcodes.GOTO ? ((opcode + 1) ^ 1) - 1 : opcode ^ 1; + Label endif = createLabel(currentBytecodeOffset + 3, labels); + methodVisitor.visitJumpInsn(opcode, endif); + methodVisitor.visitJumpInsn(Constants.GOTO_W, target); + // endif designates the instruction just after GOTO_W, and is visited as part of the + // next instruction. Since it is a jump target, we need to insert a frame here. + insertFrame = true; + } + currentOffset += 3; + break; + } + case Constants.ASM_GOTO_W: + // Replace ASM_GOTO_W with GOTO_W. + methodVisitor.visitJumpInsn( + Constants.GOTO_W, labels[currentBytecodeOffset + readInt(currentOffset + 1)]); + // The instruction just after is a jump target (because ASM_GOTO_W is used in patterns + // IFNOTxxx ASM_GOTO_W L:..., see MethodWriter), so we need to insert a frame + // here. + insertFrame = true; + currentOffset += 5; + break; + case Constants.WIDE: + opcode = classBuffer[currentOffset + 1] & 0xFF; + if (opcode == Opcodes.IINC) { + methodVisitor.visitIincInsn( + readUnsignedShort(currentOffset + 2), readShort(currentOffset + 4)); + currentOffset += 6; + } else { + methodVisitor.visitVarInsn(opcode, readUnsignedShort(currentOffset + 2)); + currentOffset += 4; + } + break; + case Opcodes.TABLESWITCH: + { + // Skip 0 to 3 padding bytes. + currentOffset += 4 - (currentBytecodeOffset & 3); + // Read the instruction. + Label defaultLabel = labels[currentBytecodeOffset + readInt(currentOffset)]; + int low = readInt(currentOffset + 4); + int high = readInt(currentOffset + 8); + currentOffset += 12; + Label[] table = new Label[high - low + 1]; + for (int i = 0; i < table.length; ++i) { + table[i] = labels[currentBytecodeOffset + readInt(currentOffset)]; + currentOffset += 4; + } + methodVisitor.visitTableSwitchInsn(low, high, defaultLabel, table); + break; + } + case Opcodes.LOOKUPSWITCH: + { + // Skip 0 to 3 padding bytes. + currentOffset += 4 - (currentBytecodeOffset & 3); + // Read the instruction. + Label defaultLabel = labels[currentBytecodeOffset + readInt(currentOffset)]; + int numPairs = readInt(currentOffset + 4); + currentOffset += 8; + int[] keys = new int[numPairs]; + Label[] values = new Label[numPairs]; + for (int i = 0; i < numPairs; ++i) { + keys[i] = readInt(currentOffset); + values[i] = labels[currentBytecodeOffset + readInt(currentOffset + 4)]; + currentOffset += 8; + } + methodVisitor.visitLookupSwitchInsn(defaultLabel, keys, values); + break; + } + case Opcodes.ILOAD: + case Opcodes.LLOAD: + case Opcodes.FLOAD: + case Opcodes.DLOAD: + case Opcodes.ALOAD: + case Opcodes.ISTORE: + case Opcodes.LSTORE: + case Opcodes.FSTORE: + case Opcodes.DSTORE: + case Opcodes.ASTORE: + case Opcodes.RET: + methodVisitor.visitVarInsn(opcode, classBuffer[currentOffset + 1] & 0xFF); + currentOffset += 2; + break; + case Opcodes.BIPUSH: + case Opcodes.NEWARRAY: + methodVisitor.visitIntInsn(opcode, classBuffer[currentOffset + 1]); + currentOffset += 2; + break; + case Opcodes.SIPUSH: + methodVisitor.visitIntInsn(opcode, readShort(currentOffset + 1)); + currentOffset += 3; + break; + case Opcodes.LDC: + methodVisitor.visitLdcInsn(readConst(classBuffer[currentOffset + 1] & 0xFF, charBuffer)); + currentOffset += 2; + break; + case Constants.LDC_W: + case Constants.LDC2_W: + methodVisitor.visitLdcInsn(readConst(readUnsignedShort(currentOffset + 1), charBuffer)); + currentOffset += 3; + break; + case Opcodes.GETSTATIC: + case Opcodes.PUTSTATIC: + case Opcodes.GETFIELD: + case Opcodes.PUTFIELD: + case Opcodes.INVOKEVIRTUAL: + case Opcodes.INVOKESPECIAL: + case Opcodes.INVOKESTATIC: + case Opcodes.INVOKEINTERFACE: + { + int cpInfoOffset = cpInfoOffsets[readUnsignedShort(currentOffset + 1)]; + int nameAndTypeCpInfoOffset = cpInfoOffsets[readUnsignedShort(cpInfoOffset + 2)]; + String owner = readClass(cpInfoOffset, charBuffer); + String name = readUTF8(nameAndTypeCpInfoOffset, charBuffer); + String descriptor = readUTF8(nameAndTypeCpInfoOffset + 2, charBuffer); + if (opcode < Opcodes.INVOKEVIRTUAL) { + methodVisitor.visitFieldInsn(opcode, owner, name, descriptor); + } else { + boolean isInterface = + classBuffer[cpInfoOffset - 1] == Symbol.CONSTANT_INTERFACE_METHODREF_TAG; + methodVisitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + if (opcode == Opcodes.INVOKEINTERFACE) { + currentOffset += 5; + } else { + currentOffset += 3; + } + break; + } + case Opcodes.INVOKEDYNAMIC: + { + int cpInfoOffset = cpInfoOffsets[readUnsignedShort(currentOffset + 1)]; + int nameAndTypeCpInfoOffset = cpInfoOffsets[readUnsignedShort(cpInfoOffset + 2)]; + String name = readUTF8(nameAndTypeCpInfoOffset, charBuffer); + String descriptor = readUTF8(nameAndTypeCpInfoOffset + 2, charBuffer); + int bootstrapMethodOffset = bootstrapMethodOffsets[readUnsignedShort(cpInfoOffset)]; + Handle handle = + (Handle) readConst(readUnsignedShort(bootstrapMethodOffset), charBuffer); + Object[] bootstrapMethodArguments = + new Object[readUnsignedShort(bootstrapMethodOffset + 2)]; + bootstrapMethodOffset += 4; + for (int i = 0; i < bootstrapMethodArguments.length; i++) { + bootstrapMethodArguments[i] = + readConst(readUnsignedShort(bootstrapMethodOffset), charBuffer); + bootstrapMethodOffset += 2; + } + methodVisitor.visitInvokeDynamicInsn( + name, descriptor, handle, bootstrapMethodArguments); + currentOffset += 5; + break; + } + case Opcodes.NEW: + case Opcodes.ANEWARRAY: + case Opcodes.CHECKCAST: + case Opcodes.INSTANCEOF: + methodVisitor.visitTypeInsn(opcode, readClass(currentOffset + 1, charBuffer)); + currentOffset += 3; + break; + case Opcodes.IINC: + methodVisitor.visitIincInsn( + classBuffer[currentOffset + 1] & 0xFF, classBuffer[currentOffset + 2]); + currentOffset += 3; + break; + case Opcodes.MULTIANEWARRAY: + methodVisitor.visitMultiANewArrayInsn( + readClass(currentOffset + 1, charBuffer), classBuffer[currentOffset + 3] & 0xFF); + currentOffset += 4; + break; + default: + throw new AssertionError(); + } + + // Visit the runtime visible instruction annotations, if any. + while (visibleTypeAnnotationOffsets != null + && currentVisibleTypeAnnotationIndex < visibleTypeAnnotationOffsets.length + && currentVisibleTypeAnnotationBytecodeOffset <= currentBytecodeOffset) { + if (currentVisibleTypeAnnotationBytecodeOffset == currentBytecodeOffset) { + // Parse the target_type, target_info and target_path fields. + int currentAnnotationOffset = + readTypeAnnotationTarget( + context, visibleTypeAnnotationOffsets[currentVisibleTypeAnnotationIndex]); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + readElementValues( + methodVisitor.visitInsnAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible= */ true), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + currentVisibleTypeAnnotationBytecodeOffset = + getTypeAnnotationBytecodeOffset( + visibleTypeAnnotationOffsets, ++currentVisibleTypeAnnotationIndex); + } + + // Visit the runtime invisible instruction annotations, if any. + while (invisibleTypeAnnotationOffsets != null + && currentInvisibleTypeAnnotationIndex < invisibleTypeAnnotationOffsets.length + && currentInvisibleTypeAnnotationBytecodeOffset <= currentBytecodeOffset) { + if (currentInvisibleTypeAnnotationBytecodeOffset == currentBytecodeOffset) { + // Parse the target_type, target_info and target_path fields. + int currentAnnotationOffset = + readTypeAnnotationTarget( + context, invisibleTypeAnnotationOffsets[currentInvisibleTypeAnnotationIndex]); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + readElementValues( + methodVisitor.visitInsnAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible= */ false), + currentAnnotationOffset, + /* named= */ true, + charBuffer); + } + currentInvisibleTypeAnnotationBytecodeOffset = + getTypeAnnotationBytecodeOffset( + invisibleTypeAnnotationOffsets, ++currentInvisibleTypeAnnotationIndex); + } + } + if (labels[codeLength] != null) { + methodVisitor.visitLabel(labels[codeLength]); + } + + // Visit LocalVariableTable and LocalVariableTypeTable attributes. + if (localVariableTableOffset != 0 && (context.parsingOptions & SKIP_DEBUG) == 0) { + // The (start_pc, index, signature_index) fields of each entry of the LocalVariableTypeTable. + int[] typeTable = null; + if (localVariableTypeTableOffset != 0) { + typeTable = new int[readUnsignedShort(localVariableTypeTableOffset) * 3]; + currentOffset = localVariableTypeTableOffset + 2; + int typeTableIndex = typeTable.length; + while (typeTableIndex > 0) { + // Store the offset of 'signature_index', and the value of 'index' and 'start_pc'. + typeTable[--typeTableIndex] = currentOffset + 6; + typeTable[--typeTableIndex] = readUnsignedShort(currentOffset + 8); + typeTable[--typeTableIndex] = readUnsignedShort(currentOffset); + currentOffset += 10; + } + } + int localVariableTableLength = readUnsignedShort(localVariableTableOffset); + currentOffset = localVariableTableOffset + 2; + while (localVariableTableLength-- > 0) { + int startPc = readUnsignedShort(currentOffset); + int length = readUnsignedShort(currentOffset + 2); + String name = readUTF8(currentOffset + 4, charBuffer); + String descriptor = readUTF8(currentOffset + 6, charBuffer); + int index = readUnsignedShort(currentOffset + 8); + currentOffset += 10; + String signature = null; + if (typeTable != null) { + for (int i = 0; i < typeTable.length; i += 3) { + if (typeTable[i] == startPc && typeTable[i + 1] == index) { + signature = readUTF8(typeTable[i + 2], charBuffer); + break; + } + } + } + methodVisitor.visitLocalVariable( + name, descriptor, signature, labels[startPc], labels[startPc + length], index); + } + } + + // Visit the local variable type annotations of the RuntimeVisibleTypeAnnotations attribute. + if (visibleTypeAnnotationOffsets != null) { + for (int typeAnnotationOffset : visibleTypeAnnotationOffsets) { + int targetType = readByte(typeAnnotationOffset); + if (targetType == TypeReference.LOCAL_VARIABLE + || targetType == TypeReference.RESOURCE_VARIABLE) { + // Parse the target_type, target_info and target_path fields. + currentOffset = readTypeAnnotationTarget(context, typeAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentOffset, charBuffer); + currentOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + readElementValues( + methodVisitor.visitLocalVariableAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + context.currentLocalVariableAnnotationRangeStarts, + context.currentLocalVariableAnnotationRangeEnds, + context.currentLocalVariableAnnotationRangeIndices, + annotationDescriptor, + /* visible= */ true), + currentOffset, + /* named= */ true, + charBuffer); + } + } + } + + // Visit the local variable type annotations of the RuntimeInvisibleTypeAnnotations attribute. + if (invisibleTypeAnnotationOffsets != null) { + for (int typeAnnotationOffset : invisibleTypeAnnotationOffsets) { + int targetType = readByte(typeAnnotationOffset); + if (targetType == TypeReference.LOCAL_VARIABLE + || targetType == TypeReference.RESOURCE_VARIABLE) { + // Parse the target_type, target_info and target_path fields. + currentOffset = readTypeAnnotationTarget(context, typeAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentOffset, charBuffer); + currentOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + readElementValues( + methodVisitor.visitLocalVariableAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + context.currentLocalVariableAnnotationRangeStarts, + context.currentLocalVariableAnnotationRangeEnds, + context.currentLocalVariableAnnotationRangeIndices, + annotationDescriptor, + /* visible= */ false), + currentOffset, + /* named= */ true, + charBuffer); + } + } + } + + // Visit the non standard attributes. + while (attributes != null) { + // Copy and reset the nextAttribute field so that it can also be used in MethodWriter. + Attribute nextAttribute = attributes.nextAttribute; + attributes.nextAttribute = null; + methodVisitor.visitAttribute(attributes); + attributes = nextAttribute; + } + + // Visit the max stack and max locals values. + methodVisitor.visitMaxs(maxStack, maxLocals); + } + + /** + * Returns the label corresponding to the given bytecode offset. The default implementation of + * this method creates a label for the given offset if it has not been already created. + * + * @param bytecodeOffset a bytecode offset in a method. + * @param labels the already created labels, indexed by their offset. If a label already exists + * for bytecodeOffset this method must not create a new one. Otherwise it must store the new + * label in this array. + * @return a non null Label, which must be equal to labels[bytecodeOffset]. + */ + protected Label readLabel(final int bytecodeOffset, final Label[] labels) { + if (labels[bytecodeOffset] == null) { + labels[bytecodeOffset] = new Label(); + } + return labels[bytecodeOffset]; + } + + /** + * Creates a label without the {@link Label#FLAG_DEBUG_ONLY} flag set, for the given bytecode + * offset. The label is created with a call to {@link #readLabel} and its {@link + * Label#FLAG_DEBUG_ONLY} flag is cleared. + * + * @param bytecodeOffset a bytecode offset in a method. + * @param labels the already created labels, indexed by their offset. + * @return a Label without the {@link Label#FLAG_DEBUG_ONLY} flag set. + */ + private Label createLabel(final int bytecodeOffset, final Label[] labels) { + Label label = readLabel(bytecodeOffset, labels); + label.flags &= ~Label.FLAG_DEBUG_ONLY; + return label; + } + + /** + * Creates a label with the {@link Label#FLAG_DEBUG_ONLY} flag set, if there is no already + * existing label for the given bytecode offset (otherwise does nothing). The label is created + * with a call to {@link #readLabel}. + * + * @param bytecodeOffset a bytecode offset in a method. + * @param labels the already created labels, indexed by their offset. + */ + private void createDebugLabel(final int bytecodeOffset, final Label[] labels) { + if (labels[bytecodeOffset] == null) { + readLabel(bytecodeOffset, labels).flags |= Label.FLAG_DEBUG_ONLY; + } + } + + // ---------------------------------------------------------------------------------------------- + // Methods to parse annotations, type annotations and parameter annotations + // ---------------------------------------------------------------------------------------------- + + /** + * Parses a Runtime[In]VisibleTypeAnnotations attribute to find the offset of each type_annotation + * entry it contains, to find the corresponding labels, and to visit the try catch block + * annotations. + * + * @param methodVisitor the method visitor to be used to visit the try catch block annotations. + * @param context information about the class being parsed. + * @param runtimeTypeAnnotationsOffset the start offset of a Runtime[In]VisibleTypeAnnotations + * attribute, excluding the attribute_info's attribute_name_index and attribute_length fields. + * @param visible true if the attribute to parse is a RuntimeVisibleTypeAnnotations attribute, + * false it is a RuntimeInvisibleTypeAnnotations attribute. + * @return the start offset of each entry of the Runtime[In]VisibleTypeAnnotations_attribute's + * 'annotations' array field. + */ + private int[] readTypeAnnotations( + final MethodVisitor methodVisitor, + final Context context, + final int runtimeTypeAnnotationsOffset, + final boolean visible) { + char[] charBuffer = context.charBuffer; + int currentOffset = runtimeTypeAnnotationsOffset; + // Read the num_annotations field and create an array to store the type_annotation offsets. + int[] typeAnnotationsOffsets = new int[readUnsignedShort(currentOffset)]; + currentOffset += 2; + // Parse the 'annotations' array field. + for (int i = 0; i < typeAnnotationsOffsets.length; ++i) { + typeAnnotationsOffsets[i] = currentOffset; + // Parse the type_annotation's target_type and the target_info fields. The size of the + // target_info field depends on the value of target_type. + int targetType = readInt(currentOffset); + switch (targetType >>> 24) { + case TypeReference.LOCAL_VARIABLE: + case TypeReference.RESOURCE_VARIABLE: + // A localvar_target has a variable size, which depends on the value of their table_length + // field. It also references bytecode offsets, for which we need labels. + int tableLength = readUnsignedShort(currentOffset + 1); + currentOffset += 3; + while (tableLength-- > 0) { + int startPc = readUnsignedShort(currentOffset); + int length = readUnsignedShort(currentOffset + 2); + // Skip the index field (2 bytes). + currentOffset += 6; + createLabel(startPc, context.currentMethodLabels); + createLabel(startPc + length, context.currentMethodLabels); + } + break; + case TypeReference.CAST: + case TypeReference.CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT: + case TypeReference.METHOD_INVOCATION_TYPE_ARGUMENT: + case TypeReference.CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT: + case TypeReference.METHOD_REFERENCE_TYPE_ARGUMENT: + currentOffset += 4; + break; + case TypeReference.CLASS_EXTENDS: + case TypeReference.CLASS_TYPE_PARAMETER_BOUND: + case TypeReference.METHOD_TYPE_PARAMETER_BOUND: + case TypeReference.THROWS: + case TypeReference.EXCEPTION_PARAMETER: + case TypeReference.INSTANCEOF: + case TypeReference.NEW: + case TypeReference.CONSTRUCTOR_REFERENCE: + case TypeReference.METHOD_REFERENCE: + currentOffset += 3; + break; + case TypeReference.CLASS_TYPE_PARAMETER: + case TypeReference.METHOD_TYPE_PARAMETER: + case TypeReference.METHOD_FORMAL_PARAMETER: + case TypeReference.FIELD: + case TypeReference.METHOD_RETURN: + case TypeReference.METHOD_RECEIVER: + default: + // TypeReference type which can't be used in Code attribute, or which is unknown. + throw new IllegalArgumentException(); + } + // Parse the rest of the type_annotation structure, starting with the target_path structure + // (whose size depends on its path_length field). + int pathLength = readByte(currentOffset); + if ((targetType >>> 24) == TypeReference.EXCEPTION_PARAMETER) { + // Parse the target_path structure and create a corresponding TypePath. + TypePath path = pathLength == 0 ? null : new TypePath(classFileBuffer, currentOffset); + currentOffset += 1 + 2 * pathLength; + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentOffset, charBuffer); + currentOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentOffset = + readElementValues( + methodVisitor.visitTryCatchAnnotation( + targetType & 0xFFFFFF00, path, annotationDescriptor, visible), + currentOffset, + /* named= */ true, + charBuffer); + } else { + // We don't want to visit the other target_type annotations, so we just skip them (which + // requires some parsing because the element_value_pairs array has a variable size). First, + // skip the target_path structure: + currentOffset += 3 + 2 * pathLength; + // Then skip the num_element_value_pairs and element_value_pairs fields (by reading them + // with a null AnnotationVisitor). + currentOffset = + readElementValues( + /* annotationVisitor= */ null, currentOffset, /* named= */ true, charBuffer); + } + } + return typeAnnotationsOffsets; + } + + /** + * Returns the bytecode offset corresponding to the specified JVMS 'type_annotation' structure, or + * -1 if there is no such type_annotation of if it does not have a bytecode offset. + * + * @param typeAnnotationOffsets the offset of each 'type_annotation' entry in a + * Runtime[In]VisibleTypeAnnotations attribute, or {@literal null}. + * @param typeAnnotationIndex the index a 'type_annotation' entry in typeAnnotationOffsets. + * @return bytecode offset corresponding to the specified JVMS 'type_annotation' structure, or -1 + * if there is no such type_annotation of if it does not have a bytecode offset. + */ + private int getTypeAnnotationBytecodeOffset( + final int[] typeAnnotationOffsets, final int typeAnnotationIndex) { + if (typeAnnotationOffsets == null + || typeAnnotationIndex >= typeAnnotationOffsets.length + || readByte(typeAnnotationOffsets[typeAnnotationIndex]) < TypeReference.INSTANCEOF) { + return -1; + } + return readUnsignedShort(typeAnnotationOffsets[typeAnnotationIndex] + 1); + } + + /** + * Parses the header of a JVMS type_annotation structure to extract its target_type, target_info + * and target_path (the result is stored in the given context), and returns the start offset of + * the rest of the type_annotation structure. + * + * @param context information about the class being parsed. This is where the extracted + * target_type and target_path must be stored. + * @param typeAnnotationOffset the start offset of a type_annotation structure. + * @return the start offset of the rest of the type_annotation structure. + */ + private int readTypeAnnotationTarget(final Context context, final int typeAnnotationOffset) { + int currentOffset = typeAnnotationOffset; + // Parse and store the target_type structure. + int targetType = readInt(typeAnnotationOffset); + switch (targetType >>> 24) { + case TypeReference.CLASS_TYPE_PARAMETER: + case TypeReference.METHOD_TYPE_PARAMETER: + case TypeReference.METHOD_FORMAL_PARAMETER: + targetType &= 0xFFFF0000; + currentOffset += 2; + break; + case TypeReference.FIELD: + case TypeReference.METHOD_RETURN: + case TypeReference.METHOD_RECEIVER: + targetType &= 0xFF000000; + currentOffset += 1; + break; + case TypeReference.LOCAL_VARIABLE: + case TypeReference.RESOURCE_VARIABLE: + targetType &= 0xFF000000; + int tableLength = readUnsignedShort(currentOffset + 1); + currentOffset += 3; + context.currentLocalVariableAnnotationRangeStarts = new Label[tableLength]; + context.currentLocalVariableAnnotationRangeEnds = new Label[tableLength]; + context.currentLocalVariableAnnotationRangeIndices = new int[tableLength]; + for (int i = 0; i < tableLength; ++i) { + int startPc = readUnsignedShort(currentOffset); + int length = readUnsignedShort(currentOffset + 2); + int index = readUnsignedShort(currentOffset + 4); + currentOffset += 6; + context.currentLocalVariableAnnotationRangeStarts[i] = + createLabel(startPc, context.currentMethodLabels); + context.currentLocalVariableAnnotationRangeEnds[i] = + createLabel(startPc + length, context.currentMethodLabels); + context.currentLocalVariableAnnotationRangeIndices[i] = index; + } + break; + case TypeReference.CAST: + case TypeReference.CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT: + case TypeReference.METHOD_INVOCATION_TYPE_ARGUMENT: + case TypeReference.CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT: + case TypeReference.METHOD_REFERENCE_TYPE_ARGUMENT: + targetType &= 0xFF0000FF; + currentOffset += 4; + break; + case TypeReference.CLASS_EXTENDS: + case TypeReference.CLASS_TYPE_PARAMETER_BOUND: + case TypeReference.METHOD_TYPE_PARAMETER_BOUND: + case TypeReference.THROWS: + case TypeReference.EXCEPTION_PARAMETER: + targetType &= 0xFFFFFF00; + currentOffset += 3; + break; + case TypeReference.INSTANCEOF: + case TypeReference.NEW: + case TypeReference.CONSTRUCTOR_REFERENCE: + case TypeReference.METHOD_REFERENCE: + targetType &= 0xFF000000; + currentOffset += 3; + break; + default: + throw new IllegalArgumentException(); + } + context.currentTypeAnnotationTarget = targetType; + // Parse and store the target_path structure. + int pathLength = readByte(currentOffset); + context.currentTypeAnnotationTargetPath = + pathLength == 0 ? null : new TypePath(classFileBuffer, currentOffset); + // Return the start offset of the rest of the type_annotation structure. + return currentOffset + 1 + 2 * pathLength; + } + + /** + * Reads a Runtime[In]VisibleParameterAnnotations attribute and makes the given visitor visit it. + * + * @param methodVisitor the visitor that must visit the parameter annotations. + * @param context information about the class being parsed. + * @param runtimeParameterAnnotationsOffset the start offset of a + * Runtime[In]VisibleParameterAnnotations attribute, excluding the attribute_info's + * attribute_name_index and attribute_length fields. + * @param visible true if the attribute to parse is a RuntimeVisibleParameterAnnotations + * attribute, false it is a RuntimeInvisibleParameterAnnotations attribute. + */ + private void readParameterAnnotations( + final MethodVisitor methodVisitor, + final Context context, + final int runtimeParameterAnnotationsOffset, + final boolean visible) { + int currentOffset = runtimeParameterAnnotationsOffset; + int numParameters = classFileBuffer[currentOffset++] & 0xFF; + methodVisitor.visitAnnotableParameterCount(numParameters, visible); + char[] charBuffer = context.charBuffer; + for (int i = 0; i < numParameters; ++i) { + int numAnnotations = readUnsignedShort(currentOffset); + currentOffset += 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentOffset, charBuffer); + currentOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentOffset = + readElementValues( + methodVisitor.visitParameterAnnotation(i, annotationDescriptor, visible), + currentOffset, + /* named= */ true, + charBuffer); + } + } + } + + /** + * Reads the element values of a JVMS 'annotation' structure and makes the given visitor visit + * them. This method can also be used to read the values of the JVMS 'array_value' field of an + * annotation's 'element_value'. + * + * @param annotationVisitor the visitor that must visit the values. + * @param annotationOffset the start offset of an 'annotation' structure (excluding its type_index + * field) or of an 'array_value' structure. + * @param named if the annotation values are named or not. This should be true to parse the values + * of a JVMS 'annotation' structure, and false to parse the JVMS 'array_value' of an + * annotation's element_value. + * @param charBuffer the buffer used to read strings in the constant pool. + * @return the end offset of the JVMS 'annotation' or 'array_value' structure. + */ + private int readElementValues( + final AnnotationVisitor annotationVisitor, + final int annotationOffset, + final boolean named, + final char[] charBuffer) { + int currentOffset = annotationOffset; + // Read the num_element_value_pairs field (or num_values field for an array_value). + int numElementValuePairs = readUnsignedShort(currentOffset); + currentOffset += 2; + if (named) { + // Parse the element_value_pairs array. + while (numElementValuePairs-- > 0) { + String elementName = readUTF8(currentOffset, charBuffer); + currentOffset = + readElementValue(annotationVisitor, currentOffset + 2, elementName, charBuffer); + } + } else { + // Parse the array_value array. + while (numElementValuePairs-- > 0) { + currentOffset = + readElementValue(annotationVisitor, currentOffset, /* elementName= */ null, charBuffer); + } + } + if (annotationVisitor != null) { + annotationVisitor.visitEnd(); + } + return currentOffset; + } + + /** + * Reads a JVMS 'element_value' structure and makes the given visitor visit it. + * + * @param annotationVisitor the visitor that must visit the element_value structure. + * @param elementValueOffset the start offset in {@link #classFileBuffer} of the element_value + * structure to be read. + * @param elementName the name of the element_value structure to be read, or {@literal null}. + * @param charBuffer the buffer used to read strings in the constant pool. + * @return the end offset of the JVMS 'element_value' structure. + */ + private int readElementValue( + final AnnotationVisitor annotationVisitor, + final int elementValueOffset, + final String elementName, + final char[] charBuffer) { + int currentOffset = elementValueOffset; + if (annotationVisitor == null) { + switch (classFileBuffer[currentOffset] & 0xFF) { + case 'e': // enum_const_value + return currentOffset + 5; + case '@': // annotation_value + return readElementValues(null, currentOffset + 3, /* named= */ true, charBuffer); + case '[': // array_value + return readElementValues(null, currentOffset + 1, /* named= */ false, charBuffer); + default: + return currentOffset + 3; + } + } + switch (classFileBuffer[currentOffset++] & 0xFF) { + case 'B': // const_value_index, CONSTANT_Integer + annotationVisitor.visit( + elementName, (byte) readInt(cpInfoOffsets[readUnsignedShort(currentOffset)])); + currentOffset += 2; + break; + case 'C': // const_value_index, CONSTANT_Integer + annotationVisitor.visit( + elementName, (char) readInt(cpInfoOffsets[readUnsignedShort(currentOffset)])); + currentOffset += 2; + break; + case 'D': // const_value_index, CONSTANT_Double + case 'F': // const_value_index, CONSTANT_Float + case 'I': // const_value_index, CONSTANT_Integer + case 'J': // const_value_index, CONSTANT_Long + annotationVisitor.visit( + elementName, readConst(readUnsignedShort(currentOffset), charBuffer)); + currentOffset += 2; + break; + case 'S': // const_value_index, CONSTANT_Integer + annotationVisitor.visit( + elementName, (short) readInt(cpInfoOffsets[readUnsignedShort(currentOffset)])); + currentOffset += 2; + break; + + case 'Z': // const_value_index, CONSTANT_Integer + annotationVisitor.visit( + elementName, + readInt(cpInfoOffsets[readUnsignedShort(currentOffset)]) == 0 + ? Boolean.FALSE + : Boolean.TRUE); + currentOffset += 2; + break; + case 's': // const_value_index, CONSTANT_Utf8 + annotationVisitor.visit(elementName, readUTF8(currentOffset, charBuffer)); + currentOffset += 2; + break; + case 'e': // enum_const_value + annotationVisitor.visitEnum( + elementName, + readUTF8(currentOffset, charBuffer), + readUTF8(currentOffset + 2, charBuffer)); + currentOffset += 4; + break; + case 'c': // class_info + annotationVisitor.visit(elementName, Type.getType(readUTF8(currentOffset, charBuffer))); + currentOffset += 2; + break; + case '@': // annotation_value + currentOffset = + readElementValues( + annotationVisitor.visitAnnotation(elementName, readUTF8(currentOffset, charBuffer)), + currentOffset + 2, + true, + charBuffer); + break; + case '[': // array_value + int numValues = readUnsignedShort(currentOffset); + currentOffset += 2; + if (numValues == 0) { + return readElementValues( + annotationVisitor.visitArray(elementName), + currentOffset - 2, + /* named= */ false, + charBuffer); + } + switch (classFileBuffer[currentOffset] & 0xFF) { + case 'B': + byte[] byteValues = new byte[numValues]; + for (int i = 0; i < numValues; i++) { + byteValues[i] = (byte) readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]); + currentOffset += 3; + } + annotationVisitor.visit(elementName, byteValues); + break; + case 'Z': + boolean[] booleanValues = new boolean[numValues]; + for (int i = 0; i < numValues; i++) { + booleanValues[i] = readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]) != 0; + currentOffset += 3; + } + annotationVisitor.visit(elementName, booleanValues); + break; + case 'S': + short[] shortValues = new short[numValues]; + for (int i = 0; i < numValues; i++) { + shortValues[i] = (short) readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]); + currentOffset += 3; + } + annotationVisitor.visit(elementName, shortValues); + break; + case 'C': + char[] charValues = new char[numValues]; + for (int i = 0; i < numValues; i++) { + charValues[i] = (char) readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]); + currentOffset += 3; + } + annotationVisitor.visit(elementName, charValues); + break; + case 'I': + int[] intValues = new int[numValues]; + for (int i = 0; i < numValues; i++) { + intValues[i] = readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]); + currentOffset += 3; + } + annotationVisitor.visit(elementName, intValues); + break; + case 'J': + long[] longValues = new long[numValues]; + for (int i = 0; i < numValues; i++) { + longValues[i] = readLong(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]); + currentOffset += 3; + } + annotationVisitor.visit(elementName, longValues); + break; + case 'F': + float[] floatValues = new float[numValues]; + for (int i = 0; i < numValues; i++) { + floatValues[i] = + Float.intBitsToFloat( + readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)])); + currentOffset += 3; + } + annotationVisitor.visit(elementName, floatValues); + break; + case 'D': + double[] doubleValues = new double[numValues]; + for (int i = 0; i < numValues; i++) { + doubleValues[i] = + Double.longBitsToDouble( + readLong(cpInfoOffsets[readUnsignedShort(currentOffset + 1)])); + currentOffset += 3; + } + annotationVisitor.visit(elementName, doubleValues); + break; + default: + currentOffset = + readElementValues( + annotationVisitor.visitArray(elementName), + currentOffset - 2, + /* named= */ false, + charBuffer); + break; + } + break; + default: + throw new IllegalArgumentException(); + } + return currentOffset; + } + + // ---------------------------------------------------------------------------------------------- + // Methods to parse stack map frames + // ---------------------------------------------------------------------------------------------- + + /** + * Computes the implicit frame of the method currently being parsed (as defined in the given + * {@link Context}) and stores it in the given context. + * + * @param context information about the class being parsed. + */ + private void computeImplicitFrame(final Context context) { + String methodDescriptor = context.currentMethodDescriptor; + Object[] locals = context.currentFrameLocalTypes; + int numLocal = 0; + if ((context.currentMethodAccessFlags & Opcodes.ACC_STATIC) == 0) { + if ("".equals(context.currentMethodName)) { + locals[numLocal++] = Opcodes.UNINITIALIZED_THIS; + } else { + locals[numLocal++] = readClass(header + 2, context.charBuffer); + } + } + // Parse the method descriptor, one argument type descriptor at each iteration. Start by + // skipping the first method descriptor character, which is always '('. + int currentMethodDescritorOffset = 1; + while (true) { + int currentArgumentDescriptorStartOffset = currentMethodDescritorOffset; + switch (methodDescriptor.charAt(currentMethodDescritorOffset++)) { + case 'Z': + case 'C': + case 'B': + case 'S': + case 'I': + locals[numLocal++] = Opcodes.INTEGER; + break; + case 'F': + locals[numLocal++] = Opcodes.FLOAT; + break; + case 'J': + locals[numLocal++] = Opcodes.LONG; + break; + case 'D': + locals[numLocal++] = Opcodes.DOUBLE; + break; + case '[': + while (methodDescriptor.charAt(currentMethodDescritorOffset) == '[') { + ++currentMethodDescritorOffset; + } + if (methodDescriptor.charAt(currentMethodDescritorOffset) == 'L') { + ++currentMethodDescritorOffset; + while (methodDescriptor.charAt(currentMethodDescritorOffset) != ';') { + ++currentMethodDescritorOffset; + } + } + locals[numLocal++] = + methodDescriptor.substring( + currentArgumentDescriptorStartOffset, ++currentMethodDescritorOffset); + break; + case 'L': + while (methodDescriptor.charAt(currentMethodDescritorOffset) != ';') { + ++currentMethodDescritorOffset; + } + locals[numLocal++] = + methodDescriptor.substring( + currentArgumentDescriptorStartOffset + 1, currentMethodDescritorOffset++); + break; + default: + context.currentFrameLocalCount = numLocal; + return; + } + } + } + + /** + * Reads a JVMS 'stack_map_frame' structure and stores the result in the given {@link Context} + * object. This method can also be used to read a full_frame structure, excluding its frame_type + * field (this is used to parse the legacy StackMap attributes). + * + * @param stackMapFrameOffset the start offset in {@link #classFileBuffer} of the + * stack_map_frame_value structure to be read, or the start offset of a full_frame structure + * (excluding its frame_type field). + * @param compressed true to read a 'stack_map_frame' structure, false to read a 'full_frame' + * structure without its frame_type field. + * @param expand if the stack map frame must be expanded. See {@link #EXPAND_FRAMES}. + * @param context where the parsed stack map frame must be stored. + * @return the end offset of the JVMS 'stack_map_frame' or 'full_frame' structure. + */ + private int readStackMapFrame( + final int stackMapFrameOffset, + final boolean compressed, + final boolean expand, + final Context context) { + int currentOffset = stackMapFrameOffset; + final char[] charBuffer = context.charBuffer; + final Label[] labels = context.currentMethodLabels; + int frameType; + if (compressed) { + // Read the frame_type field. + frameType = classFileBuffer[currentOffset++] & 0xFF; + } else { + frameType = Frame.FULL_FRAME; + context.currentFrameOffset = -1; + } + int offsetDelta; + context.currentFrameLocalCountDelta = 0; + if (frameType < Frame.SAME_LOCALS_1_STACK_ITEM_FRAME) { + offsetDelta = frameType; + context.currentFrameType = Opcodes.F_SAME; + context.currentFrameStackCount = 0; + } else if (frameType < Frame.RESERVED) { + offsetDelta = frameType - Frame.SAME_LOCALS_1_STACK_ITEM_FRAME; + currentOffset = + readVerificationTypeInfo( + currentOffset, context.currentFrameStackTypes, 0, charBuffer, labels); + context.currentFrameType = Opcodes.F_SAME1; + context.currentFrameStackCount = 1; + } else if (frameType >= Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED) { + offsetDelta = readUnsignedShort(currentOffset); + currentOffset += 2; + if (frameType == Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED) { + currentOffset = + readVerificationTypeInfo( + currentOffset, context.currentFrameStackTypes, 0, charBuffer, labels); + context.currentFrameType = Opcodes.F_SAME1; + context.currentFrameStackCount = 1; + } else if (frameType >= Frame.CHOP_FRAME && frameType < Frame.SAME_FRAME_EXTENDED) { + context.currentFrameType = Opcodes.F_CHOP; + context.currentFrameLocalCountDelta = Frame.SAME_FRAME_EXTENDED - frameType; + context.currentFrameLocalCount -= context.currentFrameLocalCountDelta; + context.currentFrameStackCount = 0; + } else if (frameType == Frame.SAME_FRAME_EXTENDED) { + context.currentFrameType = Opcodes.F_SAME; + context.currentFrameStackCount = 0; + } else if (frameType < Frame.FULL_FRAME) { + int local = expand ? context.currentFrameLocalCount : 0; + for (int k = frameType - Frame.SAME_FRAME_EXTENDED; k > 0; k--) { + currentOffset = + readVerificationTypeInfo( + currentOffset, context.currentFrameLocalTypes, local++, charBuffer, labels); + } + context.currentFrameType = Opcodes.F_APPEND; + context.currentFrameLocalCountDelta = frameType - Frame.SAME_FRAME_EXTENDED; + context.currentFrameLocalCount += context.currentFrameLocalCountDelta; + context.currentFrameStackCount = 0; + } else { + final int numberOfLocals = readUnsignedShort(currentOffset); + currentOffset += 2; + context.currentFrameType = Opcodes.F_FULL; + context.currentFrameLocalCountDelta = numberOfLocals; + context.currentFrameLocalCount = numberOfLocals; + for (int local = 0; local < numberOfLocals; ++local) { + currentOffset = + readVerificationTypeInfo( + currentOffset, context.currentFrameLocalTypes, local, charBuffer, labels); + } + final int numberOfStackItems = readUnsignedShort(currentOffset); + currentOffset += 2; + context.currentFrameStackCount = numberOfStackItems; + for (int stack = 0; stack < numberOfStackItems; ++stack) { + currentOffset = + readVerificationTypeInfo( + currentOffset, context.currentFrameStackTypes, stack, charBuffer, labels); + } + } + } else { + throw new IllegalArgumentException(); + } + context.currentFrameOffset += offsetDelta + 1; + createLabel(context.currentFrameOffset, labels); + return currentOffset; + } + + /** + * Reads a JVMS 'verification_type_info' structure and stores it at the given index in the given + * array. + * + * @param verificationTypeInfoOffset the start offset of the 'verification_type_info' structure to + * read. + * @param frame the array where the parsed type must be stored. + * @param index the index in 'frame' where the parsed type must be stored. + * @param charBuffer the buffer used to read strings in the constant pool. + * @param labels the labels of the method currently being parsed, indexed by their offset. If the + * parsed type is an ITEM_Uninitialized, a new label for the corresponding NEW instruction is + * stored in this array if it does not already exist. + * @return the end offset of the JVMS 'verification_type_info' structure. + */ + private int readVerificationTypeInfo( + final int verificationTypeInfoOffset, + final Object[] frame, + final int index, + final char[] charBuffer, + final Label[] labels) { + int currentOffset = verificationTypeInfoOffset; + int tag = classFileBuffer[currentOffset++] & 0xFF; + switch (tag) { + case Frame.ITEM_TOP: + frame[index] = Opcodes.TOP; + break; + case Frame.ITEM_INTEGER: + frame[index] = Opcodes.INTEGER; + break; + case Frame.ITEM_FLOAT: + frame[index] = Opcodes.FLOAT; + break; + case Frame.ITEM_DOUBLE: + frame[index] = Opcodes.DOUBLE; + break; + case Frame.ITEM_LONG: + frame[index] = Opcodes.LONG; + break; + case Frame.ITEM_NULL: + frame[index] = Opcodes.NULL; + break; + case Frame.ITEM_UNINITIALIZED_THIS: + frame[index] = Opcodes.UNINITIALIZED_THIS; + break; + case Frame.ITEM_OBJECT: + frame[index] = readClass(currentOffset, charBuffer); + currentOffset += 2; + break; + case Frame.ITEM_UNINITIALIZED: + frame[index] = createLabel(readUnsignedShort(currentOffset), labels); + currentOffset += 2; + break; + default: + throw new IllegalArgumentException(); + } + return currentOffset; + } + + // ---------------------------------------------------------------------------------------------- + // Methods to parse attributes + // ---------------------------------------------------------------------------------------------- + + /** + * Returns the offset in {@link #classFileBuffer} of the first ClassFile's 'attributes' array + * field entry. + * + * @return the offset in {@link #classFileBuffer} of the first ClassFile's 'attributes' array + * field entry. + */ + final int getFirstAttributeOffset() { + // Skip the access_flags, this_class, super_class, and interfaces_count fields (using 2 bytes + // each), as well as the interfaces array field (2 bytes per processer). + int currentOffset = header + 8 + readUnsignedShort(header + 6) * 2; + + // Read the fields_count field. + int fieldsCount = readUnsignedShort(currentOffset); + currentOffset += 2; + // Skip the 'fields' array field. + while (fieldsCount-- > 0) { + // Invariant: currentOffset is the offset of a field_info structure. + // Skip the access_flags, name_index and descriptor_index fields (2 bytes each), and read the + // attributes_count field. + int attributesCount = readUnsignedShort(currentOffset + 6); + currentOffset += 8; + // Skip the 'attributes' array field. + while (attributesCount-- > 0) { + // Invariant: currentOffset is the offset of an attribute_info structure. + // Read the attribute_length field (2 bytes after the start of the attribute_info) and skip + // this many bytes, plus 6 for the attribute_name_index and attribute_length fields + // (yielding the total size of the attribute_info structure). + currentOffset += 6 + readInt(currentOffset + 2); + } + } + + // Skip the methods_count and 'methods' fields, using the same method as above. + int methodsCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (methodsCount-- > 0) { + int attributesCount = readUnsignedShort(currentOffset + 6); + currentOffset += 8; + while (attributesCount-- > 0) { + currentOffset += 6 + readInt(currentOffset + 2); + } + } + + // Skip the ClassFile's attributes_count field. + return currentOffset + 2; + } + + /** + * Reads the BootstrapMethods attribute to compute the offset of each bootstrap method. + * + * @param maxStringLength a conservative estimate of the maximum length of the strings contained + * in the constant pool of the class. + * @return the offsets of the bootstrap methods. + */ + private int[] readBootstrapMethodsAttribute(final int maxStringLength) { + char[] charBuffer = new char[maxStringLength]; + int currentAttributeOffset = getFirstAttributeOffset(); + for (int i = readUnsignedShort(currentAttributeOffset - 2); i > 0; --i) { + // Read the attribute_info's attribute_name and attribute_length fields. + String attributeName = readUTF8(currentAttributeOffset, charBuffer); + int attributeLength = readInt(currentAttributeOffset + 2); + currentAttributeOffset += 6; + if (Constants.BOOTSTRAP_METHODS.equals(attributeName)) { + // Read the num_bootstrap_methods field and create an array of this size. + int[] result = new int[readUnsignedShort(currentAttributeOffset)]; + // Compute and store the offset of each 'bootstrap_methods' array field entry. + int currentBootstrapMethodOffset = currentAttributeOffset + 2; + for (int j = 0; j < result.length; ++j) { + result[j] = currentBootstrapMethodOffset; + // Skip the bootstrap_method_ref and num_bootstrap_arguments fields (2 bytes each), + // as well as the bootstrap_arguments array field (of size num_bootstrap_arguments * 2). + currentBootstrapMethodOffset += + 4 + readUnsignedShort(currentBootstrapMethodOffset + 2) * 2; + } + return result; + } + currentAttributeOffset += attributeLength; + } + throw new IllegalArgumentException(); + } + + /** + * Reads a non standard JVMS 'attribute' structure in {@link #classFileBuffer}. + * + * @param attributePrototypes prototypes of the attributes that must be parsed during the visit of + * the class. Any attribute whose type is not equal to the type of one the prototypes will not + * be parsed: its byte array value will be passed unchanged to the ClassWriter. + * @param type the type of the attribute. + * @param offset the start offset of the JVMS 'attribute' structure in {@link #classFileBuffer}. + * The 6 attribute header bytes (attribute_name_index and attribute_length) are not taken into + * account here. + * @param length the length of the attribute's content (excluding the 6 attribute header bytes). + * @param charBuffer the buffer to be used to read strings in the constant pool. + * @param codeAttributeOffset the start offset of the enclosing Code attribute in {@link + * #classFileBuffer}, or -1 if the attribute to be read is not a code attribute. The 6 + * attribute header bytes (attribute_name_index and attribute_length) are not taken into + * account here. + * @param labels the labels of the method's code, or {@literal null} if the attribute to be read + * is not a code attribute. + * @return the attribute that has been read. + */ + private Attribute readAttribute( + final Attribute[] attributePrototypes, + final String type, + final int offset, + final int length, + final char[] charBuffer, + final int codeAttributeOffset, + final Label[] labels) { + for (Attribute attributePrototype : attributePrototypes) { + if (attributePrototype.type.equals(type)) { + return attributePrototype.read( + this, offset, length, charBuffer, codeAttributeOffset, labels); + } + } + return new Attribute(type).read(this, offset, length, null, -1, null); + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods: low level parsing + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the number of entries in the class's constant pool table. + * + * @return the number of entries in the class's constant pool table. + */ + public int getItemCount() { + return cpInfoOffsets.length; + } + + /** + * Returns the start offset in this {@link ClassReader} of a JVMS 'cp_info' structure (i.e. a + * constant pool entry), plus one. This method is intended for {@link Attribute} sub classes, + * and is normally not needed by class generators or adapters. + * + * @param constantPoolEntryIndex the index a constant pool entry in the class's constant pool + * table. + * @return the start offset in this {@link ClassReader} of the corresponding JVMS 'cp_info' + * structure, plus one. + */ + public int getItem(final int constantPoolEntryIndex) { + return cpInfoOffsets[constantPoolEntryIndex]; + } + + /** + * Returns a conservative estimate of the maximum length of the strings contained in the class's + * constant pool table. + * + * @return a conservative estimate of the maximum length of the strings contained in the class's + * constant pool table. + */ + public int getMaxStringLength() { + return maxStringLength; + } + + /** + * Reads a byte value in this {@link ClassReader}. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param offset the start offset of the value to be read in this {@link ClassReader}. + * @return the read value. + */ + public int readByte(final int offset) { + return classFileBuffer[offset] & 0xFF; + } + + /** + * Reads an unsigned short value in this {@link ClassReader}. This method is intended for + * {@link Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param offset the start index of the value to be read in this {@link ClassReader}. + * @return the read value. + */ + public int readUnsignedShort(final int offset) { + byte[] classBuffer = classFileBuffer; + return ((classBuffer[offset] & 0xFF) << 8) | (classBuffer[offset + 1] & 0xFF); + } + + /** + * Reads a signed short value in this {@link ClassReader}. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param offset the start offset of the value to be read in this {@link ClassReader}. + * @return the read value. + */ + public short readShort(final int offset) { + byte[] classBuffer = classFileBuffer; + return (short) (((classBuffer[offset] & 0xFF) << 8) | (classBuffer[offset + 1] & 0xFF)); + } + + /** + * Reads a signed int value in this {@link ClassReader}. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param offset the start offset of the value to be read in this {@link ClassReader}. + * @return the read value. + */ + public int readInt(final int offset) { + byte[] classBuffer = classFileBuffer; + return ((classBuffer[offset] & 0xFF) << 24) + | ((classBuffer[offset + 1] & 0xFF) << 16) + | ((classBuffer[offset + 2] & 0xFF) << 8) + | (classBuffer[offset + 3] & 0xFF); + } + + /** + * Reads a signed long value in this {@link ClassReader}. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param offset the start offset of the value to be read in this {@link ClassReader}. + * @return the read value. + */ + public long readLong(final int offset) { + long l1 = readInt(offset); + long l0 = readInt(offset + 4) & 0xFFFFFFFFL; + return (l1 << 32) | l0; + } + + /** + * Reads a CONSTANT_Utf8 constant pool entry in this {@link ClassReader}. This method is + * intended for {@link Attribute} sub classes, and is normally not needed by class generators or + * adapters. + * + * @param offset the start offset of an unsigned short value in this {@link ClassReader}, whose + * value is the index of a CONSTANT_Utf8 entry in the class's constant pool table. + * @param charBuffer the buffer to be used to read the string. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified CONSTANT_Utf8 entry. + */ + // DontCheck(AbbreviationAsWordInName): can't be renamed (for backward binary compatibility). + public String readUTF8(final int offset, final char[] charBuffer) { + int constantPoolEntryIndex = readUnsignedShort(offset); + if (offset == 0 || constantPoolEntryIndex == 0) { + return null; + } + return readUtf(constantPoolEntryIndex, charBuffer); + } + + /** + * Reads a CONSTANT_Utf8 constant pool entry in {@link #classFileBuffer}. + * + * @param constantPoolEntryIndex the index of a CONSTANT_Utf8 entry in the class's constant pool + * table. + * @param charBuffer the buffer to be used to read the string. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified CONSTANT_Utf8 entry. + */ + final String readUtf(final int constantPoolEntryIndex, final char[] charBuffer) { + String value = constantUtf8Values[constantPoolEntryIndex]; + if (value != null) { + return value; + } + int cpInfoOffset = cpInfoOffsets[constantPoolEntryIndex]; + return constantUtf8Values[constantPoolEntryIndex] = + readUtf(cpInfoOffset + 2, readUnsignedShort(cpInfoOffset), charBuffer); + } + + /** + * Reads an UTF8 string in {@link #classFileBuffer}. + * + * @param utfOffset the start offset of the UTF8 string to be read. + * @param utfLength the length of the UTF8 string to be read. + * @param charBuffer the buffer to be used to read the string. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified UTF8 string. + */ + private String readUtf(final int utfOffset, final int utfLength, final char[] charBuffer) { + int currentOffset = utfOffset; + int endOffset = currentOffset + utfLength; + int strLength = 0; + byte[] classBuffer = classFileBuffer; + while (currentOffset < endOffset) { + int currentByte = classBuffer[currentOffset++]; + if ((currentByte & 0x80) == 0) { + charBuffer[strLength++] = (char) (currentByte & 0x7F); + } else if ((currentByte & 0xE0) == 0xC0) { + charBuffer[strLength++] = + (char) (((currentByte & 0x1F) << 6) + (classBuffer[currentOffset++] & 0x3F)); + } else { + charBuffer[strLength++] = + (char) + (((currentByte & 0xF) << 12) + + ((classBuffer[currentOffset++] & 0x3F) << 6) + + (classBuffer[currentOffset++] & 0x3F)); + } + } + return new String(charBuffer, 0, strLength); + } + + /** + * Reads a CONSTANT_Class, CONSTANT_String, CONSTANT_MethodType, CONSTANT_Module or + * CONSTANT_Package constant pool entry in {@link #classFileBuffer}. This method is intended + * for {@link Attribute} sub classes, and is normally not needed by class generators or + * adapters. + * + * @param offset the start offset of an unsigned short value in {@link #classFileBuffer}, whose + * value is the index of a CONSTANT_Class, CONSTANT_String, CONSTANT_MethodType, + * CONSTANT_Module or CONSTANT_Package entry in class's constant pool table. + * @param charBuffer the buffer to be used to read the item. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified constant pool entry. + */ + private String readStringish(final int offset, final char[] charBuffer) { + // Get the start offset of the cp_info structure (plus one), and read the CONSTANT_Utf8 entry + // designated by the first two bytes of this cp_info. + return readUTF8(cpInfoOffsets[readUnsignedShort(offset)], charBuffer); + } + + /** + * Reads a CONSTANT_Class constant pool entry in this {@link ClassReader}. This method is + * intended for {@link Attribute} sub classes, and is normally not needed by class generators or + * adapters. + * + * @param offset the start offset of an unsigned short value in this {@link ClassReader}, whose + * value is the index of a CONSTANT_Class entry in class's constant pool table. + * @param charBuffer the buffer to be used to read the item. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified CONSTANT_Class entry. + */ + public String readClass(final int offset, final char[] charBuffer) { + return readStringish(offset, charBuffer); + } + + /** + * Reads a CONSTANT_Module constant pool entry in this {@link ClassReader}. This method is + * intended for {@link Attribute} sub classes, and is normally not needed by class generators or + * adapters. + * + * @param offset the start offset of an unsigned short value in this {@link ClassReader}, whose + * value is the index of a CONSTANT_Module entry in class's constant pool table. + * @param charBuffer the buffer to be used to read the item. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified CONSTANT_Module entry. + */ + public String readModule(final int offset, final char[] charBuffer) { + return readStringish(offset, charBuffer); + } + + /** + * Reads a CONSTANT_Package constant pool entry in this {@link ClassReader}. This method is + * intended for {@link Attribute} sub classes, and is normally not needed by class generators or + * adapters. + * + * @param offset the start offset of an unsigned short value in this {@link ClassReader}, whose + * value is the index of a CONSTANT_Package entry in class's constant pool table. + * @param charBuffer the buffer to be used to read the item. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified CONSTANT_Package entry. + */ + public String readPackage(final int offset, final char[] charBuffer) { + return readStringish(offset, charBuffer); + } + + /** + * Reads a CONSTANT_Dynamic constant pool entry in {@link #classFileBuffer}. + * + * @param constantPoolEntryIndex the index of a CONSTANT_Dynamic entry in the class's constant + * pool table. + * @param charBuffer the buffer to be used to read the string. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the ConstantDynamic corresponding to the specified CONSTANT_Dynamic entry. + */ + private ConstantDynamic readConstantDynamic( + final int constantPoolEntryIndex, final char[] charBuffer) { + ConstantDynamic constantDynamic = constantDynamicValues[constantPoolEntryIndex]; + if (constantDynamic != null) { + return constantDynamic; + } + int cpInfoOffset = cpInfoOffsets[constantPoolEntryIndex]; + int nameAndTypeCpInfoOffset = cpInfoOffsets[readUnsignedShort(cpInfoOffset + 2)]; + String name = readUTF8(nameAndTypeCpInfoOffset, charBuffer); + String descriptor = readUTF8(nameAndTypeCpInfoOffset + 2, charBuffer); + int bootstrapMethodOffset = bootstrapMethodOffsets[readUnsignedShort(cpInfoOffset)]; + Handle handle = (Handle) readConst(readUnsignedShort(bootstrapMethodOffset), charBuffer); + Object[] bootstrapMethodArguments = new Object[readUnsignedShort(bootstrapMethodOffset + 2)]; + bootstrapMethodOffset += 4; + for (int i = 0; i < bootstrapMethodArguments.length; i++) { + bootstrapMethodArguments[i] = readConst(readUnsignedShort(bootstrapMethodOffset), charBuffer); + bootstrapMethodOffset += 2; + } + return constantDynamicValues[constantPoolEntryIndex] = + new ConstantDynamic(name, descriptor, handle, bootstrapMethodArguments); + } + + /** + * Reads a numeric or string constant pool entry in this {@link ClassReader}. This method is + * intended for {@link Attribute} sub classes, and is normally not needed by class generators or + * adapters. + * + * @param constantPoolEntryIndex the index of a CONSTANT_Integer, CONSTANT_Float, CONSTANT_Long, + * CONSTANT_Double, CONSTANT_Class, CONSTANT_String, CONSTANT_MethodType, + * CONSTANT_MethodHandle or CONSTANT_Dynamic entry in the class's constant pool. + * @param charBuffer the buffer to be used to read strings. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the {@link Integer}, {@link Float}, {@link Long}, {@link Double}, {@link String}, + * {@link Type}, {@link Handle} or {@link ConstantDynamic} corresponding to the specified + * constant pool entry. + */ + public Object readConst(final int constantPoolEntryIndex, final char[] charBuffer) { + int cpInfoOffset = cpInfoOffsets[constantPoolEntryIndex]; + switch (classFileBuffer[cpInfoOffset - 1]) { + case Symbol.CONSTANT_INTEGER_TAG: + return readInt(cpInfoOffset); + case Symbol.CONSTANT_FLOAT_TAG: + return Float.intBitsToFloat(readInt(cpInfoOffset)); + case Symbol.CONSTANT_LONG_TAG: + return readLong(cpInfoOffset); + case Symbol.CONSTANT_DOUBLE_TAG: + return Double.longBitsToDouble(readLong(cpInfoOffset)); + case Symbol.CONSTANT_CLASS_TAG: + return Type.getObjectType(readUTF8(cpInfoOffset, charBuffer)); + case Symbol.CONSTANT_STRING_TAG: + return readUTF8(cpInfoOffset, charBuffer); + case Symbol.CONSTANT_METHOD_TYPE_TAG: + return Type.getMethodType(readUTF8(cpInfoOffset, charBuffer)); + case Symbol.CONSTANT_METHOD_HANDLE_TAG: + int referenceKind = readByte(cpInfoOffset); + int referenceCpInfoOffset = cpInfoOffsets[readUnsignedShort(cpInfoOffset + 1)]; + int nameAndTypeCpInfoOffset = cpInfoOffsets[readUnsignedShort(referenceCpInfoOffset + 2)]; + String owner = readClass(referenceCpInfoOffset, charBuffer); + String name = readUTF8(nameAndTypeCpInfoOffset, charBuffer); + String descriptor = readUTF8(nameAndTypeCpInfoOffset + 2, charBuffer); + boolean isInterface = + classFileBuffer[referenceCpInfoOffset - 1] == Symbol.CONSTANT_INTERFACE_METHODREF_TAG; + return new Handle(referenceKind, owner, name, descriptor, isInterface); + case Symbol.CONSTANT_DYNAMIC_TAG: + return readConstantDynamic(constantPoolEntryIndex, charBuffer); + default: + throw new IllegalArgumentException(); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ClassTooLargeException.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ClassTooLargeException.java new file mode 100644 index 0000000..68a077d --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ClassTooLargeException.java @@ -0,0 +1,55 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * Exception thrown when the constant pool of a class produced by a {@link ClassWriter} is too + * large. @Author Jason Zaugg + */ +public final class ClassTooLargeException extends IndexOutOfBoundsException { + + private static final long serialVersionUID = 160715609518896765L; + + private final String className; + private final int constantPoolCount; + + /** + * Constructs a new {@link ClassTooLargeException}. + * + * @param className the internal name of the class. + * @param constantPoolCount the number of constant pool items of the class. + */ + public ClassTooLargeException(final String className, final int constantPoolCount) { + super("Class too large: " + className); + this.className = className; + this.constantPoolCount = constantPoolCount; + } + + /** + * Returns the internal name of the class. + * + * @return the internal name of the class. + */ + public String getClassName() { + return className; + } + + /** + * Returns the number of constant pool items of the class. + * + * @return the number of constant pool items of the class. + */ + public int getConstantPoolCount() { + return constantPoolCount; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ClassVisitor.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ClassVisitor.java new file mode 100644 index 0000000..239e612 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ClassVisitor.java @@ -0,0 +1,363 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A visitor to visit a Java class. The methods of this class must be called in the following order: + * {@code visit} [ {@code visitSource} ] [ {@code visitModule} ][ {@code visitNestHost} ][ {@code + * visitOuterClass} ] ( {@code visitAnnotation} | {@code visitTypeAnnotation} | {@code + * visitAttribute} )* ( {@code visitNestMember} | [ {@code * visitPermittedSubclass} ] | {@code + * visitInnerClass} | {@code visitRecordComponent} | {@code visitField} | {@code visitMethod} )* + * {@code visitEnd}. @Author Eric Bruneton + */ +public abstract class ClassVisitor { + + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + protected final int api; + + /** The class visitor to which this visitor must delegate method calls. May be {@literal null}. */ + protected ClassVisitor cv; + + /** + * Constructs a new {@link ClassVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + public ClassVisitor(final int api) { + this(api, null); + } + + /** + * Constructs a new {@link ClassVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6}, {@link Opcodes#ASM7}, {@link + * Opcodes#ASM8} or {@link Opcodes#ASM9}. + * @param classVisitor the class visitor to which this visitor must delegate method calls. May be + * null. + */ + public ClassVisitor(final int api, final ClassVisitor classVisitor) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + if (api == Opcodes.ASM10_EXPERIMENTAL) { + Constants.checkAsmExperimental(this); + } + this.api = api; + this.cv = classVisitor; + } + + /** + * Visits the header of the class. + * + * @param version the class version. The minor version is stored in the 16 most significant bits, + * and the major version in the 16 least significant bits. + * @param access the class's access flags (see {@link Opcodes}). This parameter also indicates if + * the class is deprecated {@link Opcodes#ACC_DEPRECATED} or a record {@link + * Opcodes#ACC_RECORD}. + * @param name the internal name of the class (see {@link Type#getInternalName()}). + * @param signature the signature of this class. May be {@literal null} if the class is not a + * generic one, and does not extend or implement generic classes or interfaces. + * @param superName the internal of name of the super class (see {@link Type#getInternalName()}). + * For interfaces, the super class is {@link Object}. May be {@literal null}, but only for the + * {@link Object} class. + * @param interfaces the internal names of the class's interfaces (see {@link + * Type#getInternalName()}). May be {@literal null}. + */ + public void visit( + final int version, + final int access, + final String name, + final String signature, + final String superName, + final String[] interfaces) { + if (api < Opcodes.ASM8 && (access & Opcodes.ACC_RECORD) != 0) { + throw new UnsupportedOperationException("Records requires ASM8"); + } + if (cv != null) { + cv.visit(version, access, name, signature, superName, interfaces); + } + } + + /** + * Visits the source of the class. + * + * @param source the name of the source file from which the class was compiled. May be {@literal + * null}. + * @param debug additional debug information to compute the correspondence between source and + * compiled elements of the class. May be {@literal null}. + */ + public void visitSource(final String source, final String debug) { + if (cv != null) { + cv.visitSource(source, debug); + } + } + + /** + * Visit the module corresponding to the class. + * + * @param name the fully qualified name (using dots) of the module. + * @param access the module access flags, among {@code ACC_OPEN}, {@code ACC_SYNTHETIC} and {@code + * ACC_MANDATED}. + * @param version the module version, or {@literal null}. + * @return a visitor to visit the module values, or {@literal null} if this visitor is not + * interested in visiting this module. + */ + public ModuleVisitor visitModule(final String name, final int access, final String version) { + if (api < Opcodes.ASM6) { + throw new UnsupportedOperationException("Module requires ASM6"); + } + if (cv != null) { + return cv.visitModule(name, access, version); + } + return null; + } + + /** + * Visits the nest host class of the class. A nest is a set of classes of the same package that + * share access to their private members. One of these classes, called the host, lists the other + * members of the nest, which in turn should link to the host of their nest. This method must be + * called only once and only if the visited class is a non-host member of a nest. A class is + * implicitly its own nest, so it's invalid to call this method with the visited class name as + * argument. + * + * @param nestHost the internal name of the host class of the nest. + */ + public void visitNestHost(final String nestHost) { + if (api < Opcodes.ASM7) { + throw new UnsupportedOperationException("NestHost requires ASM7"); + } + if (cv != null) { + cv.visitNestHost(nestHost); + } + } + + /** + * Visits the enclosing class of the class. This method must be called only if the class has an + * enclosing class. + * + * @param owner internal name of the enclosing class of the class. + * @param name the name of the method that contains the class, or {@literal null} if the class is + * not enclosed in a method of its enclosing class. + * @param descriptor the descriptor of the method that contains the class, or {@literal null} if + * the class is not enclosed in a method of its enclosing class. + */ + public void visitOuterClass(final String owner, final String name, final String descriptor) { + if (cv != null) { + cv.visitOuterClass(owner, name, descriptor); + } + } + + /** + * Visits an annotation of the class. + * + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (cv != null) { + return cv.visitAnnotation(descriptor, visible); + } + return null; + } + + /** + * Visits an annotation on a type in the class signature. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#CLASS_TYPE_PARAMETER}, {@link + * TypeReference#CLASS_TYPE_PARAMETER_BOUND} or {@link TypeReference#CLASS_EXTENDS}. See + * {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException("TypeAnnotation requires ASM5"); + } + if (cv != null) { + return cv.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + return null; + } + + /** + * Visits a non standard attribute of the class. + * + * @param attribute an attribute. + */ + public void visitAttribute(final Attribute attribute) { + if (cv != null) { + cv.visitAttribute(attribute); + } + } + + /** + * Visits a member of the nest. A nest is a set of classes of the same package that share access + * to their private members. One of these classes, called the host, lists the other members of the + * nest, which in turn should link to the host of their nest. This method must be called only if + * the visited class is the host of a nest. A nest host is implicitly a member of its own nest, so + * it's invalid to call this method with the visited class name as argument. + * + * @param nestMember the internal name of a nest member. + */ + public void visitNestMember(final String nestMember) { + if (api < Opcodes.ASM7) { + throw new UnsupportedOperationException("NestMember requires ASM7"); + } + if (cv != null) { + cv.visitNestMember(nestMember); + } + } + + /** + * Visits a permitted subclasses. A permitted subclass is one of the allowed subclasses of the + * current class. + * + * @param permittedSubclass the internal name of a permitted subclass. + */ + public void visitPermittedSubclass(final String permittedSubclass) { + if (api < Opcodes.ASM9) { + throw new UnsupportedOperationException("PermittedSubclasses requires ASM9"); + } + if (cv != null) { + cv.visitPermittedSubclass(permittedSubclass); + } + } + + /** + * Visits information about an inner class. This inner class is not necessarily a member of the + * class being visited. + * + * @param name the internal name of an inner class (see {@link Type#getInternalName()}). + * @param outerName the internal name of the class to which the inner class belongs (see {@link + * Type#getInternalName()}). May be {@literal null} for not member classes. + * @param innerName the (simple) name of the inner class inside its enclosing class. May be + * {@literal null} for anonymous inner classes. + * @param access the access flags of the inner class as originally declared in the enclosing + * class. + */ + public void visitInnerClass( + final String name, final String outerName, final String innerName, final int access) { + if (cv != null) { + cv.visitInnerClass(name, outerName, innerName, access); + } + } + + /** + * Visits a record component of the class. + * + * @param name the record component name. + * @param descriptor the record component descriptor (see {@link Type}). + * @param signature the record component signature. May be {@literal null} if the record component + * type does not use generic types. + * @return a visitor to visit this record component annotations and attributes, or {@literal null} + * if this class visitor is not interested in visiting these annotations and attributes. + */ + public RecordComponentVisitor visitRecordComponent( + final String name, final String descriptor, final String signature) { + if (api < Opcodes.ASM8) { + throw new UnsupportedOperationException("Record requires ASM8"); + } + if (cv != null) { + return cv.visitRecordComponent(name, descriptor, signature); + } + return null; + } + + /** + * Visits a field of the class. + * + * @param access the field's access flags (see {@link Opcodes}). This parameter also indicates if + * the field is synthetic and/or deprecated. + * @param name the field's name. + * @param descriptor the field's descriptor (see {@link Type}). + * @param signature the field's signature. May be {@literal null} if the field's type does not use + * generic types. + * @param value the field's initial value. This parameter, which may be {@literal null} if the + * field does not have an initial value, must be an {@link Integer}, a {@link Float}, a {@link + * Long}, a {@link Double} or a {@link String} (for {@code int}, {@code float}, {@code long} + * or {@code String} fields respectively). This parameter is only used for static + * fields. Its value is ignored for non static fields, which must be initialized through + * bytecode instructions in constructors or methods. + * @return a visitor to visit field annotations and attributes, or {@literal null} if this class + * visitor is not interested in visiting these annotations and attributes. + */ + public FieldVisitor visitField( + final int access, + final String name, + final String descriptor, + final String signature, + final Object value) { + if (cv != null) { + return cv.visitField(access, name, descriptor, signature, value); + } + return null; + } + + /** + * Visits a method of the class. This method must return a new {@link MethodVisitor} + * instance (or {@literal null}) each time it is called, i.e., it should not return a previously + * returned visitor. + * + * @param access the method's access flags (see {@link Opcodes}). This parameter also indicates if + * the method is synthetic and/or deprecated. + * @param name the method's name. + * @param descriptor the method's descriptor (see {@link Type}). + * @param signature the method's signature. May be {@literal null} if the method parameters, + * return type and exceptions do not use generic types. + * @param exceptions the internal names of the method's exception classes (see {@link + * Type#getInternalName()}). May be {@literal null}. + * @return an object to visit the byte code of the method, or {@literal null} if this class + * visitor is not interested in visiting the code of this method. + */ + public MethodVisitor visitMethod( + final int access, + final String name, + final String descriptor, + final String signature, + final String[] exceptions) { + if (cv != null) { + return cv.visitMethod(access, name, descriptor, signature, exceptions); + } + return null; + } + + /** + * Visits the end of the class. This method, which is the last one to be called, is used to inform + * the visitor that all the fields and methods of the class have been visited. + */ + public void visitEnd() { + if (cv != null) { + cv.visitEnd(); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ClassWriter.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ClassWriter.java new file mode 100644 index 0000000..056c01a --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ClassWriter.java @@ -0,0 +1,1037 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A {@link ClassVisitor} that generates a corresponding ClassFile structure, as defined in the Java + * Virtual Machine Specification (JVMS). It can be used alone, to generate a Java class "from + * scratch", or with one or more {@link ClassReader} and adapter {@link ClassVisitor} to generate a + * modified class from one or more existing Java classes. @Author Eric Bruneton + * + * @see JVMS 4 + */ +public class ClassWriter extends ClassVisitor { + + /** + * A flag to automatically compute the maximum stack size and the maximum number of local + * variables of methods. If this flag is set, then the arguments of the {@link + * MethodVisitor#visitMaxs} method of the {@link MethodVisitor} returned by the {@link + * #visitMethod} method will be ignored, and computed automatically from the signature and the + * bytecode of each method. + * + *

Note: for classes whose version is {@link Opcodes#V1_7} of more, this option requires + * valid stack map frames. The maximum stack size is then computed from these frames, and from the + * bytecode instructions in between. If stack map frames are not present or must be recomputed, + * used {@link #COMPUTE_FRAMES} instead. + * + * @see #ClassWriter(int) + */ + public static final int COMPUTE_MAXS = 1; + + /** + * A flag to automatically compute the stack map frames of methods from scratch. If this flag is + * set, then the calls to the {@link MethodVisitor#visitFrame} method are ignored, and the stack + * map frames are recomputed from the methods bytecode. The arguments of the {@link + * MethodVisitor#visitMaxs} method are also ignored and recomputed from the bytecode. In other + * words, {@link #COMPUTE_FRAMES} implies {@link #COMPUTE_MAXS}. + * + * @see #ClassWriter(int) + */ + public static final int COMPUTE_FRAMES = 2; + + // Note: fields are ordered as in the ClassFile structure, and those related to attributes are + // ordered as in Section 4.7 of the JVMS. + + /** + * The minor_version and major_version fields of the JVMS ClassFile structure. minor_version is + * stored in the 16 most significant bits, and major_version in the 16 least significant bits. + */ + private int version; + + /** The symbol table for this class (contains the constant_pool and the BootstrapMethods). */ + private final SymbolTable symbolTable; + + /** + * The access_flags field of the JVMS ClassFile structure. This field can contain ASM specific + * access flags, such as {@link Opcodes#ACC_DEPRECATED} or {@link Opcodes#ACC_RECORD}, which are + * removed when generating the ClassFile structure. + */ + private int accessFlags; + + /** The this_class field of the JVMS ClassFile structure. */ + private int thisClass; + + /** The super_class field of the JVMS ClassFile structure. */ + private int superClass; + + /** The interface_count field of the JVMS ClassFile structure. */ + private int interfaceCount; + + /** The 'interfaces' array of the JVMS ClassFile structure. */ + private int[] interfaces; + + /** + * The fields of this class, stored in a linked list of {@link FieldWriter} linked via their + * {@link FieldWriter#fv} field. This field stores the first element of this list. + */ + private FieldWriter firstField; + + /** + * The fields of this class, stored in a linked list of {@link FieldWriter} linked via their + * {@link FieldWriter#fv} field. This field stores the last element of this list. + */ + private FieldWriter lastField; + + /** + * The methods of this class, stored in a linked list of {@link MethodWriter} linked via their + * {@link MethodWriter#mv} field. This field stores the first element of this list. + */ + private MethodWriter firstMethod; + + /** + * The methods of this class, stored in a linked list of {@link MethodWriter} linked via their + * {@link MethodWriter#mv} field. This field stores the last element of this list. + */ + private MethodWriter lastMethod; + + /** The number_of_classes field of the InnerClasses attribute, or 0. */ + private int numberOfInnerClasses; + + /** The 'classes' array of the InnerClasses attribute, or {@literal null}. */ + private ByteVector innerClasses; + + /** The class_index field of the EnclosingMethod attribute, or 0. */ + private int enclosingClassIndex; + + /** The method_index field of the EnclosingMethod attribute. */ + private int enclosingMethodIndex; + + /** The signature_index field of the Signature attribute, or 0. */ + private int signatureIndex; + + /** The source_file_index field of the SourceFile attribute, or 0. */ + private int sourceFileIndex; + + /** The debug_extension field of the SourceDebugExtension attribute, or {@literal null}. */ + private ByteVector debugExtension; + + /** + * The last runtime visible annotation of this class. The previous ones can be accessed with the + * {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleAnnotation; + + /** + * The last runtime invisible annotation of this class. The previous ones can be accessed with the + * {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleAnnotation; + + /** + * The last runtime visible type annotation of this class. The previous ones can be accessed with + * the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleTypeAnnotation; + + /** + * The last runtime invisible type annotation of this class. The previous ones can be accessed + * with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleTypeAnnotation; + + /** The Module attribute of this class, or {@literal null}. */ + private ModuleWriter moduleWriter; + + /** The host_class_index field of the NestHost attribute, or 0. */ + private int nestHostClassIndex; + + /** The number_of_classes field of the NestMembers attribute, or 0. */ + private int numberOfNestMemberClasses; + + /** The 'classes' array of the NestMembers attribute, or {@literal null}. */ + private ByteVector nestMemberClasses; + + /** The number_of_classes field of the PermittedSubclasses attribute, or 0. */ + private int numberOfPermittedSubclasses; + + /** The 'classes' array of the PermittedSubclasses attribute, or {@literal null}. */ + private ByteVector permittedSubclasses; + + /** + * The record components of this class, stored in a linked list of {@link RecordComponentWriter} + * linked via their {@link RecordComponentWriter#delegate} field. This field stores the first + * element of this list. + */ + private RecordComponentWriter firstRecordComponent; + + /** + * The record components of this class, stored in a linked list of {@link RecordComponentWriter} + * linked via their {@link RecordComponentWriter#delegate} field. This field stores the last + * element of this list. + */ + private RecordComponentWriter lastRecordComponent; + + /** + * The first non standard attribute of this class. The next ones can be accessed with the {@link + * Attribute#nextAttribute} field. May be {@literal null}. + * + *

WARNING: this list stores the attributes in the reverse order of their visit. + * firstAttribute is actually the last attribute visited in {@link #visitAttribute}. The {@link + * #toByteArray} method writes the attributes in the order defined by this list, i.e. in the + * reverse order specified by the user. + */ + private Attribute firstAttribute; + + /** + * Indicates what must be automatically computed in {@link MethodWriter}. Must be one of {@link + * MethodWriter#COMPUTE_NOTHING}, {@link MethodWriter#COMPUTE_MAX_STACK_AND_LOCAL}, {@link + * MethodWriter#COMPUTE_INSERTED_FRAMES}, or {@link MethodWriter#COMPUTE_ALL_FRAMES}. + */ + private int compute; + + // ----------------------------------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------------------------------- + + /** + * Constructs a new {@link ClassWriter} object. + * + * @param flags option flags that can be used to modify the default behavior of this class. Must + * be zero or more of {@link #COMPUTE_MAXS} and {@link #COMPUTE_FRAMES}. + */ + public ClassWriter(final int flags) { + this(null, flags); + } + + /** + * Constructs a new {@link ClassWriter} object and enables optimizations for "mostly add" bytecode + * transformations. These optimizations are the following: + * + *

    + *
  • The constant pool and bootstrap methods from the original class are copied as is in the + * new class, which saves time. New constant pool entries and new bootstrap methods will be + * added at the end if necessary, but unused constant pool entries or bootstrap methods + * won't be removed. + *
  • Methods that are not transformed are copied as is in the new class, directly from the + * original class bytecode (i.e. without emitting visit events for all the method + * instructions), which saves a lot of time. Untransformed methods are detected by + * the fact that the {@link ClassReader} receives {@link MethodVisitor} objects that come + * from a {@link ClassWriter} (and not from any other {@link ClassVisitor} instance). + *
+ * + * @param classReader the {@link ClassReader} used to read the original class. It will be used to + * copy the entire constant pool and bootstrap methods from the original class and also to + * copy other fragments of original bytecode where applicable. + * @param flags option flags that can be used to modify the default behavior of this class.Must be + * zero or more of {@link #COMPUTE_MAXS} and {@link #COMPUTE_FRAMES}. These option flags do + * not affect methods that are copied as is in the new class. This means that neither the + * maximum stack size nor the stack frames will be computed for these methods. + */ + public ClassWriter(final ClassReader classReader, final int flags) { + super(/* latest api = */ Opcodes.ASM9); + symbolTable = classReader == null ? new SymbolTable(this) : new SymbolTable(this, classReader); + if ((flags & COMPUTE_FRAMES) != 0) { + this.compute = MethodWriter.COMPUTE_ALL_FRAMES; + } else if ((flags & COMPUTE_MAXS) != 0) { + this.compute = MethodWriter.COMPUTE_MAX_STACK_AND_LOCAL; + } else { + this.compute = MethodWriter.COMPUTE_NOTHING; + } + } + + // ----------------------------------------------------------------------------------------------- + // Implementation of the ClassVisitor abstract class + // ----------------------------------------------------------------------------------------------- + + @Override + public final void visit( + final int version, + final int access, + final String name, + final String signature, + final String superName, + final String[] interfaces) { + this.version = version; + this.accessFlags = access; + this.thisClass = symbolTable.setMajorVersionAndClassName(version & 0xFFFF, name); + if (signature != null) { + this.signatureIndex = symbolTable.addConstantUtf8(signature); + } + this.superClass = superName == null ? 0 : symbolTable.addConstantClass(superName).index; + if (interfaces != null && interfaces.length > 0) { + interfaceCount = interfaces.length; + this.interfaces = new int[interfaceCount]; + for (int i = 0; i < interfaceCount; ++i) { + this.interfaces[i] = symbolTable.addConstantClass(interfaces[i]).index; + } + } + if (compute == MethodWriter.COMPUTE_MAX_STACK_AND_LOCAL && (version & 0xFFFF) >= Opcodes.V1_7) { + compute = MethodWriter.COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES; + } + } + + @Override + public final void visitSource(final String file, final String debug) { + if (file != null) { + sourceFileIndex = symbolTable.addConstantUtf8(file); + } + if (debug != null) { + debugExtension = new ByteVector().encodeUtf8(debug, 0, Integer.MAX_VALUE); + } + } + + @Override + public final ModuleVisitor visitModule( + final String name, final int access, final String version) { + return moduleWriter = + new ModuleWriter( + symbolTable, + symbolTable.addConstantModule(name).index, + access, + version == null ? 0 : symbolTable.addConstantUtf8(version)); + } + + @Override + public final void visitNestHost(final String nestHost) { + nestHostClassIndex = symbolTable.addConstantClass(nestHost).index; + } + + @Override + public final void visitOuterClass( + final String owner, final String name, final String descriptor) { + enclosingClassIndex = symbolTable.addConstantClass(owner).index; + if (name != null && descriptor != null) { + enclosingMethodIndex = symbolTable.addConstantNameAndType(name, descriptor); + } + } + + @Override + public final AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeVisibleAnnotation); + } else { + return lastRuntimeInvisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeInvisibleAnnotation); + } + } + + @Override + public final AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeVisibleTypeAnnotation); + } else { + return lastRuntimeInvisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public final void visitAttribute(final Attribute attribute) { + // Store the attributes in the reverse order of their visit by this method. + attribute.nextAttribute = firstAttribute; + firstAttribute = attribute; + } + + @Override + public final void visitNestMember(final String nestMember) { + if (nestMemberClasses == null) { + nestMemberClasses = new ByteVector(); + } + ++numberOfNestMemberClasses; + nestMemberClasses.putShort(symbolTable.addConstantClass(nestMember).index); + } + + @Override + public final void visitPermittedSubclass(final String permittedSubclass) { + if (permittedSubclasses == null) { + permittedSubclasses = new ByteVector(); + } + ++numberOfPermittedSubclasses; + permittedSubclasses.putShort(symbolTable.addConstantClass(permittedSubclass).index); + } + + @Override + public final void visitInnerClass( + final String name, final String outerName, final String innerName, final int access) { + if (innerClasses == null) { + innerClasses = new ByteVector(); + } + // Section 4.7.6 of the JVMS states "Every CONSTANT_Class_info entry in the constant_pool table + // which represents a class or processer C that is not a package member must have exactly one + // corresponding entry in the classes array". To avoid duplicates we keep track in the info + // field of the Symbol of each CONSTANT_Class_info entry C whether an inner class entry has + // already been added for C. If so, we store the index of this inner class entry (plus one) in + // the info field. This trick allows duplicate detection in O(1) time. + Symbol nameSymbol = symbolTable.addConstantClass(name); + if (nameSymbol.info == 0) { + ++numberOfInnerClasses; + innerClasses.putShort(nameSymbol.index); + innerClasses.putShort(outerName == null ? 0 : symbolTable.addConstantClass(outerName).index); + innerClasses.putShort(innerName == null ? 0 : symbolTable.addConstantUtf8(innerName)); + innerClasses.putShort(access); + nameSymbol.info = numberOfInnerClasses; + } + // Else, compare the inner classes entry nameSymbol.info - 1 with the arguments of this method + // and throw an exception if there is a difference? + } + + @Override + public final RecordComponentVisitor visitRecordComponent( + final String name, final String descriptor, final String signature) { + RecordComponentWriter recordComponentWriter = + new RecordComponentWriter(symbolTable, name, descriptor, signature); + if (firstRecordComponent == null) { + firstRecordComponent = recordComponentWriter; + } else { + lastRecordComponent.delegate = recordComponentWriter; + } + return lastRecordComponent = recordComponentWriter; + } + + @Override + public final FieldVisitor visitField( + final int access, + final String name, + final String descriptor, + final String signature, + final Object value) { + FieldWriter fieldWriter = + new FieldWriter(symbolTable, access, name, descriptor, signature, value); + if (firstField == null) { + firstField = fieldWriter; + } else { + lastField.fv = fieldWriter; + } + return lastField = fieldWriter; + } + + @Override + public final MethodVisitor visitMethod( + final int access, + final String name, + final String descriptor, + final String signature, + final String[] exceptions) { + MethodWriter methodWriter = + new MethodWriter(symbolTable, access, name, descriptor, signature, exceptions, compute); + if (firstMethod == null) { + firstMethod = methodWriter; + } else { + lastMethod.mv = methodWriter; + } + return lastMethod = methodWriter; + } + + @Override + public final void visitEnd() { + // Nothing to do. + } + + // ----------------------------------------------------------------------------------------------- + // Other public methods + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the content of the class file that was built by this ClassWriter. + * + * @return the binary content of the JVMS ClassFile structure that was built by this ClassWriter. + * @throws ClassTooLargeException if the constant pool of the class is too large. + * @throws MethodTooLargeException if the Code attribute of a method is too large. + */ + public byte[] toByteArray() { + // First step: compute the size in bytes of the ClassFile structure. + // The magic field uses 4 bytes, 10 mandatory fields (minor_version, major_version, + // constant_pool_count, access_flags, this_class, super_class, interfaces_count, fields_count, + // methods_count and attributes_count) use 2 bytes each, and each processer uses 2 bytes too. + int size = 24 + 2 * interfaceCount; + int fieldsCount = 0; + FieldWriter fieldWriter = firstField; + while (fieldWriter != null) { + ++fieldsCount; + size += fieldWriter.computeFieldInfoSize(); + fieldWriter = (FieldWriter) fieldWriter.fv; + } + int methodsCount = 0; + MethodWriter methodWriter = firstMethod; + while (methodWriter != null) { + ++methodsCount; + size += methodWriter.computeMethodInfoSize(); + methodWriter = (MethodWriter) methodWriter.mv; + } + + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + int attributesCount = 0; + if (innerClasses != null) { + ++attributesCount; + size += 8 + innerClasses.length; + symbolTable.addConstantUtf8(Constants.INNER_CLASSES); + } + if (enclosingClassIndex != 0) { + ++attributesCount; + size += 10; + symbolTable.addConstantUtf8(Constants.ENCLOSING_METHOD); + } + if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0 && (version & 0xFFFF) < Opcodes.V1_5) { + ++attributesCount; + size += 6; + symbolTable.addConstantUtf8(Constants.SYNTHETIC); + } + if (signatureIndex != 0) { + ++attributesCount; + size += 8; + symbolTable.addConstantUtf8(Constants.SIGNATURE); + } + if (sourceFileIndex != 0) { + ++attributesCount; + size += 8; + symbolTable.addConstantUtf8(Constants.SOURCE_FILE); + } + if (debugExtension != null) { + ++attributesCount; + size += 6 + debugExtension.length; + symbolTable.addConstantUtf8(Constants.SOURCE_DEBUG_EXTENSION); + } + if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) { + ++attributesCount; + size += 6; + symbolTable.addConstantUtf8(Constants.DEPRECATED); + } + if (lastRuntimeVisibleAnnotation != null) { + ++attributesCount; + size += + lastRuntimeVisibleAnnotation.computeAnnotationsSize( + Constants.RUNTIME_VISIBLE_ANNOTATIONS); + } + if (lastRuntimeInvisibleAnnotation != null) { + ++attributesCount; + size += + lastRuntimeInvisibleAnnotation.computeAnnotationsSize( + Constants.RUNTIME_INVISIBLE_ANNOTATIONS); + } + if (lastRuntimeVisibleTypeAnnotation != null) { + ++attributesCount; + size += + lastRuntimeVisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + } + if (lastRuntimeInvisibleTypeAnnotation != null) { + ++attributesCount; + size += + lastRuntimeInvisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS); + } + if (symbolTable.computeBootstrapMethodsSize() > 0) { + ++attributesCount; + size += symbolTable.computeBootstrapMethodsSize(); + } + if (moduleWriter != null) { + attributesCount += moduleWriter.getAttributeCount(); + size += moduleWriter.computeAttributesSize(); + } + if (nestHostClassIndex != 0) { + ++attributesCount; + size += 8; + symbolTable.addConstantUtf8(Constants.NEST_HOST); + } + if (nestMemberClasses != null) { + ++attributesCount; + size += 8 + nestMemberClasses.length; + symbolTable.addConstantUtf8(Constants.NEST_MEMBERS); + } + if (permittedSubclasses != null) { + ++attributesCount; + size += 8 + permittedSubclasses.length; + symbolTable.addConstantUtf8(Constants.PERMITTED_SUBCLASSES); + } + int recordComponentCount = 0; + int recordSize = 0; + if ((accessFlags & Opcodes.ACC_RECORD) != 0 || firstRecordComponent != null) { + RecordComponentWriter recordComponentWriter = firstRecordComponent; + while (recordComponentWriter != null) { + ++recordComponentCount; + recordSize += recordComponentWriter.computeRecordComponentInfoSize(); + recordComponentWriter = (RecordComponentWriter) recordComponentWriter.delegate; + } + ++attributesCount; + size += 8 + recordSize; + symbolTable.addConstantUtf8(Constants.RECORD); + } + if (firstAttribute != null) { + attributesCount += firstAttribute.getAttributeCount(); + size += firstAttribute.computeAttributesSize(symbolTable); + } + // IMPORTANT: this must be the last part of the ClassFile size computation, because the previous + // statements can add attribute names to the constant pool, thereby changing its size! + size += symbolTable.getConstantPoolLength(); + int constantPoolCount = symbolTable.getConstantPoolCount(); + if (constantPoolCount > 0xFFFF) { + throw new ClassTooLargeException(symbolTable.getClassName(), constantPoolCount); + } + + // Second step: allocate a ByteVector of the correct size (in order to avoid any array copy in + // dynamic resizes) and fill it with the ClassFile content. + ByteVector result = new ByteVector(size); + result.putInt(0xCAFEBABE).putInt(version); + symbolTable.putConstantPool(result); + int mask = (version & 0xFFFF) < Opcodes.V1_5 ? Opcodes.ACC_SYNTHETIC : 0; + result.putShort(accessFlags & ~mask).putShort(thisClass).putShort(superClass); + result.putShort(interfaceCount); + for (int i = 0; i < interfaceCount; ++i) { + result.putShort(interfaces[i]); + } + result.putShort(fieldsCount); + fieldWriter = firstField; + while (fieldWriter != null) { + fieldWriter.putFieldInfo(result); + fieldWriter = (FieldWriter) fieldWriter.fv; + } + result.putShort(methodsCount); + boolean hasFrames = false; + boolean hasAsmInstructions = false; + methodWriter = firstMethod; + while (methodWriter != null) { + hasFrames |= methodWriter.hasFrames(); + hasAsmInstructions |= methodWriter.hasAsmInstructions(); + methodWriter.putMethodInfo(result); + methodWriter = (MethodWriter) methodWriter.mv; + } + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + result.putShort(attributesCount); + if (innerClasses != null) { + result + .putShort(symbolTable.addConstantUtf8(Constants.INNER_CLASSES)) + .putInt(innerClasses.length + 2) + .putShort(numberOfInnerClasses) + .putByteArray(innerClasses.data, 0, innerClasses.length); + } + if (enclosingClassIndex != 0) { + result + .putShort(symbolTable.addConstantUtf8(Constants.ENCLOSING_METHOD)) + .putInt(4) + .putShort(enclosingClassIndex) + .putShort(enclosingMethodIndex); + } + if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0 && (version & 0xFFFF) < Opcodes.V1_5) { + result.putShort(symbolTable.addConstantUtf8(Constants.SYNTHETIC)).putInt(0); + } + if (signatureIndex != 0) { + result + .putShort(symbolTable.addConstantUtf8(Constants.SIGNATURE)) + .putInt(2) + .putShort(signatureIndex); + } + if (sourceFileIndex != 0) { + result + .putShort(symbolTable.addConstantUtf8(Constants.SOURCE_FILE)) + .putInt(2) + .putShort(sourceFileIndex); + } + if (debugExtension != null) { + int length = debugExtension.length; + result + .putShort(symbolTable.addConstantUtf8(Constants.SOURCE_DEBUG_EXTENSION)) + .putInt(length) + .putByteArray(debugExtension.data, 0, length); + } + if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) { + result.putShort(symbolTable.addConstantUtf8(Constants.DEPRECATED)).putInt(0); + } + AnnotationWriter.putAnnotations( + symbolTable, + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation, + result); + symbolTable.putBootstrapMethods(result); + if (moduleWriter != null) { + moduleWriter.putAttributes(result); + } + if (nestHostClassIndex != 0) { + result + .putShort(symbolTable.addConstantUtf8(Constants.NEST_HOST)) + .putInt(2) + .putShort(nestHostClassIndex); + } + if (nestMemberClasses != null) { + result + .putShort(symbolTable.addConstantUtf8(Constants.NEST_MEMBERS)) + .putInt(nestMemberClasses.length + 2) + .putShort(numberOfNestMemberClasses) + .putByteArray(nestMemberClasses.data, 0, nestMemberClasses.length); + } + if (permittedSubclasses != null) { + result + .putShort(symbolTable.addConstantUtf8(Constants.PERMITTED_SUBCLASSES)) + .putInt(permittedSubclasses.length + 2) + .putShort(numberOfPermittedSubclasses) + .putByteArray(permittedSubclasses.data, 0, permittedSubclasses.length); + } + if ((accessFlags & Opcodes.ACC_RECORD) != 0 || firstRecordComponent != null) { + result + .putShort(symbolTable.addConstantUtf8(Constants.RECORD)) + .putInt(recordSize + 2) + .putShort(recordComponentCount); + RecordComponentWriter recordComponentWriter = firstRecordComponent; + while (recordComponentWriter != null) { + recordComponentWriter.putRecordComponentInfo(result); + recordComponentWriter = (RecordComponentWriter) recordComponentWriter.delegate; + } + } + if (firstAttribute != null) { + firstAttribute.putAttributes(symbolTable, result); + } + + // Third step: replace the ASM specific instructions, if any. + if (hasAsmInstructions) { + return replaceAsmInstructions(result.data, hasFrames); + } else { + return result.data; + } + } + + /** + * Returns the equivalent of the given class file, with the ASM specific instructions replaced + * with standard ones. This is done with a ClassReader -> ClassWriter round trip. + * + * @param classFile a class file containing ASM specific instructions, generated by this + * ClassWriter. + * @param hasFrames whether there is at least one stack map frames in 'classFile'. + * @return an equivalent of 'classFile', with the ASM specific instructions replaced with standard + * ones. + */ + private byte[] replaceAsmInstructions(final byte[] classFile, final boolean hasFrames) { + final Attribute[] attributes = getAttributePrototypes(); + firstField = null; + lastField = null; + firstMethod = null; + lastMethod = null; + lastRuntimeVisibleAnnotation = null; + lastRuntimeInvisibleAnnotation = null; + lastRuntimeVisibleTypeAnnotation = null; + lastRuntimeInvisibleTypeAnnotation = null; + moduleWriter = null; + nestHostClassIndex = 0; + numberOfNestMemberClasses = 0; + nestMemberClasses = null; + numberOfPermittedSubclasses = 0; + permittedSubclasses = null; + firstRecordComponent = null; + lastRecordComponent = null; + firstAttribute = null; + compute = hasFrames ? MethodWriter.COMPUTE_INSERTED_FRAMES : MethodWriter.COMPUTE_NOTHING; + new ClassReader(classFile, 0, /* checkClassVersion= */ false) + .accept( + this, + attributes, + (hasFrames ? ClassReader.EXPAND_FRAMES : 0) | ClassReader.EXPAND_ASM_INSNS); + return toByteArray(); + } + + /** + * Returns the prototypes of the attributes used by this class, its fields and its methods. + * + * @return the prototypes of the attributes used by this class, its fields and its methods. + */ + private Attribute[] getAttributePrototypes() { + Attribute.Set attributePrototypes = new Attribute.Set(); + attributePrototypes.addAttributes(firstAttribute); + FieldWriter fieldWriter = firstField; + while (fieldWriter != null) { + fieldWriter.collectAttributePrototypes(attributePrototypes); + fieldWriter = (FieldWriter) fieldWriter.fv; + } + MethodWriter methodWriter = firstMethod; + while (methodWriter != null) { + methodWriter.collectAttributePrototypes(attributePrototypes); + methodWriter = (MethodWriter) methodWriter.mv; + } + RecordComponentWriter recordComponentWriter = firstRecordComponent; + while (recordComponentWriter != null) { + recordComponentWriter.collectAttributePrototypes(attributePrototypes); + recordComponentWriter = (RecordComponentWriter) recordComponentWriter.delegate; + } + return attributePrototypes.toArray(); + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods: constant pool management for Attribute sub classes + // ----------------------------------------------------------------------------------------------- + + /** + * Adds a number or string constant to the constant pool of the class being build. Does nothing if + * the constant pool already contains a similar item. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param value the value of the constant to be added to the constant pool. This parameter must be + * an {@link Integer}, a {@link Float}, a {@link Long}, a {@link Double} or a {@link String}. + * @return the index of a new or already existing constant item with the given value. + */ + public int newConst(final Object value) { + return symbolTable.addConstant(value).index; + } + + /** + * Adds an UTF8 string to the constant pool of the class being build. Does nothing if the constant + * pool already contains a similar item. This method is intended for {@link Attribute} sub + * classes, and is normally not needed by class generators or adapters. + * + * @param value the String value. + * @return the index of a new or already existing UTF8 item. + */ + // DontCheck(AbbreviationAsWordInName): can't be renamed (for backward binary compatibility). + public int newUTF8(final String value) { + return symbolTable.addConstantUtf8(value); + } + + /** + * Adds a class reference to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param value the internal name of the class. + * @return the index of a new or already existing class reference item. + */ + public int newClass(final String value) { + return symbolTable.addConstantClass(value).index; + } + + /** + * Adds a method type reference to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param methodDescriptor method descriptor of the method type. + * @return the index of a new or already existing method type reference item. + */ + public int newMethodType(final String methodDescriptor) { + return symbolTable.addConstantMethodType(methodDescriptor).index; + } + + /** + * Adds a module reference to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param moduleName name of the module. + * @return the index of a new or already existing module reference item. + */ + public int newModule(final String moduleName) { + return symbolTable.addConstantModule(moduleName).index; + } + + /** + * Adds a package reference to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param packageName name of the package in its internal form. + * @return the index of a new or already existing module reference item. + */ + public int newPackage(final String packageName) { + return symbolTable.addConstantPackage(packageName).index; + } + + /** + * Adds a handle to the constant pool of the class being build. Does nothing if the constant pool + * already contains a similar item. This method is intended for {@link Attribute} sub classes, + * and is normally not needed by class generators or adapters. + * + * @param tag the kind of this handle. Must be {@link Opcodes#H_GETFIELD}, {@link + * Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link + * Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, + * {@link Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. + * @param owner the internal name of the field or method owner class. + * @param name the name of the field or method. + * @param descriptor the descriptor of the field or method. + * @return the index of a new or already existing method type reference item. + * @deprecated this method is superseded by {@link #newHandle(int, String, String, String, + * boolean)}. + */ + @Deprecated + public int newHandle( + final int tag, final String owner, final String name, final String descriptor) { + return newHandle(tag, owner, name, descriptor, tag == Opcodes.H_INVOKEINTERFACE); + } + + /** + * Adds a handle to the constant pool of the class being build. Does nothing if the constant pool + * already contains a similar item. This method is intended for {@link Attribute} sub classes, + * and is normally not needed by class generators or adapters. + * + * @param tag the kind of this handle. Must be {@link Opcodes#H_GETFIELD}, {@link + * Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link + * Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, + * {@link Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. + * @param owner the internal name of the field or method owner class. + * @param name the name of the field or method. + * @param descriptor the descriptor of the field or method. + * @param isInterface true if the owner is an processer. + * @return the index of a new or already existing method type reference item. + */ + public int newHandle( + final int tag, + final String owner, + final String name, + final String descriptor, + final boolean isInterface) { + return symbolTable.addConstantMethodHandle(tag, owner, name, descriptor, isInterface).index; + } + + /** + * Adds a dynamic constant reference to the constant pool of the class being build. Does nothing + * if the constant pool already contains a similar item. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param name name of the invoked method. + * @param descriptor field descriptor of the constant type. + * @param bootstrapMethodHandle the bootstrap method. + * @param bootstrapMethodArguments the bootstrap method constant arguments. + * @return the index of a new or already existing dynamic constant reference item. + */ + public int newConstantDynamic( + final String name, + final String descriptor, + final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + return symbolTable.addConstantDynamic( + name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments) + .index; + } + + /** + * Adds an invokedynamic reference to the constant pool of the class being build. Does nothing if + * the constant pool already contains a similar item. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param name name of the invoked method. + * @param descriptor descriptor of the invoke method. + * @param bootstrapMethodHandle the bootstrap method. + * @param bootstrapMethodArguments the bootstrap method constant arguments. + * @return the index of a new or already existing invokedynamic reference item. + */ + public int newInvokeDynamic( + final String name, + final String descriptor, + final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + return symbolTable.addConstantInvokeDynamic( + name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments) + .index; + } + + /** + * Adds a field reference to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param owner the internal name of the field's owner class. + * @param name the field's name. + * @param descriptor the field's descriptor. + * @return the index of a new or already existing field reference item. + */ + public int newField(final String owner, final String name, final String descriptor) { + return symbolTable.addConstantFieldref(owner, name, descriptor).index; + } + + /** + * Adds a method reference to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param owner the internal name of the method's owner class. + * @param name the method's name. + * @param descriptor the method's descriptor. + * @param isInterface {@literal true} if {@code owner} is an processer. + * @return the index of a new or already existing method reference item. + */ + public int newMethod( + final String owner, final String name, final String descriptor, final boolean isInterface) { + return symbolTable.addConstantMethodref(owner, name, descriptor, isInterface).index; + } + + /** + * Adds a name and type to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param name a name. + * @param descriptor a type descriptor. + * @return the index of a new or already existing name and type item. + */ + public int newNameType(final String name, final String descriptor) { + return symbolTable.addConstantNameAndType(name, descriptor); + } + + // ----------------------------------------------------------------------------------------------- + // Default method to compute common super classes when computing stack map frames + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the common super type of the two given types. The default implementation of this method + * loads the two given classes and uses the java.lang.Class methods to find the common + * super class. It can be overridden to compute this common super type in other ways, in + * particular without actually loading any class, or to take into account the class that is + * currently being generated by this ClassWriter, which can of course not be loaded since it is + * under construction. + * + * @param type1 the internal name of a class. + * @param type2 the internal name of another class. + * @return the internal name of the common super class of the two given classes. + */ + protected String getCommonSuperClass(final String type1, final String type2) { + ClassLoader classLoader = getClassLoader(); + Class class1; + try { + class1 = Class.forName(type1.replace('/', '.'), false, classLoader); + } catch (ClassNotFoundException e) { + throw new TypeNotPresentException(type1, e); + } + Class class2; + try { + class2 = Class.forName(type2.replace('/', '.'), false, classLoader); + } catch (ClassNotFoundException e) { + throw new TypeNotPresentException(type2, e); + } + if (class1.isAssignableFrom(class2)) { + return type1; + } + if (class2.isAssignableFrom(class1)) { + return type2; + } + if (class1.isInterface() || class2.isInterface()) { + return "java/lang/Object"; + } else { + do { + class1 = class1.getSuperclass(); + } while (!class1.isAssignableFrom(class2)); + return class1.getName().replace('.', '/'); + } + } + + /** + * Returns the {@link ClassLoader} to be used by the default implementation of {@link + * #getCommonSuperClass(String, String)}, that of this {@link ClassWriter}'s runtime type by + * default. + * + * @return ClassLoader + */ + protected ClassLoader getClassLoader() { + return getClass().getClassLoader(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ConstantDynamic.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ConstantDynamic.java new file mode 100644 index 0000000..9fc08f8 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ConstantDynamic.java @@ -0,0 +1,159 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +import java.util.Arrays; + +/** A constant whose value is computed at runtime, with a bootstrap method. @Author Remi Forax */ +public final class ConstantDynamic { + + /** The constant name (can be arbitrary). */ + private final String name; + + /** The constant type (must be a field descriptor). */ + private final String descriptor; + + /** The bootstrap method to use to compute the constant value at runtime. */ + private final Handle bootstrapMethod; + + /** + * The arguments to pass to the bootstrap method, in order to compute the constant value at + * runtime. + */ + private final Object[] bootstrapMethodArguments; + + /** + * Constructs a new {@link ConstantDynamic}. + * + * @param name the constant name (can be arbitrary). + * @param descriptor the constant type (must be a field descriptor). + * @param bootstrapMethod the bootstrap method to use to compute the constant value at runtime. + * @param bootstrapMethodArguments the arguments to pass to the bootstrap method, in order to + * compute the constant value at runtime. + */ + public ConstantDynamic( + final String name, + final String descriptor, + final Handle bootstrapMethod, + final Object... bootstrapMethodArguments) { + this.name = name; + this.descriptor = descriptor; + this.bootstrapMethod = bootstrapMethod; + this.bootstrapMethodArguments = bootstrapMethodArguments; + } + + /** + * Returns the name of this constant. + * + * @return the name of this constant. + */ + public String getName() { + return name; + } + + /** + * Returns the type of this constant. + * + * @return the type of this constant, as a field descriptor. + */ + public String getDescriptor() { + return descriptor; + } + + /** + * Returns the bootstrap method used to compute the value of this constant. + * + * @return the bootstrap method used to compute the value of this constant. + */ + public Handle getBootstrapMethod() { + return bootstrapMethod; + } + + /** + * Returns the number of arguments passed to the bootstrap method, in order to compute the value + * of this constant. + * + * @return the number of arguments passed to the bootstrap method, in order to compute the value + * of this constant. + */ + public int getBootstrapMethodArgumentCount() { + return bootstrapMethodArguments.length; + } + + /** + * Returns an argument passed to the bootstrap method, in order to compute the value of this + * constant. + * + * @param index an argument index, between 0 and {@link #getBootstrapMethodArgumentCount()} + * (exclusive). + * @return the argument passed to the bootstrap method, with the given index. + */ + public Object getBootstrapMethodArgument(final int index) { + return bootstrapMethodArguments[index]; + } + + /** + * Returns the arguments to pass to the bootstrap method, in order to compute the value of this + * constant. WARNING: this array must not be modified, and must not be returned to the user. + * + * @return the arguments to pass to the bootstrap method, in order to compute the value of this + * constant. + */ + Object[] getBootstrapMethodArgumentsUnsafe() { + return bootstrapMethodArguments; + } + + /** + * Returns the size of this constant. + * + * @return the size of this constant, i.e., 2 for {@code long} and {@code double}, 1 otherwise. + */ + public int getSize() { + char firstCharOfDescriptor = descriptor.charAt(0); + return (firstCharOfDescriptor == 'J' || firstCharOfDescriptor == 'D') ? 2 : 1; + } + + @Override + public boolean equals(final Object object) { + if (object == this) { + return true; + } + if (!(object instanceof ConstantDynamic)) { + return false; + } + ConstantDynamic constantDynamic = (ConstantDynamic) object; + return name.equals(constantDynamic.name) + && descriptor.equals(constantDynamic.descriptor) + && bootstrapMethod.equals(constantDynamic.bootstrapMethod) + && Arrays.equals(bootstrapMethodArguments, constantDynamic.bootstrapMethodArguments); + } + + @Override + public int hashCode() { + return name.hashCode() + ^ Integer.rotateLeft(descriptor.hashCode(), 8) + ^ Integer.rotateLeft(bootstrapMethod.hashCode(), 16) + ^ Integer.rotateLeft(Arrays.hashCode(bootstrapMethodArguments), 24); + } + + @Override + public String toString() { + return name + + " : " + + descriptor + + ' ' + + bootstrapMethod + + ' ' + + Arrays.toString(bootstrapMethodArguments); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Constants.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Constants.java new file mode 100644 index 0000000..f2e4210 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Constants.java @@ -0,0 +1,205 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.regex.Pattern; + +/** + * Defines additional JVM opcodes, access flags and constants which are not part of the ASM public + * API. @Author Eric Bruneton + * + * @see JVMS 6 + */ +final class Constants { + + // The ClassFile attribute names, in the order they are defined in + // https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7-300. + + static final String CONSTANT_VALUE = "ConstantValue"; + static final String CODE = "Code"; + static final String STACK_MAP_TABLE = "StackMapTable"; + static final String EXCEPTIONS = "Exceptions"; + static final String INNER_CLASSES = "InnerClasses"; + static final String ENCLOSING_METHOD = "EnclosingMethod"; + static final String SYNTHETIC = "Synthetic"; + static final String SIGNATURE = "Signature"; + static final String SOURCE_FILE = "SourceFile"; + static final String SOURCE_DEBUG_EXTENSION = "SourceDebugExtension"; + static final String LINE_NUMBER_TABLE = "LineNumberTable"; + static final String LOCAL_VARIABLE_TABLE = "LocalVariableTable"; + static final String LOCAL_VARIABLE_TYPE_TABLE = "LocalVariableTypeTable"; + static final String DEPRECATED = "Deprecated"; + static final String RUNTIME_VISIBLE_ANNOTATIONS = "RuntimeVisibleAnnotations"; + static final String RUNTIME_INVISIBLE_ANNOTATIONS = "RuntimeInvisibleAnnotations"; + static final String RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS = "RuntimeVisibleParameterAnnotations"; + static final String RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS = + "RuntimeInvisibleParameterAnnotations"; + static final String RUNTIME_VISIBLE_TYPE_ANNOTATIONS = "RuntimeVisibleTypeAnnotations"; + static final String RUNTIME_INVISIBLE_TYPE_ANNOTATIONS = "RuntimeInvisibleTypeAnnotations"; + static final String ANNOTATION_DEFAULT = "AnnotationDefault"; + static final String BOOTSTRAP_METHODS = "BootstrapMethods"; + static final String METHOD_PARAMETERS = "MethodParameters"; + static final String MODULE = "Module"; + static final String MODULE_PACKAGES = "ModulePackages"; + static final String MODULE_MAIN_CLASS = "ModuleMainClass"; + static final String NEST_HOST = "NestHost"; + static final String NEST_MEMBERS = "NestMembers"; + static final String PERMITTED_SUBCLASSES = "PermittedSubclasses"; + static final String RECORD = "Record"; + + // ASM specific access flags. + // WARNING: the 16 least significant bits must NOT be used, to avoid conflicts with standard + // access flags, and also to make sure that these flags are automatically filtered out when + // written in class files (because access flags are stored using 16 bits only). + + static final int ACC_CONSTRUCTOR = 0x40000; // method access flag. + + // ASM specific stack map frame types, used in {@link ClassVisitor#visitFrame}. + + /** + * A frame inserted between already existing frames. This internal stack map frame type (in + * addition to the ones declared in {@link Opcodes}) can only be used if the frame content can be + * computed from the previous existing frame and from the instructions between this existing frame + * and the inserted one, without any knowledge of the type hierarchy. This kind of frame is only + * used when an unconditional jump is inserted in a method while expanding an ASM specific + * instruction. Keep in sync with Opcodes.java. + */ + static final int F_INSERT = 256; + + // The JVM opcode values which are not part of the ASM public API. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-6.html. + + static final int LDC_W = 19; + static final int LDC2_W = 20; + static final int ILOAD_0 = 26; + static final int ILOAD_1 = 27; + static final int ILOAD_2 = 28; + static final int ILOAD_3 = 29; + static final int LLOAD_0 = 30; + static final int LLOAD_1 = 31; + static final int LLOAD_2 = 32; + static final int LLOAD_3 = 33; + static final int FLOAD_0 = 34; + static final int FLOAD_1 = 35; + static final int FLOAD_2 = 36; + static final int FLOAD_3 = 37; + static final int DLOAD_0 = 38; + static final int DLOAD_1 = 39; + static final int DLOAD_2 = 40; + static final int DLOAD_3 = 41; + static final int ALOAD_0 = 42; + static final int ALOAD_1 = 43; + static final int ALOAD_2 = 44; + static final int ALOAD_3 = 45; + static final int ISTORE_0 = 59; + static final int ISTORE_1 = 60; + static final int ISTORE_2 = 61; + static final int ISTORE_3 = 62; + static final int LSTORE_0 = 63; + static final int LSTORE_1 = 64; + static final int LSTORE_2 = 65; + static final int LSTORE_3 = 66; + static final int FSTORE_0 = 67; + static final int FSTORE_1 = 68; + static final int FSTORE_2 = 69; + static final int FSTORE_3 = 70; + static final int DSTORE_0 = 71; + static final int DSTORE_1 = 72; + static final int DSTORE_2 = 73; + static final int DSTORE_3 = 74; + static final int ASTORE_0 = 75; + static final int ASTORE_1 = 76; + static final int ASTORE_2 = 77; + static final int ASTORE_3 = 78; + static final int WIDE = 196; + static final int GOTO_W = 200; + static final int JSR_W = 201; + + // Constants to convert between normal and wide jump instructions. + + // The delta between the GOTO_W and JSR_W opcodes and GOTO and JUMP. + static final int WIDE_JUMP_OPCODE_DELTA = GOTO_W - Opcodes.GOTO; + + // Constants to convert JVM opcodes to the equivalent ASM specific opcodes, and vice versa. + + // The delta between the ASM_IFEQ, ..., ASM_IF_ACMPNE, ASM_GOTO and ASM_JSR opcodes + // and IFEQ, ..., IF_ACMPNE, GOTO and JSR. + static final int ASM_OPCODE_DELTA = 49; + + // The delta between the ASM_IFNULL and ASM_IFNONNULL opcodes and IFNULL and IFNONNULL. + static final int ASM_IFNULL_OPCODE_DELTA = 20; + + // ASM specific opcodes, used for long forward jump instructions. + + static final int ASM_IFEQ = Opcodes.IFEQ + ASM_OPCODE_DELTA; + static final int ASM_IFNE = Opcodes.IFNE + ASM_OPCODE_DELTA; + static final int ASM_IFLT = Opcodes.IFLT + ASM_OPCODE_DELTA; + static final int ASM_IFGE = Opcodes.IFGE + ASM_OPCODE_DELTA; + static final int ASM_IFGT = Opcodes.IFGT + ASM_OPCODE_DELTA; + static final int ASM_IFLE = Opcodes.IFLE + ASM_OPCODE_DELTA; + static final int ASM_IF_ICMPEQ = Opcodes.IF_ICMPEQ + ASM_OPCODE_DELTA; + static final int ASM_IF_ICMPNE = Opcodes.IF_ICMPNE + ASM_OPCODE_DELTA; + static final int ASM_IF_ICMPLT = Opcodes.IF_ICMPLT + ASM_OPCODE_DELTA; + static final int ASM_IF_ICMPGE = Opcodes.IF_ICMPGE + ASM_OPCODE_DELTA; + static final int ASM_IF_ICMPGT = Opcodes.IF_ICMPGT + ASM_OPCODE_DELTA; + static final int ASM_IF_ICMPLE = Opcodes.IF_ICMPLE + ASM_OPCODE_DELTA; + static final int ASM_IF_ACMPEQ = Opcodes.IF_ACMPEQ + ASM_OPCODE_DELTA; + static final int ASM_IF_ACMPNE = Opcodes.IF_ACMPNE + ASM_OPCODE_DELTA; + static final int ASM_GOTO = Opcodes.GOTO + ASM_OPCODE_DELTA; + static final int ASM_JSR = Opcodes.JSR + ASM_OPCODE_DELTA; + static final int ASM_IFNULL = Opcodes.IFNULL + ASM_IFNULL_OPCODE_DELTA; + static final int ASM_IFNONNULL = Opcodes.IFNONNULL + ASM_IFNULL_OPCODE_DELTA; + static final int ASM_GOTO_W = 220; + + private Constants() {} + + static void checkAsmExperimental(final Object caller) { + Class callerClass = caller.getClass(); + String internalName = callerClass.getName().replace('.', '/'); + if (!isWhitelisted(internalName)) { + checkIsPreview(callerClass.getClassLoader().getResourceAsStream(internalName + ".class")); + } + } + + static boolean isWhitelisted(final String internalName) { + if (!internalName.startsWith("org/objectweb/asm/")) { + return false; + } + String member = "(Annotation|Class|Field|Method|Module|RecordComponent|Signature)"; + return internalName.contains("Test$") + || Pattern.matches( + "org/objectweb/asm/util/Trace" + member + "Visitor(\\$.*)?", internalName) + || Pattern.matches( + "org/objectweb/asm/util/Check" + member + "Adapter(\\$.*)?", internalName); + } + + static void checkIsPreview(final InputStream classInputStream) { + if (classInputStream == null) { + throw new IllegalStateException("Bytecode not available, can't check class version"); + } + int minorVersion; + try (DataInputStream callerClassStream = new DataInputStream(classInputStream)) { + callerClassStream.readInt(); + minorVersion = callerClassStream.readUnsignedShort(); + } catch (IOException ioe) { + throw new IllegalStateException("I/O error, can't check class version", ioe); + } + if (minorVersion != 0xFFFF) { + throw new IllegalStateException( + "ASM9_EXPERIMENTAL can only be used by classes compiled with --enable-preview"); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Context.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Context.java new file mode 100644 index 0000000..8e313a0 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Context.java @@ -0,0 +1,117 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** Information about a class being parsed in a {@link ClassReader}. @Author Eric Bruneton */ +final class Context { + + /** The prototypes of the attributes that must be parsed in this class. */ + Attribute[] attributePrototypes; + + /** + * The options used to parse this class. One or more of {@link ClassReader#SKIP_CODE}, {@link + * ClassReader#SKIP_DEBUG}, {@link ClassReader#SKIP_FRAMES}, {@link ClassReader#EXPAND_FRAMES} or + * {@link ClassReader#EXPAND_ASM_INSNS}. + */ + int parsingOptions; + + /** The buffer used to read strings in the constant pool. */ + char[] charBuffer; + + // Information about the current method, i.e. the one read in the current (or latest) call + // to {@link ClassReader#readMethod()}. + + /** The access flags of the current method. */ + int currentMethodAccessFlags; + + /** The name of the current method. */ + String currentMethodName; + + /** The descriptor of the current method. */ + String currentMethodDescriptor; + + /** + * The labels of the current method, indexed by bytecode offset (only bytecode offsets for which a + * label is needed have a non null associated Label). + */ + Label[] currentMethodLabels; + + // Information about the current type annotation target, i.e. the one read in the current + // (or latest) call to {@link ClassReader#readAnnotationTarget()}. + + /** + * The target_type and target_info of the current type annotation target, encoded as described in + * {@link TypeReference}. + */ + int currentTypeAnnotationTarget; + + /** The target_path of the current type annotation target. */ + TypePath currentTypeAnnotationTargetPath; + + /** The start of each local variable range in the current local variable annotation. */ + Label[] currentLocalVariableAnnotationRangeStarts; + + /** The end of each local variable range in the current local variable annotation. */ + Label[] currentLocalVariableAnnotationRangeEnds; + + /** + * The local variable index of each local variable range in the current local variable annotation. + */ + int[] currentLocalVariableAnnotationRangeIndices; + + // Information about the current stack map frame, i.e. the one read in the current (or latest) + // call to {@link ClassReader#readFrame()}. + + /** The bytecode offset of the current stack map frame. */ + int currentFrameOffset; + + /** + * The type of the current stack map frame. One of {@link Opcodes#F_FULL}, {@link + * Opcodes#F_APPEND}, {@link Opcodes#F_CHOP}, {@link Opcodes#F_SAME} or {@link Opcodes#F_SAME1}. + */ + int currentFrameType; + + /** + * The number of local variable types in the current stack map frame. Each type is represented + * with a single array element (even long and double). + */ + int currentFrameLocalCount; + + /** + * The delta number of local variable types in the current stack map frame (each type is + * represented with a single array element - even long and double). This is the number of local + * variable types in this frame, minus the number of local variable types in the previous frame. + */ + int currentFrameLocalCountDelta; + + /** + * The types of the local variables in the current stack map frame. Each type is represented with + * a single array element (even long and double), using the format described in {@link + * MethodVisitor#visitFrame}. Depending on {@link #currentFrameType}, this contains the types of + * all the local variables, or only those of the additional ones (compared to the previous frame). + */ + Object[] currentFrameLocalTypes; + + /** + * The number stack element types in the current stack map frame. Each type is represented with a + * single array element (even long and double). + */ + int currentFrameStackCount; + + /** + * The types of the stack elements in the current stack map frame. Each type is represented with a + * single array element (even long and double), using the format described in {@link + * MethodVisitor#visitFrame}. + */ + Object[] currentFrameStackTypes; +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/CurrentFrame.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/CurrentFrame.java new file mode 100644 index 0000000..23eabc7 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/CurrentFrame.java @@ -0,0 +1,39 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * Information about the input stack map frame at the "current" instruction of a method. This is + * implemented as a Frame subclass for a "basic block" containing only one instruction. @Author Eric + * Bruneton + */ +final class CurrentFrame extends Frame { + + CurrentFrame(final Label owner) { + super(owner); + } + + /** + * Sets this CurrentFrame to the input stack map frame of the next "current" instruction, i.e. the + * instruction just after the given one. It is assumed that the value of this object when this + * method is called is the stack map frame status just before the given instruction is executed. + */ + @Override + void execute( + final int opcode, final int arg, final Symbol symbolArg, final SymbolTable symbolTable) { + super.execute(opcode, arg, symbolArg, symbolTable); + Frame successor = new Frame(null); + merge(symbolTable, successor, 0); + copyFrom(successor); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Edge.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Edge.java new file mode 100644 index 0000000..35bb7a9 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Edge.java @@ -0,0 +1,75 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * An edge in the control flow graph of a method. Each node of this graph is a basic block, + * represented with the Label corresponding to its first instruction. Each edge goes from one node + * to another, i.e. from one basic block to another (called the predecessor and successor blocks, + * respectively). An edge corresponds either to a jump or ret instruction or to an exception + * handler. @Author Eric Bruneton + * + * @see Label + */ +final class Edge { + + /** + * A control flow graph edge corresponding to a jump or ret instruction. Only used with {@link + * ClassWriter#COMPUTE_FRAMES}. + */ + static final int JUMP = 0; + + /** + * A control flow graph edge corresponding to an exception handler. Only used with {@link + * ClassWriter#COMPUTE_MAXS}. + */ + static final int EXCEPTION = 0x7FFFFFFF; + + /** + * Information about this control flow graph edge. + * + *
    + *
  • If {@link ClassWriter#COMPUTE_MAXS} is used, this field contains either a stack size + * delta (for an edge corresponding to a jump instruction), or the value EXCEPTION (for an + * edge corresponding to an exception handler). The stack size delta is the stack size just + * after the jump instruction, minus the stack size at the beginning of the predecessor + * basic block, i.e. the one containing the jump instruction. + *
  • If {@link ClassWriter#COMPUTE_FRAMES} is used, this field contains either the value JUMP + * (for an edge corresponding to a jump instruction), or the index, in the {@link + * ClassWriter} type table, of the exception type that is handled (for an edge corresponding + * to an exception handler). + *
+ */ + final int info; + + /** The successor block of this control flow graph edge. */ + final Label successor; + + /** + * The next edge in the list of outgoing edges of a basic block. See {@link Label#outgoingEdges}. + */ + Edge nextEdge; + + /** + * Constructs a new Edge. + * + * @param info see {@link #info}. + * @param successor see {@link #successor}. + * @param nextEdge see {@link #nextEdge}. + */ + Edge(final int info, final Label successor, final Edge nextEdge) { + this.info = info; + this.successor = successor; + this.nextEdge = nextEdge; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/FieldVisitor.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/FieldVisitor.java new file mode 100644 index 0000000..8b2723b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/FieldVisitor.java @@ -0,0 +1,128 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A visitor to visit a Java field. The methods of this class must be called in the following order: + * ( {@code visitAnnotation} | {@code visitTypeAnnotation} | {@code visitAttribute} )* {@code + * visitEnd}. @Author Eric Bruneton + */ +public abstract class FieldVisitor { + + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6}, {@link Opcodes#ASM7}, {@link + * Opcodes#ASM8} or {@link Opcodes#ASM9}. + */ + protected final int api; + + /** The field visitor to which this visitor must delegate method calls. May be {@literal null}. */ + protected FieldVisitor fv; + + /** + * Constructs a new {@link FieldVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6}, {@link Opcodes#ASM7}, {@link + * Opcodes#ASM8} or {@link Opcodes#ASM9}. + */ + public FieldVisitor(final int api) { + this(api, null); + } + + /** + * Constructs a new {@link FieldVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6}, {@link Opcodes#ASM7} or {@link + * Opcodes#ASM8}. + * @param fieldVisitor the field visitor to which this visitor must delegate method calls. May be + * null. + */ + public FieldVisitor(final int api, final FieldVisitor fieldVisitor) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + if (api == Opcodes.ASM10_EXPERIMENTAL) { + Constants.checkAsmExperimental(this); + } + this.api = api; + this.fv = fieldVisitor; + } + + /** + * Visits an annotation of the field. + * + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (fv != null) { + return fv.visitAnnotation(descriptor, visible); + } + return null; + } + + /** + * Visits an annotation on the type of the field. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#FIELD}. See {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException("This feature requires ASM5"); + } + if (fv != null) { + return fv.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + return null; + } + + /** + * Visits a non standard attribute of the field. + * + * @param attribute an attribute. + */ + public void visitAttribute(final Attribute attribute) { + if (fv != null) { + fv.visitAttribute(attribute); + } + } + + /** + * Visits the end of the field. This method, which is the last one to be called, is used to inform + * the visitor that all the annotations and attributes of the field have been visited. + */ + public void visitEnd() { + if (fv != null) { + fv.visitEnd(); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/FieldWriter.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/FieldWriter.java new file mode 100644 index 0000000..9826f5e --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/FieldWriter.java @@ -0,0 +1,268 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A {@link FieldVisitor} that generates a corresponding 'field_info' structure, as defined in the + * Java Virtual Machine Specification (JVMS). @Author Eric Bruneton + * + * @see JVMS + * 4.5 + */ +final class FieldWriter extends FieldVisitor { + + /** Where the constants used in this FieldWriter must be stored. */ + private final SymbolTable symbolTable; + + // Note: fields are ordered as in the field_info structure, and those related to attributes are + // ordered as in Section 4.7 of the JVMS. + + /** + * The access_flags field of the field_info JVMS structure. This field can contain ASM specific + * access flags, such as {@link Opcodes#ACC_DEPRECATED}, which are removed when generating the + * ClassFile structure. + */ + private final int accessFlags; + + /** The name_index field of the field_info JVMS structure. */ + private final int nameIndex; + + /** The descriptor_index field of the field_info JVMS structure. */ + private final int descriptorIndex; + + /** + * The signature_index field of the Signature attribute of this field_info, or 0 if there is no + * Signature attribute. + */ + private int signatureIndex; + + /** + * The constantvalue_index field of the ConstantValue attribute of this field_info, or 0 if there + * is no ConstantValue attribute. + */ + private int constantValueIndex; + + /** + * The last runtime visible annotation of this field. The previous ones can be accessed with the + * {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleAnnotation; + + /** + * The last runtime invisible annotation of this field. The previous ones can be accessed with the + * {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleAnnotation; + + /** + * The last runtime visible type annotation of this field. The previous ones can be accessed with + * the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleTypeAnnotation; + + /** + * The last runtime invisible type annotation of this field. The previous ones can be accessed + * with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleTypeAnnotation; + + /** + * The first non standard attribute of this field. The next ones can be accessed with the {@link + * Attribute#nextAttribute} field. May be {@literal null}. + * + *

WARNING: this list stores the attributes in the reverse order of their visit. + * firstAttribute is actually the last attribute visited in {@link #visitAttribute}. The {@link + * #putFieldInfo} method writes the attributes in the order defined by this list, i.e. in the + * reverse order specified by the user. + */ + private Attribute firstAttribute; + + // ----------------------------------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------------------------------- + + /** + * Constructs a new {@link FieldWriter}. + * + * @param symbolTable where the constants used in this FieldWriter must be stored. + * @param access the field's access flags (see {@link Opcodes}). + * @param name the field's name. + * @param descriptor the field's descriptor (see {@link Type}). + * @param signature the field's signature. May be {@literal null}. + * @param constantValue the field's constant value. May be {@literal null}. + */ + FieldWriter( + final SymbolTable symbolTable, + final int access, + final String name, + final String descriptor, + final String signature, + final Object constantValue) { + super(/* latest api = */ Opcodes.ASM9); + this.symbolTable = symbolTable; + this.accessFlags = access; + this.nameIndex = symbolTable.addConstantUtf8(name); + this.descriptorIndex = symbolTable.addConstantUtf8(descriptor); + if (signature != null) { + this.signatureIndex = symbolTable.addConstantUtf8(signature); + } + if (constantValue != null) { + this.constantValueIndex = symbolTable.addConstant(constantValue).index; + } + } + + // ----------------------------------------------------------------------------------------------- + // Implementation of the FieldVisitor abstract class + // ----------------------------------------------------------------------------------------------- + + @Override + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeVisibleAnnotation); + } else { + return lastRuntimeInvisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeInvisibleAnnotation); + } + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeVisibleTypeAnnotation); + } else { + return lastRuntimeInvisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public void visitAttribute(final Attribute attribute) { + // Store the attributes in the reverse order of their visit by this method. + attribute.nextAttribute = firstAttribute; + firstAttribute = attribute; + } + + @Override + public void visitEnd() { + // Nothing to do. + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the size of the field_info JVMS structure generated by this FieldWriter. Also adds the + * names of the attributes of this field in the constant pool. + * + * @return the size in bytes of the field_info JVMS structure. + */ + int computeFieldInfoSize() { + // The access_flags, name_index, descriptor_index and attributes_count fields use 8 bytes. + int size = 8; + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + if (constantValueIndex != 0) { + // ConstantValue attributes always use 8 bytes. + symbolTable.addConstantUtf8(Constants.CONSTANT_VALUE); + size += 8; + } + size += Attribute.computeAttributesSize(symbolTable, accessFlags, signatureIndex); + size += + AnnotationWriter.computeAnnotationsSize( + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation); + if (firstAttribute != null) { + size += firstAttribute.computeAttributesSize(symbolTable); + } + return size; + } + + /** + * Puts the content of the field_info JVMS structure generated by this FieldWriter into the given + * ByteVector. + * + * @param output where the field_info structure must be put. + */ + void putFieldInfo(final ByteVector output) { + boolean useSyntheticAttribute = symbolTable.getMajorVersion() < Opcodes.V1_5; + // Put the access_flags, name_index and descriptor_index fields. + int mask = useSyntheticAttribute ? Opcodes.ACC_SYNTHETIC : 0; + output.putShort(accessFlags & ~mask).putShort(nameIndex).putShort(descriptorIndex); + // Compute and put the attributes_count field. + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + int attributesCount = 0; + if (constantValueIndex != 0) { + ++attributesCount; + } + if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0 && useSyntheticAttribute) { + ++attributesCount; + } + if (signatureIndex != 0) { + ++attributesCount; + } + if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) { + ++attributesCount; + } + if (lastRuntimeVisibleAnnotation != null) { + ++attributesCount; + } + if (lastRuntimeInvisibleAnnotation != null) { + ++attributesCount; + } + if (lastRuntimeVisibleTypeAnnotation != null) { + ++attributesCount; + } + if (lastRuntimeInvisibleTypeAnnotation != null) { + ++attributesCount; + } + if (firstAttribute != null) { + attributesCount += firstAttribute.getAttributeCount(); + } + output.putShort(attributesCount); + // Put the field_info attributes. + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + if (constantValueIndex != 0) { + output + .putShort(symbolTable.addConstantUtf8(Constants.CONSTANT_VALUE)) + .putInt(2) + .putShort(constantValueIndex); + } + Attribute.putAttributes(symbolTable, accessFlags, signatureIndex, output); + AnnotationWriter.putAnnotations( + symbolTable, + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation, + output); + if (firstAttribute != null) { + firstAttribute.putAttributes(symbolTable, output); + } + } + + /** + * Collects the attributes of this field into the given set of attribute prototypes. + * + * @param attributePrototypes a set of attribute prototypes. + */ + final void collectAttributePrototypes(final Attribute.Set attributePrototypes) { + attributePrototypes.addAttributes(firstAttribute); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Frame.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Frame.java new file mode 100644 index 0000000..e693b91 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Frame.java @@ -0,0 +1,1456 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * The input and output stack map frames of a basic block. + * + *

Stack map frames are computed in two steps: + * + *

    + *
  • During the visit of each instruction in MethodWriter, the state of the frame at the end of + * the current basic block is updated by simulating the action of the instruction on the + * previous state of this so called "output frame". + *
  • After all instructions have been visited, a fix point algorithm is used in MethodWriter to + * compute the "input frame" of each basic block (i.e. the stack map frame at the beginning of + * the basic block). See {@link MethodWriter#computeAllFrames}. + *
+ * + *

Output stack map frames are computed relatively to the input frame of the basic block, which + * is not yet known when output frames are computed. It is therefore necessary to be able to + * represent abstract types such as "the type at position x in the input frame locals" or "the type + * at position x from the top of the input frame stack" or even "the type at position x in the input + * frame, with y more (or less) array dimensions". This explains the rather complicated type format + * used in this class, explained below. + * + *

The local variables and the operand stack of input and output frames contain values called + * "abstract types" hereafter. An abstract type is represented with 4 fields named DIM, KIND, FLAGS + * and VALUE, packed in a single int value for better performance and memory efficiency: + * + *

+ *   =====================================
+ *   |...DIM|KIND|.F|...............VALUE|
+ *   =====================================
+ * 
+ * + *
    + *
  • the DIM field, stored in the 6 most significant bits, is a signed number of array + * dimensions (from -32 to 31, included). It can be retrieved with {@link #DIM_MASK} and a + * right shift of {@link #DIM_SHIFT}. + *
  • the KIND field, stored in 4 bits, indicates the kind of VALUE used. These 4 bits can be + * retrieved with {@link #KIND_MASK} and, without any shift, must be equal to {@link + * #CONSTANT_KIND}, {@link #REFERENCE_KIND}, {@link #UNINITIALIZED_KIND}, {@link #LOCAL_KIND} + * or {@link #STACK_KIND}. + *
  • the FLAGS field, stored in 2 bits, contains up to 2 boolean flags. Currently only one flag + * is defined, namely {@link #TOP_IF_LONG_OR_DOUBLE_FLAG}. + *
  • the VALUE field, stored in the remaining 20 bits, contains either + *
      + *
    • one of the constants {@link #ITEM_TOP}, {@link #ITEM_ASM_BOOLEAN}, {@link + * #ITEM_ASM_BYTE}, {@link #ITEM_ASM_CHAR} or {@link #ITEM_ASM_SHORT}, {@link + * #ITEM_INTEGER}, {@link #ITEM_FLOAT}, {@link #ITEM_LONG}, {@link #ITEM_DOUBLE}, {@link + * #ITEM_NULL} or {@link #ITEM_UNINITIALIZED_THIS}, if KIND is equal to {@link + * #CONSTANT_KIND}. + *
    • the index of a {@link Symbol#TYPE_TAG} {@link Symbol} in the type table of a {@link + * SymbolTable}, if KIND is equal to {@link #REFERENCE_KIND}. + *
    • the index of an {@link Symbol#UNINITIALIZED_TYPE_TAG} {@link Symbol} in the type + * table of a SymbolTable, if KIND is equal to {@link #UNINITIALIZED_KIND}. + *
    • the index of a local variable in the input stack frame, if KIND is equal to {@link + * #LOCAL_KIND}. + *
    • a position relatively to the top of the stack of the input stack frame, if KIND is + * equal to {@link #STACK_KIND}, + *
    + *
+ * + *

Output frames can contain abstract types of any kind and with a positive or negative array + * dimension (and even unassigned types, represented by 0 - which does not correspond to any valid + * abstract type value). Input frames can only contain CONSTANT_KIND, REFERENCE_KIND or + * UNINITIALIZED_KIND abstract types of positive or {@literal null} array dimension. In all cases + * the type table contains only internal type names (array type descriptors are forbidden - array + * dimensions must be represented through the DIM field). + * + *

The LONG and DOUBLE types are always represented by using two slots (LONG + TOP or DOUBLE + + * TOP), for local variables as well as in the operand stack. This is necessary to be able to + * simulate DUPx_y instructions, whose effect would be dependent on the concrete types represented + * by the abstract types in the stack (which are not always known). @Author Eric Bruneton + */ +class Frame { + + // Constants used in the StackMapTable attribute. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.4. + + static final int SAME_FRAME = 0; + static final int SAME_LOCALS_1_STACK_ITEM_FRAME = 64; + static final int RESERVED = 128; + static final int SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED = 247; + static final int CHOP_FRAME = 248; + static final int SAME_FRAME_EXTENDED = 251; + static final int APPEND_FRAME = 252; + static final int FULL_FRAME = 255; + + static final int ITEM_TOP = 0; + static final int ITEM_INTEGER = 1; + static final int ITEM_FLOAT = 2; + static final int ITEM_DOUBLE = 3; + static final int ITEM_LONG = 4; + static final int ITEM_NULL = 5; + static final int ITEM_UNINITIALIZED_THIS = 6; + static final int ITEM_OBJECT = 7; + static final int ITEM_UNINITIALIZED = 8; + // Additional, ASM specific constants used in abstract types below. + private static final int ITEM_ASM_BOOLEAN = 9; + private static final int ITEM_ASM_BYTE = 10; + private static final int ITEM_ASM_CHAR = 11; + private static final int ITEM_ASM_SHORT = 12; + + // The size and offset in bits of each field of an abstract type. + + private static final int DIM_SIZE = 6; + private static final int KIND_SIZE = 4; + private static final int FLAGS_SIZE = 2; + private static final int VALUE_SIZE = 32 - DIM_SIZE - KIND_SIZE - FLAGS_SIZE; + + private static final int DIM_SHIFT = KIND_SIZE + FLAGS_SIZE + VALUE_SIZE; + private static final int KIND_SHIFT = FLAGS_SIZE + VALUE_SIZE; + private static final int FLAGS_SHIFT = VALUE_SIZE; + + // Bitmasks to get each field of an abstract type. + + private static final int DIM_MASK = ((1 << DIM_SIZE) - 1) << DIM_SHIFT; + private static final int KIND_MASK = ((1 << KIND_SIZE) - 1) << KIND_SHIFT; + private static final int VALUE_MASK = (1 << VALUE_SIZE) - 1; + + // Constants to manipulate the DIM field of an abstract type. + + /** The constant to be added to an abstract type to get one with one more array dimension. */ + private static final int ARRAY_OF = +1 << DIM_SHIFT; + + /** The constant to be added to an abstract type to get one with one less array dimension. */ + private static final int ELEMENT_OF = -1 << DIM_SHIFT; + + // Possible values for the KIND field of an abstract type. + + private static final int CONSTANT_KIND = 1 << KIND_SHIFT; + private static final int REFERENCE_KIND = 2 << KIND_SHIFT; + private static final int UNINITIALIZED_KIND = 3 << KIND_SHIFT; + private static final int LOCAL_KIND = 4 << KIND_SHIFT; + private static final int STACK_KIND = 5 << KIND_SHIFT; + + // Possible flags for the FLAGS field of an abstract type. + + /** + * A flag used for LOCAL_KIND and STACK_KIND abstract types, indicating that if the resolved, + * concrete type is LONG or DOUBLE, TOP should be used instead (because the value has been + * partially overridden with an xSTORE instruction). + */ + private static final int TOP_IF_LONG_OR_DOUBLE_FLAG = 1 << FLAGS_SHIFT; + + // Useful predefined abstract types (all the possible CONSTANT_KIND types). + + private static final int TOP = CONSTANT_KIND | ITEM_TOP; + private static final int BOOLEAN = CONSTANT_KIND | ITEM_ASM_BOOLEAN; + private static final int BYTE = CONSTANT_KIND | ITEM_ASM_BYTE; + private static final int CHAR = CONSTANT_KIND | ITEM_ASM_CHAR; + private static final int SHORT = CONSTANT_KIND | ITEM_ASM_SHORT; + private static final int INTEGER = CONSTANT_KIND | ITEM_INTEGER; + private static final int FLOAT = CONSTANT_KIND | ITEM_FLOAT; + private static final int LONG = CONSTANT_KIND | ITEM_LONG; + private static final int DOUBLE = CONSTANT_KIND | ITEM_DOUBLE; + private static final int NULL = CONSTANT_KIND | ITEM_NULL; + private static final int UNINITIALIZED_THIS = CONSTANT_KIND | ITEM_UNINITIALIZED_THIS; + + // ----------------------------------------------------------------------------------------------- + // Instance fields + // ----------------------------------------------------------------------------------------------- + + /** The basic block to which these input and output stack map frames correspond. */ + Label owner; + + /** The input stack map frame locals. This is an array of abstract types. */ + private int[] inputLocals; + + /** The input stack map frame stack. This is an array of abstract types. */ + private int[] inputStack; + + /** The output stack map frame locals. This is an array of abstract types. */ + private int[] outputLocals; + + /** The output stack map frame stack. This is an array of abstract types. */ + private int[] outputStack; + + /** + * The start of the output stack, relatively to the input stack. This offset is always negative or + * null. A null offset means that the output stack must be appended to the input stack. A -n + * offset means that the first n output stack elements must replace the top n input stack + * elements, and that the other elements must be appended to the input stack. + */ + private short outputStackStart; + + /** The index of the top stack element in {@link #outputStack}. */ + private short outputStackTop; + + /** The number of types that are initialized in the basic block. See {@link #initializations}. */ + private int initializationCount; + + /** + * The abstract types that are initialized in the basic block. A constructor invocation on an + * UNINITIALIZED or UNINITIALIZED_THIS abstract type must replace every occurrence of this + * type in the local variables and in the operand stack. This cannot be done during the first step + * of the algorithm since, during this step, the local variables and the operand stack types are + * still abstract. It is therefore necessary to store the abstract types of the constructors which + * are invoked in the basic block, in order to do this replacement during the second step of the + * algorithm, where the frames are fully computed. Note that this array can contain abstract types + * that are relative to the input locals or to the input stack. + */ + private int[] initializations; + + // ----------------------------------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------------------------------- + + /** + * Constructs a new Frame. + * + * @param owner the basic block to which these input and output stack map frames correspond. + */ + Frame(final Label owner) { + this.owner = owner; + } + + /** + * Sets this frame to the value of the given frame. + * + *

WARNING: after this method is called the two frames share the same data structures. It is + * recommended to discard the given frame to avoid unexpected side effects. + * + * @param frame The new frame value. + */ + final void copyFrom(final Frame frame) { + inputLocals = frame.inputLocals; + inputStack = frame.inputStack; + outputStackStart = 0; + outputLocals = frame.outputLocals; + outputStack = frame.outputStack; + outputStackTop = frame.outputStackTop; + initializationCount = frame.initializationCount; + initializations = frame.initializations; + } + + // ----------------------------------------------------------------------------------------------- + // Static methods to get abstract types from other type formats + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the abstract type corresponding to the given public API frame element type. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param type a frame element type described using the same format as in {@link + * MethodVisitor#visitFrame}, i.e. either {@link Opcodes#TOP}, {@link Opcodes#INTEGER}, {@link + * Opcodes#FLOAT}, {@link Opcodes#LONG}, {@link Opcodes#DOUBLE}, {@link Opcodes#NULL}, or + * {@link Opcodes#UNINITIALIZED_THIS}, or the internal name of a class, or a Label designating + * a NEW instruction (for uninitialized types). + * @return the abstract type corresponding to the given frame element type. + */ + static int getAbstractTypeFromApiFormat(final SymbolTable symbolTable, final Object type) { + if (type instanceof Integer) { + return CONSTANT_KIND | ((Integer) type).intValue(); + } else if (type instanceof String) { + String descriptor = Type.getObjectType((String) type).getDescriptor(); + return getAbstractTypeFromDescriptor(symbolTable, descriptor, 0); + } else { + return UNINITIALIZED_KIND + | symbolTable.addUninitializedType("", ((Label) type).bytecodeOffset); + } + } + + /** + * Returns the abstract type corresponding to the internal name of a class. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param internalName the internal name of a class. This must not be an array type + * descriptor. + * @return the abstract type value corresponding to the given internal name. + */ + static int getAbstractTypeFromInternalName( + final SymbolTable symbolTable, final String internalName) { + return REFERENCE_KIND | symbolTable.addType(internalName); + } + + /** + * Returns the abstract type corresponding to the given type descriptor. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param buffer a string ending with a type descriptor. + * @param offset the start offset of the type descriptor in buffer. + * @return the abstract type corresponding to the given type descriptor. + */ + private static int getAbstractTypeFromDescriptor( + final SymbolTable symbolTable, final String buffer, final int offset) { + String internalName; + switch (buffer.charAt(offset)) { + case 'V': + return 0; + case 'Z': + case 'C': + case 'B': + case 'S': + case 'I': + return INTEGER; + case 'F': + return FLOAT; + case 'J': + return LONG; + case 'D': + return DOUBLE; + case 'L': + internalName = buffer.substring(offset + 1, buffer.length() - 1); + return REFERENCE_KIND | symbolTable.addType(internalName); + case '[': + int elementDescriptorOffset = offset + 1; + while (buffer.charAt(elementDescriptorOffset) == '[') { + ++elementDescriptorOffset; + } + int typeValue; + switch (buffer.charAt(elementDescriptorOffset)) { + case 'Z': + typeValue = BOOLEAN; + break; + case 'C': + typeValue = CHAR; + break; + case 'B': + typeValue = BYTE; + break; + case 'S': + typeValue = SHORT; + break; + case 'I': + typeValue = INTEGER; + break; + case 'F': + typeValue = FLOAT; + break; + case 'J': + typeValue = LONG; + break; + case 'D': + typeValue = DOUBLE; + break; + case 'L': + internalName = buffer.substring(elementDescriptorOffset + 1, buffer.length() - 1); + typeValue = REFERENCE_KIND | symbolTable.addType(internalName); + break; + default: + throw new IllegalArgumentException(); + } + return ((elementDescriptorOffset - offset) << DIM_SHIFT) | typeValue; + default: + throw new IllegalArgumentException(); + } + } + + // ----------------------------------------------------------------------------------------------- + // Methods related to the input frame + // ----------------------------------------------------------------------------------------------- + + /** + * Sets the input frame from the given method description. This method is used to initialize the + * first frame of a method, which is implicit (i.e. not stored explicitly in the StackMapTable + * attribute). + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param access the method's access flags. + * @param descriptor the method descriptor. + * @param maxLocals the maximum number of local variables of the method. + */ + final void setInputFrameFromDescriptor( + final SymbolTable symbolTable, + final int access, + final String descriptor, + final int maxLocals) { + inputLocals = new int[maxLocals]; + inputStack = new int[0]; + int inputLocalIndex = 0; + if ((access & Opcodes.ACC_STATIC) == 0) { + if ((access & Constants.ACC_CONSTRUCTOR) == 0) { + inputLocals[inputLocalIndex++] = + REFERENCE_KIND | symbolTable.addType(symbolTable.getClassName()); + } else { + inputLocals[inputLocalIndex++] = UNINITIALIZED_THIS; + } + } + for (Type argumentType : Type.getArgumentTypes(descriptor)) { + int abstractType = + getAbstractTypeFromDescriptor(symbolTable, argumentType.getDescriptor(), 0); + inputLocals[inputLocalIndex++] = abstractType; + if (abstractType == LONG || abstractType == DOUBLE) { + inputLocals[inputLocalIndex++] = TOP; + } + } + while (inputLocalIndex < maxLocals) { + inputLocals[inputLocalIndex++] = TOP; + } + } + + /** + * Sets the input frame from the given public API frame description. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param numLocal the number of local variables. + * @param local the local variable types, described using the same format as in {@link + * MethodVisitor#visitFrame}. + * @param numStack the number of operand stack elements. + * @param stack the operand stack types, described using the same format as in {@link + * MethodVisitor#visitFrame}. + */ + final void setInputFrameFromApiFormat( + final SymbolTable symbolTable, + final int numLocal, + final Object[] local, + final int numStack, + final Object[] stack) { + int inputLocalIndex = 0; + for (int i = 0; i < numLocal; ++i) { + inputLocals[inputLocalIndex++] = getAbstractTypeFromApiFormat(symbolTable, local[i]); + if (local[i] == Opcodes.LONG || local[i] == Opcodes.DOUBLE) { + inputLocals[inputLocalIndex++] = TOP; + } + } + while (inputLocalIndex < inputLocals.length) { + inputLocals[inputLocalIndex++] = TOP; + } + int numStackTop = 0; + for (int i = 0; i < numStack; ++i) { + if (stack[i] == Opcodes.LONG || stack[i] == Opcodes.DOUBLE) { + ++numStackTop; + } + } + inputStack = new int[numStack + numStackTop]; + int inputStackIndex = 0; + for (int i = 0; i < numStack; ++i) { + inputStack[inputStackIndex++] = getAbstractTypeFromApiFormat(symbolTable, stack[i]); + if (stack[i] == Opcodes.LONG || stack[i] == Opcodes.DOUBLE) { + inputStack[inputStackIndex++] = TOP; + } + } + outputStackTop = 0; + initializationCount = 0; + } + + final int getInputStackSize() { + return inputStack.length; + } + + // ----------------------------------------------------------------------------------------------- + // Methods related to the output frame + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the abstract type stored at the given local variable index in the output frame. + * + * @param localIndex the index of the local variable whose value must be returned. + * @return the abstract type stored at the given local variable index in the output frame. + */ + private int getLocal(final int localIndex) { + if (outputLocals == null || localIndex >= outputLocals.length) { + // If this local has never been assigned in this basic block, it is still equal to its value + // in the input frame. + return LOCAL_KIND | localIndex; + } else { + int abstractType = outputLocals[localIndex]; + if (abstractType == 0) { + // If this local has never been assigned in this basic block, so it is still equal to its + // value in the input frame. + abstractType = outputLocals[localIndex] = LOCAL_KIND | localIndex; + } + return abstractType; + } + } + + /** + * Replaces the abstract type stored at the given local variable index in the output frame. + * + * @param localIndex the index of the output frame local variable that must be set. + * @param abstractType the value that must be set. + */ + private void setLocal(final int localIndex, final int abstractType) { + // Create and/or resize the output local variables array if necessary. + if (outputLocals == null) { + outputLocals = new int[10]; + } + int outputLocalsLength = outputLocals.length; + if (localIndex >= outputLocalsLength) { + int[] newOutputLocals = new int[Math.max(localIndex + 1, 2 * outputLocalsLength)]; + System.arraycopy(outputLocals, 0, newOutputLocals, 0, outputLocalsLength); + outputLocals = newOutputLocals; + } + // Set the local variable. + outputLocals[localIndex] = abstractType; + } + + /** + * Pushes the given abstract type on the output frame stack. + * + * @param abstractType an abstract type. + */ + private void push(final int abstractType) { + // Create and/or resize the output stack array if necessary. + if (outputStack == null) { + outputStack = new int[10]; + } + int outputStackLength = outputStack.length; + if (outputStackTop >= outputStackLength) { + int[] newOutputStack = new int[Math.max(outputStackTop + 1, 2 * outputStackLength)]; + System.arraycopy(outputStack, 0, newOutputStack, 0, outputStackLength); + outputStack = newOutputStack; + } + // Pushes the abstract type on the output stack. + outputStack[outputStackTop++] = abstractType; + // Updates the maximum size reached by the output stack, if needed (note that this size is + // relative to the input stack size, which is not known yet). + short outputStackSize = (short) (outputStackStart + outputStackTop); + if (outputStackSize > owner.outputStackMax) { + owner.outputStackMax = outputStackSize; + } + } + + /** + * Pushes the abstract type corresponding to the given descriptor on the output frame stack. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param descriptor a type or method descriptor (in which case its return type is pushed). + */ + private void push(final SymbolTable symbolTable, final String descriptor) { + int typeDescriptorOffset = + descriptor.charAt(0) == '(' ? Type.getReturnTypeOffset(descriptor) : 0; + int abstractType = getAbstractTypeFromDescriptor(symbolTable, descriptor, typeDescriptorOffset); + if (abstractType != 0) { + push(abstractType); + if (abstractType == LONG || abstractType == DOUBLE) { + push(TOP); + } + } + } + + /** + * Pops an abstract type from the output frame stack and returns its value. + * + * @return the abstract type that has been popped from the output frame stack. + */ + private int pop() { + if (outputStackTop > 0) { + return outputStack[--outputStackTop]; + } else { + // If the output frame stack is empty, pop from the input stack. + return STACK_KIND | -(--outputStackStart); + } + } + + /** + * Pops the given number of abstract types from the output frame stack. + * + * @param elements the number of abstract types that must be popped. + */ + private void pop(final int elements) { + if (outputStackTop >= elements) { + outputStackTop -= elements; + } else { + // If the number of elements to be popped is greater than the number of elements in the output + // stack, clear it, and pop the remaining elements from the input stack. + outputStackStart -= elements - outputStackTop; + outputStackTop = 0; + } + } + + /** + * Pops as many abstract types from the output frame stack as described by the given descriptor. + * + * @param descriptor a type or method descriptor (in which case its argument types are popped). + */ + private void pop(final String descriptor) { + char firstDescriptorChar = descriptor.charAt(0); + if (firstDescriptorChar == '(') { + pop((Type.getArgumentsAndReturnSizes(descriptor) >> 2) - 1); + } else if (firstDescriptorChar == 'J' || firstDescriptorChar == 'D') { + pop(2); + } else { + pop(1); + } + } + + // ----------------------------------------------------------------------------------------------- + // Methods to handle uninitialized types + // ----------------------------------------------------------------------------------------------- + + /** + * Adds an abstract type to the list of types on which a constructor is invoked in the basic + * block. + * + * @param abstractType an abstract type on a which a constructor is invoked. + */ + private void addInitializedType(final int abstractType) { + // Create and/or resize the initializations array if necessary. + if (initializations == null) { + initializations = new int[2]; + } + int initializationsLength = initializations.length; + if (initializationCount >= initializationsLength) { + int[] newInitializations = + new int[Math.max(initializationCount + 1, 2 * initializationsLength)]; + System.arraycopy(initializations, 0, newInitializations, 0, initializationsLength); + initializations = newInitializations; + } + // Store the abstract type. + initializations[initializationCount++] = abstractType; + } + + /** + * Returns the "initialized" abstract type corresponding to the given abstract type. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param abstractType an abstract type. + * @return the REFERENCE_KIND abstract type corresponding to abstractType if it is + * UNINITIALIZED_THIS or an UNINITIALIZED_KIND abstract type for one of the types on which a + * constructor is invoked in the basic block. Otherwise returns abstractType. + */ + private int getInitializedType(final SymbolTable symbolTable, final int abstractType) { + if (abstractType == UNINITIALIZED_THIS + || (abstractType & (DIM_MASK | KIND_MASK)) == UNINITIALIZED_KIND) { + for (int i = 0; i < initializationCount; ++i) { + int initializedType = initializations[i]; + int dim = initializedType & DIM_MASK; + int kind = initializedType & KIND_MASK; + int value = initializedType & VALUE_MASK; + if (kind == LOCAL_KIND) { + initializedType = dim + inputLocals[value]; + } else if (kind == STACK_KIND) { + initializedType = dim + inputStack[inputStack.length - value]; + } + if (abstractType == initializedType) { + if (abstractType == UNINITIALIZED_THIS) { + return REFERENCE_KIND | symbolTable.addType(symbolTable.getClassName()); + } else { + return REFERENCE_KIND + | symbolTable.addType(symbolTable.getType(abstractType & VALUE_MASK).value); + } + } + } + } + return abstractType; + } + + // ----------------------------------------------------------------------------------------------- + // Main method, to simulate the execution of each instruction on the output frame + // ----------------------------------------------------------------------------------------------- + + /** + * Simulates the action of the given instruction on the output stack frame. + * + * @param opcode the opcode of the instruction. + * @param arg the numeric operand of the instruction, if any. + * @param argSymbol the Symbol operand of the instruction, if any. + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + */ + void execute( + final int opcode, final int arg, final Symbol argSymbol, final SymbolTable symbolTable) { + // Abstract types popped from the stack or read from local variables. + int abstractType1; + int abstractType2; + int abstractType3; + int abstractType4; + switch (opcode) { + case Opcodes.NOP: + case Opcodes.INEG: + case Opcodes.LNEG: + case Opcodes.FNEG: + case Opcodes.DNEG: + case Opcodes.I2B: + case Opcodes.I2C: + case Opcodes.I2S: + case Opcodes.GOTO: + case Opcodes.RETURN: + break; + case Opcodes.ACONST_NULL: + push(NULL); + break; + case Opcodes.ICONST_M1: + case Opcodes.ICONST_0: + case Opcodes.ICONST_1: + case Opcodes.ICONST_2: + case Opcodes.ICONST_3: + case Opcodes.ICONST_4: + case Opcodes.ICONST_5: + case Opcodes.BIPUSH: + case Opcodes.SIPUSH: + case Opcodes.ILOAD: + push(INTEGER); + break; + case Opcodes.LCONST_0: + case Opcodes.LCONST_1: + case Opcodes.LLOAD: + push(LONG); + push(TOP); + break; + case Opcodes.FCONST_0: + case Opcodes.FCONST_1: + case Opcodes.FCONST_2: + case Opcodes.FLOAD: + push(FLOAT); + break; + case Opcodes.DCONST_0: + case Opcodes.DCONST_1: + case Opcodes.DLOAD: + push(DOUBLE); + push(TOP); + break; + case Opcodes.LDC: + switch (argSymbol.tag) { + case Symbol.CONSTANT_INTEGER_TAG: + push(INTEGER); + break; + case Symbol.CONSTANT_LONG_TAG: + push(LONG); + push(TOP); + break; + case Symbol.CONSTANT_FLOAT_TAG: + push(FLOAT); + break; + case Symbol.CONSTANT_DOUBLE_TAG: + push(DOUBLE); + push(TOP); + break; + case Symbol.CONSTANT_CLASS_TAG: + push(REFERENCE_KIND | symbolTable.addType("java/lang/Class")); + break; + case Symbol.CONSTANT_STRING_TAG: + push(REFERENCE_KIND | symbolTable.addType("java/lang/String")); + break; + case Symbol.CONSTANT_METHOD_TYPE_TAG: + push(REFERENCE_KIND | symbolTable.addType("java/lang/invoke/MethodType")); + break; + case Symbol.CONSTANT_METHOD_HANDLE_TAG: + push(REFERENCE_KIND | symbolTable.addType("java/lang/invoke/MethodHandle")); + break; + case Symbol.CONSTANT_DYNAMIC_TAG: + push(symbolTable, argSymbol.value); + break; + default: + throw new AssertionError(); + } + break; + case Opcodes.ALOAD: + push(getLocal(arg)); + break; + case Opcodes.LALOAD: + case Opcodes.D2L: + pop(2); + push(LONG); + push(TOP); + break; + case Opcodes.DALOAD: + case Opcodes.L2D: + pop(2); + push(DOUBLE); + push(TOP); + break; + case Opcodes.AALOAD: + pop(1); + abstractType1 = pop(); + push(abstractType1 == NULL ? abstractType1 : ELEMENT_OF + abstractType1); + break; + case Opcodes.ISTORE: + case Opcodes.FSTORE: + case Opcodes.ASTORE: + abstractType1 = pop(); + setLocal(arg, abstractType1); + if (arg > 0) { + int previousLocalType = getLocal(arg - 1); + if (previousLocalType == LONG || previousLocalType == DOUBLE) { + setLocal(arg - 1, TOP); + } else if ((previousLocalType & KIND_MASK) == LOCAL_KIND + || (previousLocalType & KIND_MASK) == STACK_KIND) { + // The type of the previous local variable is not known yet, but if it later appears + // to be LONG or DOUBLE, we should then use TOP instead. + setLocal(arg - 1, previousLocalType | TOP_IF_LONG_OR_DOUBLE_FLAG); + } + } + break; + case Opcodes.LSTORE: + case Opcodes.DSTORE: + pop(1); + abstractType1 = pop(); + setLocal(arg, abstractType1); + setLocal(arg + 1, TOP); + if (arg > 0) { + int previousLocalType = getLocal(arg - 1); + if (previousLocalType == LONG || previousLocalType == DOUBLE) { + setLocal(arg - 1, TOP); + } else if ((previousLocalType & KIND_MASK) == LOCAL_KIND + || (previousLocalType & KIND_MASK) == STACK_KIND) { + // The type of the previous local variable is not known yet, but if it later appears + // to be LONG or DOUBLE, we should then use TOP instead. + setLocal(arg - 1, previousLocalType | TOP_IF_LONG_OR_DOUBLE_FLAG); + } + } + break; + case Opcodes.IASTORE: + case Opcodes.BASTORE: + case Opcodes.CASTORE: + case Opcodes.SASTORE: + case Opcodes.FASTORE: + case Opcodes.AASTORE: + pop(3); + break; + case Opcodes.LASTORE: + case Opcodes.DASTORE: + pop(4); + break; + case Opcodes.POP: + case Opcodes.IFEQ: + case Opcodes.IFNE: + case Opcodes.IFLT: + case Opcodes.IFGE: + case Opcodes.IFGT: + case Opcodes.IFLE: + case Opcodes.IRETURN: + case Opcodes.FRETURN: + case Opcodes.ARETURN: + case Opcodes.TABLESWITCH: + case Opcodes.LOOKUPSWITCH: + case Opcodes.ATHROW: + case Opcodes.MONITORENTER: + case Opcodes.MONITOREXIT: + case Opcodes.IFNULL: + case Opcodes.IFNONNULL: + pop(1); + break; + case Opcodes.POP2: + case Opcodes.IF_ICMPEQ: + case Opcodes.IF_ICMPNE: + case Opcodes.IF_ICMPLT: + case Opcodes.IF_ICMPGE: + case Opcodes.IF_ICMPGT: + case Opcodes.IF_ICMPLE: + case Opcodes.IF_ACMPEQ: + case Opcodes.IF_ACMPNE: + case Opcodes.LRETURN: + case Opcodes.DRETURN: + pop(2); + break; + case Opcodes.DUP: + abstractType1 = pop(); + push(abstractType1); + push(abstractType1); + break; + case Opcodes.DUP_X1: + abstractType1 = pop(); + abstractType2 = pop(); + push(abstractType1); + push(abstractType2); + push(abstractType1); + break; + case Opcodes.DUP_X2: + abstractType1 = pop(); + abstractType2 = pop(); + abstractType3 = pop(); + push(abstractType1); + push(abstractType3); + push(abstractType2); + push(abstractType1); + break; + case Opcodes.DUP2: + abstractType1 = pop(); + abstractType2 = pop(); + push(abstractType2); + push(abstractType1); + push(abstractType2); + push(abstractType1); + break; + case Opcodes.DUP2_X1: + abstractType1 = pop(); + abstractType2 = pop(); + abstractType3 = pop(); + push(abstractType2); + push(abstractType1); + push(abstractType3); + push(abstractType2); + push(abstractType1); + break; + case Opcodes.DUP2_X2: + abstractType1 = pop(); + abstractType2 = pop(); + abstractType3 = pop(); + abstractType4 = pop(); + push(abstractType2); + push(abstractType1); + push(abstractType4); + push(abstractType3); + push(abstractType2); + push(abstractType1); + break; + case Opcodes.SWAP: + abstractType1 = pop(); + abstractType2 = pop(); + push(abstractType1); + push(abstractType2); + break; + case Opcodes.IALOAD: + case Opcodes.BALOAD: + case Opcodes.CALOAD: + case Opcodes.SALOAD: + case Opcodes.IADD: + case Opcodes.ISUB: + case Opcodes.IMUL: + case Opcodes.IDIV: + case Opcodes.IREM: + case Opcodes.IAND: + case Opcodes.IOR: + case Opcodes.IXOR: + case Opcodes.ISHL: + case Opcodes.ISHR: + case Opcodes.IUSHR: + case Opcodes.L2I: + case Opcodes.D2I: + case Opcodes.FCMPL: + case Opcodes.FCMPG: + pop(2); + push(INTEGER); + break; + case Opcodes.LADD: + case Opcodes.LSUB: + case Opcodes.LMUL: + case Opcodes.LDIV: + case Opcodes.LREM: + case Opcodes.LAND: + case Opcodes.LOR: + case Opcodes.LXOR: + pop(4); + push(LONG); + push(TOP); + break; + case Opcodes.FALOAD: + case Opcodes.FADD: + case Opcodes.FSUB: + case Opcodes.FMUL: + case Opcodes.FDIV: + case Opcodes.FREM: + case Opcodes.L2F: + case Opcodes.D2F: + pop(2); + push(FLOAT); + break; + case Opcodes.DADD: + case Opcodes.DSUB: + case Opcodes.DMUL: + case Opcodes.DDIV: + case Opcodes.DREM: + pop(4); + push(DOUBLE); + push(TOP); + break; + case Opcodes.LSHL: + case Opcodes.LSHR: + case Opcodes.LUSHR: + pop(3); + push(LONG); + push(TOP); + break; + case Opcodes.IINC: + setLocal(arg, INTEGER); + break; + case Opcodes.I2L: + case Opcodes.F2L: + pop(1); + push(LONG); + push(TOP); + break; + case Opcodes.I2F: + pop(1); + push(FLOAT); + break; + case Opcodes.I2D: + case Opcodes.F2D: + pop(1); + push(DOUBLE); + push(TOP); + break; + case Opcodes.F2I: + case Opcodes.ARRAYLENGTH: + case Opcodes.INSTANCEOF: + pop(1); + push(INTEGER); + break; + case Opcodes.LCMP: + case Opcodes.DCMPL: + case Opcodes.DCMPG: + pop(4); + push(INTEGER); + break; + case Opcodes.JSR: + case Opcodes.RET: + throw new IllegalArgumentException("JSR/RET are not supported with computeFrames option"); + case Opcodes.GETSTATIC: + push(symbolTable, argSymbol.value); + break; + case Opcodes.PUTSTATIC: + pop(argSymbol.value); + break; + case Opcodes.GETFIELD: + pop(1); + push(symbolTable, argSymbol.value); + break; + case Opcodes.PUTFIELD: + pop(argSymbol.value); + pop(); + break; + case Opcodes.INVOKEVIRTUAL: + case Opcodes.INVOKESPECIAL: + case Opcodes.INVOKESTATIC: + case Opcodes.INVOKEINTERFACE: + pop(argSymbol.value); + if (opcode != Opcodes.INVOKESTATIC) { + abstractType1 = pop(); + if (opcode == Opcodes.INVOKESPECIAL && argSymbol.name.charAt(0) == '<') { + addInitializedType(abstractType1); + } + } + push(symbolTable, argSymbol.value); + break; + case Opcodes.INVOKEDYNAMIC: + pop(argSymbol.value); + push(symbolTable, argSymbol.value); + break; + case Opcodes.NEW: + push(UNINITIALIZED_KIND | symbolTable.addUninitializedType(argSymbol.value, arg)); + break; + case Opcodes.NEWARRAY: + pop(); + switch (arg) { + case Opcodes.T_BOOLEAN: + push(ARRAY_OF | BOOLEAN); + break; + case Opcodes.T_CHAR: + push(ARRAY_OF | CHAR); + break; + case Opcodes.T_BYTE: + push(ARRAY_OF | BYTE); + break; + case Opcodes.T_SHORT: + push(ARRAY_OF | SHORT); + break; + case Opcodes.T_INT: + push(ARRAY_OF | INTEGER); + break; + case Opcodes.T_FLOAT: + push(ARRAY_OF | FLOAT); + break; + case Opcodes.T_DOUBLE: + push(ARRAY_OF | DOUBLE); + break; + case Opcodes.T_LONG: + push(ARRAY_OF | LONG); + break; + default: + throw new IllegalArgumentException(); + } + break; + case Opcodes.ANEWARRAY: + String arrayElementType = argSymbol.value; + pop(); + if (arrayElementType.charAt(0) == '[') { + push(symbolTable, '[' + arrayElementType); + } else { + push(ARRAY_OF | REFERENCE_KIND | symbolTable.addType(arrayElementType)); + } + break; + case Opcodes.CHECKCAST: + String castType = argSymbol.value; + pop(); + if (castType.charAt(0) == '[') { + push(symbolTable, castType); + } else { + push(REFERENCE_KIND | symbolTable.addType(castType)); + } + break; + case Opcodes.MULTIANEWARRAY: + pop(arg); + push(symbolTable, argSymbol.value); + break; + default: + throw new IllegalArgumentException(); + } + } + + // ----------------------------------------------------------------------------------------------- + // Frame merging methods, used in the second step of the stack map frame computation algorithm + // ----------------------------------------------------------------------------------------------- + + /** + * Computes the concrete output type corresponding to a given abstract output type. + * + * @param abstractOutputType an abstract output type. + * @param numStack the size of the input stack, used to resolve abstract output types of + * STACK_KIND kind. + * @return the concrete output type corresponding to 'abstractOutputType'. + */ + private int getConcreteOutputType(final int abstractOutputType, final int numStack) { + int dim = abstractOutputType & DIM_MASK; + int kind = abstractOutputType & KIND_MASK; + if (kind == LOCAL_KIND) { + // By definition, a LOCAL_KIND type designates the concrete type of a local variable at + // the beginning of the basic block corresponding to this frame (which is known when + // this method is called, but was not when the abstract type was computed). + int concreteOutputType = dim + inputLocals[abstractOutputType & VALUE_MASK]; + if ((abstractOutputType & TOP_IF_LONG_OR_DOUBLE_FLAG) != 0 + && (concreteOutputType == LONG || concreteOutputType == DOUBLE)) { + concreteOutputType = TOP; + } + return concreteOutputType; + } else if (kind == STACK_KIND) { + // By definition, a STACK_KIND type designates the concrete type of a local variable at + // the beginning of the basic block corresponding to this frame (which is known when + // this method is called, but was not when the abstract type was computed). + int concreteOutputType = dim + inputStack[numStack - (abstractOutputType & VALUE_MASK)]; + if ((abstractOutputType & TOP_IF_LONG_OR_DOUBLE_FLAG) != 0 + && (concreteOutputType == LONG || concreteOutputType == DOUBLE)) { + concreteOutputType = TOP; + } + return concreteOutputType; + } else { + return abstractOutputType; + } + } + + /** + * Merges the input frame of the given {@link Frame} with the input and output frames of this + * {@link Frame}. Returns {@literal true} if the given frame has been changed by this operation + * (the input and output frames of this {@link Frame} are never changed). + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param dstFrame the {@link Frame} whose input frame must be updated. This should be the frame + * of a successor, in the control flow graph, of the basic block corresponding to this frame. + * @param catchTypeIndex if 'frame' corresponds to an exception handler basic block, the type + * table index of the caught exception type, otherwise 0. + * @return {@literal true} if the input frame of 'frame' has been changed by this operation. + */ + final boolean merge( + final SymbolTable symbolTable, final Frame dstFrame, final int catchTypeIndex) { + boolean frameChanged = false; + + // Compute the concrete types of the local variables at the end of the basic block corresponding + // to this frame, by resolving its abstract output types, and merge these concrete types with + // those of the local variables in the input frame of dstFrame. + int numLocal = inputLocals.length; + int numStack = inputStack.length; + if (dstFrame.inputLocals == null) { + dstFrame.inputLocals = new int[numLocal]; + frameChanged = true; + } + for (int i = 0; i < numLocal; ++i) { + int concreteOutputType; + if (outputLocals != null && i < outputLocals.length) { + int abstractOutputType = outputLocals[i]; + if (abstractOutputType == 0) { + // If the local variable has never been assigned in this basic block, it is equal to its + // value at the beginning of the block. + concreteOutputType = inputLocals[i]; + } else { + concreteOutputType = getConcreteOutputType(abstractOutputType, numStack); + } + } else { + // If the local variable has never been assigned in this basic block, it is equal to its + // value at the beginning of the block. + concreteOutputType = inputLocals[i]; + } + // concreteOutputType might be an uninitialized type from the input locals or from the input + // stack. However, if a constructor has been called for this class type in the basic block, + // then this type is no longer uninitialized at the end of basic block. + if (initializations != null) { + concreteOutputType = getInitializedType(symbolTable, concreteOutputType); + } + frameChanged |= merge(symbolTable, concreteOutputType, dstFrame.inputLocals, i); + } + + // If dstFrame is an exception handler block, it can be reached from any instruction of the + // basic block corresponding to this frame, in particular from the first one. Therefore, the + // input locals of dstFrame should be compatible (i.e. merged) with the input locals of this + // frame (and the input stack of dstFrame should be compatible, i.e. merged, with a one + // element stack containing the caught exception type). + if (catchTypeIndex > 0) { + for (int i = 0; i < numLocal; ++i) { + frameChanged |= merge(symbolTable, inputLocals[i], dstFrame.inputLocals, i); + } + if (dstFrame.inputStack == null) { + dstFrame.inputStack = new int[1]; + frameChanged = true; + } + frameChanged |= merge(symbolTable, catchTypeIndex, dstFrame.inputStack, 0); + return frameChanged; + } + + // Compute the concrete types of the stack operands at the end of the basic block corresponding + // to this frame, by resolving its abstract output types, and merge these concrete types with + // those of the stack operands in the input frame of dstFrame. + int numInputStack = inputStack.length + outputStackStart; + if (dstFrame.inputStack == null) { + dstFrame.inputStack = new int[numInputStack + outputStackTop]; + frameChanged = true; + } + // First, do this for the stack operands that have not been popped in the basic block + // corresponding to this frame, and which are therefore equal to their value in the input + // frame (except for uninitialized types, which may have been initialized). + for (int i = 0; i < numInputStack; ++i) { + int concreteOutputType = inputStack[i]; + if (initializations != null) { + concreteOutputType = getInitializedType(symbolTable, concreteOutputType); + } + frameChanged |= merge(symbolTable, concreteOutputType, dstFrame.inputStack, i); + } + // Then, do this for the stack operands that have pushed in the basic block (this code is the + // same as the one above for local variables). + for (int i = 0; i < outputStackTop; ++i) { + int abstractOutputType = outputStack[i]; + int concreteOutputType = getConcreteOutputType(abstractOutputType, numStack); + if (initializations != null) { + concreteOutputType = getInitializedType(symbolTable, concreteOutputType); + } + frameChanged |= + merge(symbolTable, concreteOutputType, dstFrame.inputStack, numInputStack + i); + } + return frameChanged; + } + + /** + * Merges the type at the given index in the given abstract type array with the given type. + * Returns {@literal true} if the type array has been modified by this operation. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param sourceType the abstract type with which the abstract type array element must be merged. + * This type should be of {@link #CONSTANT_KIND}, {@link #REFERENCE_KIND} or {@link + * #UNINITIALIZED_KIND} kind, with positive or {@literal null} array dimensions. + * @param dstTypes an array of abstract types. These types should be of {@link #CONSTANT_KIND}, + * {@link #REFERENCE_KIND} or {@link #UNINITIALIZED_KIND} kind, with positive or {@literal + * null} array dimensions. + * @param dstIndex the index of the type that must be merged in dstTypes. + * @return {@literal true} if the type array has been modified by this operation. + */ + private static boolean merge( + final SymbolTable symbolTable, + final int sourceType, + final int[] dstTypes, + final int dstIndex) { + int dstType = dstTypes[dstIndex]; + if (dstType == sourceType) { + // If the types are equal, merge(sourceType, dstType) = dstType, so there is no change. + return false; + } + int srcType = sourceType; + if ((sourceType & ~DIM_MASK) == NULL) { + if (dstType == NULL) { + return false; + } + srcType = NULL; + } + if (dstType == 0) { + // If dstTypes[dstIndex] has never been assigned, merge(srcType, dstType) = srcType. + dstTypes[dstIndex] = srcType; + return true; + } + int mergedType; + if ((dstType & DIM_MASK) != 0 || (dstType & KIND_MASK) == REFERENCE_KIND) { + // If dstType is a reference type of any array dimension. + if (srcType == NULL) { + // If srcType is the NULL type, merge(srcType, dstType) = dstType, so there is no change. + return false; + } else if ((srcType & (DIM_MASK | KIND_MASK)) == (dstType & (DIM_MASK | KIND_MASK))) { + // If srcType has the same array dimension and the same kind as dstType. + if ((dstType & KIND_MASK) == REFERENCE_KIND) { + // If srcType and dstType are reference types with the same array dimension, + // merge(srcType, dstType) = dim(srcType) | common super class of srcType and dstType. + mergedType = + (srcType & DIM_MASK) + | REFERENCE_KIND + | symbolTable.addMergedType(srcType & VALUE_MASK, dstType & VALUE_MASK); + } else { + // If srcType and dstType are array types of equal dimension but different element types, + // merge(srcType, dstType) = dim(srcType) - 1 | java/lang/Object. + int mergedDim = ELEMENT_OF + (srcType & DIM_MASK); + mergedType = mergedDim | REFERENCE_KIND | symbolTable.addType("java/lang/Object"); + } + } else if ((srcType & DIM_MASK) != 0 || (srcType & KIND_MASK) == REFERENCE_KIND) { + // If srcType is any other reference or array type, + // merge(srcType, dstType) = min(srcDdim, dstDim) | java/lang/Object + // where srcDim is the array dimension of srcType, minus 1 if srcType is an array type + // with a non reference element type (and similarly for dstDim). + int srcDim = srcType & DIM_MASK; + if (srcDim != 0 && (srcType & KIND_MASK) != REFERENCE_KIND) { + srcDim = ELEMENT_OF + srcDim; + } + int dstDim = dstType & DIM_MASK; + if (dstDim != 0 && (dstType & KIND_MASK) != REFERENCE_KIND) { + dstDim = ELEMENT_OF + dstDim; + } + mergedType = + Math.min(srcDim, dstDim) | REFERENCE_KIND | symbolTable.addType("java/lang/Object"); + } else { + // If srcType is any other type, merge(srcType, dstType) = TOP. + mergedType = TOP; + } + } else if (dstType == NULL) { + // If dstType is the NULL type, merge(srcType, dstType) = srcType, or TOP if srcType is not a + // an array type or a reference type. + mergedType = + (srcType & DIM_MASK) != 0 || (srcType & KIND_MASK) == REFERENCE_KIND ? srcType : TOP; + } else { + // If dstType is any other type, merge(srcType, dstType) = TOP whatever srcType. + mergedType = TOP; + } + if (mergedType != dstType) { + dstTypes[dstIndex] = mergedType; + return true; + } + return false; + } + + // ----------------------------------------------------------------------------------------------- + // Frame output methods, to generate StackMapFrame attributes + // ----------------------------------------------------------------------------------------------- + + /** + * Makes the given {@link MethodWriter} visit the input frame of this {@link Frame}. The visit is + * done with the {@link MethodWriter#visitFrameStart}, {@link MethodWriter#visitAbstractType} and + * {@link MethodWriter#visitFrameEnd} methods. + * + * @param methodWriter the {@link MethodWriter} that should visit the input frame of this {@link + * Frame}. + */ + final void accept(final MethodWriter methodWriter) { + // Compute the number of locals, ignoring TOP types that are just after a LONG or a DOUBLE, and + // all trailing TOP types. + int[] localTypes = inputLocals; + int numLocal = 0; + int numTrailingTop = 0; + int i = 0; + while (i < localTypes.length) { + int localType = localTypes[i]; + i += (localType == LONG || localType == DOUBLE) ? 2 : 1; + if (localType == TOP) { + numTrailingTop++; + } else { + numLocal += numTrailingTop + 1; + numTrailingTop = 0; + } + } + // Compute the stack size, ignoring TOP types that are just after a LONG or a DOUBLE. + int[] stackTypes = inputStack; + int numStack = 0; + i = 0; + while (i < stackTypes.length) { + int stackType = stackTypes[i]; + i += (stackType == LONG || stackType == DOUBLE) ? 2 : 1; + numStack++; + } + // Visit the frame and its content. + int frameIndex = methodWriter.visitFrameStart(owner.bytecodeOffset, numLocal, numStack); + i = 0; + while (numLocal-- > 0) { + int localType = localTypes[i]; + i += (localType == LONG || localType == DOUBLE) ? 2 : 1; + methodWriter.visitAbstractType(frameIndex++, localType); + } + i = 0; + while (numStack-- > 0) { + int stackType = stackTypes[i]; + i += (stackType == LONG || stackType == DOUBLE) ? 2 : 1; + methodWriter.visitAbstractType(frameIndex++, stackType); + } + methodWriter.visitFrameEnd(); + } + + /** + * Put the given abstract type in the given ByteVector, using the JVMS verification_type_info + * format used in StackMapTable attributes. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param abstractType an abstract type, restricted to {@link Frame#CONSTANT_KIND}, {@link + * Frame#REFERENCE_KIND} or {@link Frame#UNINITIALIZED_KIND} types. + * @param output where the abstract type must be put. + * @see JVMS + * 4.7.4 + */ + static void putAbstractType( + final SymbolTable symbolTable, final int abstractType, final ByteVector output) { + int arrayDimensions = (abstractType & Frame.DIM_MASK) >> DIM_SHIFT; + if (arrayDimensions == 0) { + int typeValue = abstractType & VALUE_MASK; + switch (abstractType & KIND_MASK) { + case CONSTANT_KIND: + output.putByte(typeValue); + break; + case REFERENCE_KIND: + output + .putByte(ITEM_OBJECT) + .putShort(symbolTable.addConstantClass(symbolTable.getType(typeValue).value).index); + break; + case UNINITIALIZED_KIND: + output.putByte(ITEM_UNINITIALIZED).putShort((int) symbolTable.getType(typeValue).data); + break; + default: + throw new AssertionError(); + } + } else { + // Case of an array type, we need to build its descriptor first. + StringBuilder typeDescriptor = new StringBuilder(); + while (arrayDimensions-- > 0) { + typeDescriptor.append('['); + } + if ((abstractType & KIND_MASK) == REFERENCE_KIND) { + typeDescriptor + .append('L') + .append(symbolTable.getType(abstractType & VALUE_MASK).value) + .append(';'); + } else { + switch (abstractType & VALUE_MASK) { + case Frame.ITEM_ASM_BOOLEAN: + typeDescriptor.append('Z'); + break; + case Frame.ITEM_ASM_BYTE: + typeDescriptor.append('B'); + break; + case Frame.ITEM_ASM_CHAR: + typeDescriptor.append('C'); + break; + case Frame.ITEM_ASM_SHORT: + typeDescriptor.append('S'); + break; + case Frame.ITEM_INTEGER: + typeDescriptor.append('I'); + break; + case Frame.ITEM_FLOAT: + typeDescriptor.append('F'); + break; + case Frame.ITEM_LONG: + typeDescriptor.append('J'); + break; + case Frame.ITEM_DOUBLE: + typeDescriptor.append('D'); + break; + default: + throw new AssertionError(); + } + } + output + .putByte(ITEM_OBJECT) + .putShort(symbolTable.addConstantClass(typeDescriptor.toString()).index); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Handle.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Handle.java new file mode 100644 index 0000000..75c9261 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Handle.java @@ -0,0 +1,168 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** A reference to a field or a method. @Author Remi Forax @Author Eric Bruneton */ +public final class Handle { + + /** + * The kind of field or method designated by this Handle. Should be {@link Opcodes#H_GETFIELD}, + * {@link Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link + * Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, + * {@link Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. + */ + private final int tag; + + /** The internal name of the class that owns the field or method designated by this handle. */ + private final String owner; + + /** The name of the field or method designated by this handle. */ + private final String name; + + /** The descriptor of the field or method designated by this handle. */ + private final String descriptor; + + /** Whether the owner is an processer or not. */ + private final boolean isInterface; + + /** + * Constructs a new field or method handle. + * + * @param tag the kind of field or method designated by this Handle. Must be {@link + * Opcodes#H_GETFIELD}, {@link Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link + * Opcodes#H_PUTSTATIC}, {@link Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, + * {@link Opcodes#H_INVOKESPECIAL}, {@link Opcodes#H_NEWINVOKESPECIAL} or {@link + * Opcodes#H_INVOKEINTERFACE}. + * @param owner the internal name of the class that owns the field or method designated by this + * handle. + * @param name the name of the field or method designated by this handle. + * @param descriptor the descriptor of the field or method designated by this handle. + * @deprecated this constructor has been superseded by {@link #Handle(int, String, String, String, + * boolean)}. + */ + @Deprecated + public Handle(final int tag, final String owner, final String name, final String descriptor) { + this(tag, owner, name, descriptor, tag == Opcodes.H_INVOKEINTERFACE); + } + + /** + * Constructs a new field or method handle. + * + * @param tag the kind of field or method designated by this Handle. Must be {@link + * Opcodes#H_GETFIELD}, {@link Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link + * Opcodes#H_PUTSTATIC}, {@link Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, + * {@link Opcodes#H_INVOKESPECIAL}, {@link Opcodes#H_NEWINVOKESPECIAL} or {@link + * Opcodes#H_INVOKEINTERFACE}. + * @param owner the internal name of the class that owns the field or method designated by this + * handle. + * @param name the name of the field or method designated by this handle. + * @param descriptor the descriptor of the field or method designated by this handle. + * @param isInterface whether the owner is an processer or not. + */ + public Handle( + final int tag, + final String owner, + final String name, + final String descriptor, + final boolean isInterface) { + this.tag = tag; + this.owner = owner; + this.name = name; + this.descriptor = descriptor; + this.isInterface = isInterface; + } + + /** + * Returns the kind of field or method designated by this handle. + * + * @return {@link Opcodes#H_GETFIELD}, {@link Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, + * {@link Opcodes#H_PUTSTATIC}, {@link Opcodes#H_INVOKEVIRTUAL}, {@link + * Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, {@link + * Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. + */ + public int getTag() { + return tag; + } + + /** + * Returns the internal name of the class that owns the field or method designated by this handle. + * + * @return the internal name of the class that owns the field or method designated by this handle. + */ + public String getOwner() { + return owner; + } + + /** + * Returns the name of the field or method designated by this handle. + * + * @return the name of the field or method designated by this handle. + */ + public String getName() { + return name; + } + + /** + * Returns the descriptor of the field or method designated by this handle. + * + * @return the descriptor of the field or method designated by this handle. + */ + public String getDesc() { + return descriptor; + } + + /** + * Returns true if the owner of the field or method designated by this handle is an processer. + * + * @return true if the owner of the field or method designated by this handle is an processer. + */ + public boolean isInterface() { + return isInterface; + } + + @Override + public boolean equals(final Object object) { + if (object == this) { + return true; + } + if (!(object instanceof Handle)) { + return false; + } + Handle handle = (Handle) object; + return tag == handle.tag + && isInterface == handle.isInterface + && owner.equals(handle.owner) + && name.equals(handle.name) + && descriptor.equals(handle.descriptor); + } + + @Override + public int hashCode() { + return tag + + (isInterface ? 64 : 0) + + owner.hashCode() * name.hashCode() * descriptor.hashCode(); + } + + /** + * Returns the textual representation of this handle. The textual representation is: + * + *

    + *
  • for a reference to a class: owner "." name descriptor " (" tag ")", + *
  • for a reference to an processer: owner "." name descriptor " (" tag " itf)". + *
+ */ + @Override + public String toString() { + return owner + '.' + name + descriptor + " (" + tag + (isInterface ? " itf" : "") + ')'; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Handler.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Handler.java new file mode 100644 index 0000000..a9c04e7 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Handler.java @@ -0,0 +1,182 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * Information about an exception handler. Corresponds to an element of the exception_table array of + * a Code attribute, as defined in the Java Virtual Machine Specification (JVMS). Handler instances + * can be chained together, with their {@link #nextHandler} field, to describe a full JVMS + * exception_table array. @Author Eric Bruneton + * + * @see JVMS + * 4.7.3 + */ +final class Handler { + + /** + * The start_pc field of this JVMS exception_table entry. Corresponds to the beginning of the + * exception handler's scope (inclusive). + */ + final Label startPc; + + /** + * The end_pc field of this JVMS exception_table entry. Corresponds to the end of the exception + * handler's scope (exclusive). + */ + final Label endPc; + + /** + * The handler_pc field of this JVMS exception_table entry. Corresponding to the beginning of the + * exception handler's code. + */ + final Label handlerPc; + + /** + * The catch_type field of this JVMS exception_table entry. This is the constant pool index of the + * internal name of the type of exceptions handled by this handler, or 0 to catch any exceptions. + */ + final int catchType; + + /** + * The internal name of the type of exceptions handled by this handler, or {@literal null} to + * catch any exceptions. + */ + final String catchTypeDescriptor; + + /** The next exception handler. */ + Handler nextHandler; + + /** + * Constructs a new Handler. + * + * @param startPc the start_pc field of this JVMS exception_table entry. + * @param endPc the end_pc field of this JVMS exception_table entry. + * @param handlerPc the handler_pc field of this JVMS exception_table entry. + * @param catchType The catch_type field of this JVMS exception_table entry. + * @param catchTypeDescriptor The internal name of the type of exceptions handled by this handler, + * or {@literal null} to catch any exceptions. + */ + Handler( + final Label startPc, + final Label endPc, + final Label handlerPc, + final int catchType, + final String catchTypeDescriptor) { + this.startPc = startPc; + this.endPc = endPc; + this.handlerPc = handlerPc; + this.catchType = catchType; + this.catchTypeDescriptor = catchTypeDescriptor; + } + + /** + * Constructs a new Handler from the given one, with a different scope. + * + * @param handler an existing Handler. + * @param startPc the start_pc field of this JVMS exception_table entry. + * @param endPc the end_pc field of this JVMS exception_table entry. + */ + Handler(final Handler handler, final Label startPc, final Label endPc) { + this(startPc, endPc, handler.handlerPc, handler.catchType, handler.catchTypeDescriptor); + this.nextHandler = handler.nextHandler; + } + + /** + * Removes the range between start and end from the Handler list that begins with the given + * element. + * + * @param firstHandler the beginning of a Handler list. May be {@literal null}. + * @param start the start of the range to be removed. + * @param end the end of the range to be removed. Maybe {@literal null}. + * @return the exception handler list with the start-end range removed. + */ + static Handler removeRange(final Handler firstHandler, final Label start, final Label end) { + if (firstHandler == null) { + return null; + } else { + firstHandler.nextHandler = removeRange(firstHandler.nextHandler, start, end); + } + int handlerStart = firstHandler.startPc.bytecodeOffset; + int handlerEnd = firstHandler.endPc.bytecodeOffset; + int rangeStart = start.bytecodeOffset; + int rangeEnd = end == null ? Integer.MAX_VALUE : end.bytecodeOffset; + // Return early if [handlerStart,handlerEnd[ and [rangeStart,rangeEnd[ don't intersect. + if (rangeStart >= handlerEnd || rangeEnd <= handlerStart) { + return firstHandler; + } + if (rangeStart <= handlerStart) { + if (rangeEnd >= handlerEnd) { + // If [handlerStart,handlerEnd[ is included in [rangeStart,rangeEnd[, remove firstHandler. + return firstHandler.nextHandler; + } else { + // [handlerStart,handlerEnd[ - [rangeStart,rangeEnd[ = [rangeEnd,handlerEnd[ + return new Handler(firstHandler, end, firstHandler.endPc); + } + } else if (rangeEnd >= handlerEnd) { + // [handlerStart,handlerEnd[ - [rangeStart,rangeEnd[ = [handlerStart,rangeStart[ + return new Handler(firstHandler, firstHandler.startPc, start); + } else { + // [handlerStart,handlerEnd[ - [rangeStart,rangeEnd[ = + // [handlerStart,rangeStart[ + [rangeEnd,handerEnd[ + firstHandler.nextHandler = new Handler(firstHandler, end, firstHandler.endPc); + return new Handler(firstHandler, firstHandler.startPc, start); + } + } + + /** + * Returns the number of elements of the Handler list that begins with the given element. + * + * @param firstHandler the beginning of a Handler list. May be {@literal null}. + * @return the number of elements of the Handler list that begins with 'handler'. + */ + static int getExceptionTableLength(final Handler firstHandler) { + int length = 0; + Handler handler = firstHandler; + while (handler != null) { + length++; + handler = handler.nextHandler; + } + return length; + } + + /** + * Returns the size in bytes of the JVMS exception_table corresponding to the Handler list that + * begins with the given element. This includes the exception_table_length field. + * + * @param firstHandler the beginning of a Handler list. May be {@literal null}. + * @return the size in bytes of the exception_table_length and exception_table structures. + */ + static int getExceptionTableSize(final Handler firstHandler) { + return 2 + 8 * getExceptionTableLength(firstHandler); + } + + /** + * Puts the JVMS exception_table corresponding to the Handler list that begins with the given + * element. This includes the exception_table_length field. + * + * @param firstHandler the beginning of a Handler list. May be {@literal null}. + * @param output where the exception_table_length and exception_table structures must be put. + */ + static void putExceptionTable(final Handler firstHandler, final ByteVector output) { + output.putShort(getExceptionTableLength(firstHandler)); + Handler handler = firstHandler; + while (handler != null) { + output + .putShort(handler.startPc.bytecodeOffset) + .putShort(handler.endPc.bytecodeOffset) + .putShort(handler.handlerPc.bytecodeOffset) + .putShort(handler.catchType); + handler = handler.nextHandler; + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Label.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Label.java new file mode 100644 index 0000000..5837d40 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Label.java @@ -0,0 +1,605 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A position in the bytecode of a method. Labels are used for jump, goto, and switch instructions, + * and for try catch blocks. A label designates the instruction that is just after. Note + * however that there can be other elements between a label and the instruction it designates (such + * as other labels, stack map frames, line numbers, etc.). @Author Eric Bruneton + */ +public class Label { + + /** + * A flag indicating that a label is only used for debug attributes. Such a label is not the start + * of a basic block, the target of a jump instruction, or an exception handler. It can be safely + * ignored in control flow graph analysis algorithms (for optimization purposes). + */ + static final int FLAG_DEBUG_ONLY = 1; + + /** + * A flag indicating that a label is the target of a jump instruction, or the start of an + * exception handler. + */ + static final int FLAG_JUMP_TARGET = 2; + + /** A flag indicating that the bytecode offset of a label is known. */ + static final int FLAG_RESOLVED = 4; + + /** A flag indicating that a label corresponds to a reachable basic block. */ + static final int FLAG_REACHABLE = 8; + + /** + * A flag indicating that the basic block corresponding to a label ends with a subroutine call. By + * construction in {@link MethodWriter#visitJumpInsn}, labels with this flag set have at least two + * outgoing edges: + * + *
    + *
  • the first one corresponds to the instruction that follows the jsr instruction in the + * bytecode, i.e. where execution continues when it returns from the jsr call. This is a + * virtual control flow edge, since execution never goes directly from the jsr to the next + * instruction. Instead, it goes to the subroutine and eventually returns to the instruction + * following the jsr. This virtual edge is used to compute the real outgoing edges of the + * basic blocks ending with a ret instruction, in {@link #addSubroutineRetSuccessors}. + *
  • the second one corresponds to the target of the jsr instruction, + *
+ */ + static final int FLAG_SUBROUTINE_CALLER = 16; + + /** + * A flag indicating that the basic block corresponding to a label is the start of a subroutine. + */ + static final int FLAG_SUBROUTINE_START = 32; + + /** A flag indicating that the basic block corresponding to a label is the end of a subroutine. */ + static final int FLAG_SUBROUTINE_END = 64; + + /** + * The number of elements to add to the {@link #otherLineNumbers} array when it needs to be + * resized to store a new source line number. + */ + static final int LINE_NUMBERS_CAPACITY_INCREMENT = 4; + + /** + * The number of elements to add to the {@link #forwardReferences} array when it needs to be + * resized to store a new forward reference. + */ + static final int FORWARD_REFERENCES_CAPACITY_INCREMENT = 6; + + /** + * The bit mask to extract the type of a forward reference to this label. The extracted type is + * either {@link #FORWARD_REFERENCE_TYPE_SHORT} or {@link #FORWARD_REFERENCE_TYPE_WIDE}. + * + * @see #forwardReferences + */ + static final int FORWARD_REFERENCE_TYPE_MASK = 0xF0000000; + + /** + * The type of forward references stored with two bytes in the bytecode. This is the case, for + * instance, of a forward reference from an ifnull instruction. + */ + static final int FORWARD_REFERENCE_TYPE_SHORT = 0x10000000; + + /** + * The type of forward references stored in four bytes in the bytecode. This is the case, for + * instance, of a forward reference from a lookupswitch instruction. + */ + static final int FORWARD_REFERENCE_TYPE_WIDE = 0x20000000; + + /** + * The bit mask to extract the 'handle' of a forward reference to this label. The extracted handle + * is the bytecode offset where the forward reference value is stored (using either 2 or 4 bytes, + * as indicated by the {@link #FORWARD_REFERENCE_TYPE_MASK}). + * + * @see #forwardReferences + */ + static final int FORWARD_REFERENCE_HANDLE_MASK = 0x0FFFFFFF; + + /** + * A sentinel element used to indicate the end of a list of labels. + * + * @see #nextListElement + */ + static final Label EMPTY_LIST = new Label(); + + /** + * A user managed state associated with this label. Warning: this field is used by the ASM tree + * package. In order to use it with the ASM tree package you must override the getLabelNode method + * in MethodNode. + */ + public Object info; + + /** + * The type and status of this label or its corresponding basic block. Must be zero or more of + * {@link #FLAG_DEBUG_ONLY}, {@link #FLAG_JUMP_TARGET}, {@link #FLAG_RESOLVED}, {@link + * #FLAG_REACHABLE}, {@link #FLAG_SUBROUTINE_CALLER}, {@link #FLAG_SUBROUTINE_START}, {@link + * #FLAG_SUBROUTINE_END}. + */ + short flags; + + /** + * The source line number corresponding to this label, or 0. If there are several source line + * numbers corresponding to this label, the first one is stored in this field, and the remaining + * ones are stored in {@link #otherLineNumbers}. + */ + private short lineNumber; + + /** + * The source line numbers corresponding to this label, in addition to {@link #lineNumber}, or + * null. The first element of this array is the number n of source line numbers it contains, which + * are stored between indices 1 and n (inclusive). + */ + private int[] otherLineNumbers; + + /** + * The offset of this label in the bytecode of its method, in bytes. This value is set if and only + * if the {@link #FLAG_RESOLVED} flag is set. + */ + int bytecodeOffset; + + /** + * The forward references to this label. The first element is the number of forward references, + * times 2 (this corresponds to the index of the last element actually used in this array). Then, + * each forward reference is described with two consecutive integers noted + * 'sourceInsnBytecodeOffset' and 'reference': + * + *
    + *
  • 'sourceInsnBytecodeOffset' is the bytecode offset of the instruction that contains the + * forward reference, + *
  • 'reference' contains the type and the offset in the bytecode where the forward reference + * value must be stored, which can be extracted with {@link #FORWARD_REFERENCE_TYPE_MASK} + * and {@link #FORWARD_REFERENCE_HANDLE_MASK}. + *
+ * + *

For instance, for an ifnull instruction at bytecode offset x, 'sourceInsnBytecodeOffset' is + * equal to x, and 'reference' is of type {@link #FORWARD_REFERENCE_TYPE_SHORT} with value x + 1 + * (because the ifnull instruction uses a 2 bytes bytecode offset operand stored one byte after + * the start of the instruction itself). For the default case of a lookupswitch instruction at + * bytecode offset x, 'sourceInsnBytecodeOffset' is equal to x, and 'reference' is of type {@link + * #FORWARD_REFERENCE_TYPE_WIDE} with value between x + 1 and x + 4 (because the lookupswitch + * instruction uses a 4 bytes bytecode offset operand stored one to four bytes after the start of + * the instruction itself). + */ + private int[] forwardReferences; + + // ----------------------------------------------------------------------------------------------- + + // Fields for the control flow and data flow graph analysis algorithms (used to compute the + // maximum stack size or the stack map frames). A control flow graph contains one node per "basic + // block", and one edge per "jump" from one basic block to another. Each node (i.e., each basic + // block) is represented with the Label object that corresponds to the first instruction of this + // basic block. Each node also stores the list of its successors in the graph, as a linked list of + // Edge objects. + // + // The control flow analysis algorithms used to compute the maximum stack size or the stack map + // frames are similar and use two steps. The first step, during the visit of each instruction, + // builds information about the state of the local variables and the operand stack at the end of + // each basic block, called the "output frame", relatively to the frame state at the + // beginning of the basic block, which is called the "input frame", and which is unknown + // during this step. The second step, in {@link MethodWriter#computeAllFrames} and {@link + // MethodWriter#computeMaxStackAndLocal}, is a fix point algorithm + // that computes information about the input frame of each basic block, from the input state of + // the first basic block (known from the method signature), and by the using the previously + // computed relative output frames. + // + // The algorithm used to compute the maximum stack size only computes the relative output and + // absolute input stack heights, while the algorithm used to compute stack map frames computes + // relative output frames and absolute input frames. + + /** + * The number of elements in the input stack of the basic block corresponding to this label. This + * field is computed in {@link MethodWriter#computeMaxStackAndLocal}. + */ + short inputStackSize; + + /** + * The number of elements in the output stack, at the end of the basic block corresponding to this + * label. This field is only computed for basic blocks that end with a RET instruction. + */ + short outputStackSize; + + /** + * The maximum height reached by the output stack, relatively to the top of the input stack, in + * the basic block corresponding to this label. This maximum is always positive or {@literal + * null}. + */ + short outputStackMax; + + /** + * The id of the subroutine to which this basic block belongs, or 0. If the basic block belongs to + * several subroutines, this is the id of the "oldest" subroutine that contains it (with the + * convention that a subroutine calling another one is "older" than the callee). This field is + * computed in {@link MethodWriter#computeMaxStackAndLocal}, if the method contains JSR + * instructions. + */ + short subroutineId; + + /** + * The input and output stack map frames of the basic block corresponding to this label. This + * field is only used when the {@link MethodWriter#COMPUTE_ALL_FRAMES} or {@link + * MethodWriter#COMPUTE_INSERTED_FRAMES} option is used. + */ + Frame frame; + + /** + * The successor of this label, in the order they are visited in {@link MethodVisitor#visitLabel}. + * This linked list does not include labels used for debug info only. If the {@link + * MethodWriter#COMPUTE_ALL_FRAMES} or {@link MethodWriter#COMPUTE_INSERTED_FRAMES} option is used + * then it does not contain either successive labels that denote the same bytecode offset (in this + * case only the first label appears in this list). + */ + Label nextBasicBlock; + + /** + * The outgoing edges of the basic block corresponding to this label, in the control flow graph of + * its method. These edges are stored in a linked list of {@link Edge} objects, linked to each + * other by their {@link Edge#nextEdge} field. + */ + Edge outgoingEdges; + + /** + * The next element in the list of labels to which this label belongs, or {@literal null} if it + * does not belong to any list. All lists of labels must end with the {@link #EMPTY_LIST} + * sentinel, in order to ensure that this field is null if and only if this label does not belong + * to a list of labels. Note that there can be several lists of labels at the same time, but that + * a label can belong to at most one list at a time (unless some lists share a common tail, but + * this is not used in practice). + * + *

List of labels are used in {@link MethodWriter#computeAllFrames} and {@link + * MethodWriter#computeMaxStackAndLocal} to compute stack map frames and the maximum stack size, + * respectively, as well as in {@link #markSubroutine} and {@link #addSubroutineRetSuccessors} to + * compute the basic blocks belonging to subroutines and their outgoing edges. Outside of these + * methods, this field should be null (this property is a precondition and a postcondition of + * these methods). + */ + Label nextListElement; + + // ----------------------------------------------------------------------------------------------- + // Constructor and accessors + // ----------------------------------------------------------------------------------------------- + + /** Constructs a new label. */ + public Label() { + // Nothing to do. + } + + /** + * Returns the bytecode offset corresponding to this label. This offset is computed from the start + * of the method's bytecode. This method is intended for {@link Attribute} sub classes, and is + * normally not needed by class generators or adapters. + * + * @return the bytecode offset corresponding to this label. + * @throws IllegalStateException if this label is not resolved yet. + */ + public int getOffset() { + if ((flags & FLAG_RESOLVED) == 0) { + throw new IllegalStateException("Label offset position has not been resolved yet"); + } + return bytecodeOffset; + } + + /** + * Returns the "canonical" {@link Label} instance corresponding to this label's bytecode offset, + * if known, otherwise the label itself. The canonical instance is the first label (in the order + * of their visit by {@link MethodVisitor#visitLabel}) corresponding to this bytecode offset. It + * cannot be known for labels which have not been visited yet. + * + *

This method should only be used when the {@link MethodWriter#COMPUTE_ALL_FRAMES} option + * is used. + * + * @return the label itself if {@link #frame} is null, otherwise the Label's frame owner. This + * corresponds to the "canonical" label instance described above thanks to the way the label + * frame is set in {@link MethodWriter#visitLabel}. + */ + final Label getCanonicalInstance() { + return frame == null ? this : frame.owner; + } + + // ----------------------------------------------------------------------------------------------- + // Methods to manage line numbers + // ----------------------------------------------------------------------------------------------- + + /** + * Adds a source line number corresponding to this label. + * + * @param lineNumber a source line number (which should be strictly positive). + */ + final void addLineNumber(final int lineNumber) { + if (this.lineNumber == 0) { + this.lineNumber = (short) lineNumber; + } else { + if (otherLineNumbers == null) { + otherLineNumbers = new int[LINE_NUMBERS_CAPACITY_INCREMENT]; + } + int otherLineNumberIndex = ++otherLineNumbers[0]; + if (otherLineNumberIndex >= otherLineNumbers.length) { + int[] newLineNumbers = new int[otherLineNumbers.length + LINE_NUMBERS_CAPACITY_INCREMENT]; + System.arraycopy(otherLineNumbers, 0, newLineNumbers, 0, otherLineNumbers.length); + otherLineNumbers = newLineNumbers; + } + otherLineNumbers[otherLineNumberIndex] = lineNumber; + } + } + + /** + * Makes the given visitor visit this label and its source line numbers, if applicable. + * + * @param methodVisitor a method visitor. + * @param visitLineNumbers whether to visit of the label's source line numbers, if any. + */ + final void accept(final MethodVisitor methodVisitor, final boolean visitLineNumbers) { + methodVisitor.visitLabel(this); + if (visitLineNumbers && lineNumber != 0) { + methodVisitor.visitLineNumber(lineNumber & 0xFFFF, this); + if (otherLineNumbers != null) { + for (int i = 1; i <= otherLineNumbers[0]; ++i) { + methodVisitor.visitLineNumber(otherLineNumbers[i], this); + } + } + } + } + + // ----------------------------------------------------------------------------------------------- + // Methods to compute offsets and to manage forward references + // ----------------------------------------------------------------------------------------------- + + /** + * Puts a reference to this label in the bytecode of a method. If the bytecode offset of the label + * is known, the relative bytecode offset between the label and the instruction referencing it is + * computed and written directly. Otherwise, a null relative offset is written and a new forward + * reference is declared for this label. + * + * @param code the bytecode of the method. This is where the reference is appended. + * @param sourceInsnBytecodeOffset the bytecode offset of the instruction that contains the + * reference to be appended. + * @param wideReference whether the reference must be stored in 4 bytes (instead of 2 bytes). + */ + final void put( + final ByteVector code, final int sourceInsnBytecodeOffset, final boolean wideReference) { + if ((flags & FLAG_RESOLVED) == 0) { + if (wideReference) { + addForwardReference(sourceInsnBytecodeOffset, FORWARD_REFERENCE_TYPE_WIDE, code.length); + code.putInt(-1); + } else { + addForwardReference(sourceInsnBytecodeOffset, FORWARD_REFERENCE_TYPE_SHORT, code.length); + code.putShort(-1); + } + } else { + if (wideReference) { + code.putInt(bytecodeOffset - sourceInsnBytecodeOffset); + } else { + code.putShort(bytecodeOffset - sourceInsnBytecodeOffset); + } + } + } + + /** + * Adds a forward reference to this label. This method must be called only for a true forward + * reference, i.e. only if this label is not resolved yet. For backward references, the relative + * bytecode offset of the reference can be, and must be, computed and stored directly. + * + * @param sourceInsnBytecodeOffset the bytecode offset of the instruction that contains the + * reference stored at referenceHandle. + * @param referenceType either {@link #FORWARD_REFERENCE_TYPE_SHORT} or {@link + * #FORWARD_REFERENCE_TYPE_WIDE}. + * @param referenceHandle the offset in the bytecode where the forward reference value must be + * stored. + */ + private void addForwardReference( + final int sourceInsnBytecodeOffset, final int referenceType, final int referenceHandle) { + if (forwardReferences == null) { + forwardReferences = new int[FORWARD_REFERENCES_CAPACITY_INCREMENT]; + } + int lastElementIndex = forwardReferences[0]; + if (lastElementIndex + 2 >= forwardReferences.length) { + int[] newValues = new int[forwardReferences.length + FORWARD_REFERENCES_CAPACITY_INCREMENT]; + System.arraycopy(forwardReferences, 0, newValues, 0, forwardReferences.length); + forwardReferences = newValues; + } + forwardReferences[++lastElementIndex] = sourceInsnBytecodeOffset; + forwardReferences[++lastElementIndex] = referenceType | referenceHandle; + forwardReferences[0] = lastElementIndex; + } + + /** + * Sets the bytecode offset of this label to the given value and resolves the forward references + * to this label, if any. This method must be called when this label is added to the bytecode of + * the method, i.e. when its bytecode offset becomes known. This method fills in the blanks that + * where left in the bytecode by each forward reference previously added to this label. + * + * @param code the bytecode of the method. + * @param bytecodeOffset the bytecode offset of this label. + * @return {@literal true} if a blank that was left for this label was too small to store the + * offset. In such a case the corresponding jump instruction is replaced with an equivalent + * ASM specific instruction using an unsigned two bytes offset. These ASM specific + * instructions are later replaced with standard bytecode instructions with wider offsets (4 + * bytes instead of 2), in ClassReader. + */ + final boolean resolve(final byte[] code, final int bytecodeOffset) { + this.flags |= FLAG_RESOLVED; + this.bytecodeOffset = bytecodeOffset; + if (forwardReferences == null) { + return false; + } + boolean hasAsmInstructions = false; + for (int i = forwardReferences[0]; i > 0; i -= 2) { + final int sourceInsnBytecodeOffset = forwardReferences[i - 1]; + final int reference = forwardReferences[i]; + final int relativeOffset = bytecodeOffset - sourceInsnBytecodeOffset; + int handle = reference & FORWARD_REFERENCE_HANDLE_MASK; + if ((reference & FORWARD_REFERENCE_TYPE_MASK) == FORWARD_REFERENCE_TYPE_SHORT) { + if (relativeOffset < Short.MIN_VALUE || relativeOffset > Short.MAX_VALUE) { + // Change the opcode of the jump instruction, in order to be able to find it later in + // ClassReader. These ASM specific opcodes are similar to jump instruction opcodes, except + // that the 2 bytes offset is unsigned (and can therefore represent values from 0 to + // 65535, which is sufficient since the size of a method is limited to 65535 bytes). + int opcode = code[sourceInsnBytecodeOffset] & 0xFF; + if (opcode < Opcodes.IFNULL) { + // Change IFEQ ... JSR to ASM_IFEQ ... ASM_JSR. + code[sourceInsnBytecodeOffset] = (byte) (opcode + Constants.ASM_OPCODE_DELTA); + } else { + // Change IFNULL and IFNONNULL to ASM_IFNULL and ASM_IFNONNULL. + code[sourceInsnBytecodeOffset] = (byte) (opcode + Constants.ASM_IFNULL_OPCODE_DELTA); + } + hasAsmInstructions = true; + } + code[handle++] = (byte) (relativeOffset >>> 8); + code[handle] = (byte) relativeOffset; + } else { + code[handle++] = (byte) (relativeOffset >>> 24); + code[handle++] = (byte) (relativeOffset >>> 16); + code[handle++] = (byte) (relativeOffset >>> 8); + code[handle] = (byte) relativeOffset; + } + } + return hasAsmInstructions; + } + + // ----------------------------------------------------------------------------------------------- + // Methods related to subroutines + // ----------------------------------------------------------------------------------------------- + + /** + * Finds the basic blocks that belong to the subroutine starting with the basic block + * corresponding to this label, and marks these blocks as belonging to this subroutine. This + * method follows the control flow graph to find all the blocks that are reachable from the + * current basic block WITHOUT following any jsr target. + * + *

Note: a precondition and postcondition of this method is that all labels must have a null + * {@link #nextListElement}. + * + * @param subroutineId the id of the subroutine starting with the basic block corresponding to + * this label. + */ + final void markSubroutine(final short subroutineId) { + // Data flow algorithm: put this basic block in a list of blocks to process (which are blocks + // belonging to subroutine subroutineId) and, while there are blocks to process, remove one from + // the list, mark it as belonging to the subroutine, and add its successor basic blocks in the + // control flow graph to the list of blocks to process (if not already done). + Label listOfBlocksToProcess = this; + listOfBlocksToProcess.nextListElement = EMPTY_LIST; + while (listOfBlocksToProcess != EMPTY_LIST) { + // Remove a basic block from the list of blocks to process. + Label basicBlock = listOfBlocksToProcess; + listOfBlocksToProcess = listOfBlocksToProcess.nextListElement; + basicBlock.nextListElement = null; + + // If it is not already marked as belonging to a subroutine, mark it as belonging to + // subroutineId and add its successors to the list of blocks to process (unless already done). + if (basicBlock.subroutineId == 0) { + basicBlock.subroutineId = subroutineId; + listOfBlocksToProcess = basicBlock.pushSuccessors(listOfBlocksToProcess); + } + } + } + + /** + * Finds the basic blocks that end a subroutine starting with the basic block corresponding to + * this label and, for each one of them, adds an outgoing edge to the basic block following the + * given subroutine call. In other words, completes the control flow graph by adding the edges + * corresponding to the return from this subroutine, when called from the given caller basic + * block. + * + *

Note: a precondition and postcondition of this method is that all labels must have a null + * {@link #nextListElement}. + * + * @param subroutineCaller a basic block that ends with a jsr to the basic block corresponding to + * this label. This label is supposed to correspond to the start of a subroutine. + */ + final void addSubroutineRetSuccessors(final Label subroutineCaller) { + // Data flow algorithm: put this basic block in a list blocks to process (which are blocks + // belonging to a subroutine starting with this label) and, while there are blocks to process, + // remove one from the list, put it in a list of blocks that have been processed, add a return + // edge to the successor of subroutineCaller if applicable, and add its successor basic blocks + // in the control flow graph to the list of blocks to process (if not already done). + Label listOfProcessedBlocks = EMPTY_LIST; + Label listOfBlocksToProcess = this; + listOfBlocksToProcess.nextListElement = EMPTY_LIST; + while (listOfBlocksToProcess != EMPTY_LIST) { + // Move a basic block from the list of blocks to process to the list of processed blocks. + Label basicBlock = listOfBlocksToProcess; + listOfBlocksToProcess = basicBlock.nextListElement; + basicBlock.nextListElement = listOfProcessedBlocks; + listOfProcessedBlocks = basicBlock; + + // Add an edge from this block to the successor of the caller basic block, if this block is + // the end of a subroutine and if this block and subroutineCaller do not belong to the same + // subroutine. + if ((basicBlock.flags & FLAG_SUBROUTINE_END) != 0 + && basicBlock.subroutineId != subroutineCaller.subroutineId) { + basicBlock.outgoingEdges = + new Edge( + basicBlock.outputStackSize, + // By construction, the first outgoing edge of a basic block that ends with a jsr + // instruction leads to the jsr continuation block, i.e. where execution continues + // when ret is called (see {@link #FLAG_SUBROUTINE_CALLER}). + subroutineCaller.outgoingEdges.successor, + basicBlock.outgoingEdges); + } + // Add its successors to the list of blocks to process. Note that {@link #pushSuccessors} does + // not push basic blocks which are already in a list. Here this means either in the list of + // blocks to process, or in the list of already processed blocks. This second list is + // important to make sure we don't reprocess an already processed block. + listOfBlocksToProcess = basicBlock.pushSuccessors(listOfBlocksToProcess); + } + // Reset the {@link #nextListElement} of all the basic blocks that have been processed to null, + // so that this method can be called again with a different subroutine or subroutine caller. + while (listOfProcessedBlocks != EMPTY_LIST) { + Label newListOfProcessedBlocks = listOfProcessedBlocks.nextListElement; + listOfProcessedBlocks.nextListElement = null; + listOfProcessedBlocks = newListOfProcessedBlocks; + } + } + + /** + * Adds the successors of this label in the method's control flow graph (except those + * corresponding to a jsr target, and those already in a list of labels) to the given list of + * blocks to process, and returns the new list. + * + * @param listOfLabelsToProcess a list of basic blocks to process, linked together with their + * {@link #nextListElement} field. + * @return the new list of blocks to process. + */ + private Label pushSuccessors(final Label listOfLabelsToProcess) { + Label newListOfLabelsToProcess = listOfLabelsToProcess; + Edge outgoingEdge = outgoingEdges; + while (outgoingEdge != null) { + // By construction, the second outgoing edge of a basic block that ends with a jsr instruction + // leads to the jsr target (see {@link #FLAG_SUBROUTINE_CALLER}). + boolean isJsrTarget = + (flags & Label.FLAG_SUBROUTINE_CALLER) != 0 && outgoingEdge == outgoingEdges.nextEdge; + if (!isJsrTarget && outgoingEdge.successor.nextListElement == null) { + // Add this successor to the list of blocks to process, if it does not already belong to a + // list of labels. + outgoingEdge.successor.nextListElement = newListOfLabelsToProcess; + newListOfLabelsToProcess = outgoingEdge.successor; + } + outgoingEdge = outgoingEdge.nextEdge; + } + return newListOfLabelsToProcess; + } + + // ----------------------------------------------------------------------------------------------- + // Overridden Object methods + // ----------------------------------------------------------------------------------------------- + + /** + * Returns a string representation of this label. + * + * @return a string representation of this label. + */ + @Override + public String toString() { + return "L" + System.identityHashCode(this); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/MethodTooLargeException.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/MethodTooLargeException.java new file mode 100644 index 0000000..00b56ab --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/MethodTooLargeException.java @@ -0,0 +1,83 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * Exception thrown when the Code attribute of a method produced by a {@link ClassWriter} is too + * large. @Author Jason Zaugg + */ +public final class MethodTooLargeException extends IndexOutOfBoundsException { + + private static final long serialVersionUID = 6807380416709738314L; + + private final String className; + private final String methodName; + private final String descriptor; + private final int codeSize; + + /** + * Constructs a new {@link MethodTooLargeException}. + * + * @param className the internal name of the owner class. + * @param methodName the name of the method. + * @param descriptor the descriptor of the method. + * @param codeSize the size of the method's Code attribute, in bytes. + */ + public MethodTooLargeException( + final String className, + final String methodName, + final String descriptor, + final int codeSize) { + super("Method too large: " + className + "." + methodName + " " + descriptor); + this.className = className; + this.methodName = methodName; + this.descriptor = descriptor; + this.codeSize = codeSize; + } + + /** + * Returns the internal name of the owner class. + * + * @return the internal name of the owner class. + */ + public String getClassName() { + return className; + } + + /** + * Returns the name of the method. + * + * @return the name of the method. + */ + public String getMethodName() { + return methodName; + } + + /** + * Returns the descriptor of the method. + * + * @return the descriptor of the method. + */ + public String getDescriptor() { + return descriptor; + } + + /** + * Returns the size of the method's Code attribute, in bytes. + * + * @return the size of the method's Code attribute, in bytes. + */ + public int getCodeSize() { + return codeSize; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/MethodVisitor.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/MethodVisitor.java new file mode 100644 index 0000000..34df99a --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/MethodVisitor.java @@ -0,0 +1,769 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A visitor to visit a Java method. The methods of this class must be called in the following + * order: ( {@code visitParameter} )* [ {@code visitAnnotationDefault} ] ( {@code visitAnnotation} | + * {@code visitAnnotableParameterCount} | {@code visitParameterAnnotation} {@code + * visitTypeAnnotation} | {@code visitAttribute} )* [ {@code visitCode} ( {@code visitFrame} | + * {@code visitXInsn} | {@code visitLabel} | {@code visitInsnAnnotation} | {@code + * visitTryCatchBlock} | {@code visitTryCatchAnnotation} | {@code visitLocalVariable} | {@code + * visitLocalVariableAnnotation} | {@code visitLineNumber} )* {@code visitMaxs} ] {@code visitEnd}. + * In addition, the {@code visitXInsn} and {@code visitLabel} methods must be called in the + * sequential order of the bytecode instructions of the visited code, {@code visitInsnAnnotation} + * must be called after the annotated instruction, {@code visitTryCatchBlock} must be called + * before the labels passed as arguments have been visited, {@code + * visitTryCatchBlockAnnotation} must be called after the corresponding try catch block has + * been visited, and the {@code visitLocalVariable}, {@code visitLocalVariableAnnotation} and {@code + * visitLineNumber} methods must be called after the labels passed as arguments have been + * visited. @Author Eric Bruneton + */ +public abstract class MethodVisitor { + + private static final String REQUIRES_ASM5 = "This feature requires ASM5"; + + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + protected final int api; + + /** + * The method visitor to which this visitor must delegate method calls. May be {@literal null}. + */ + protected MethodVisitor mv; + + /** + * Constructs a new {@link MethodVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + public MethodVisitor(final int api) { + this(api, null); + } + + /** + * Constructs a new {@link MethodVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + * @param methodVisitor the method visitor to which this visitor must delegate method calls. May + * be null. + */ + public MethodVisitor(final int api, final MethodVisitor methodVisitor) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + if (api == Opcodes.ASM10_EXPERIMENTAL) { + Constants.checkAsmExperimental(this); + } + this.api = api; + this.mv = methodVisitor; + } + + // ----------------------------------------------------------------------------------------------- + // Parameters, annotations and non standard attributes + // ----------------------------------------------------------------------------------------------- + + /** + * Visits a parameter of this method. + * + * @param name parameter name or {@literal null} if none is provided. + * @param access the parameter's access flags, only {@code ACC_FINAL}, {@code ACC_SYNTHETIC} + * or/and {@code ACC_MANDATED} are allowed (see {@link Opcodes}). + */ + public void visitParameter(final String name, final int access) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (mv != null) { + mv.visitParameter(name, access); + } + } + + /** + * Visits the default value of this annotation processer method. + * + * @return a visitor to the visit the actual default value of this annotation processer method, or + * {@literal null} if this visitor is not interested in visiting this default value. The + * 'name' parameters passed to the methods of this annotation visitor are ignored. Moreover, + * exacly one visit method must be called on this annotation visitor, followed by visitEnd. + */ + public AnnotationVisitor visitAnnotationDefault() { + if (mv != null) { + return mv.visitAnnotationDefault(); + } + return null; + } + + /** + * Visits an annotation of this method. + * + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (mv != null) { + return mv.visitAnnotation(descriptor, visible); + } + return null; + } + + /** + * Visits an annotation on a type in the method signature. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#METHOD_TYPE_PARAMETER}, {@link + * TypeReference#METHOD_TYPE_PARAMETER_BOUND}, {@link TypeReference#METHOD_RETURN}, {@link + * TypeReference#METHOD_RECEIVER}, {@link TypeReference#METHOD_FORMAL_PARAMETER} or {@link + * TypeReference#THROWS}. See {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (mv != null) { + return mv.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + return null; + } + + /** + * Visits the number of method parameters that can have annotations. By default (i.e. when this + * method is not called), all the method parameters defined by the method descriptor can have + * annotations. + * + * @param parameterCount the number of method parameters than can have annotations. This number + * must be less or equal than the number of parameter types in the method descriptor. It can + * be strictly less when a method has synthetic parameters and when these parameters are + * ignored when computing parameter indices for the purpose of parameter annotations (see + * https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.18). + * @param visible {@literal true} to define the number of method parameters that can have + * annotations visible at runtime, {@literal false} to define the number of method parameters + * that can have annotations invisible at runtime. + */ + public void visitAnnotableParameterCount(final int parameterCount, final boolean visible) { + if (mv != null) { + mv.visitAnnotableParameterCount(parameterCount, visible); + } + } + + /** + * Visits an annotation of a parameter this method. + * + * @param parameter the parameter index. This index must be strictly smaller than the number of + * parameters in the method descriptor, and strictly smaller than the parameter count + * specified in {@link #visitAnnotableParameterCount}. Important note: a parameter index i + * is not required to correspond to the i'th parameter descriptor in the method + * descriptor, in particular in case of synthetic parameters (see + * https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.18). + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitParameterAnnotation( + final int parameter, final String descriptor, final boolean visible) { + if (mv != null) { + return mv.visitParameterAnnotation(parameter, descriptor, visible); + } + return null; + } + + /** + * Visits a non standard attribute of this method. + * + * @param attribute an attribute. + */ + public void visitAttribute(final Attribute attribute) { + if (mv != null) { + mv.visitAttribute(attribute); + } + } + + /** Starts the visit of the method's code, if any (i.e. non abstract method). */ + public void visitCode() { + if (mv != null) { + mv.visitCode(); + } + } + + /** + * Visits the current state of the local variables and operand stack elements. This method must(*) + * be called just before any instruction i that follows an unconditional branch + * instruction such as GOTO or THROW, that is the target of a jump instruction, or that starts an + * exception handler block. The visited types must describe the values of the local variables and + * of the operand stack elements just before i is executed.
+ *
+ * (*) this is mandatory only for classes whose version is greater than or equal to {@link + * Opcodes#V1_6}.
+ *
+ * The frames of a method must be given either in expanded form, or in compressed form (all frames + * must use the same format, i.e. you must not mix expanded and compressed frames within a single + * method): + * + *

    + *
  • In expanded form, all frames must have the F_NEW type. + *
  • In compressed form, frames are basically "deltas" from the state of the previous frame: + *
      + *
    • {@link Opcodes#F_SAME} representing frame with exactly the same locals as the + * previous frame and with the empty stack. + *
    • {@link Opcodes#F_SAME1} representing frame with exactly the same locals as the + * previous frame and with single value on the stack ( numStack is 1 and + * stack[0] contains value for the type of the stack item). + *
    • {@link Opcodes#F_APPEND} representing frame with current locals are the same as the + * locals in the previous frame, except that additional locals are defined ( + * numLocal is 1, 2 or 3 and local elements contains values + * representing added types). + *
    • {@link Opcodes#F_CHOP} representing frame with current locals are the same as the + * locals in the previous frame, except that the last 1-3 locals are absent and with + * the empty stack (numLocal is 1, 2 or 3). + *
    • {@link Opcodes#F_FULL} representing complete frame data. + *
    + *
+ * + *
+ * In both cases the first frame, corresponding to the method's parameters and access flags, is + * implicit and must not be visited. Also, it is illegal to visit two or more frames for the same + * code location (i.e., at least one instruction must be visited between two calls to visitFrame). + * + * @param type the type of this stack map frame. Must be {@link Opcodes#F_NEW} for expanded + * frames, or {@link Opcodes#F_FULL}, {@link Opcodes#F_APPEND}, {@link Opcodes#F_CHOP}, {@link + * Opcodes#F_SAME} or {@link Opcodes#F_APPEND}, {@link Opcodes#F_SAME1} for compressed frames. + * @param numLocal the number of local variables in the visited frame. + * @param local the local variable types in this frame. This array must not be modified. Primitive + * types are represented by {@link Opcodes#TOP}, {@link Opcodes#INTEGER}, {@link + * Opcodes#FLOAT}, {@link Opcodes#LONG}, {@link Opcodes#DOUBLE}, {@link Opcodes#NULL} or + * {@link Opcodes#UNINITIALIZED_THIS} (long and double are represented by a single element). + * Reference types are represented by String objects (representing internal names), and + * uninitialized types by Label objects (this label designates the NEW instruction that + * created this uninitialized value). + * @param numStack the number of operand stack elements in the visited frame. + * @param stack the operand stack types in this frame. This array must not be modified. Its + * content has the same format as the "local" array. + * @throws IllegalStateException if a frame is visited just after another one, without any + * instruction between the two (unless this frame is a Opcodes#F_SAME frame, in which case it + * is silently ignored). + */ + public void visitFrame( + final int type, + final int numLocal, + final Object[] local, + final int numStack, + final Object[] stack) { + if (mv != null) { + mv.visitFrame(type, numLocal, local, numStack, stack); + } + } + + // ----------------------------------------------------------------------------------------------- + // Normal instructions + // ----------------------------------------------------------------------------------------------- + + /** + * Visits a zero operand instruction. + * + * @param opcode the opcode of the instruction to be visited. This opcode is either NOP, + * ACONST_NULL, ICONST_M1, ICONST_0, ICONST_1, ICONST_2, ICONST_3, ICONST_4, ICONST_5, + * LCONST_0, LCONST_1, FCONST_0, FCONST_1, FCONST_2, DCONST_0, DCONST_1, IALOAD, LALOAD, + * FALOAD, DALOAD, AALOAD, BALOAD, CALOAD, SALOAD, IASTORE, LASTORE, FASTORE, DASTORE, + * AASTORE, BASTORE, CASTORE, SASTORE, POP, POP2, DUP, DUP_X1, DUP_X2, DUP2, DUP2_X1, DUP2_X2, + * SWAP, IADD, LADD, FADD, DADD, ISUB, LSUB, FSUB, DSUB, IMUL, LMUL, FMUL, DMUL, IDIV, LDIV, + * FDIV, DDIV, IREM, LREM, FREM, DREM, INEG, LNEG, FNEG, DNEG, ISHL, LSHL, ISHR, LSHR, IUSHR, + * LUSHR, IAND, LAND, IOR, LOR, IXOR, LXOR, I2L, I2F, I2D, L2I, L2F, L2D, F2I, F2L, F2D, D2I, + * D2L, D2F, I2B, I2C, I2S, LCMP, FCMPL, FCMPG, DCMPL, DCMPG, IRETURN, LRETURN, FRETURN, + * DRETURN, ARETURN, RETURN, ARRAYLENGTH, ATHROW, MONITORENTER, or MONITOREXIT. + */ + public void visitInsn(final int opcode) { + if (mv != null) { + mv.visitInsn(opcode); + } + } + + /** + * Visits an instruction with a single int operand. + * + * @param opcode the opcode of the instruction to be visited. This opcode is either BIPUSH, SIPUSH + * or NEWARRAY. + * @param operand the operand of the instruction to be visited.
+ * When opcode is BIPUSH, operand value should be between Byte.MIN_VALUE and Byte.MAX_VALUE. + *
+ * When opcode is SIPUSH, operand value should be between Short.MIN_VALUE and Short.MAX_VALUE. + *
+ * When opcode is NEWARRAY, operand value should be one of {@link Opcodes#T_BOOLEAN}, {@link + * Opcodes#T_CHAR}, {@link Opcodes#T_FLOAT}, {@link Opcodes#T_DOUBLE}, {@link Opcodes#T_BYTE}, + * {@link Opcodes#T_SHORT}, {@link Opcodes#T_INT} or {@link Opcodes#T_LONG}. + */ + public void visitIntInsn(final int opcode, final int operand) { + if (mv != null) { + mv.visitIntInsn(opcode, operand); + } + } + + /** + * Visits a local variable instruction. A local variable instruction is an instruction that loads + * or stores the value of a local variable. + * + * @param opcode the opcode of the local variable instruction to be visited. This opcode is either + * ILOAD, LLOAD, FLOAD, DLOAD, ALOAD, ISTORE, LSTORE, FSTORE, DSTORE, ASTORE or RET. + * @param var the operand of the instruction to be visited. This operand is the index of a local + * variable. + */ + public void visitVarInsn(final int opcode, final int var) { + if (mv != null) { + mv.visitVarInsn(opcode, var); + } + } + + /** + * Visits a type instruction. A type instruction is an instruction that takes the internal name of + * a class as parameter. + * + * @param opcode the opcode of the type instruction to be visited. This opcode is either NEW, + * ANEWARRAY, CHECKCAST or INSTANCEOF. + * @param type the operand of the instruction to be visited. This operand must be the internal + * name of an object or array class (see {@link Type#getInternalName()}). + */ + public void visitTypeInsn(final int opcode, final String type) { + if (mv != null) { + mv.visitTypeInsn(opcode, type); + } + } + + /** + * Visits a field instruction. A field instruction is an instruction that loads or stores the + * value of a field of an object. + * + * @param opcode the opcode of the type instruction to be visited. This opcode is either + * GETSTATIC, PUTSTATIC, GETFIELD or PUTFIELD. + * @param owner the internal name of the field's owner class (see {@link Type#getInternalName()}). + * @param name the field's name. + * @param descriptor the field's descriptor (see {@link Type}). + */ + public void visitFieldInsn( + final int opcode, final String owner, final String name, final String descriptor) { + if (mv != null) { + mv.visitFieldInsn(opcode, owner, name, descriptor); + } + } + + /** + * Visits a method instruction. A method instruction is an instruction that invokes a method. + * + * @param opcode the opcode of the type instruction to be visited. This opcode is either + * INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE. + * @param owner the internal name of the method's owner class (see {@link + * Type#getInternalName()}). + * @param name the method's name. + * @param descriptor the method's descriptor (see {@link Type}). + * @deprecated use {@link #visitMethodInsn(int, String, String, String, boolean)} instead. + */ + @Deprecated + public void visitMethodInsn( + final int opcode, final String owner, final String name, final String descriptor) { + int opcodeAndSource = opcode | (api < Opcodes.ASM5 ? Opcodes.SOURCE_DEPRECATED : 0); + visitMethodInsn(opcodeAndSource, owner, name, descriptor, opcode == Opcodes.INVOKEINTERFACE); + } + + /** + * Visits a method instruction. A method instruction is an instruction that invokes a method. + * + * @param opcode the opcode of the type instruction to be visited. This opcode is either + * INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE. + * @param owner the internal name of the method's owner class (see {@link + * Type#getInternalName()}). + * @param name the method's name. + * @param descriptor the method's descriptor (see {@link Type}). + * @param isInterface if the method's owner class is an processer. + */ + public void visitMethodInsn( + final int opcode, + final String owner, + final String name, + final String descriptor, + final boolean isInterface) { + if (api < Opcodes.ASM5 && (opcode & Opcodes.SOURCE_DEPRECATED) == 0) { + if (isInterface != (opcode == Opcodes.INVOKEINTERFACE)) { + throw new UnsupportedOperationException("INVOKESPECIAL/STATIC on interfaces requires ASM5"); + } + visitMethodInsn(opcode, owner, name, descriptor); + return; + } + if (mv != null) { + mv.visitMethodInsn(opcode & ~Opcodes.SOURCE_MASK, owner, name, descriptor, isInterface); + } + } + + /** + * Visits an invokedynamic instruction. + * + * @param name the method's name. + * @param descriptor the method's descriptor (see {@link Type}). + * @param bootstrapMethodHandle the bootstrap method. + * @param bootstrapMethodArguments the bootstrap method constant arguments. Each argument must be + * an {@link Integer}, {@link Float}, {@link Long}, {@link Double}, {@link String}, {@link + * Type}, {@link Handle} or {@link ConstantDynamic} value. This method is allowed to modify + * the content of the array so a caller should expect that this array may change. + */ + public void visitInvokeDynamicInsn( + final String name, + final String descriptor, + final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (mv != null) { + mv.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + } + } + + /** + * Visits a jump instruction. A jump instruction is an instruction that may jump to another + * instruction. + * + * @param opcode the opcode of the type instruction to be visited. This opcode is either IFEQ, + * IFNE, IFLT, IFGE, IFGT, IFLE, IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, IF_ICMPGT, + * IF_ICMPLE, IF_ACMPEQ, IF_ACMPNE, GOTO, JSR, IFNULL or IFNONNULL. + * @param label the operand of the instruction to be visited. This operand is a label that + * designates the instruction to which the jump instruction may jump. + */ + public void visitJumpInsn(final int opcode, final Label label) { + if (mv != null) { + mv.visitJumpInsn(opcode, label); + } + } + + /** + * Visits a label. A label designates the instruction that will be visited just after it. + * + * @param label a {@link Label} object. + */ + public void visitLabel(final Label label) { + if (mv != null) { + mv.visitLabel(label); + } + } + + // ----------------------------------------------------------------------------------------------- + // Special instructions + // ----------------------------------------------------------------------------------------------- + + /** + * Visits a LDC instruction. Note that new constant types may be added in future versions of the + * Java Virtual Machine. To easily detect new constant types, implementations of this method + * should check for unexpected constant types, like this: + * + *
+   * if (cst instanceof Integer) {
+   *     // ...
+   * } else if (cst instanceof Float) {
+   *     // ...
+   * } else if (cst instanceof Long) {
+   *     // ...
+   * } else if (cst instanceof Double) {
+   *     // ...
+   * } else if (cst instanceof String) {
+   *     // ...
+   * } else if (cst instanceof Type) {
+   *     int sort = ((Type) cst).getSort();
+   *     if (sort == Type.OBJECT) {
+   *         // ...
+   *     } else if (sort == Type.ARRAY) {
+   *         // ...
+   *     } else if (sort == Type.METHOD) {
+   *         // ...
+   *     } else {
+   *         // throw an exception
+   *     }
+   * } else if (cst instanceof Handle) {
+   *     // ...
+   * } else if (cst instanceof ConstantDynamic) {
+   *     // ...
+   * } else {
+   *     // throw an exception
+   * }
+   * 
+ * + * @param value the constant to be loaded on the stack. This parameter must be a non null {@link + * Integer}, a {@link Float}, a {@link Long}, a {@link Double}, a {@link String}, a {@link + * Type} of OBJECT or ARRAY sort for {@code .class} constants, for classes whose version is + * 49, a {@link Type} of METHOD sort for MethodType, a {@link Handle} for MethodHandle + * constants, for classes whose version is 51 or a {@link ConstantDynamic} for a constant + * dynamic for classes whose version is 55. + */ + public void visitLdcInsn(final Object value) { + if (api < Opcodes.ASM5 + && (value instanceof Handle + || (value instanceof Type && ((Type) value).getSort() == Type.METHOD))) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (api < Opcodes.ASM7 && value instanceof ConstantDynamic) { + throw new UnsupportedOperationException("This feature requires ASM7"); + } + if (mv != null) { + mv.visitLdcInsn(value); + } + } + + /** + * Visits an IINC instruction. + * + * @param var index of the local variable to be incremented. + * @param increment amount to increment the local variable by. + */ + public void visitIincInsn(final int var, final int increment) { + if (mv != null) { + mv.visitIincInsn(var, increment); + } + } + + /** + * Visits a TABLESWITCH instruction. + * + * @param min the minimum key value. + * @param max the maximum key value. + * @param dflt beginning of the default handler block. + * @param labels beginnings of the handler blocks. {@code labels[i]} is the beginning of the + * handler block for the {@code min + i} key. + */ + public void visitTableSwitchInsn( + final int min, final int max, final Label dflt, final Label... labels) { + if (mv != null) { + mv.visitTableSwitchInsn(min, max, dflt, labels); + } + } + + /** + * Visits a LOOKUPSWITCH instruction. + * + * @param dflt beginning of the default handler block. + * @param keys the values of the keys. + * @param labels beginnings of the handler blocks. {@code labels[i]} is the beginning of the + * handler block for the {@code keys[i]} key. + */ + public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) { + if (mv != null) { + mv.visitLookupSwitchInsn(dflt, keys, labels); + } + } + + /** + * Visits a MULTIANEWARRAY instruction. + * + * @param descriptor an array type descriptor (see {@link Type}). + * @param numDimensions the number of dimensions of the array to allocate. + */ + public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions) { + if (mv != null) { + mv.visitMultiANewArrayInsn(descriptor, numDimensions); + } + } + + /** + * Visits an annotation on an instruction. This method must be called just after the + * annotated instruction. It can be called several times for the same instruction. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#INSTANCEOF}, {@link TypeReference#NEW}, {@link + * TypeReference#CONSTRUCTOR_REFERENCE}, {@link TypeReference#METHOD_REFERENCE}, {@link + * TypeReference#CAST}, {@link TypeReference#CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT}, {@link + * TypeReference#METHOD_INVOCATION_TYPE_ARGUMENT}, {@link + * TypeReference#CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT}, or {@link + * TypeReference#METHOD_REFERENCE_TYPE_ARGUMENT}. See {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitInsnAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (mv != null) { + return mv.visitInsnAnnotation(typeRef, typePath, descriptor, visible); + } + return null; + } + + // ----------------------------------------------------------------------------------------------- + // Exceptions table entries, debug information, max stack and max locals + // ----------------------------------------------------------------------------------------------- + + /** + * Visits a try catch block. + * + * @param start the beginning of the exception handler's scope (inclusive). + * @param end the end of the exception handler's scope (exclusive). + * @param handler the beginning of the exception handler's code. + * @param type the internal name of the type of exceptions handled by the handler, or {@literal + * null} to catch any exceptions (for "finally" blocks). + * @throws IllegalArgumentException if one of the labels has already been visited by this visitor + * (by the {@link #visitLabel} method). + */ + public void visitTryCatchBlock( + final Label start, final Label end, final Label handler, final String type) { + if (mv != null) { + mv.visitTryCatchBlock(start, end, handler, type); + } + } + + /** + * Visits an annotation on an exception handler type. This method must be called after the + * {@link #visitTryCatchBlock} for the annotated exception handler. It can be called several times + * for the same exception handler. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#EXCEPTION_PARAMETER}. See {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitTryCatchAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (mv != null) { + return mv.visitTryCatchAnnotation(typeRef, typePath, descriptor, visible); + } + return null; + } + + /** + * Visits a local variable declaration. + * + * @param name the name of a local variable. + * @param descriptor the type descriptor of this local variable. + * @param signature the type signature of this local variable. May be {@literal null} if the local + * variable type does not use generic types. + * @param start the first instruction corresponding to the scope of this local variable + * (inclusive). + * @param end the last instruction corresponding to the scope of this local variable (exclusive). + * @param index the local variable's index. + * @throws IllegalArgumentException if one of the labels has not already been visited by this + * visitor (by the {@link #visitLabel} method). + */ + public void visitLocalVariable( + final String name, + final String descriptor, + final String signature, + final Label start, + final Label end, + final int index) { + if (mv != null) { + mv.visitLocalVariable(name, descriptor, signature, start, end, index); + } + } + + /** + * Visits an annotation on a local variable type. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#LOCAL_VARIABLE} or {@link TypeReference#RESOURCE_VARIABLE}. See {@link + * TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param start the fist instructions corresponding to the continuous ranges that make the scope + * of this local variable (inclusive). + * @param end the last instructions corresponding to the continuous ranges that make the scope of + * this local variable (exclusive). This array must have the same size as the 'start' array. + * @param index the local variable's index in each range. This array must have the same size as + * the 'start' array. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitLocalVariableAnnotation( + final int typeRef, + final TypePath typePath, + final Label[] start, + final Label[] end, + final int[] index, + final String descriptor, + final boolean visible) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (mv != null) { + return mv.visitLocalVariableAnnotation( + typeRef, typePath, start, end, index, descriptor, visible); + } + return null; + } + + /** + * Visits a line number declaration. + * + * @param line a line number. This number refers to the source file from which the class was + * compiled. + * @param start the first instruction corresponding to this line number. + * @throws IllegalArgumentException if {@code start} has not already been visited by this visitor + * (by the {@link #visitLabel} method). + */ + public void visitLineNumber(final int line, final Label start) { + if (mv != null) { + mv.visitLineNumber(line, start); + } + } + + /** + * Visits the maximum stack size and the maximum number of local variables of the method. + * + * @param maxStack maximum stack size of the method. + * @param maxLocals maximum number of local variables for the method. + */ + public void visitMaxs(final int maxStack, final int maxLocals) { + if (mv != null) { + mv.visitMaxs(maxStack, maxLocals); + } + } + + /** + * Visits the end of the method. This method, which is the last one to be called, is used to + * inform the visitor that all the annotations and attributes of the method have been visited. + */ + public void visitEnd() { + if (mv != null) { + mv.visitEnd(); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/MethodWriter.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/MethodWriter.java new file mode 100644 index 0000000..c932795 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/MethodWriter.java @@ -0,0 +1,2375 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A {@link MethodVisitor} that generates a corresponding 'method_info' structure, as defined in the + * Java Virtual Machine Specification (JVMS). @Author Eric Bruneton @Author Eugene Kuleshov + * + * @see JVMS + * 4.6 + */ +final class MethodWriter extends MethodVisitor { + + /** Indicates that nothing must be computed. */ + static final int COMPUTE_NOTHING = 0; + + /** + * Indicates that the maximum stack size and the maximum number of local variables must be + * computed, from scratch. + */ + static final int COMPUTE_MAX_STACK_AND_LOCAL = 1; + + /** + * Indicates that the maximum stack size and the maximum number of local variables must be + * computed, from the existing stack map frames. This can be done more efficiently than with the + * control flow graph algorithm used for {@link #COMPUTE_MAX_STACK_AND_LOCAL}, by using a linear + * scan of the bytecode instructions. + */ + static final int COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES = 2; + + /** + * Indicates that the stack map frames of type F_INSERT must be computed. The other frames are not + * computed. They should all be of type F_NEW and should be sufficient to compute the content of + * the F_INSERT frames, together with the bytecode instructions between a F_NEW and a F_INSERT + * frame - and without any knowledge of the type hierarchy (by definition of F_INSERT). + */ + static final int COMPUTE_INSERTED_FRAMES = 3; + + /** + * Indicates that all the stack map frames must be computed. In this case the maximum stack size + * and the maximum number of local variables is also computed. + */ + static final int COMPUTE_ALL_FRAMES = 4; + + /** Indicates that {@link #STACK_SIZE_DELTA} is not applicable (not constant or never used). */ + private static final int NA = 0; + + /** + * The stack size variation corresponding to each JVM opcode. The stack size variation for opcode + * 'o' is given by the array element at index 'o'. + * + * @see JVMS 6 + */ + private static final int[] STACK_SIZE_DELTA = { + 0, // nop = 0 (0x0) + 1, // aconst_null = 1 (0x1) + 1, // iconst_m1 = 2 (0x2) + 1, // iconst_0 = 3 (0x3) + 1, // iconst_1 = 4 (0x4) + 1, // iconst_2 = 5 (0x5) + 1, // iconst_3 = 6 (0x6) + 1, // iconst_4 = 7 (0x7) + 1, // iconst_5 = 8 (0x8) + 2, // lconst_0 = 9 (0x9) + 2, // lconst_1 = 10 (0xa) + 1, // fconst_0 = 11 (0xb) + 1, // fconst_1 = 12 (0xc) + 1, // fconst_2 = 13 (0xd) + 2, // dconst_0 = 14 (0xe) + 2, // dconst_1 = 15 (0xf) + 1, // bipush = 16 (0x10) + 1, // sipush = 17 (0x11) + 1, // ldc = 18 (0x12) + NA, // ldc_w = 19 (0x13) + NA, // ldc2_w = 20 (0x14) + 1, // iload = 21 (0x15) + 2, // lload = 22 (0x16) + 1, // fload = 23 (0x17) + 2, // dload = 24 (0x18) + 1, // aload = 25 (0x19) + NA, // iload_0 = 26 (0x1a) + NA, // iload_1 = 27 (0x1b) + NA, // iload_2 = 28 (0x1c) + NA, // iload_3 = 29 (0x1d) + NA, // lload_0 = 30 (0x1e) + NA, // lload_1 = 31 (0x1f) + NA, // lload_2 = 32 (0x20) + NA, // lload_3 = 33 (0x21) + NA, // fload_0 = 34 (0x22) + NA, // fload_1 = 35 (0x23) + NA, // fload_2 = 36 (0x24) + NA, // fload_3 = 37 (0x25) + NA, // dload_0 = 38 (0x26) + NA, // dload_1 = 39 (0x27) + NA, // dload_2 = 40 (0x28) + NA, // dload_3 = 41 (0x29) + NA, // aload_0 = 42 (0x2a) + NA, // aload_1 = 43 (0x2b) + NA, // aload_2 = 44 (0x2c) + NA, // aload_3 = 45 (0x2d) + -1, // iaload = 46 (0x2e) + 0, // laload = 47 (0x2f) + -1, // faload = 48 (0x30) + 0, // daload = 49 (0x31) + -1, // aaload = 50 (0x32) + -1, // baload = 51 (0x33) + -1, // caload = 52 (0x34) + -1, // saload = 53 (0x35) + -1, // istore = 54 (0x36) + -2, // lstore = 55 (0x37) + -1, // fstore = 56 (0x38) + -2, // dstore = 57 (0x39) + -1, // astore = 58 (0x3a) + NA, // istore_0 = 59 (0x3b) + NA, // istore_1 = 60 (0x3c) + NA, // istore_2 = 61 (0x3d) + NA, // istore_3 = 62 (0x3e) + NA, // lstore_0 = 63 (0x3f) + NA, // lstore_1 = 64 (0x40) + NA, // lstore_2 = 65 (0x41) + NA, // lstore_3 = 66 (0x42) + NA, // fstore_0 = 67 (0x43) + NA, // fstore_1 = 68 (0x44) + NA, // fstore_2 = 69 (0x45) + NA, // fstore_3 = 70 (0x46) + NA, // dstore_0 = 71 (0x47) + NA, // dstore_1 = 72 (0x48) + NA, // dstore_2 = 73 (0x49) + NA, // dstore_3 = 74 (0x4a) + NA, // astore_0 = 75 (0x4b) + NA, // astore_1 = 76 (0x4c) + NA, // astore_2 = 77 (0x4d) + NA, // astore_3 = 78 (0x4e) + -3, // iastore = 79 (0x4f) + -4, // lastore = 80 (0x50) + -3, // fastore = 81 (0x51) + -4, // dastore = 82 (0x52) + -3, // aastore = 83 (0x53) + -3, // bastore = 84 (0x54) + -3, // castore = 85 (0x55) + -3, // sastore = 86 (0x56) + -1, // pop = 87 (0x57) + -2, // pop2 = 88 (0x58) + 1, // dup = 89 (0x59) + 1, // dup_x1 = 90 (0x5a) + 1, // dup_x2 = 91 (0x5b) + 2, // dup2 = 92 (0x5c) + 2, // dup2_x1 = 93 (0x5d) + 2, // dup2_x2 = 94 (0x5e) + 0, // swap = 95 (0x5f) + -1, // iadd = 96 (0x60) + -2, // ladd = 97 (0x61) + -1, // fadd = 98 (0x62) + -2, // dadd = 99 (0x63) + -1, // isub = 100 (0x64) + -2, // lsub = 101 (0x65) + -1, // fsub = 102 (0x66) + -2, // dsub = 103 (0x67) + -1, // imul = 104 (0x68) + -2, // lmul = 105 (0x69) + -1, // fmul = 106 (0x6a) + -2, // dmul = 107 (0x6b) + -1, // idiv = 108 (0x6c) + -2, // ldiv = 109 (0x6d) + -1, // fdiv = 110 (0x6e) + -2, // ddiv = 111 (0x6f) + -1, // irem = 112 (0x70) + -2, // lrem = 113 (0x71) + -1, // frem = 114 (0x72) + -2, // drem = 115 (0x73) + 0, // ineg = 116 (0x74) + 0, // lneg = 117 (0x75) + 0, // fneg = 118 (0x76) + 0, // dneg = 119 (0x77) + -1, // ishl = 120 (0x78) + -1, // lshl = 121 (0x79) + -1, // ishr = 122 (0x7a) + -1, // lshr = 123 (0x7b) + -1, // iushr = 124 (0x7c) + -1, // lushr = 125 (0x7d) + -1, // iand = 126 (0x7e) + -2, // land = 127 (0x7f) + -1, // ior = 128 (0x80) + -2, // lor = 129 (0x81) + -1, // ixor = 130 (0x82) + -2, // lxor = 131 (0x83) + 0, // iinc = 132 (0x84) + 1, // i2l = 133 (0x85) + 0, // i2f = 134 (0x86) + 1, // i2d = 135 (0x87) + -1, // l2i = 136 (0x88) + -1, // l2f = 137 (0x89) + 0, // l2d = 138 (0x8a) + 0, // f2i = 139 (0x8b) + 1, // f2l = 140 (0x8c) + 1, // f2d = 141 (0x8d) + -1, // d2i = 142 (0x8e) + 0, // d2l = 143 (0x8f) + -1, // d2f = 144 (0x90) + 0, // i2b = 145 (0x91) + 0, // i2c = 146 (0x92) + 0, // i2s = 147 (0x93) + -3, // lcmp = 148 (0x94) + -1, // fcmpl = 149 (0x95) + -1, // fcmpg = 150 (0x96) + -3, // dcmpl = 151 (0x97) + -3, // dcmpg = 152 (0x98) + -1, // ifeq = 153 (0x99) + -1, // ifne = 154 (0x9a) + -1, // iflt = 155 (0x9b) + -1, // ifge = 156 (0x9c) + -1, // ifgt = 157 (0x9d) + -1, // ifle = 158 (0x9e) + -2, // if_icmpeq = 159 (0x9f) + -2, // if_icmpne = 160 (0xa0) + -2, // if_icmplt = 161 (0xa1) + -2, // if_icmpge = 162 (0xa2) + -2, // if_icmpgt = 163 (0xa3) + -2, // if_icmple = 164 (0xa4) + -2, // if_acmpeq = 165 (0xa5) + -2, // if_acmpne = 166 (0xa6) + 0, // goto = 167 (0xa7) + 1, // jsr = 168 (0xa8) + 0, // ret = 169 (0xa9) + -1, // tableswitch = 170 (0xaa) + -1, // lookupswitch = 171 (0xab) + -1, // ireturn = 172 (0xac) + -2, // lreturn = 173 (0xad) + -1, // freturn = 174 (0xae) + -2, // dreturn = 175 (0xaf) + -1, // areturn = 176 (0xb0) + 0, // return = 177 (0xb1) + NA, // getstatic = 178 (0xb2) + NA, // putstatic = 179 (0xb3) + NA, // getfield = 180 (0xb4) + NA, // putfield = 181 (0xb5) + NA, // invokevirtual = 182 (0xb6) + NA, // invokespecial = 183 (0xb7) + NA, // invokestatic = 184 (0xb8) + NA, // invokeinterface = 185 (0xb9) + NA, // invokedynamic = 186 (0xba) + 1, // new = 187 (0xbb) + 0, // newarray = 188 (0xbc) + 0, // anewarray = 189 (0xbd) + 0, // arraylength = 190 (0xbe) + NA, // athrow = 191 (0xbf) + 0, // checkcast = 192 (0xc0) + 0, // instanceof = 193 (0xc1) + -1, // monitorenter = 194 (0xc2) + -1, // monitorexit = 195 (0xc3) + NA, // wide = 196 (0xc4) + NA, // multianewarray = 197 (0xc5) + -1, // ifnull = 198 (0xc6) + -1, // ifnonnull = 199 (0xc7) + NA, // goto_w = 200 (0xc8) + NA // jsr_w = 201 (0xc9) + }; + + /** Where the constants used in this MethodWriter must be stored. */ + private final SymbolTable symbolTable; + + // Note: fields are ordered as in the method_info structure, and those related to attributes are + // ordered as in Section 4.7 of the JVMS. + + /** + * The access_flags field of the method_info JVMS structure. This field can contain ASM specific + * access flags, such as {@link Opcodes#ACC_DEPRECATED}, which are removed when generating the + * ClassFile structure. + */ + private final int accessFlags; + + /** The name_index field of the method_info JVMS structure. */ + private final int nameIndex; + + /** The name of this method. */ + private final String name; + + /** The descriptor_index field of the method_info JVMS structure. */ + private final int descriptorIndex; + + /** The descriptor of this method. */ + private final String descriptor; + + // Code attribute fields and sub attributes: + + /** The max_stack field of the Code attribute. */ + private int maxStack; + + /** The max_locals field of the Code attribute. */ + private int maxLocals; + + /** The 'code' field of the Code attribute. */ + private final ByteVector code = new ByteVector(); + + /** + * The first element in the exception handler list (used to generate the exception_table of the + * Code attribute). The next ones can be accessed with the {@link Handler#nextHandler} field. May + * be {@literal null}. + */ + private Handler firstHandler; + + /** + * The last element in the exception handler list (used to generate the exception_table of the + * Code attribute). The next ones can be accessed with the {@link Handler#nextHandler} field. May + * be {@literal null}. + */ + private Handler lastHandler; + + /** The line_number_table_length field of the LineNumberTable code attribute. */ + private int lineNumberTableLength; + + /** The line_number_table array of the LineNumberTable code attribute, or {@literal null}. */ + private ByteVector lineNumberTable; + + /** The local_variable_table_length field of the LocalVariableTable code attribute. */ + private int localVariableTableLength; + + /** + * The local_variable_table array of the LocalVariableTable code attribute, or {@literal null}. + */ + private ByteVector localVariableTable; + + /** The local_variable_type_table_length field of the LocalVariableTypeTable code attribute. */ + private int localVariableTypeTableLength; + + /** + * The local_variable_type_table array of the LocalVariableTypeTable code attribute, or {@literal + * null}. + */ + private ByteVector localVariableTypeTable; + + /** The number_of_entries field of the StackMapTable code attribute. */ + private int stackMapTableNumberOfEntries; + + /** The 'entries' array of the StackMapTable code attribute. */ + private ByteVector stackMapTableEntries; + + /** + * The last runtime visible type annotation of the Code attribute. The previous ones can be + * accessed with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastCodeRuntimeVisibleTypeAnnotation; + + /** + * The last runtime invisible type annotation of the Code attribute. The previous ones can be + * accessed with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastCodeRuntimeInvisibleTypeAnnotation; + + /** + * The first non standard attribute of the Code attribute. The next ones can be accessed with the + * {@link Attribute#nextAttribute} field. May be {@literal null}. + * + *

WARNING: this list stores the attributes in the reverse order of their visit. + * firstAttribute is actually the last attribute visited in {@link #visitAttribute}. The {@link + * #putMethodInfo} method writes the attributes in the order defined by this list, i.e. in the + * reverse order specified by the user. + */ + private Attribute firstCodeAttribute; + + // Other method_info attributes: + + /** The number_of_exceptions field of the Exceptions attribute. */ + private final int numberOfExceptions; + + /** The exception_index_table array of the Exceptions attribute, or {@literal null}. */ + private final int[] exceptionIndexTable; + + /** The signature_index field of the Signature attribute. */ + private final int signatureIndex; + + /** + * The last runtime visible annotation of this method. The previous ones can be accessed with the + * {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleAnnotation; + + /** + * The last runtime invisible annotation of this method. The previous ones can be accessed with + * the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleAnnotation; + + /** The number of method parameters that can have runtime visible annotations, or 0. */ + private int visibleAnnotableParameterCount; + + /** + * The runtime visible parameter annotations of this method. Each array element contains the last + * annotation of a parameter (which can be {@literal null} - the previous ones can be accessed + * with the {@link AnnotationWriter#previousAnnotation} field). May be {@literal null}. + */ + private AnnotationWriter[] lastRuntimeVisibleParameterAnnotations; + + /** The number of method parameters that can have runtime visible annotations, or 0. */ + private int invisibleAnnotableParameterCount; + + /** + * The runtime invisible parameter annotations of this method. Each array element contains the + * last annotation of a parameter (which can be {@literal null} - the previous ones can be + * accessed with the {@link AnnotationWriter#previousAnnotation} field). May be {@literal null}. + */ + private AnnotationWriter[] lastRuntimeInvisibleParameterAnnotations; + + /** + * The last runtime visible type annotation of this method. The previous ones can be accessed with + * the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleTypeAnnotation; + + /** + * The last runtime invisible type annotation of this method. The previous ones can be accessed + * with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleTypeAnnotation; + + /** The default_value field of the AnnotationDefault attribute, or {@literal null}. */ + private ByteVector defaultValue; + + /** The parameters_count field of the MethodParameters attribute. */ + private int parametersCount; + + /** The 'parameters' array of the MethodParameters attribute, or {@literal null}. */ + private ByteVector parameters; + + /** + * The first non standard attribute of this method. The next ones can be accessed with the {@link + * Attribute#nextAttribute} field. May be {@literal null}. + * + *

WARNING: this list stores the attributes in the reverse order of their visit. + * firstAttribute is actually the last attribute visited in {@link #visitAttribute}. The {@link + * #putMethodInfo} method writes the attributes in the order defined by this list, i.e. in the + * reverse order specified by the user. + */ + private Attribute firstAttribute; + + // ----------------------------------------------------------------------------------------------- + // Fields used to compute the maximum stack size and number of locals, and the stack map frames + // ----------------------------------------------------------------------------------------------- + + /** + * Indicates what must be computed. Must be one of {@link #COMPUTE_ALL_FRAMES}, {@link + * #COMPUTE_INSERTED_FRAMES}, {@link #COMPUTE_MAX_STACK_AND_LOCAL} or {@link #COMPUTE_NOTHING}. + */ + private final int compute; + + /** + * The first basic block of the method. The next ones (in bytecode offset order) can be accessed + * with the {@link Label#nextBasicBlock} field. + */ + private Label firstBasicBlock; + + /** + * The last basic block of the method (in bytecode offset order). This field is updated each time + * a basic block is encountered, and is used to append it at the end of the basic block list. + */ + private Label lastBasicBlock; + + /** + * The current basic block, i.e. the basic block of the last visited instruction. When {@link + * #compute} is equal to {@link #COMPUTE_MAX_STACK_AND_LOCAL} or {@link #COMPUTE_ALL_FRAMES}, this + * field is {@literal null} for unreachable code. When {@link #compute} is equal to {@link + * #COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES} or {@link #COMPUTE_INSERTED_FRAMES}, this field stays + * unchanged throughout the whole method (i.e. the whole code is seen as a single basic block; + * indeed, the existing frames are sufficient by hypothesis to compute any intermediate frame - + * and the maximum stack size as well - without using any control flow graph). + */ + private Label currentBasicBlock; + + /** + * The relative stack size after the last visited instruction. This size is relative to the + * beginning of {@link #currentBasicBlock}, i.e. the true stack size after the last visited + * instruction is equal to the {@link Label#inputStackSize} of the current basic block plus {@link + * #relativeStackSize}. When {@link #compute} is equal to {@link + * #COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES}, {@link #currentBasicBlock} is always the start of + * the method, so this relative size is also equal to the absolute stack size after the last + * visited instruction. + */ + private int relativeStackSize; + + /** + * The maximum relative stack size after the last visited instruction. This size is relative to + * the beginning of {@link #currentBasicBlock}, i.e. the true maximum stack size after the last + * visited instruction is equal to the {@link Label#inputStackSize} of the current basic block + * plus {@link #maxRelativeStackSize}.When {@link #compute} is equal to {@link + * #COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES}, {@link #currentBasicBlock} is always the start of + * the method, so this relative size is also equal to the absolute maximum stack size after the + * last visited instruction. + */ + private int maxRelativeStackSize; + + /** The number of local variables in the last visited stack map frame. */ + private int currentLocals; + + /** The bytecode offset of the last frame that was written in {@link #stackMapTableEntries}. */ + private int previousFrameOffset; + + /** + * The last frame that was written in {@link #stackMapTableEntries}. This field has the same + * format as {@link #currentFrame}. + */ + private int[] previousFrame; + + /** + * The current stack map frame. The first element contains the bytecode offset of the instruction + * to which the frame corresponds, the second element is the number of locals and the third one is + * the number of stack elements. The local variables start at index 3 and are followed by the + * operand stack elements. In summary frame[0] = offset, frame[1] = numLocal, frame[2] = numStack. + * Local variables and operand stack entries contain abstract types, as defined in {@link Frame}, + * but restricted to {@link Frame#CONSTANT_KIND}, {@link Frame#REFERENCE_KIND} or {@link + * Frame#UNINITIALIZED_KIND} abstract types. Long and double types use only one array entry. + */ + private int[] currentFrame; + + /** Whether this method contains subroutines. */ + private boolean hasSubroutines; + + // ----------------------------------------------------------------------------------------------- + // Other miscellaneous status fields + // ----------------------------------------------------------------------------------------------- + + /** Whether the bytecode of this method contains ASM specific instructions. */ + private boolean hasAsmInstructions; + + /** + * The start offset of the last visited instruction. Used to set the offset field of type + * annotations of type 'offset_target' (see JVMS + * 4.7.20.1). + */ + private int lastBytecodeOffset; + + /** + * The offset in bytes in {@link SymbolTable#getSource} from which the method_info for this method + * (excluding its first 6 bytes) must be copied, or 0. + */ + private int sourceOffset; + + /** + * The length in bytes in {@link SymbolTable#getSource} which must be copied to get the + * method_info for this method (excluding its first 6 bytes for access_flags, name_index and + * descriptor_index). + */ + private int sourceLength; + + // ----------------------------------------------------------------------------------------------- + // Constructor and accessors + // ----------------------------------------------------------------------------------------------- + + /** + * Constructs a new {@link MethodWriter}. + * + * @param symbolTable where the constants used in this AnnotationWriter must be stored. + * @param access the method's access flags (see {@link Opcodes}). + * @param name the method's name. + * @param descriptor the method's descriptor (see {@link Type}). + * @param signature the method's signature. May be {@literal null}. + * @param exceptions the internal names of the method's exceptions. May be {@literal null}. + * @param compute indicates what must be computed (see #compute). + */ + MethodWriter( + final SymbolTable symbolTable, + final int access, + final String name, + final String descriptor, + final String signature, + final String[] exceptions, + final int compute) { + super(/* latest api = */ Opcodes.ASM9); + this.symbolTable = symbolTable; + this.accessFlags = "".equals(name) ? access | Constants.ACC_CONSTRUCTOR : access; + this.nameIndex = symbolTable.addConstantUtf8(name); + this.name = name; + this.descriptorIndex = symbolTable.addConstantUtf8(descriptor); + this.descriptor = descriptor; + this.signatureIndex = signature == null ? 0 : symbolTable.addConstantUtf8(signature); + if (exceptions != null && exceptions.length > 0) { + numberOfExceptions = exceptions.length; + this.exceptionIndexTable = new int[numberOfExceptions]; + for (int i = 0; i < numberOfExceptions; ++i) { + this.exceptionIndexTable[i] = symbolTable.addConstantClass(exceptions[i]).index; + } + } else { + numberOfExceptions = 0; + this.exceptionIndexTable = null; + } + this.compute = compute; + if (compute != COMPUTE_NOTHING) { + // Update maxLocals and currentLocals. + int argumentsSize = Type.getArgumentsAndReturnSizes(descriptor) >> 2; + if ((access & Opcodes.ACC_STATIC) != 0) { + --argumentsSize; + } + maxLocals = argumentsSize; + currentLocals = argumentsSize; + // Create and visit the label for the first basic block. + firstBasicBlock = new Label(); + visitLabel(firstBasicBlock); + } + } + + boolean hasFrames() { + return stackMapTableNumberOfEntries > 0; + } + + boolean hasAsmInstructions() { + return hasAsmInstructions; + } + + // ----------------------------------------------------------------------------------------------- + // Implementation of the MethodVisitor abstract class + // ----------------------------------------------------------------------------------------------- + + @Override + public void visitParameter(final String name, final int access) { + if (parameters == null) { + parameters = new ByteVector(); + } + ++parametersCount; + parameters.putShort((name == null) ? 0 : symbolTable.addConstantUtf8(name)).putShort(access); + } + + @Override + public AnnotationVisitor visitAnnotationDefault() { + defaultValue = new ByteVector(); + return new AnnotationWriter(symbolTable, /* useNamedValues= */ false, defaultValue, null); + } + + @Override + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeVisibleAnnotation); + } else { + return lastRuntimeInvisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeInvisibleAnnotation); + } + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeVisibleTypeAnnotation); + } else { + return lastRuntimeInvisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public void visitAnnotableParameterCount(final int parameterCount, final boolean visible) { + if (visible) { + visibleAnnotableParameterCount = parameterCount; + } else { + invisibleAnnotableParameterCount = parameterCount; + } + } + + @Override + public AnnotationVisitor visitParameterAnnotation( + final int parameter, final String annotationDescriptor, final boolean visible) { + if (visible) { + if (lastRuntimeVisibleParameterAnnotations == null) { + lastRuntimeVisibleParameterAnnotations = + new AnnotationWriter[Type.getArgumentTypes(descriptor).length]; + } + return lastRuntimeVisibleParameterAnnotations[parameter] = + AnnotationWriter.create( + symbolTable, annotationDescriptor, lastRuntimeVisibleParameterAnnotations[parameter]); + } else { + if (lastRuntimeInvisibleParameterAnnotations == null) { + lastRuntimeInvisibleParameterAnnotations = + new AnnotationWriter[Type.getArgumentTypes(descriptor).length]; + } + return lastRuntimeInvisibleParameterAnnotations[parameter] = + AnnotationWriter.create( + symbolTable, + annotationDescriptor, + lastRuntimeInvisibleParameterAnnotations[parameter]); + } + } + + @Override + public void visitAttribute(final Attribute attribute) { + // Store the attributes in the reverse order of their visit by this method. + if (attribute.isCodeAttribute()) { + attribute.nextAttribute = firstCodeAttribute; + firstCodeAttribute = attribute; + } else { + attribute.nextAttribute = firstAttribute; + firstAttribute = attribute; + } + } + + @Override + public void visitCode() { + // Nothing to do. + } + + @Override + public void visitFrame( + final int type, + final int numLocal, + final Object[] local, + final int numStack, + final Object[] stack) { + if (compute == COMPUTE_ALL_FRAMES) { + return; + } + + if (compute == COMPUTE_INSERTED_FRAMES) { + if (currentBasicBlock.frame == null) { + // This should happen only once, for the implicit first frame (which is explicitly visited + // in ClassReader if the EXPAND_ASM_INSNS option is used - and COMPUTE_INSERTED_FRAMES + // can't be set if EXPAND_ASM_INSNS is not used). + currentBasicBlock.frame = new CurrentFrame(currentBasicBlock); + currentBasicBlock.frame.setInputFrameFromDescriptor( + symbolTable, accessFlags, descriptor, numLocal); + currentBasicBlock.frame.accept(this); + } else { + if (type == Opcodes.F_NEW) { + currentBasicBlock.frame.setInputFrameFromApiFormat( + symbolTable, numLocal, local, numStack, stack); + } + // If type is not F_NEW then it is F_INSERT by hypothesis, and currentBlock.frame contains + // the stack map frame at the current instruction, computed from the last F_NEW frame and + // the bytecode instructions in between (via calls to CurrentFrame#execute). + currentBasicBlock.frame.accept(this); + } + } else if (type == Opcodes.F_NEW) { + if (previousFrame == null) { + int argumentsSize = Type.getArgumentsAndReturnSizes(descriptor) >> 2; + Frame implicitFirstFrame = new Frame(new Label()); + implicitFirstFrame.setInputFrameFromDescriptor( + symbolTable, accessFlags, descriptor, argumentsSize); + implicitFirstFrame.accept(this); + } + currentLocals = numLocal; + int frameIndex = visitFrameStart(code.length, numLocal, numStack); + for (int i = 0; i < numLocal; ++i) { + currentFrame[frameIndex++] = Frame.getAbstractTypeFromApiFormat(symbolTable, local[i]); + } + for (int i = 0; i < numStack; ++i) { + currentFrame[frameIndex++] = Frame.getAbstractTypeFromApiFormat(symbolTable, stack[i]); + } + visitFrameEnd(); + } else { + if (symbolTable.getMajorVersion() < Opcodes.V1_6) { + throw new IllegalArgumentException("Class versions V1_5 or less must use F_NEW frames."); + } + int offsetDelta; + if (stackMapTableEntries == null) { + stackMapTableEntries = new ByteVector(); + offsetDelta = code.length; + } else { + offsetDelta = code.length - previousFrameOffset - 1; + if (offsetDelta < 0) { + if (type == Opcodes.F_SAME) { + return; + } else { + throw new IllegalStateException(); + } + } + } + + switch (type) { + case Opcodes.F_FULL: + currentLocals = numLocal; + stackMapTableEntries.putByte(Frame.FULL_FRAME).putShort(offsetDelta).putShort(numLocal); + for (int i = 0; i < numLocal; ++i) { + putFrameType(local[i]); + } + stackMapTableEntries.putShort(numStack); + for (int i = 0; i < numStack; ++i) { + putFrameType(stack[i]); + } + break; + case Opcodes.F_APPEND: + currentLocals += numLocal; + stackMapTableEntries.putByte(Frame.SAME_FRAME_EXTENDED + numLocal).putShort(offsetDelta); + for (int i = 0; i < numLocal; ++i) { + putFrameType(local[i]); + } + break; + case Opcodes.F_CHOP: + currentLocals -= numLocal; + stackMapTableEntries.putByte(Frame.SAME_FRAME_EXTENDED - numLocal).putShort(offsetDelta); + break; + case Opcodes.F_SAME: + if (offsetDelta < 64) { + stackMapTableEntries.putByte(offsetDelta); + } else { + stackMapTableEntries.putByte(Frame.SAME_FRAME_EXTENDED).putShort(offsetDelta); + } + break; + case Opcodes.F_SAME1: + if (offsetDelta < 64) { + stackMapTableEntries.putByte(Frame.SAME_LOCALS_1_STACK_ITEM_FRAME + offsetDelta); + } else { + stackMapTableEntries + .putByte(Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED) + .putShort(offsetDelta); + } + putFrameType(stack[0]); + break; + default: + throw new IllegalArgumentException(); + } + + previousFrameOffset = code.length; + ++stackMapTableNumberOfEntries; + } + + if (compute == COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES) { + relativeStackSize = numStack; + for (int i = 0; i < numStack; ++i) { + if (stack[i] == Opcodes.LONG || stack[i] == Opcodes.DOUBLE) { + relativeStackSize++; + } + } + if (relativeStackSize > maxRelativeStackSize) { + maxRelativeStackSize = relativeStackSize; + } + } + + maxStack = Math.max(maxStack, numStack); + maxLocals = Math.max(maxLocals, currentLocals); + } + + @Override + public void visitInsn(final int opcode) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + code.putByte(opcode); + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(opcode, 0, null, null); + } else { + int size = relativeStackSize + STACK_SIZE_DELTA[opcode]; + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { + endCurrentBasicBlockWithNoSuccessor(); + } + } + } + + @Override + public void visitIntInsn(final int opcode, final int operand) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + if (opcode == Opcodes.SIPUSH) { + code.put12(opcode, operand); + } else { // BIPUSH or NEWARRAY + code.put11(opcode, operand); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(opcode, operand, null, null); + } else if (opcode != Opcodes.NEWARRAY) { + // The stack size delta is 1 for BIPUSH or SIPUSH, and 0 for NEWARRAY. + int size = relativeStackSize + 1; + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + + @Override + public void visitVarInsn(final int opcode, final int var) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + if (var < 4 && opcode != Opcodes.RET) { + int optimizedOpcode; + if (opcode < Opcodes.ISTORE) { + optimizedOpcode = Constants.ILOAD_0 + ((opcode - Opcodes.ILOAD) << 2) + var; + } else { + optimizedOpcode = Constants.ISTORE_0 + ((opcode - Opcodes.ISTORE) << 2) + var; + } + code.putByte(optimizedOpcode); + } else if (var >= 256) { + code.putByte(Constants.WIDE).put12(opcode, var); + } else { + code.put11(opcode, var); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(opcode, var, null, null); + } else { + if (opcode == Opcodes.RET) { + // No stack size delta. + currentBasicBlock.flags |= Label.FLAG_SUBROUTINE_END; + currentBasicBlock.outputStackSize = (short) relativeStackSize; + endCurrentBasicBlockWithNoSuccessor(); + } else { // xLOAD or xSTORE + int size = relativeStackSize + STACK_SIZE_DELTA[opcode]; + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + if (compute != COMPUTE_NOTHING) { + int currentMaxLocals; + if (opcode == Opcodes.LLOAD + || opcode == Opcodes.DLOAD + || opcode == Opcodes.LSTORE + || opcode == Opcodes.DSTORE) { + currentMaxLocals = var + 2; + } else { + currentMaxLocals = var + 1; + } + if (currentMaxLocals > maxLocals) { + maxLocals = currentMaxLocals; + } + } + if (opcode >= Opcodes.ISTORE && compute == COMPUTE_ALL_FRAMES && firstHandler != null) { + // If there are exception handler blocks, each instruction within a handler range is, in + // theory, a basic block (since execution can jump from this instruction to the exception + // handler). As a consequence, the local variable types at the beginning of the handler + // block should be the merge of the local variable types at all the instructions within the + // handler range. However, instead of creating a basic block for each instruction, we can + // get the same result in a more efficient way. Namely, by starting a new basic block after + // each xSTORE instruction, which is what we do here. + visitLabel(new Label()); + } + } + + @Override + public void visitTypeInsn(final int opcode, final String type) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + Symbol typeSymbol = symbolTable.addConstantClass(type); + code.put12(opcode, typeSymbol.index); + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(opcode, lastBytecodeOffset, typeSymbol, symbolTable); + } else if (opcode == Opcodes.NEW) { + // The stack size delta is 1 for NEW, and 0 for ANEWARRAY, CHECKCAST, or INSTANCEOF. + int size = relativeStackSize + 1; + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + + @Override + public void visitFieldInsn( + final int opcode, final String owner, final String name, final String descriptor) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + Symbol fieldrefSymbol = symbolTable.addConstantFieldref(owner, name, descriptor); + code.put12(opcode, fieldrefSymbol.index); + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(opcode, 0, fieldrefSymbol, symbolTable); + } else { + int size; + char firstDescChar = descriptor.charAt(0); + switch (opcode) { + case Opcodes.GETSTATIC: + size = relativeStackSize + (firstDescChar == 'D' || firstDescChar == 'J' ? 2 : 1); + break; + case Opcodes.PUTSTATIC: + size = relativeStackSize + (firstDescChar == 'D' || firstDescChar == 'J' ? -2 : -1); + break; + case Opcodes.GETFIELD: + size = relativeStackSize + (firstDescChar == 'D' || firstDescChar == 'J' ? 1 : 0); + break; + case Opcodes.PUTFIELD: + default: + size = relativeStackSize + (firstDescChar == 'D' || firstDescChar == 'J' ? -3 : -2); + break; + } + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + + @Override + public void visitMethodInsn( + final int opcode, + final String owner, + final String name, + final String descriptor, + final boolean isInterface) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + Symbol methodrefSymbol = symbolTable.addConstantMethodref(owner, name, descriptor, isInterface); + if (opcode == Opcodes.INVOKEINTERFACE) { + code.put12(Opcodes.INVOKEINTERFACE, methodrefSymbol.index) + .put11(methodrefSymbol.getArgumentsAndReturnSizes() >> 2, 0); + } else { + code.put12(opcode, methodrefSymbol.index); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(opcode, 0, methodrefSymbol, symbolTable); + } else { + int argumentsAndReturnSize = methodrefSymbol.getArgumentsAndReturnSizes(); + int stackSizeDelta = (argumentsAndReturnSize & 3) - (argumentsAndReturnSize >> 2); + int size; + if (opcode == Opcodes.INVOKESTATIC) { + size = relativeStackSize + stackSizeDelta + 1; + } else { + size = relativeStackSize + stackSizeDelta; + } + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + + @Override + public void visitInvokeDynamicInsn( + final String name, + final String descriptor, + final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + Symbol invokeDynamicSymbol = + symbolTable.addConstantInvokeDynamic( + name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + code.put12(Opcodes.INVOKEDYNAMIC, invokeDynamicSymbol.index); + code.putShort(0); + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(Opcodes.INVOKEDYNAMIC, 0, invokeDynamicSymbol, symbolTable); + } else { + int argumentsAndReturnSize = invokeDynamicSymbol.getArgumentsAndReturnSizes(); + int stackSizeDelta = (argumentsAndReturnSize & 3) - (argumentsAndReturnSize >> 2) + 1; + int size = relativeStackSize + stackSizeDelta; + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + + @Override + public void visitJumpInsn(final int opcode, final Label label) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + // Compute the 'service' opcode, i.e. GOTO or JSR if opcode is GOTO_W or JSR_W, otherwise + // opcode. + int baseOpcode = + opcode >= Constants.GOTO_W ? opcode - Constants.WIDE_JUMP_OPCODE_DELTA : opcode; + boolean nextInsnIsJumpTarget = false; + if ((label.flags & Label.FLAG_RESOLVED) != 0 + && label.bytecodeOffset - code.length < Short.MIN_VALUE) { + // Case of a backward jump with an offset < -32768. In this case we automatically replace GOTO + // with GOTO_W, JSR with JSR_W and IFxxx with IFNOTxxx GOTO_W L:..., where + // IFNOTxxx is the "opposite" opcode of IFxxx (e.g. IFNE for IFEQ) and where designates + // the instruction just after the GOTO_W. + if (baseOpcode == Opcodes.GOTO) { + code.putByte(Constants.GOTO_W); + } else if (baseOpcode == Opcodes.JSR) { + code.putByte(Constants.JSR_W); + } else { + // Put the "opposite" opcode of baseOpcode. This can be done by flipping the least + // significant bit for IFNULL and IFNONNULL, and similarly for IFEQ ... IF_ACMPEQ (with a + // pre and post offset by 1). The jump offset is 8 bytes (3 for IFNOTxxx, 5 for GOTO_W). + code.putByte(baseOpcode >= Opcodes.IFNULL ? baseOpcode ^ 1 : ((baseOpcode + 1) ^ 1) - 1); + code.putShort(8); + // Here we could put a GOTO_W in theory, but if ASM specific instructions are used in this + // method or another one, and if the class has frames, we will need to insert a frame after + // this GOTO_W during the additional ClassReader -> ClassWriter round trip to remove the ASM + // specific instructions. To not miss this additional frame, we need to use an ASM_GOTO_W + // here, which has the unfortunate effect of forcing this additional round trip (which in + // some case would not have been really necessary, but we can't know this at this point). + code.putByte(Constants.ASM_GOTO_W); + hasAsmInstructions = true; + // The instruction after the GOTO_W becomes the target of the IFNOT instruction. + nextInsnIsJumpTarget = true; + } + label.put(code, code.length - 1, true); + } else if (baseOpcode != opcode) { + // Case of a GOTO_W or JSR_W specified by the user (normally ClassReader when used to remove + // ASM specific instructions). In this case we keep the original instruction. + code.putByte(opcode); + label.put(code, code.length - 1, true); + } else { + // Case of a jump with an offset >= -32768, or of a jump with an unknown offset. In these + // cases we store the offset in 2 bytes (which will be increased via a ClassReader -> + // ClassWriter round trip if it turns out that 2 bytes are not sufficient). + code.putByte(baseOpcode); + label.put(code, code.length - 1, false); + } + + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + Label nextBasicBlock = null; + if (compute == COMPUTE_ALL_FRAMES) { + currentBasicBlock.frame.execute(baseOpcode, 0, null, null); + // Record the fact that 'label' is the target of a jump instruction. + label.getCanonicalInstance().flags |= Label.FLAG_JUMP_TARGET; + // Add 'label' as a successor of the current basic block. + addSuccessorToCurrentBasicBlock(Edge.JUMP, label); + if (baseOpcode != Opcodes.GOTO) { + // The next instruction starts a new basic block (except for GOTO: by default the code + // following a goto is unreachable - unless there is an explicit label for it - and we + // should not compute stack frame types for its instructions). + nextBasicBlock = new Label(); + } + } else if (compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(baseOpcode, 0, null, null); + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES) { + // No need to update maxRelativeStackSize (the stack size delta is always negative). + relativeStackSize += STACK_SIZE_DELTA[baseOpcode]; + } else { + if (baseOpcode == Opcodes.JSR) { + // Record the fact that 'label' designates a subroutine, if not already done. + if ((label.flags & Label.FLAG_SUBROUTINE_START) == 0) { + label.flags |= Label.FLAG_SUBROUTINE_START; + hasSubroutines = true; + } + currentBasicBlock.flags |= Label.FLAG_SUBROUTINE_CALLER; + // Note that, by construction in this method, a block which calls a subroutine has at + // least two successors in the control flow graph: the first one (added below) leads to + // the instruction after the JSR, while the second one (added here) leads to the JSR + // target. Note that the first successor is virtual (it does not correspond to a possible + // execution path): it is only used to compute the successors of the basic blocks ending + // with a ret, in {@link Label#addSubroutineRetSuccessors}. + addSuccessorToCurrentBasicBlock(relativeStackSize + 1, label); + // The instruction after the JSR starts a new basic block. + nextBasicBlock = new Label(); + } else { + // No need to update maxRelativeStackSize (the stack size delta is always negative). + relativeStackSize += STACK_SIZE_DELTA[baseOpcode]; + addSuccessorToCurrentBasicBlock(relativeStackSize, label); + } + } + // If the next instruction starts a new basic block, call visitLabel to add the label of this + // instruction as a successor of the current block, and to start a new basic block. + if (nextBasicBlock != null) { + if (nextInsnIsJumpTarget) { + nextBasicBlock.flags |= Label.FLAG_JUMP_TARGET; + } + visitLabel(nextBasicBlock); + } + if (baseOpcode == Opcodes.GOTO) { + endCurrentBasicBlockWithNoSuccessor(); + } + } + } + + @Override + public void visitLabel(final Label label) { + // Resolve the forward references to this label, if any. + hasAsmInstructions |= label.resolve(code.data, code.length); + // visitLabel starts a new basic block (except for debug only labels), so we need to update the + // previous and current block references and list of successors. + if ((label.flags & Label.FLAG_DEBUG_ONLY) != 0) { + return; + } + if (compute == COMPUTE_ALL_FRAMES) { + if (currentBasicBlock != null) { + if (label.bytecodeOffset == currentBasicBlock.bytecodeOffset) { + // We use {@link Label#getCanonicalInstance} to store the state of a basic block in only + // one place, but this does not work for labels which have not been visited yet. + // Therefore, when we detect here two labels having the same bytecode offset, we need to + // - consolidate the state scattered in these two instances into the canonical instance: + currentBasicBlock.flags |= (label.flags & Label.FLAG_JUMP_TARGET); + // - make sure the two instances share the same Frame instance (the implementation of + // {@link Label#getCanonicalInstance} relies on this property; here label.frame should be + // null): + label.frame = currentBasicBlock.frame; + // - and make sure to NOT assign 'label' into 'currentBasicBlock' or 'lastBasicBlock', so + // that they still refer to the canonical instance for this bytecode offset. + return; + } + // End the current basic block (with one new successor). + addSuccessorToCurrentBasicBlock(Edge.JUMP, label); + } + // Append 'label' at the end of the basic block list. + if (lastBasicBlock != null) { + if (label.bytecodeOffset == lastBasicBlock.bytecodeOffset) { + // Same comment as above. + lastBasicBlock.flags |= (label.flags & Label.FLAG_JUMP_TARGET); + // Here label.frame should be null. + label.frame = lastBasicBlock.frame; + currentBasicBlock = lastBasicBlock; + return; + } + lastBasicBlock.nextBasicBlock = label; + } + lastBasicBlock = label; + // Make it the new current basic block. + currentBasicBlock = label; + // Here label.frame should be null. + label.frame = new Frame(label); + } else if (compute == COMPUTE_INSERTED_FRAMES) { + if (currentBasicBlock == null) { + // This case should happen only once, for the visitLabel call in the constructor. Indeed, if + // compute is equal to COMPUTE_INSERTED_FRAMES, currentBasicBlock stays unchanged. + currentBasicBlock = label; + } else { + // Update the frame owner so that a correct frame offset is computed in Frame.accept(). + currentBasicBlock.frame.owner = label; + } + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL) { + if (currentBasicBlock != null) { + // End the current basic block (with one new successor). + currentBasicBlock.outputStackMax = (short) maxRelativeStackSize; + addSuccessorToCurrentBasicBlock(relativeStackSize, label); + } + // Start a new current basic block, and reset the current and maximum relative stack sizes. + currentBasicBlock = label; + relativeStackSize = 0; + maxRelativeStackSize = 0; + // Append the new basic block at the end of the basic block list. + if (lastBasicBlock != null) { + lastBasicBlock.nextBasicBlock = label; + } + lastBasicBlock = label; + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES && currentBasicBlock == null) { + // This case should happen only once, for the visitLabel call in the constructor. Indeed, if + // compute is equal to COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES, currentBasicBlock stays + // unchanged. + currentBasicBlock = label; + } + } + + @Override + public void visitLdcInsn(final Object value) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + Symbol constantSymbol = symbolTable.addConstant(value); + int constantIndex = constantSymbol.index; + char firstDescriptorChar; + boolean isLongOrDouble = + constantSymbol.tag == Symbol.CONSTANT_LONG_TAG + || constantSymbol.tag == Symbol.CONSTANT_DOUBLE_TAG + || (constantSymbol.tag == Symbol.CONSTANT_DYNAMIC_TAG + && ((firstDescriptorChar = constantSymbol.value.charAt(0)) == 'J' + || firstDescriptorChar == 'D')); + if (isLongOrDouble) { + code.put12(Constants.LDC2_W, constantIndex); + } else if (constantIndex >= 256) { + code.put12(Constants.LDC_W, constantIndex); + } else { + code.put11(Opcodes.LDC, constantIndex); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(Opcodes.LDC, 0, constantSymbol, symbolTable); + } else { + int size = relativeStackSize + (isLongOrDouble ? 2 : 1); + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + + @Override + public void visitIincInsn(final int var, final int increment) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + if ((var > 255) || (increment > 127) || (increment < -128)) { + code.putByte(Constants.WIDE).put12(Opcodes.IINC, var).putShort(increment); + } else { + code.putByte(Opcodes.IINC).put11(var, increment); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null + && (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES)) { + currentBasicBlock.frame.execute(Opcodes.IINC, var, null, null); + } + if (compute != COMPUTE_NOTHING) { + int currentMaxLocals = var + 1; + if (currentMaxLocals > maxLocals) { + maxLocals = currentMaxLocals; + } + } + } + + @Override + public void visitTableSwitchInsn( + final int min, final int max, final Label dflt, final Label... labels) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + code.putByte(Opcodes.TABLESWITCH).putByteArray(null, 0, (4 - code.length % 4) % 4); + dflt.put(code, lastBytecodeOffset, true); + code.putInt(min).putInt(max); + for (Label label : labels) { + label.put(code, lastBytecodeOffset, true); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + visitSwitchInsn(dflt, labels); + } + + @Override + public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + code.putByte(Opcodes.LOOKUPSWITCH).putByteArray(null, 0, (4 - code.length % 4) % 4); + dflt.put(code, lastBytecodeOffset, true); + code.putInt(labels.length); + for (int i = 0; i < labels.length; ++i) { + code.putInt(keys[i]); + labels[i].put(code, lastBytecodeOffset, true); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + visitSwitchInsn(dflt, labels); + } + + private void visitSwitchInsn(final Label dflt, final Label[] labels) { + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES) { + currentBasicBlock.frame.execute(Opcodes.LOOKUPSWITCH, 0, null, null); + // Add all the labels as successors of the current basic block. + addSuccessorToCurrentBasicBlock(Edge.JUMP, dflt); + dflt.getCanonicalInstance().flags |= Label.FLAG_JUMP_TARGET; + for (Label label : labels) { + addSuccessorToCurrentBasicBlock(Edge.JUMP, label); + label.getCanonicalInstance().flags |= Label.FLAG_JUMP_TARGET; + } + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL) { + // No need to update maxRelativeStackSize (the stack size delta is always negative). + --relativeStackSize; + // Add all the labels as successors of the current basic block. + addSuccessorToCurrentBasicBlock(relativeStackSize, dflt); + for (Label label : labels) { + addSuccessorToCurrentBasicBlock(relativeStackSize, label); + } + } + // End the current basic block. + endCurrentBasicBlockWithNoSuccessor(); + } + } + + @Override + public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + Symbol descSymbol = symbolTable.addConstantClass(descriptor); + code.put12(Opcodes.MULTIANEWARRAY, descSymbol.index).putByte(numDimensions); + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute( + Opcodes.MULTIANEWARRAY, numDimensions, descSymbol, symbolTable); + } else { + // No need to update maxRelativeStackSize (the stack size delta is always negative). + relativeStackSize += 1 - numDimensions; + } + } + } + + @Override + public AnnotationVisitor visitInsnAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (visible) { + return lastCodeRuntimeVisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, + (typeRef & 0xFF0000FF) | (lastBytecodeOffset << 8), + typePath, + descriptor, + lastCodeRuntimeVisibleTypeAnnotation); + } else { + return lastCodeRuntimeInvisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, + (typeRef & 0xFF0000FF) | (lastBytecodeOffset << 8), + typePath, + descriptor, + lastCodeRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public void visitTryCatchBlock( + final Label start, final Label end, final Label handler, final String type) { + Handler newHandler = + new Handler( + start, end, handler, type != null ? symbolTable.addConstantClass(type).index : 0, type); + if (firstHandler == null) { + firstHandler = newHandler; + } else { + lastHandler.nextHandler = newHandler; + } + lastHandler = newHandler; + } + + @Override + public AnnotationVisitor visitTryCatchAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (visible) { + return lastCodeRuntimeVisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastCodeRuntimeVisibleTypeAnnotation); + } else { + return lastCodeRuntimeInvisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastCodeRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public void visitLocalVariable( + final String name, + final String descriptor, + final String signature, + final Label start, + final Label end, + final int index) { + if (signature != null) { + if (localVariableTypeTable == null) { + localVariableTypeTable = new ByteVector(); + } + ++localVariableTypeTableLength; + localVariableTypeTable + .putShort(start.bytecodeOffset) + .putShort(end.bytecodeOffset - start.bytecodeOffset) + .putShort(symbolTable.addConstantUtf8(name)) + .putShort(symbolTable.addConstantUtf8(signature)) + .putShort(index); + } + if (localVariableTable == null) { + localVariableTable = new ByteVector(); + } + ++localVariableTableLength; + localVariableTable + .putShort(start.bytecodeOffset) + .putShort(end.bytecodeOffset - start.bytecodeOffset) + .putShort(symbolTable.addConstantUtf8(name)) + .putShort(symbolTable.addConstantUtf8(descriptor)) + .putShort(index); + if (compute != COMPUTE_NOTHING) { + char firstDescChar = descriptor.charAt(0); + int currentMaxLocals = index + (firstDescChar == 'J' || firstDescChar == 'D' ? 2 : 1); + if (currentMaxLocals > maxLocals) { + maxLocals = currentMaxLocals; + } + } + } + + @Override + public AnnotationVisitor visitLocalVariableAnnotation( + final int typeRef, + final TypePath typePath, + final Label[] start, + final Label[] end, + final int[] index, + final String descriptor, + final boolean visible) { + // Create a ByteVector to hold a 'type_annotation' JVMS structure. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.20. + ByteVector typeAnnotation = new ByteVector(); + // Write target_type, target_info, and target_path. + typeAnnotation.putByte(typeRef >>> 24).putShort(start.length); + for (int i = 0; i < start.length; ++i) { + typeAnnotation + .putShort(start[i].bytecodeOffset) + .putShort(end[i].bytecodeOffset - start[i].bytecodeOffset) + .putShort(index[i]); + } + TypePath.put(typePath, typeAnnotation); + // Write type_index and reserve space for num_element_value_pairs. + typeAnnotation.putShort(symbolTable.addConstantUtf8(descriptor)).putShort(0); + if (visible) { + return lastCodeRuntimeVisibleTypeAnnotation = + new AnnotationWriter( + symbolTable, + /* useNamedValues= */ true, + typeAnnotation, + lastCodeRuntimeVisibleTypeAnnotation); + } else { + return lastCodeRuntimeInvisibleTypeAnnotation = + new AnnotationWriter( + symbolTable, + /* useNamedValues= */ true, + typeAnnotation, + lastCodeRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public void visitLineNumber(final int line, final Label start) { + if (lineNumberTable == null) { + lineNumberTable = new ByteVector(); + } + ++lineNumberTableLength; + lineNumberTable.putShort(start.bytecodeOffset); + lineNumberTable.putShort(line); + } + + @Override + public void visitMaxs(final int maxStack, final int maxLocals) { + if (compute == COMPUTE_ALL_FRAMES) { + computeAllFrames(); + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL) { + computeMaxStackAndLocal(); + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES) { + this.maxStack = maxRelativeStackSize; + } else { + this.maxStack = maxStack; + this.maxLocals = maxLocals; + } + } + + /** Computes all the stack map frames of the method, from scratch. */ + private void computeAllFrames() { + // Complete the control flow graph with exception handler blocks. + Handler handler = firstHandler; + while (handler != null) { + String catchTypeDescriptor = + handler.catchTypeDescriptor == null ? "java/lang/Throwable" : handler.catchTypeDescriptor; + int catchType = Frame.getAbstractTypeFromInternalName(symbolTable, catchTypeDescriptor); + // Mark handlerBlock as an exception handler. + Label handlerBlock = handler.handlerPc.getCanonicalInstance(); + handlerBlock.flags |= Label.FLAG_JUMP_TARGET; + // Add handlerBlock as a successor of all the basic blocks in the exception handler range. + Label handlerRangeBlock = handler.startPc.getCanonicalInstance(); + Label handlerRangeEnd = handler.endPc.getCanonicalInstance(); + while (handlerRangeBlock != handlerRangeEnd) { + handlerRangeBlock.outgoingEdges = + new Edge(catchType, handlerBlock, handlerRangeBlock.outgoingEdges); + handlerRangeBlock = handlerRangeBlock.nextBasicBlock; + } + handler = handler.nextHandler; + } + + // Create and visit the first (implicit) frame. + Frame firstFrame = firstBasicBlock.frame; + firstFrame.setInputFrameFromDescriptor(symbolTable, accessFlags, descriptor, this.maxLocals); + firstFrame.accept(this); + + // Fix point algorithm: add the first basic block to a list of blocks to process (i.e. blocks + // whose stack map frame has changed) and, while there are blocks to process, remove one from + // the list and update the stack map frames of its successor blocks in the control flow graph + // (which might change them, in which case these blocks must be processed too, and are thus + // added to the list of blocks to process). Also compute the maximum stack size of the method, + // as a by-product. + Label listOfBlocksToProcess = firstBasicBlock; + listOfBlocksToProcess.nextListElement = Label.EMPTY_LIST; + int maxStackSize = 0; + while (listOfBlocksToProcess != Label.EMPTY_LIST) { + // Remove a basic block from the list of blocks to process. + Label basicBlock = listOfBlocksToProcess; + listOfBlocksToProcess = listOfBlocksToProcess.nextListElement; + basicBlock.nextListElement = null; + // By definition, basicBlock is reachable. + basicBlock.flags |= Label.FLAG_REACHABLE; + // Update the (absolute) maximum stack size. + int maxBlockStackSize = basicBlock.frame.getInputStackSize() + basicBlock.outputStackMax; + if (maxBlockStackSize > maxStackSize) { + maxStackSize = maxBlockStackSize; + } + // Update the successor blocks of basicBlock in the control flow graph. + Edge outgoingEdge = basicBlock.outgoingEdges; + while (outgoingEdge != null) { + Label successorBlock = outgoingEdge.successor.getCanonicalInstance(); + boolean successorBlockChanged = + basicBlock.frame.merge(symbolTable, successorBlock.frame, outgoingEdge.info); + if (successorBlockChanged && successorBlock.nextListElement == null) { + // If successorBlock has changed it must be processed. Thus, if it is not already in the + // list of blocks to process, add it to this list. + successorBlock.nextListElement = listOfBlocksToProcess; + listOfBlocksToProcess = successorBlock; + } + outgoingEdge = outgoingEdge.nextEdge; + } + } + + // Loop over all the basic blocks and visit the stack map frames that must be stored in the + // StackMapTable attribute. Also replace unreachable code with NOP* ATHROW, and remove it from + // exception handler ranges. + Label basicBlock = firstBasicBlock; + while (basicBlock != null) { + if ((basicBlock.flags & (Label.FLAG_JUMP_TARGET | Label.FLAG_REACHABLE)) + == (Label.FLAG_JUMP_TARGET | Label.FLAG_REACHABLE)) { + basicBlock.frame.accept(this); + } + if ((basicBlock.flags & Label.FLAG_REACHABLE) == 0) { + // Find the start and end bytecode offsets of this unreachable block. + Label nextBasicBlock = basicBlock.nextBasicBlock; + int startOffset = basicBlock.bytecodeOffset; + int endOffset = (nextBasicBlock == null ? code.length : nextBasicBlock.bytecodeOffset) - 1; + if (endOffset >= startOffset) { + // Replace its instructions with NOP ... NOP ATHROW. + for (int i = startOffset; i < endOffset; ++i) { + code.data[i] = Opcodes.NOP; + } + code.data[endOffset] = (byte) Opcodes.ATHROW; + // Emit a frame for this unreachable block, with no local and a Throwable on the stack + // (so that the ATHROW could consume this Throwable if it were reachable). + int frameIndex = visitFrameStart(startOffset, /* numLocal= */ 0, /* numStack= */ 1); + currentFrame[frameIndex] = + Frame.getAbstractTypeFromInternalName(symbolTable, "java/lang/Throwable"); + visitFrameEnd(); + // Remove this unreachable basic block from the exception handler ranges. + firstHandler = Handler.removeRange(firstHandler, basicBlock, nextBasicBlock); + // The maximum stack size is now at least one, because of the Throwable declared above. + maxStackSize = Math.max(maxStackSize, 1); + } + } + basicBlock = basicBlock.nextBasicBlock; + } + + this.maxStack = maxStackSize; + } + + /** Computes the maximum stack size of the method. */ + private void computeMaxStackAndLocal() { + // Complete the control flow graph with exception handler blocks. + Handler handler = firstHandler; + while (handler != null) { + Label handlerBlock = handler.handlerPc; + Label handlerRangeBlock = handler.startPc; + Label handlerRangeEnd = handler.endPc; + // Add handlerBlock as a successor of all the basic blocks in the exception handler range. + while (handlerRangeBlock != handlerRangeEnd) { + if ((handlerRangeBlock.flags & Label.FLAG_SUBROUTINE_CALLER) == 0) { + handlerRangeBlock.outgoingEdges = + new Edge(Edge.EXCEPTION, handlerBlock, handlerRangeBlock.outgoingEdges); + } else { + // If handlerRangeBlock is a JSR block, add handlerBlock after the first two outgoing + // edges to preserve the hypothesis about JSR block successors order (see + // {@link #visitJumpInsn}). + handlerRangeBlock.outgoingEdges.nextEdge.nextEdge = + new Edge( + Edge.EXCEPTION, handlerBlock, handlerRangeBlock.outgoingEdges.nextEdge.nextEdge); + } + handlerRangeBlock = handlerRangeBlock.nextBasicBlock; + } + handler = handler.nextHandler; + } + + // Complete the control flow graph with the successor blocks of subroutines, if needed. + if (hasSubroutines) { + // First step: find the subroutines. This step determines, for each basic block, to which + // subroutine(s) it belongs. Start with the main "subroutine": + short numSubroutines = 1; + firstBasicBlock.markSubroutine(numSubroutines); + // Then, mark the subroutines called by the main subroutine, then the subroutines called by + // those called by the main subroutine, etc. + for (short currentSubroutine = 1; currentSubroutine <= numSubroutines; ++currentSubroutine) { + Label basicBlock = firstBasicBlock; + while (basicBlock != null) { + if ((basicBlock.flags & Label.FLAG_SUBROUTINE_CALLER) != 0 + && basicBlock.subroutineId == currentSubroutine) { + Label jsrTarget = basicBlock.outgoingEdges.nextEdge.successor; + if (jsrTarget.subroutineId == 0) { + // If this subroutine has not been marked yet, find its basic blocks. + jsrTarget.markSubroutine(++numSubroutines); + } + } + basicBlock = basicBlock.nextBasicBlock; + } + } + // Second step: find the successors in the control flow graph of each subroutine basic block + // 'r' ending with a RET instruction. These successors are the virtual successors of the basic + // blocks ending with JSR instructions (see {@link #visitJumpInsn)} that can reach 'r'. + Label basicBlock = firstBasicBlock; + while (basicBlock != null) { + if ((basicBlock.flags & Label.FLAG_SUBROUTINE_CALLER) != 0) { + // By construction, jsr targets are stored in the second outgoing edge of basic blocks + // that ends with a jsr instruction (see {@link #FLAG_SUBROUTINE_CALLER}). + Label subroutine = basicBlock.outgoingEdges.nextEdge.successor; + subroutine.addSubroutineRetSuccessors(basicBlock); + } + basicBlock = basicBlock.nextBasicBlock; + } + } + + // Data flow algorithm: put the first basic block in a list of blocks to process (i.e. blocks + // whose input stack size has changed) and, while there are blocks to process, remove one + // from the list, update the input stack size of its successor blocks in the control flow + // graph, and add these blocks to the list of blocks to process (if not already done). + Label listOfBlocksToProcess = firstBasicBlock; + listOfBlocksToProcess.nextListElement = Label.EMPTY_LIST; + int maxStackSize = maxStack; + while (listOfBlocksToProcess != Label.EMPTY_LIST) { + // Remove a basic block from the list of blocks to process. Note that we don't reset + // basicBlock.nextListElement to null on purpose, to make sure we don't reprocess already + // processed basic blocks. + Label basicBlock = listOfBlocksToProcess; + listOfBlocksToProcess = listOfBlocksToProcess.nextListElement; + // Compute the (absolute) input stack size and maximum stack size of this block. + int inputStackTop = basicBlock.inputStackSize; + int maxBlockStackSize = inputStackTop + basicBlock.outputStackMax; + // Update the absolute maximum stack size of the method. + if (maxBlockStackSize > maxStackSize) { + maxStackSize = maxBlockStackSize; + } + // Update the input stack size of the successor blocks of basicBlock in the control flow + // graph, and add these blocks to the list of blocks to process, if not already done. + Edge outgoingEdge = basicBlock.outgoingEdges; + if ((basicBlock.flags & Label.FLAG_SUBROUTINE_CALLER) != 0) { + // Ignore the first outgoing edge of the basic blocks ending with a jsr: these are virtual + // edges which lead to the instruction just after the jsr, and do not correspond to a + // possible execution path (see {@link #visitJumpInsn} and + // {@link Label#FLAG_SUBROUTINE_CALLER}). + outgoingEdge = outgoingEdge.nextEdge; + } + while (outgoingEdge != null) { + Label successorBlock = outgoingEdge.successor; + if (successorBlock.nextListElement == null) { + successorBlock.inputStackSize = + (short) (outgoingEdge.info == Edge.EXCEPTION ? 1 : inputStackTop + outgoingEdge.info); + successorBlock.nextListElement = listOfBlocksToProcess; + listOfBlocksToProcess = successorBlock; + } + outgoingEdge = outgoingEdge.nextEdge; + } + } + this.maxStack = maxStackSize; + } + + @Override + public void visitEnd() { + // Nothing to do. + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods: control flow analysis algorithm + // ----------------------------------------------------------------------------------------------- + + /** + * Adds a successor to {@link #currentBasicBlock} in the control flow graph. + * + * @param info information about the control flow edge to be added. + * @param successor the successor block to be added to the current basic block. + */ + private void addSuccessorToCurrentBasicBlock(final int info, final Label successor) { + currentBasicBlock.outgoingEdges = new Edge(info, successor, currentBasicBlock.outgoingEdges); + } + + /** + * Ends the current basic block. This method must be used in the case where the current basic + * block does not have any successor. + * + *

WARNING: this method must be called after the currently visited instruction has been put in + * {@link #code} (if frames are computed, this method inserts a new Label to start a new basic + * block after the current instruction). + */ + private void endCurrentBasicBlockWithNoSuccessor() { + if (compute == COMPUTE_ALL_FRAMES) { + Label nextBasicBlock = new Label(); + nextBasicBlock.frame = new Frame(nextBasicBlock); + nextBasicBlock.resolve(code.data, code.length); + lastBasicBlock.nextBasicBlock = nextBasicBlock; + lastBasicBlock = nextBasicBlock; + currentBasicBlock = null; + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL) { + currentBasicBlock.outputStackMax = (short) maxRelativeStackSize; + currentBasicBlock = null; + } + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods: stack map frames + // ----------------------------------------------------------------------------------------------- + + /** + * Starts the visit of a new stack map frame, stored in {@link #currentFrame}. + * + * @param offset the bytecode offset of the instruction to which the frame corresponds. + * @param numLocal the number of local variables in the frame. + * @param numStack the number of stack elements in the frame. + * @return the index of the next element to be written in this frame. + */ + int visitFrameStart(final int offset, final int numLocal, final int numStack) { + int frameLength = 3 + numLocal + numStack; + if (currentFrame == null || currentFrame.length < frameLength) { + currentFrame = new int[frameLength]; + } + currentFrame[0] = offset; + currentFrame[1] = numLocal; + currentFrame[2] = numStack; + return 3; + } + + /** + * Sets an abstract type in {@link #currentFrame}. + * + * @param frameIndex the index of the element to be set in {@link #currentFrame}. + * @param abstractType an abstract type. + */ + void visitAbstractType(final int frameIndex, final int abstractType) { + currentFrame[frameIndex] = abstractType; + } + + /** + * Ends the visit of {@link #currentFrame} by writing it in the StackMapTable entries and by + * updating the StackMapTable number_of_entries (except if the current frame is the first one, + * which is implicit in StackMapTable). Then resets {@link #currentFrame} to {@literal null}. + */ + void visitFrameEnd() { + if (previousFrame != null) { + if (stackMapTableEntries == null) { + stackMapTableEntries = new ByteVector(); + } + putFrame(); + ++stackMapTableNumberOfEntries; + } + previousFrame = currentFrame; + currentFrame = null; + } + + /** Compresses and writes {@link #currentFrame} in a new StackMapTable entry. */ + private void putFrame() { + final int numLocal = currentFrame[1]; + final int numStack = currentFrame[2]; + if (symbolTable.getMajorVersion() < Opcodes.V1_6) { + // Generate a StackMap attribute entry, which are always uncompressed. + stackMapTableEntries.putShort(currentFrame[0]).putShort(numLocal); + putAbstractTypes(3, 3 + numLocal); + stackMapTableEntries.putShort(numStack); + putAbstractTypes(3 + numLocal, 3 + numLocal + numStack); + return; + } + final int offsetDelta = + stackMapTableNumberOfEntries == 0 + ? currentFrame[0] + : currentFrame[0] - previousFrame[0] - 1; + final int previousNumlocal = previousFrame[1]; + final int numLocalDelta = numLocal - previousNumlocal; + int type = Frame.FULL_FRAME; + if (numStack == 0) { + switch (numLocalDelta) { + case -3: + case -2: + case -1: + type = Frame.CHOP_FRAME; + break; + case 0: + type = offsetDelta < 64 ? Frame.SAME_FRAME : Frame.SAME_FRAME_EXTENDED; + break; + case 1: + case 2: + case 3: + type = Frame.APPEND_FRAME; + break; + default: + // Keep the FULL_FRAME type. + break; + } + } else if (numLocalDelta == 0 && numStack == 1) { + type = + offsetDelta < 63 + ? Frame.SAME_LOCALS_1_STACK_ITEM_FRAME + : Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED; + } + if (type != Frame.FULL_FRAME) { + // Verify if locals are the same as in the previous frame. + int frameIndex = 3; + for (int i = 0; i < previousNumlocal && i < numLocal; i++) { + if (currentFrame[frameIndex] != previousFrame[frameIndex]) { + type = Frame.FULL_FRAME; + break; + } + frameIndex++; + } + } + switch (type) { + case Frame.SAME_FRAME: + stackMapTableEntries.putByte(offsetDelta); + break; + case Frame.SAME_LOCALS_1_STACK_ITEM_FRAME: + stackMapTableEntries.putByte(Frame.SAME_LOCALS_1_STACK_ITEM_FRAME + offsetDelta); + putAbstractTypes(3 + numLocal, 4 + numLocal); + break; + case Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED: + stackMapTableEntries + .putByte(Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED) + .putShort(offsetDelta); + putAbstractTypes(3 + numLocal, 4 + numLocal); + break; + case Frame.SAME_FRAME_EXTENDED: + stackMapTableEntries.putByte(Frame.SAME_FRAME_EXTENDED).putShort(offsetDelta); + break; + case Frame.CHOP_FRAME: + stackMapTableEntries + .putByte(Frame.SAME_FRAME_EXTENDED + numLocalDelta) + .putShort(offsetDelta); + break; + case Frame.APPEND_FRAME: + stackMapTableEntries + .putByte(Frame.SAME_FRAME_EXTENDED + numLocalDelta) + .putShort(offsetDelta); + putAbstractTypes(3 + previousNumlocal, 3 + numLocal); + break; + case Frame.FULL_FRAME: + default: + stackMapTableEntries.putByte(Frame.FULL_FRAME).putShort(offsetDelta).putShort(numLocal); + putAbstractTypes(3, 3 + numLocal); + stackMapTableEntries.putShort(numStack); + putAbstractTypes(3 + numLocal, 3 + numLocal + numStack); + break; + } + } + + /** + * Puts some abstract types of {@link #currentFrame} in {@link #stackMapTableEntries} , using the + * JVMS verification_type_info format used in StackMapTable attributes. + * + * @param start index of the first type in {@link #currentFrame} to write. + * @param end index of last type in {@link #currentFrame} to write (exclusive). + */ + private void putAbstractTypes(final int start, final int end) { + for (int i = start; i < end; ++i) { + Frame.putAbstractType(symbolTable, currentFrame[i], stackMapTableEntries); + } + } + + /** + * Puts the given public API frame element type in {@link #stackMapTableEntries} , using the JVMS + * verification_type_info format used in StackMapTable attributes. + * + * @param type a frame element type described using the same format as in {@link + * MethodVisitor#visitFrame}, i.e. either {@link Opcodes#TOP}, {@link Opcodes#INTEGER}, {@link + * Opcodes#FLOAT}, {@link Opcodes#LONG}, {@link Opcodes#DOUBLE}, {@link Opcodes#NULL}, or + * {@link Opcodes#UNINITIALIZED_THIS}, or the internal name of a class, or a Label designating + * a NEW instruction (for uninitialized types). + */ + private void putFrameType(final Object type) { + if (type instanceof Integer) { + stackMapTableEntries.putByte(((Integer) type).intValue()); + } else if (type instanceof String) { + stackMapTableEntries + .putByte(Frame.ITEM_OBJECT) + .putShort(symbolTable.addConstantClass((String) type).index); + } else { + stackMapTableEntries + .putByte(Frame.ITEM_UNINITIALIZED) + .putShort(((Label) type).bytecodeOffset); + } + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods + // ----------------------------------------------------------------------------------------------- + + /** + * Returns whether the attributes of this method can be copied from the attributes of the given + * method (assuming there is no method visitor between the given ClassReader and this + * MethodWriter). This method should only be called just after this MethodWriter has been created, + * and before any content is visited. It returns true if the attributes corresponding to the + * constructor arguments (at most a Signature, an Exception, a Deprecated and a Synthetic + * attribute) are the same as the corresponding attributes in the given method. + * + * @param source the source ClassReader from which the attributes of this method might be copied. + * @param hasSyntheticAttribute whether the method_info JVMS structure from which the attributes + * of this method might be copied contains a Synthetic attribute. + * @param hasDeprecatedAttribute whether the method_info JVMS structure from which the attributes + * of this method might be copied contains a Deprecated attribute. + * @param descriptorIndex the descriptor_index field of the method_info JVMS structure from which + * the attributes of this method might be copied. + * @param signatureIndex the constant pool index contained in the Signature attribute of the + * method_info JVMS structure from which the attributes of this method might be copied, or 0. + * @param exceptionsOffset the offset in 'source.b' of the Exceptions attribute of the method_info + * JVMS structure from which the attributes of this method might be copied, or 0. + * @return whether the attributes of this method can be copied from the attributes of the + * method_info JVMS structure in 'source.b', between 'methodInfoOffset' and 'methodInfoOffset' + * + 'methodInfoLength'. + */ + boolean canCopyMethodAttributes( + final ClassReader source, + final boolean hasSyntheticAttribute, + final boolean hasDeprecatedAttribute, + final int descriptorIndex, + final int signatureIndex, + final int exceptionsOffset) { + // If the method descriptor has changed, with more locals than the max_locals field of the + // original Code attribute, if any, then the original method attributes can't be copied. A + // conservative check on the descriptor changes alone ensures this (being more precise is not + // worth the additional complexity, because these cases should be rare -- if a transform changes + // a method descriptor, most of the time it needs to change the method's code too). + if (source != symbolTable.getSource() + || descriptorIndex != this.descriptorIndex + || signatureIndex != this.signatureIndex + || hasDeprecatedAttribute != ((accessFlags & Opcodes.ACC_DEPRECATED) != 0)) { + return false; + } + boolean needSyntheticAttribute = + symbolTable.getMajorVersion() < Opcodes.V1_5 && (accessFlags & Opcodes.ACC_SYNTHETIC) != 0; + if (hasSyntheticAttribute != needSyntheticAttribute) { + return false; + } + if (exceptionsOffset == 0) { + return numberOfExceptions == 0; + } else if (source.readUnsignedShort(exceptionsOffset) == numberOfExceptions) { + int currentExceptionOffset = exceptionsOffset + 2; + for (int i = 0; i < numberOfExceptions; ++i) { + if (source.readUnsignedShort(currentExceptionOffset) != exceptionIndexTable[i]) { + return false; + } + currentExceptionOffset += 2; + } + } + return true; + } + + /** + * Sets the source from which the attributes of this method will be copied. + * + * @param methodInfoOffset the offset in 'symbolTable.getSource()' of the method_info JVMS + * structure from which the attributes of this method will be copied. + * @param methodInfoLength the length in 'symbolTable.getSource()' of the method_info JVMS + * structure from which the attributes of this method will be copied. + */ + void setMethodAttributesSource(final int methodInfoOffset, final int methodInfoLength) { + // Don't copy the attributes yet, instead store their location in the source class reader so + // they can be copied later, in {@link #putMethodInfo}. Note that we skip the 6 header bytes + // of the method_info JVMS structure. + this.sourceOffset = methodInfoOffset + 6; + this.sourceLength = methodInfoLength - 6; + } + + /** + * Returns the size of the method_info JVMS structure generated by this MethodWriter. Also add the + * names of the attributes of this method in the constant pool. + * + * @return the size in bytes of the method_info JVMS structure. + */ + int computeMethodInfoSize() { + // If this method_info must be copied from an existing one, the size computation is trivial. + if (sourceOffset != 0) { + // sourceLength excludes the first 6 bytes for access_flags, name_index and descriptor_index. + return 6 + sourceLength; + } + // 2 bytes each for access_flags, name_index, descriptor_index and attributes_count. + int size = 8; + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + if (code.length > 0) { + if (code.length > 65535) { + throw new MethodTooLargeException( + symbolTable.getClassName(), name, descriptor, code.length); + } + symbolTable.addConstantUtf8(Constants.CODE); + // The Code attribute has 6 header bytes, plus 2, 2, 4 and 2 bytes respectively for max_stack, + // max_locals, code_length and attributes_count, plus the bytecode and the exception table. + size += 16 + code.length + Handler.getExceptionTableSize(firstHandler); + if (stackMapTableEntries != null) { + boolean useStackMapTable = symbolTable.getMajorVersion() >= Opcodes.V1_6; + symbolTable.addConstantUtf8(useStackMapTable ? Constants.STACK_MAP_TABLE : "StackMap"); + // 6 header bytes and 2 bytes for number_of_entries. + size += 8 + stackMapTableEntries.length; + } + if (lineNumberTable != null) { + symbolTable.addConstantUtf8(Constants.LINE_NUMBER_TABLE); + // 6 header bytes and 2 bytes for line_number_table_length. + size += 8 + lineNumberTable.length; + } + if (localVariableTable != null) { + symbolTable.addConstantUtf8(Constants.LOCAL_VARIABLE_TABLE); + // 6 header bytes and 2 bytes for local_variable_table_length. + size += 8 + localVariableTable.length; + } + if (localVariableTypeTable != null) { + symbolTable.addConstantUtf8(Constants.LOCAL_VARIABLE_TYPE_TABLE); + // 6 header bytes and 2 bytes for local_variable_type_table_length. + size += 8 + localVariableTypeTable.length; + } + if (lastCodeRuntimeVisibleTypeAnnotation != null) { + size += + lastCodeRuntimeVisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + } + if (lastCodeRuntimeInvisibleTypeAnnotation != null) { + size += + lastCodeRuntimeInvisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS); + } + if (firstCodeAttribute != null) { + size += + firstCodeAttribute.computeAttributesSize( + symbolTable, code.data, code.length, maxStack, maxLocals); + } + } + if (numberOfExceptions > 0) { + symbolTable.addConstantUtf8(Constants.EXCEPTIONS); + size += 8 + 2 * numberOfExceptions; + } + size += Attribute.computeAttributesSize(symbolTable, accessFlags, signatureIndex); + size += + AnnotationWriter.computeAnnotationsSize( + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation); + if (lastRuntimeVisibleParameterAnnotations != null) { + size += + AnnotationWriter.computeParameterAnnotationsSize( + Constants.RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS, + lastRuntimeVisibleParameterAnnotations, + visibleAnnotableParameterCount == 0 + ? lastRuntimeVisibleParameterAnnotations.length + : visibleAnnotableParameterCount); + } + if (lastRuntimeInvisibleParameterAnnotations != null) { + size += + AnnotationWriter.computeParameterAnnotationsSize( + Constants.RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS, + lastRuntimeInvisibleParameterAnnotations, + invisibleAnnotableParameterCount == 0 + ? lastRuntimeInvisibleParameterAnnotations.length + : invisibleAnnotableParameterCount); + } + if (defaultValue != null) { + symbolTable.addConstantUtf8(Constants.ANNOTATION_DEFAULT); + size += 6 + defaultValue.length; + } + if (parameters != null) { + symbolTable.addConstantUtf8(Constants.METHOD_PARAMETERS); + // 6 header bytes and 1 byte for parameters_count. + size += 7 + parameters.length; + } + if (firstAttribute != null) { + size += firstAttribute.computeAttributesSize(symbolTable); + } + return size; + } + + /** + * Puts the content of the method_info JVMS structure generated by this MethodWriter into the + * given ByteVector. + * + * @param output where the method_info structure must be put. + */ + void putMethodInfo(final ByteVector output) { + boolean useSyntheticAttribute = symbolTable.getMajorVersion() < Opcodes.V1_5; + int mask = useSyntheticAttribute ? Opcodes.ACC_SYNTHETIC : 0; + output.putShort(accessFlags & ~mask).putShort(nameIndex).putShort(descriptorIndex); + // If this method_info must be copied from an existing one, copy it now and return early. + if (sourceOffset != 0) { + output.putByteArray(symbolTable.getSource().classFileBuffer, sourceOffset, sourceLength); + return; + } + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + int attributeCount = 0; + if (code.length > 0) { + ++attributeCount; + } + if (numberOfExceptions > 0) { + ++attributeCount; + } + if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0 && useSyntheticAttribute) { + ++attributeCount; + } + if (signatureIndex != 0) { + ++attributeCount; + } + if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) { + ++attributeCount; + } + if (lastRuntimeVisibleAnnotation != null) { + ++attributeCount; + } + if (lastRuntimeInvisibleAnnotation != null) { + ++attributeCount; + } + if (lastRuntimeVisibleParameterAnnotations != null) { + ++attributeCount; + } + if (lastRuntimeInvisibleParameterAnnotations != null) { + ++attributeCount; + } + if (lastRuntimeVisibleTypeAnnotation != null) { + ++attributeCount; + } + if (lastRuntimeInvisibleTypeAnnotation != null) { + ++attributeCount; + } + if (defaultValue != null) { + ++attributeCount; + } + if (parameters != null) { + ++attributeCount; + } + if (firstAttribute != null) { + attributeCount += firstAttribute.getAttributeCount(); + } + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + output.putShort(attributeCount); + if (code.length > 0) { + // 2, 2, 4 and 2 bytes respectively for max_stack, max_locals, code_length and + // attributes_count, plus the bytecode and the exception table. + int size = 10 + code.length + Handler.getExceptionTableSize(firstHandler); + int codeAttributeCount = 0; + if (stackMapTableEntries != null) { + // 6 header bytes and 2 bytes for number_of_entries. + size += 8 + stackMapTableEntries.length; + ++codeAttributeCount; + } + if (lineNumberTable != null) { + // 6 header bytes and 2 bytes for line_number_table_length. + size += 8 + lineNumberTable.length; + ++codeAttributeCount; + } + if (localVariableTable != null) { + // 6 header bytes and 2 bytes for local_variable_table_length. + size += 8 + localVariableTable.length; + ++codeAttributeCount; + } + if (localVariableTypeTable != null) { + // 6 header bytes and 2 bytes for local_variable_type_table_length. + size += 8 + localVariableTypeTable.length; + ++codeAttributeCount; + } + if (lastCodeRuntimeVisibleTypeAnnotation != null) { + size += + lastCodeRuntimeVisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + ++codeAttributeCount; + } + if (lastCodeRuntimeInvisibleTypeAnnotation != null) { + size += + lastCodeRuntimeInvisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS); + ++codeAttributeCount; + } + if (firstCodeAttribute != null) { + size += + firstCodeAttribute.computeAttributesSize( + symbolTable, code.data, code.length, maxStack, maxLocals); + codeAttributeCount += firstCodeAttribute.getAttributeCount(); + } + output + .putShort(symbolTable.addConstantUtf8(Constants.CODE)) + .putInt(size) + .putShort(maxStack) + .putShort(maxLocals) + .putInt(code.length) + .putByteArray(code.data, 0, code.length); + Handler.putExceptionTable(firstHandler, output); + output.putShort(codeAttributeCount); + if (stackMapTableEntries != null) { + boolean useStackMapTable = symbolTable.getMajorVersion() >= Opcodes.V1_6; + output + .putShort( + symbolTable.addConstantUtf8( + useStackMapTable ? Constants.STACK_MAP_TABLE : "StackMap")) + .putInt(2 + stackMapTableEntries.length) + .putShort(stackMapTableNumberOfEntries) + .putByteArray(stackMapTableEntries.data, 0, stackMapTableEntries.length); + } + if (lineNumberTable != null) { + output + .putShort(symbolTable.addConstantUtf8(Constants.LINE_NUMBER_TABLE)) + .putInt(2 + lineNumberTable.length) + .putShort(lineNumberTableLength) + .putByteArray(lineNumberTable.data, 0, lineNumberTable.length); + } + if (localVariableTable != null) { + output + .putShort(symbolTable.addConstantUtf8(Constants.LOCAL_VARIABLE_TABLE)) + .putInt(2 + localVariableTable.length) + .putShort(localVariableTableLength) + .putByteArray(localVariableTable.data, 0, localVariableTable.length); + } + if (localVariableTypeTable != null) { + output + .putShort(symbolTable.addConstantUtf8(Constants.LOCAL_VARIABLE_TYPE_TABLE)) + .putInt(2 + localVariableTypeTable.length) + .putShort(localVariableTypeTableLength) + .putByteArray(localVariableTypeTable.data, 0, localVariableTypeTable.length); + } + if (lastCodeRuntimeVisibleTypeAnnotation != null) { + lastCodeRuntimeVisibleTypeAnnotation.putAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS), output); + } + if (lastCodeRuntimeInvisibleTypeAnnotation != null) { + lastCodeRuntimeInvisibleTypeAnnotation.putAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS), output); + } + if (firstCodeAttribute != null) { + firstCodeAttribute.putAttributes( + symbolTable, code.data, code.length, maxStack, maxLocals, output); + } + } + if (numberOfExceptions > 0) { + output + .putShort(symbolTable.addConstantUtf8(Constants.EXCEPTIONS)) + .putInt(2 + 2 * numberOfExceptions) + .putShort(numberOfExceptions); + for (int exceptionIndex : exceptionIndexTable) { + output.putShort(exceptionIndex); + } + } + Attribute.putAttributes(symbolTable, accessFlags, signatureIndex, output); + AnnotationWriter.putAnnotations( + symbolTable, + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation, + output); + if (lastRuntimeVisibleParameterAnnotations != null) { + AnnotationWriter.putParameterAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS), + lastRuntimeVisibleParameterAnnotations, + visibleAnnotableParameterCount == 0 + ? lastRuntimeVisibleParameterAnnotations.length + : visibleAnnotableParameterCount, + output); + } + if (lastRuntimeInvisibleParameterAnnotations != null) { + AnnotationWriter.putParameterAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS), + lastRuntimeInvisibleParameterAnnotations, + invisibleAnnotableParameterCount == 0 + ? lastRuntimeInvisibleParameterAnnotations.length + : invisibleAnnotableParameterCount, + output); + } + if (defaultValue != null) { + output + .putShort(symbolTable.addConstantUtf8(Constants.ANNOTATION_DEFAULT)) + .putInt(defaultValue.length) + .putByteArray(defaultValue.data, 0, defaultValue.length); + } + if (parameters != null) { + output + .putShort(symbolTable.addConstantUtf8(Constants.METHOD_PARAMETERS)) + .putInt(1 + parameters.length) + .putByte(parametersCount) + .putByteArray(parameters.data, 0, parameters.length); + } + if (firstAttribute != null) { + firstAttribute.putAttributes(symbolTable, output); + } + } + + /** + * Collects the attributes of this method into the given set of attribute prototypes. + * + * @param attributePrototypes a set of attribute prototypes. + */ + final void collectAttributePrototypes(final Attribute.Set attributePrototypes) { + attributePrototypes.addAttributes(firstAttribute); + attributePrototypes.addAttributes(firstCodeAttribute); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ModuleVisitor.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ModuleVisitor.java new file mode 100644 index 0000000..ad31861 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ModuleVisitor.java @@ -0,0 +1,169 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A visitor to visit a Java module. The methods of this class must be called in the following + * order: ( {@code visitMainClass} | ( {@code visitPackage} | {@code visitRequire} | {@code + * visitExport} | {@code visitOpen} | {@code visitUse} | {@code visitProvide} )* ) {@code + * visitEnd}. @Author Remi Forax @Author Eric Bruneton + */ +public abstract class ModuleVisitor { + + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + protected final int api; + + /** + * The module visitor to which this visitor must delegate method calls. May be {@literal null}. + */ + protected ModuleVisitor mv; + + /** + * Constructs a new {@link ModuleVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link Opcodes#ASM6} + * or {@link Opcodes#ASM7}. + */ + public ModuleVisitor(final int api) { + this(api, null); + } + + /** + * Constructs a new {@link ModuleVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link Opcodes#ASM6} + * or {@link Opcodes#ASM7}. + * @param moduleVisitor the module visitor to which this visitor must delegate method calls. May + * be null. + */ + public ModuleVisitor(final int api, final ModuleVisitor moduleVisitor) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + if (api == Opcodes.ASM10_EXPERIMENTAL) { + Constants.checkAsmExperimental(this); + } + this.api = api; + this.mv = moduleVisitor; + } + + /** + * Visit the main class of the current module. + * + * @param mainClass the internal name of the main class of the current module. + */ + public void visitMainClass(final String mainClass) { + if (mv != null) { + mv.visitMainClass(mainClass); + } + } + + /** + * Visit a package of the current module. + * + * @param packaze the internal name of a package. + */ + public void visitPackage(final String packaze) { + if (mv != null) { + mv.visitPackage(packaze); + } + } + + /** + * Visits a dependence of the current module. + * + * @param module the fully qualified name (using dots) of the dependence. + * @param access the access flag of the dependence among {@code ACC_TRANSITIVE}, {@code + * ACC_STATIC_PHASE}, {@code ACC_SYNTHETIC} and {@code ACC_MANDATED}. + * @param version the module version at compile time, or {@literal null}. + */ + public void visitRequire(final String module, final int access, final String version) { + if (mv != null) { + mv.visitRequire(module, access, version); + } + } + + /** + * Visit an exported package of the current module. + * + * @param packaze the internal name of the exported package. + * @param access the access flag of the exported package, valid values are among {@code + * ACC_SYNTHETIC} and {@code ACC_MANDATED}. + * @param modules the fully qualified names (using dots) of the modules that can access the public + * classes of the exported package, or {@literal null}. + */ + public void visitExport(final String packaze, final int access, final String... modules) { + if (mv != null) { + mv.visitExport(packaze, access, modules); + } + } + + /** + * Visit an open package of the current module. + * + * @param packaze the internal name of the opened package. + * @param access the access flag of the opened package, valid values are among {@code + * ACC_SYNTHETIC} and {@code ACC_MANDATED}. + * @param modules the fully qualified names (using dots) of the modules that can use deep + * reflection to the classes of the open package, or {@literal null}. + */ + public void visitOpen(final String packaze, final int access, final String... modules) { + if (mv != null) { + mv.visitOpen(packaze, access, modules); + } + } + + /** + * Visit a third used by the current module. The name must be the internal name of an processer or + * a class. + * + * @param service the internal name of the third. + */ + public void visitUse(final String service) { + if (mv != null) { + mv.visitUse(service); + } + } + + /** + * Visit an implementation of a third. + * + * @param service the internal name of the third. + * @param providers the internal names of the implementations of the third (there is at least one + * provider). + */ + public void visitProvide(final String service, final String... providers) { + if (mv != null) { + mv.visitProvide(service, providers); + } + } + + /** + * Visits the end of the module. This method, which is the last one to be called, is used to + * inform the visitor that everything have been visited. + */ + public void visitEnd() { + if (mv != null) { + mv.visitEnd(); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ModuleWriter.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ModuleWriter.java new file mode 100644 index 0000000..c3bf295 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/ModuleWriter.java @@ -0,0 +1,237 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A {@link ModuleVisitor} that generates the corresponding Module, ModulePackages and + * ModuleMainClass attributes, as defined in the Java Virtual Machine Specification (JVMS). @Author + * Remi Forax @Author Eric Bruneton + * + * @see JVMS + * 4.7.25 + * @see JVMS + * 4.7.26 + * @see JVMS + * 4.7.27 + */ +final class ModuleWriter extends ModuleVisitor { + + /** Where the constants used in this AnnotationWriter must be stored. */ + private final SymbolTable symbolTable; + + /** The module_name_index field of the JVMS Module attribute. */ + private final int moduleNameIndex; + + /** The module_flags field of the JVMS Module attribute. */ + private final int moduleFlags; + + /** The module_version_index field of the JVMS Module attribute. */ + private final int moduleVersionIndex; + + /** The requires_count field of the JVMS Module attribute. */ + private int requiresCount; + + /** The binary content of the 'requires' array of the JVMS Module attribute. */ + private final ByteVector requires; + + /** The exports_count field of the JVMS Module attribute. */ + private int exportsCount; + + /** The binary content of the 'exports' array of the JVMS Module attribute. */ + private final ByteVector exports; + + /** The opens_count field of the JVMS Module attribute. */ + private int opensCount; + + /** The binary content of the 'opens' array of the JVMS Module attribute. */ + private final ByteVector opens; + + /** The uses_count field of the JVMS Module attribute. */ + private int usesCount; + + /** The binary content of the 'uses_index' array of the JVMS Module attribute. */ + private final ByteVector usesIndex; + + /** The provides_count field of the JVMS Module attribute. */ + private int providesCount; + + /** The binary content of the 'provides' array of the JVMS Module attribute. */ + private final ByteVector provides; + + /** The provides_count field of the JVMS ModulePackages attribute. */ + private int packageCount; + + /** The binary content of the 'package_index' array of the JVMS ModulePackages attribute. */ + private final ByteVector packageIndex; + + /** The main_class_index field of the JVMS ModuleMainClass attribute, or 0. */ + private int mainClassIndex; + + ModuleWriter(final SymbolTable symbolTable, final int name, final int access, final int version) { + super(/* latest api = */ Opcodes.ASM9); + this.symbolTable = symbolTable; + this.moduleNameIndex = name; + this.moduleFlags = access; + this.moduleVersionIndex = version; + this.requires = new ByteVector(); + this.exports = new ByteVector(); + this.opens = new ByteVector(); + this.usesIndex = new ByteVector(); + this.provides = new ByteVector(); + this.packageIndex = new ByteVector(); + } + + @Override + public void visitMainClass(final String mainClass) { + this.mainClassIndex = symbolTable.addConstantClass(mainClass).index; + } + + @Override + public void visitPackage(final String packaze) { + packageIndex.putShort(symbolTable.addConstantPackage(packaze).index); + packageCount++; + } + + @Override + public void visitRequire(final String module, final int access, final String version) { + requires + .putShort(symbolTable.addConstantModule(module).index) + .putShort(access) + .putShort(version == null ? 0 : symbolTable.addConstantUtf8(version)); + requiresCount++; + } + + @Override + public void visitExport(final String packaze, final int access, final String... modules) { + exports.putShort(symbolTable.addConstantPackage(packaze).index).putShort(access); + if (modules == null) { + exports.putShort(0); + } else { + exports.putShort(modules.length); + for (String module : modules) { + exports.putShort(symbolTable.addConstantModule(module).index); + } + } + exportsCount++; + } + + @Override + public void visitOpen(final String packaze, final int access, final String... modules) { + opens.putShort(symbolTable.addConstantPackage(packaze).index).putShort(access); + if (modules == null) { + opens.putShort(0); + } else { + opens.putShort(modules.length); + for (String module : modules) { + opens.putShort(symbolTable.addConstantModule(module).index); + } + } + opensCount++; + } + + @Override + public void visitUse(final String service) { + usesIndex.putShort(symbolTable.addConstantClass(service).index); + usesCount++; + } + + @Override + public void visitProvide(final String service, final String... providers) { + provides.putShort(symbolTable.addConstantClass(service).index); + provides.putShort(providers.length); + for (String provider : providers) { + provides.putShort(symbolTable.addConstantClass(provider).index); + } + providesCount++; + } + + @Override + public void visitEnd() { + // Nothing to do. + } + + /** + * Returns the number of Module, ModulePackages and ModuleMainClass attributes generated by this + * ModuleWriter. + * + * @return the number of Module, ModulePackages and ModuleMainClass attributes (between 1 and 3). + */ + int getAttributeCount() { + return 1 + (packageCount > 0 ? 1 : 0) + (mainClassIndex > 0 ? 1 : 0); + } + + /** + * Returns the size of the Module, ModulePackages and ModuleMainClass attributes generated by this + * ModuleWriter. Also add the names of these attributes in the constant pool. + * + * @return the size in bytes of the Module, ModulePackages and ModuleMainClass attributes. + */ + int computeAttributesSize() { + symbolTable.addConstantUtf8(Constants.MODULE); + // 6 attribute header bytes, 6 bytes for name, flags and version, and 5 * 2 bytes for counts. + int size = + 22 + requires.length + exports.length + opens.length + usesIndex.length + provides.length; + if (packageCount > 0) { + symbolTable.addConstantUtf8(Constants.MODULE_PACKAGES); + // 6 attribute header bytes, and 2 bytes for package_count. + size += 8 + packageIndex.length; + } + if (mainClassIndex > 0) { + symbolTable.addConstantUtf8(Constants.MODULE_MAIN_CLASS); + // 6 attribute header bytes, and 2 bytes for main_class_index. + size += 8; + } + return size; + } + + /** + * Puts the Module, ModulePackages and ModuleMainClass attributes generated by this ModuleWriter + * in the given ByteVector. + * + * @param output where the attributes must be put. + */ + void putAttributes(final ByteVector output) { + // 6 bytes for name, flags and version, and 5 * 2 bytes for counts. + int moduleAttributeLength = + 16 + requires.length + exports.length + opens.length + usesIndex.length + provides.length; + output + .putShort(symbolTable.addConstantUtf8(Constants.MODULE)) + .putInt(moduleAttributeLength) + .putShort(moduleNameIndex) + .putShort(moduleFlags) + .putShort(moduleVersionIndex) + .putShort(requiresCount) + .putByteArray(requires.data, 0, requires.length) + .putShort(exportsCount) + .putByteArray(exports.data, 0, exports.length) + .putShort(opensCount) + .putByteArray(opens.data, 0, opens.length) + .putShort(usesCount) + .putByteArray(usesIndex.data, 0, usesIndex.length) + .putShort(providesCount) + .putByteArray(provides.data, 0, provides.length); + if (packageCount > 0) { + output + .putShort(symbolTable.addConstantUtf8(Constants.MODULE_PACKAGES)) + .putInt(2 + packageIndex.length) + .putShort(packageCount) + .putByteArray(packageIndex.data, 0, packageIndex.length); + } + if (mainClassIndex > 0) { + output + .putShort(symbolTable.addConstantUtf8(Constants.MODULE_MAIN_CLASS)) + .putInt(2) + .putShort(mainClassIndex); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Opcodes.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Opcodes.java new file mode 100644 index 0000000..31f0c41 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Opcodes.java @@ -0,0 +1,545 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * The JVM opcodes, access flags and array type codes. This processer does not define all the JVM + * opcodes because some opcodes are automatically handled. For example, the xLOAD and xSTORE opcodes + * are automatically replaced by xLOAD_n and xSTORE_n opcodes when possible. The xLOAD_n and + * xSTORE_n opcodes are therefore not defined in this processer. Likewise for LDC, automatically + * replaced by LDC_W or LDC2_W when necessary, WIDE, GOTO_W and JSR_W. @Author Eric Bruneton @Author + * Eugene Kuleshov + * + * @see JVMS 6 + */ +// DontCheck(InterfaceIsType): can't be fixed (for backward binary compatibility). +public interface Opcodes { + + // ASM API versions. + + int ASM4 = 4 << 16 | 0 << 8; + int ASM5 = 5 << 16 | 0 << 8; + int ASM6 = 6 << 16 | 0 << 8; + int ASM7 = 7 << 16 | 0 << 8; + int ASM8 = 8 << 16 | 0 << 8; + int ASM9 = 9 << 16 | 0 << 8; + + /** + * Experimental, use at your own risk. This field will be renamed when it becomes stable, this + * will break existing code using it. Only code compiled with --enable-preview can use this. + * + * @deprecated This API is experimental. + */ + @Deprecated int ASM10_EXPERIMENTAL = 1 << 24 | 10 << 16 | 0 << 8; + + /* + * Internal flags used to redirect calls to deprecated methods. For instance, if a visitOldStuff + * method in API_OLD is deprecated and replaced with visitNewStuff in API_NEW, then the + * redirection should be done as follows: + * + *

+   * public class StuffVisitor {
+   *   ...
+   *
+   *   @Deprecated public void visitOldStuff(int arg, ...) {
+   *     // SOURCE_DEPRECATED means "a call from a deprecated method using the old 'api' value".
+   *     visitNewStuf(arg | (api < API_NEW ? SOURCE_DEPRECATED : 0), ...);
+   *   }
+   *
+   *   public void visitNewStuff(int argAndSource, ...) {
+   *     if (api < API_NEW && (argAndSource & SOURCE_DEPRECATED) == 0) {
+   *       visitOldStuff(argAndSource, ...);
+   *     } else {
+   *       int arg = argAndSource & ~SOURCE_MASK;
+   *       [ do stuff ]
+   *     }
+   *   }
+   * }
+   * 
+ * + *

If 'api' is equal to API_NEW, there are two cases: + * + *

    + *
  • call visitNewStuff: the redirection test is skipped and 'do stuff' is executed directly. + *
  • call visitOldSuff: the source is not set to SOURCE_DEPRECATED before calling + * visitNewStuff, but the redirection test is skipped anyway in visitNewStuff, which + * directly executes 'do stuff'. + *
+ * + *

If 'api' is equal to API_OLD, there are two cases: + * + *

    + *
  • call visitOldSuff: the source is set to SOURCE_DEPRECATED before calling visitNewStuff. + * Because of this visitNewStuff does not redirect back to visitOldStuff, and instead + * executes 'do stuff'. + *
  • call visitNewStuff: the call is redirected to visitOldStuff because the source is 0. + * visitOldStuff now sets the source to SOURCE_DEPRECATED and calls visitNewStuff back. This + * time visitNewStuff does not redirect the call, and instead executes 'do stuff'. + *
+ * + *

User subclasses

+ * + *

If a user subclass overrides one of these methods, there are only two cases: either 'api' is + * API_OLD and visitOldStuff is overridden (and visitNewStuff is not), or 'api' is API_NEW or + * more, and visitNewStuff is overridden (and visitOldStuff is not). Any other case is a user + * programming error. + * + *

If 'api' is equal to API_NEW, the class hierarchy is equivalent to + * + *

+   * public class StuffVisitor {
+   *   @Deprecated public void visitOldStuff(int arg, ...) { visitNewStuf(arg, ...); }
+   *   public void visitNewStuff(int arg, ...) { [ do stuff ] }
+   * }
+   * class UserStuffVisitor extends StuffVisitor {
+   *   @Override public void visitNewStuff(int arg, ...) {
+   *     super.visitNewStuff(int arg, ...); // optional
+   *     [ do user stuff ]
+   *   }
+   * }
+   * 
+ * + *

It is then obvious that whether visitNewStuff or visitOldStuff is called, 'do stuff' and 'do + * user stuff' will be executed, in this order. + * + *

If 'api' is equal to API_OLD, the class hierarchy is equivalent to + * + *

+   * public class StuffVisitor {
+   *   @Deprecated public void visitOldStuff(int arg, ...) {
+   *     visitNewStuff(arg | SOURCE_DEPRECATED, ...);
+   *   }
+   *   public void visitNewStuff(int argAndSource...) {
+   *     if ((argAndSource & SOURCE_DEPRECATED) == 0) {
+   *       visitOldStuff(argAndSource, ...);
+   *     } else {
+   *       int arg = argAndSource & ~SOURCE_MASK;
+   *       [ do stuff ]
+   *     }
+   *   }
+   * }
+   * class UserStuffVisitor extends StuffVisitor {
+   *   @Override public void visitOldStuff(int arg, ...) {
+   *     super.visitOldStuff(int arg, ...); // optional
+   *     [ do user stuff ]
+   *   }
+   * }
+   * 
+ * + *

and there are two cases: + * + *

    + *
  • call visitOldStuff: in the call to super.visitOldStuff, the source is set to + * SOURCE_DEPRECATED and visitNewStuff is called. Here 'do stuff' is run because the source + * was previously set to SOURCE_DEPRECATED, and execution eventually returns to + * UserStuffVisitor.visitOldStuff, where 'do user stuff' is run. + *
  • call visitNewStuff: the call is redirected to UserStuffVisitor.visitOldStuff because the + * source is 0. Execution continues as in the previous case, resulting in 'do stuff' and 'do + * user stuff' being executed, in this order. + *
+ * + *

ASM subclasses

+ * + *

In ASM packages, subclasses of StuffVisitor can typically be sub classed again by the user, + * and can be used with API_OLD or API_NEW. Because of this, if such a subclass must override + * visitNewStuff, it must do so in the following way (and must not override visitOldStuff): + * + *

+   * public class AsmStuffVisitor extends StuffVisitor {
+   *   @Override public void visitNewStuff(int argAndSource, ...) {
+   *     if (api < API_NEW && (argAndSource & SOURCE_DEPRECATED) == 0) {
+   *       super.visitNewStuff(argAndSource, ...);
+   *       return;
+   *     }
+   *     super.visitNewStuff(argAndSource, ...); // optional
+   *     int arg = argAndSource & ~SOURCE_MASK;
+   *     [ do other stuff ]
+   *   }
+   * }
+   * 
+ * + *

If a user class extends this with 'api' equal to API_NEW, the class hierarchy is equivalent + * to + * + *

+   * public class StuffVisitor {
+   *   @Deprecated public void visitOldStuff(int arg, ...) { visitNewStuf(arg, ...); }
+   *   public void visitNewStuff(int arg, ...) { [ do stuff ] }
+   * }
+   * public class AsmStuffVisitor extends StuffVisitor {
+   *   @Override public void visitNewStuff(int arg, ...) {
+   *     super.visitNewStuff(arg, ...);
+   *     [ do other stuff ]
+   *   }
+   * }
+   * class UserStuffVisitor extends StuffVisitor {
+   *   @Override public void visitNewStuff(int arg, ...) {
+   *     super.visitNewStuff(int arg, ...);
+   *     [ do user stuff ]
+   *   }
+   * }
+   * 
+ * + *

It is then obvious that whether visitNewStuff or visitOldStuff is called, 'do stuff', 'do + * other stuff' and 'do user stuff' will be executed, in this order. If, on the other hand, a user + * class extends AsmStuffVisitor with 'api' equal to API_OLD, the class hierarchy is equivalent to + * + *

+   * public class StuffVisitor {
+   *   @Deprecated public void visitOldStuff(int arg, ...) {
+   *     visitNewStuf(arg | SOURCE_DEPRECATED, ...);
+   *   }
+   *   public void visitNewStuff(int argAndSource, ...) {
+   *     if ((argAndSource & SOURCE_DEPRECATED) == 0) {
+   *       visitOldStuff(argAndSource, ...);
+   *     } else {
+   *       int arg = argAndSource & ~SOURCE_MASK;
+   *       [ do stuff ]
+   *     }
+   *   }
+   * }
+   * public class AsmStuffVisitor extends StuffVisitor {
+   *   @Override public void visitNewStuff(int argAndSource, ...) {
+   *     if ((argAndSource & SOURCE_DEPRECATED) == 0) {
+   *       super.visitNewStuff(argAndSource, ...);
+   *       return;
+   *     }
+   *     super.visitNewStuff(argAndSource, ...); // optional
+   *     int arg = argAndSource & ~SOURCE_MASK;
+   *     [ do other stuff ]
+   *   }
+   * }
+   * class UserStuffVisitor extends StuffVisitor {
+   *   @Override public void visitOldStuff(int arg, ...) {
+   *     super.visitOldStuff(arg, ...);
+   *     [ do user stuff ]
+   *   }
+   * }
+   * 
+ * + *

and, here again, whether visitNewStuff or visitOldStuff is called, 'do stuff', 'do other + * stuff' and 'do user stuff' will be executed, in this order (exercise left to the reader). + * + *

Notes

+ * + *
    + *
  • the SOURCE_DEPRECATED flag is set only if 'api' is API_OLD, just before calling + * visitNewStuff. By hypothesis, this method is not overridden by the user. Therefore, user + * classes can never see this flag. Only ASM subclasses must take care of extracting the + * actual argument value by clearing the source flags. + *
  • because the SOURCE_DEPRECATED flag is immediately cleared in the caller, the caller can + * call visitOldStuff or visitNewStuff (in 'do stuff' and 'do user stuff') on a delegate + * visitor without any risks (breaking the redirection logic, "leaking" the flag, etc). + *
  • all the scenarios discussed above are unit tested in MethodVisitorTest. + *
+ */ + + int SOURCE_DEPRECATED = 0x100; + int SOURCE_MASK = SOURCE_DEPRECATED; + + // Java ClassFile versions (the minor version is stored in the 16 most significant bits, and the + // major version in the 16 least significant bits). + + int V1_1 = 3 << 16 | 45; + int V1_2 = 0 << 16 | 46; + int V1_3 = 0 << 16 | 47; + int V1_4 = 0 << 16 | 48; + int V1_5 = 0 << 16 | 49; + int V1_6 = 0 << 16 | 50; + int V1_7 = 0 << 16 | 51; + int V1_8 = 0 << 16 | 52; + int V9 = 0 << 16 | 53; + int V10 = 0 << 16 | 54; + int V11 = 0 << 16 | 55; + int V12 = 0 << 16 | 56; + int V13 = 0 << 16 | 57; + int V14 = 0 << 16 | 58; + int V15 = 0 << 16 | 59; + int V16 = 0 << 16 | 60; + int V17 = 0 << 16 | 61; + int V18 = 0 << 16 | 62; + + /** + * Version flag indicating that the class is using 'preview' features. + * + *

{@code version & V_PREVIEW == V_PREVIEW} tests if a version is flagged with {@code + * V_PREVIEW}. + */ + int V_PREVIEW = 0xFFFF0000; + + // Access flags values, defined in + // - https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.1-200-E.1 + // - https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.5-200-A.1 + // - https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.6-200-A.1 + // - https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.25 + + int ACC_PUBLIC = 0x0001; // class, field, method + int ACC_PRIVATE = 0x0002; // class, field, method + int ACC_PROTECTED = 0x0004; // class, field, method + int ACC_STATIC = 0x0008; // field, method + int ACC_FINAL = 0x0010; // class, field, method, parameter + int ACC_SUPER = 0x0020; // class + int ACC_SYNCHRONIZED = 0x0020; // method + int ACC_OPEN = 0x0020; // module + int ACC_TRANSITIVE = 0x0020; // module requires + int ACC_VOLATILE = 0x0040; // field + int ACC_BRIDGE = 0x0040; // method + int ACC_STATIC_PHASE = 0x0040; // module requires + int ACC_VARARGS = 0x0080; // method + int ACC_TRANSIENT = 0x0080; // field + int ACC_NATIVE = 0x0100; // method + int ACC_INTERFACE = 0x0200; // class + int ACC_ABSTRACT = 0x0400; // class, method + int ACC_STRICT = 0x0800; // method + int ACC_SYNTHETIC = 0x1000; // class, field, method, parameter, module * + int ACC_ANNOTATION = 0x2000; // class + int ACC_ENUM = 0x4000; // class(?) field inner + int ACC_MANDATED = 0x8000; // field, method, parameter, module, module * + int ACC_MODULE = 0x8000; // class + + // ASM specific access flags. + // WARNING: the 16 least significant bits must NOT be used, to avoid conflicts with standard + // access flags, and also to make sure that these flags are automatically filtered out when + // written in class files (because access flags are stored using 16 bits only). + + int ACC_RECORD = 0x10000; // class + int ACC_DEPRECATED = 0x20000; // class, field, method + + // Possible values for the type operand of the NEWARRAY instruction. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-6.html#jvms-6.5.newarray. + + int T_BOOLEAN = 4; + int T_CHAR = 5; + int T_FLOAT = 6; + int T_DOUBLE = 7; + int T_BYTE = 8; + int T_SHORT = 9; + int T_INT = 10; + int T_LONG = 11; + + // Possible values for the reference_kind field of CONSTANT_MethodHandle_info structures. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.4.8. + + int H_GETFIELD = 1; + int H_GETSTATIC = 2; + int H_PUTFIELD = 3; + int H_PUTSTATIC = 4; + int H_INVOKEVIRTUAL = 5; + int H_INVOKESTATIC = 6; + int H_INVOKESPECIAL = 7; + int H_NEWINVOKESPECIAL = 8; + int H_INVOKEINTERFACE = 9; + + // ASM specific stack map frame types, used in {@link ClassVisitor#visitFrame}. + + /** An expanded frame. See {@link ClassReader#EXPAND_FRAMES}. */ + int F_NEW = -1; + + /** A compressed frame with complete frame data. */ + int F_FULL = 0; + + /** + * A compressed frame where locals are the same as the locals in the previous frame, except that + * additional 1-3 locals are defined, and with an empty stack. + */ + int F_APPEND = 1; + + /** + * A compressed frame where locals are the same as the locals in the previous frame, except that + * the last 1-3 locals are absent and with an empty stack. + */ + int F_CHOP = 2; + + /** + * A compressed frame with exactly the same locals as the previous frame and with an empty stack. + */ + int F_SAME = 3; + + /** + * A compressed frame with exactly the same locals as the previous frame and with a single value + * on the stack. + */ + int F_SAME1 = 4; + + // Standard stack map frame element types, used in {@link ClassVisitor#visitFrame}. + + Integer TOP = Frame.ITEM_TOP; + Integer INTEGER = Frame.ITEM_INTEGER; + Integer FLOAT = Frame.ITEM_FLOAT; + Integer DOUBLE = Frame.ITEM_DOUBLE; + Integer LONG = Frame.ITEM_LONG; + Integer NULL = Frame.ITEM_NULL; + Integer UNINITIALIZED_THIS = Frame.ITEM_UNINITIALIZED_THIS; + + // The JVM opcode values (with the MethodVisitor method name used to visit them in comment, and + // where '-' means 'same method name as on the previous line'). + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-6.html. + + int NOP = 0; // visitInsn + int ACONST_NULL = 1; // - + int ICONST_M1 = 2; // - + int ICONST_0 = 3; // - + int ICONST_1 = 4; // - + int ICONST_2 = 5; // - + int ICONST_3 = 6; // - + int ICONST_4 = 7; // - + int ICONST_5 = 8; // - + int LCONST_0 = 9; // - + int LCONST_1 = 10; // - + int FCONST_0 = 11; // - + int FCONST_1 = 12; // - + int FCONST_2 = 13; // - + int DCONST_0 = 14; // - + int DCONST_1 = 15; // - + int BIPUSH = 16; // visitIntInsn + int SIPUSH = 17; // - + int LDC = 18; // visitLdcInsn + int ILOAD = 21; // visitVarInsn + int LLOAD = 22; // - + int FLOAD = 23; // - + int DLOAD = 24; // - + int ALOAD = 25; // - + int IALOAD = 46; // visitInsn + int LALOAD = 47; // - + int FALOAD = 48; // - + int DALOAD = 49; // - + int AALOAD = 50; // - + int BALOAD = 51; // - + int CALOAD = 52; // - + int SALOAD = 53; // - + int ISTORE = 54; // visitVarInsn + int LSTORE = 55; // - + int FSTORE = 56; // - + int DSTORE = 57; // - + int ASTORE = 58; // - + int IASTORE = 79; // visitInsn + int LASTORE = 80; // - + int FASTORE = 81; // - + int DASTORE = 82; // - + int AASTORE = 83; // - + int BASTORE = 84; // - + int CASTORE = 85; // - + int SASTORE = 86; // - + int POP = 87; // - + int POP2 = 88; // - + int DUP = 89; // - + int DUP_X1 = 90; // - + int DUP_X2 = 91; // - + int DUP2 = 92; // - + int DUP2_X1 = 93; // - + int DUP2_X2 = 94; // - + int SWAP = 95; // - + int IADD = 96; // - + int LADD = 97; // - + int FADD = 98; // - + int DADD = 99; // - + int ISUB = 100; // - + int LSUB = 101; // - + int FSUB = 102; // - + int DSUB = 103; // - + int IMUL = 104; // - + int LMUL = 105; // - + int FMUL = 106; // - + int DMUL = 107; // - + int IDIV = 108; // - + int LDIV = 109; // - + int FDIV = 110; // - + int DDIV = 111; // - + int IREM = 112; // - + int LREM = 113; // - + int FREM = 114; // - + int DREM = 115; // - + int INEG = 116; // - + int LNEG = 117; // - + int FNEG = 118; // - + int DNEG = 119; // - + int ISHL = 120; // - + int LSHL = 121; // - + int ISHR = 122; // - + int LSHR = 123; // - + int IUSHR = 124; // - + int LUSHR = 125; // - + int IAND = 126; // - + int LAND = 127; // - + int IOR = 128; // - + int LOR = 129; // - + int IXOR = 130; // - + int LXOR = 131; // - + int IINC = 132; // visitIincInsn + int I2L = 133; // visitInsn + int I2F = 134; // - + int I2D = 135; // - + int L2I = 136; // - + int L2F = 137; // - + int L2D = 138; // - + int F2I = 139; // - + int F2L = 140; // - + int F2D = 141; // - + int D2I = 142; // - + int D2L = 143; // - + int D2F = 144; // - + int I2B = 145; // - + int I2C = 146; // - + int I2S = 147; // - + int LCMP = 148; // - + int FCMPL = 149; // - + int FCMPG = 150; // - + int DCMPL = 151; // - + int DCMPG = 152; // - + int IFEQ = 153; // visitJumpInsn + int IFNE = 154; // - + int IFLT = 155; // - + int IFGE = 156; // - + int IFGT = 157; // - + int IFLE = 158; // - + int IF_ICMPEQ = 159; // - + int IF_ICMPNE = 160; // - + int IF_ICMPLT = 161; // - + int IF_ICMPGE = 162; // - + int IF_ICMPGT = 163; // - + int IF_ICMPLE = 164; // - + int IF_ACMPEQ = 165; // - + int IF_ACMPNE = 166; // - + int GOTO = 167; // - + int JSR = 168; // - + int RET = 169; // visitVarInsn + int TABLESWITCH = 170; // visiTableSwitchInsn + int LOOKUPSWITCH = 171; // visitLookupSwitch + int IRETURN = 172; // visitInsn + int LRETURN = 173; // - + int FRETURN = 174; // - + int DRETURN = 175; // - + int ARETURN = 176; // - + int RETURN = 177; // - + int GETSTATIC = 178; // visitFieldInsn + int PUTSTATIC = 179; // - + int GETFIELD = 180; // - + int PUTFIELD = 181; // - + int INVOKEVIRTUAL = 182; // visitMethodInsn + int INVOKESPECIAL = 183; // - + int INVOKESTATIC = 184; // - + int INVOKEINTERFACE = 185; // - + int INVOKEDYNAMIC = 186; // visitInvokeDynamicInsn + int NEW = 187; // visitTypeInsn + int NEWARRAY = 188; // visitIntInsn + int ANEWARRAY = 189; // visitTypeInsn + int ARRAYLENGTH = 190; // visitInsn + int ATHROW = 191; // - + int CHECKCAST = 192; // visitTypeInsn + int INSTANCEOF = 193; // - + int MONITORENTER = 194; // visitInsn + int MONITOREXIT = 195; // - + int MULTIANEWARRAY = 197; // visitMultiANewArrayInsn + int IFNULL = 198; // visitJumpInsn + int IFNONNULL = 199; // - +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/RecordComponentVisitor.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/RecordComponentVisitor.java new file mode 100644 index 0000000..8d6ea79 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/RecordComponentVisitor.java @@ -0,0 +1,135 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A visitor to visit a record component. The methods of this class must be called in the following + * order: ( {@code visitAnnotation} | {@code visitTypeAnnotation} | {@code visitAttribute} )* {@code + * visitEnd}. @Author Remi Forax @Author Eric Bruneton + */ +public abstract class RecordComponentVisitor { + + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM8} or {@link Opcodes#ASM9}. + */ + protected final int api; + + /** + * The record visitor to which this visitor must delegate method calls. May be {@literal null}. + */ + /*package-private*/ RecordComponentVisitor delegate; + + /** + * Constructs a new {@link RecordComponentVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link Opcodes#ASM8} + * or {@link Opcodes#ASM9}. + */ + public RecordComponentVisitor(final int api) { + this(api, null); + } + + /** + * Constructs a new {@link RecordComponentVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be {@link Opcodes#ASM8}. + * @param recordComponentVisitor the record component visitor to which this visitor must delegate + * method calls. May be null. + */ + public RecordComponentVisitor( + final int api, final RecordComponentVisitor recordComponentVisitor) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + if (api == Opcodes.ASM10_EXPERIMENTAL) { + Constants.checkAsmExperimental(this); + } + this.api = api; + this.delegate = recordComponentVisitor; + } + + /** + * The record visitor to which this visitor must delegate method calls. May be {@literal null}. + * + * @return the record visitor to which this visitor must delegate method calls or {@literal null}. + */ + public RecordComponentVisitor getDelegate() { + return delegate; + } + + /** + * Visits an annotation of the record component. + * + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (delegate != null) { + return delegate.visitAnnotation(descriptor, visible); + } + return null; + } + + /** + * Visits an annotation on a type in the record component signature. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#CLASS_TYPE_PARAMETER}, {@link + * TypeReference#CLASS_TYPE_PARAMETER_BOUND} or {@link TypeReference#CLASS_EXTENDS}. See + * {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (delegate != null) { + return delegate.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + return null; + } + + /** + * Visits a non standard attribute of the record component. + * + * @param attribute an attribute. + */ + public void visitAttribute(final Attribute attribute) { + if (delegate != null) { + delegate.visitAttribute(attribute); + } + } + + /** + * Visits the end of the record component. This method, which is the last one to be called, is + * used to inform the visitor that everything have been visited. + */ + public void visitEnd() { + if (delegate != null) { + delegate.visitEnd(); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/RecordComponentWriter.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/RecordComponentWriter.java new file mode 100644 index 0000000..71acc4f --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/RecordComponentWriter.java @@ -0,0 +1,211 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +final class RecordComponentWriter extends RecordComponentVisitor { + + /** Where the constants used in this RecordComponentWriter must be stored. */ + private final SymbolTable symbolTable; + + // Note: fields are ordered as in the record_component_info structure, and those related to + // attributes are ordered as in Section 4.7 of the JVMS. + + /** The name_index field of the Record attribute. */ + private final int nameIndex; + + /** The descriptor_index field of the the Record attribute. */ + private final int descriptorIndex; + + /** + * The signature_index field of the Signature attribute of this record component, or 0 if there is + * no Signature attribute. + */ + private int signatureIndex; + + /** + * The last runtime visible annotation of this record component. The previous ones can be accessed + * with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleAnnotation; + + /** + * The last runtime invisible annotation of this record component. The previous ones can be + * accessed with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleAnnotation; + + /** + * The last runtime visible type annotation of this record component. The previous ones can be + * accessed with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleTypeAnnotation; + + /** + * The last runtime invisible type annotation of this record component. The previous ones can be + * accessed with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleTypeAnnotation; + + /** + * The first non standard attribute of this record component. The next ones can be accessed with + * the {@link Attribute#nextAttribute} field. May be {@literal null}. + * + *

WARNING: this list stores the attributes in the reverse order of their visit. + * firstAttribute is actually the last attribute visited in {@link #visitAttribute(Attribute)}. + * The {@link #putRecordComponentInfo(ByteVector)} method writes the attributes in the order + * defined by this list, i.e. in the reverse order specified by the user. + */ + private Attribute firstAttribute; + + /** + * Constructs a new {@link RecordComponentWriter}. + * + * @param symbolTable where the constants used in this RecordComponentWriter must be stored. + * @param name the record component name. + * @param descriptor the record component descriptor (see {@link Type}). + * @param signature the record component signature. May be {@literal null}. + */ + RecordComponentWriter( + final SymbolTable symbolTable, + final String name, + final String descriptor, + final String signature) { + super(/* latest api = */ Opcodes.ASM9); + this.symbolTable = symbolTable; + this.nameIndex = symbolTable.addConstantUtf8(name); + this.descriptorIndex = symbolTable.addConstantUtf8(descriptor); + if (signature != null) { + this.signatureIndex = symbolTable.addConstantUtf8(signature); + } + } + + // ----------------------------------------------------------------------------------------------- + // Implementation of the FieldVisitor abstract class + // ----------------------------------------------------------------------------------------------- + + @Override + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeVisibleAnnotation); + } else { + return lastRuntimeInvisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeInvisibleAnnotation); + } + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeVisibleTypeAnnotation); + } else { + return lastRuntimeInvisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public void visitAttribute(final Attribute attribute) { + // Store the attributes in the reverse order of their visit by this method. + attribute.nextAttribute = firstAttribute; + firstAttribute = attribute; + } + + @Override + public void visitEnd() { + // Nothing to do. + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the size of the record component JVMS structure generated by this + * RecordComponentWriter. Also adds the names of the attributes of this record component in the + * constant pool. + * + * @return the size in bytes of the record_component_info of the Record attribute. + */ + int computeRecordComponentInfoSize() { + // name_index, descriptor_index and attributes_count fields use 6 bytes. + int size = 6; + size += Attribute.computeAttributesSize(symbolTable, 0, signatureIndex); + size += + AnnotationWriter.computeAnnotationsSize( + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation); + if (firstAttribute != null) { + size += firstAttribute.computeAttributesSize(symbolTable); + } + return size; + } + + /** + * Puts the content of the record component generated by this RecordComponentWriter into the given + * ByteVector. + * + * @param output where the record_component_info structure must be put. + */ + void putRecordComponentInfo(final ByteVector output) { + output.putShort(nameIndex).putShort(descriptorIndex); + // Compute and put the attributes_count field. + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + int attributesCount = 0; + if (signatureIndex != 0) { + ++attributesCount; + } + if (lastRuntimeVisibleAnnotation != null) { + ++attributesCount; + } + if (lastRuntimeInvisibleAnnotation != null) { + ++attributesCount; + } + if (lastRuntimeVisibleTypeAnnotation != null) { + ++attributesCount; + } + if (lastRuntimeInvisibleTypeAnnotation != null) { + ++attributesCount; + } + if (firstAttribute != null) { + attributesCount += firstAttribute.getAttributeCount(); + } + output.putShort(attributesCount); + Attribute.putAttributes(symbolTable, 0, signatureIndex, output); + AnnotationWriter.putAnnotations( + symbolTable, + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation, + output); + if (firstAttribute != null) { + firstAttribute.putAttributes(symbolTable, output); + } + } + + /** + * Collects the attributes of this record component into the given set of attribute prototypes. + * + * @param attributePrototypes a set of attribute prototypes. + */ + final void collectAttributePrototypes(final Attribute.Set attributePrototypes) { + attributePrototypes.addAttributes(firstAttribute); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Symbol.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Symbol.java new file mode 100644 index 0000000..06768d1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Symbol.java @@ -0,0 +1,227 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * An entry of the constant pool, of the BootstrapMethods attribute, or of the (ASM specific) type + * table of a class. @Author Eric Bruneton + * + * @see JVMS + * 4.4 + * @see JVMS + * 4.7.23 + */ +abstract class Symbol { + + // Tag values for the constant pool entries (using the same order as in the JVMS). + + /** The tag value of CONSTANT_Class_info JVMS structures. */ + static final int CONSTANT_CLASS_TAG = 7; + + /** The tag value of CONSTANT_Fieldref_info JVMS structures. */ + static final int CONSTANT_FIELDREF_TAG = 9; + + /** The tag value of CONSTANT_Methodref_info JVMS structures. */ + static final int CONSTANT_METHODREF_TAG = 10; + + /** The tag value of CONSTANT_InterfaceMethodref_info JVMS structures. */ + static final int CONSTANT_INTERFACE_METHODREF_TAG = 11; + + /** The tag value of CONSTANT_String_info JVMS structures. */ + static final int CONSTANT_STRING_TAG = 8; + + /** The tag value of CONSTANT_Integer_info JVMS structures. */ + static final int CONSTANT_INTEGER_TAG = 3; + + /** The tag value of CONSTANT_Float_info JVMS structures. */ + static final int CONSTANT_FLOAT_TAG = 4; + + /** The tag value of CONSTANT_Long_info JVMS structures. */ + static final int CONSTANT_LONG_TAG = 5; + + /** The tag value of CONSTANT_Double_info JVMS structures. */ + static final int CONSTANT_DOUBLE_TAG = 6; + + /** The tag value of CONSTANT_NameAndType_info JVMS structures. */ + static final int CONSTANT_NAME_AND_TYPE_TAG = 12; + + /** The tag value of CONSTANT_Utf8_info JVMS structures. */ + static final int CONSTANT_UTF8_TAG = 1; + + /** The tag value of CONSTANT_MethodHandle_info JVMS structures. */ + static final int CONSTANT_METHOD_HANDLE_TAG = 15; + + /** The tag value of CONSTANT_MethodType_info JVMS structures. */ + static final int CONSTANT_METHOD_TYPE_TAG = 16; + + /** The tag value of CONSTANT_Dynamic_info JVMS structures. */ + static final int CONSTANT_DYNAMIC_TAG = 17; + + /** The tag value of CONSTANT_InvokeDynamic_info JVMS structures. */ + static final int CONSTANT_INVOKE_DYNAMIC_TAG = 18; + + /** The tag value of CONSTANT_Module_info JVMS structures. */ + static final int CONSTANT_MODULE_TAG = 19; + + /** The tag value of CONSTANT_Package_info JVMS structures. */ + static final int CONSTANT_PACKAGE_TAG = 20; + + // Tag values for the BootstrapMethods attribute entries (ASM specific tag). + + /** The tag value of the BootstrapMethods attribute entries. */ + static final int BOOTSTRAP_METHOD_TAG = 64; + + // Tag values for the type table entries (ASM specific tags). + + /** The tag value of a normal type entry in the (ASM specific) type table of a class. */ + static final int TYPE_TAG = 128; + + /** + * The tag value of an {@link Frame#ITEM_UNINITIALIZED} type entry in the type table of a class. + */ + static final int UNINITIALIZED_TYPE_TAG = 129; + + /** The tag value of a merged type entry in the (ASM specific) type table of a class. */ + static final int MERGED_TYPE_TAG = 130; + + // Instance fields. + + /** + * The index of this symbol in the constant pool, in the BootstrapMethods attribute, or in the + * (ASM specific) type table of a class (depending on the {@link #tag} value). + */ + final int index; + + /** + * A tag indicating the type of this symbol. Must be one of the static tag values defined in this + * class. + */ + final int tag; + + /** + * The internal name of the owner class of this symbol. Only used for {@link + * #CONSTANT_FIELDREF_TAG}, {@link #CONSTANT_METHODREF_TAG}, {@link + * #CONSTANT_INTERFACE_METHODREF_TAG}, and {@link #CONSTANT_METHOD_HANDLE_TAG} symbols. + */ + final String owner; + + /** + * The name of the class field or method corresponding to this symbol. Only used for {@link + * #CONSTANT_FIELDREF_TAG}, {@link #CONSTANT_METHODREF_TAG}, {@link + * #CONSTANT_INTERFACE_METHODREF_TAG}, {@link #CONSTANT_NAME_AND_TYPE_TAG}, {@link + * #CONSTANT_METHOD_HANDLE_TAG}, {@link #CONSTANT_DYNAMIC_TAG} and {@link + * #CONSTANT_INVOKE_DYNAMIC_TAG} symbols. + */ + final String name; + + /** + * The string value of this symbol. This is: + * + *

    + *
  • a field or method descriptor for {@link #CONSTANT_FIELDREF_TAG}, {@link + * #CONSTANT_METHODREF_TAG}, {@link #CONSTANT_INTERFACE_METHODREF_TAG}, {@link + * #CONSTANT_NAME_AND_TYPE_TAG}, {@link #CONSTANT_METHOD_HANDLE_TAG}, {@link + * #CONSTANT_METHOD_TYPE_TAG}, {@link #CONSTANT_DYNAMIC_TAG} and {@link + * #CONSTANT_INVOKE_DYNAMIC_TAG} symbols, + *
  • an arbitrary string for {@link #CONSTANT_UTF8_TAG} and {@link #CONSTANT_STRING_TAG} + * symbols, + *
  • an internal class name for {@link #CONSTANT_CLASS_TAG}, {@link #TYPE_TAG} and {@link + * #UNINITIALIZED_TYPE_TAG} symbols, + *
  • {@literal null} for the other types of symbol. + *
+ */ + final String value; + + /** + * The numeric value of this symbol. This is: + * + *
    + *
  • the symbol's value for {@link #CONSTANT_INTEGER_TAG},{@link #CONSTANT_FLOAT_TAG}, {@link + * #CONSTANT_LONG_TAG}, {@link #CONSTANT_DOUBLE_TAG}, + *
  • the CONSTANT_MethodHandle_info reference_kind field value for {@link + * #CONSTANT_METHOD_HANDLE_TAG} symbols, + *
  • the CONSTANT_InvokeDynamic_info bootstrap_method_attr_index field value for {@link + * #CONSTANT_INVOKE_DYNAMIC_TAG} symbols, + *
  • the offset of a bootstrap method in the BootstrapMethods boostrap_methods array, for + * {@link #CONSTANT_DYNAMIC_TAG} or {@link #BOOTSTRAP_METHOD_TAG} symbols, + *
  • the bytecode offset of the NEW instruction that created an {@link + * Frame#ITEM_UNINITIALIZED} type for {@link #UNINITIALIZED_TYPE_TAG} symbols, + *
  • the indices (in the class' type table) of two {@link #TYPE_TAG} source types for {@link + * #MERGED_TYPE_TAG} symbols, + *
  • 0 for the other types of symbol. + *
+ */ + final long data; + + /** + * Additional information about this symbol, generally computed lazily. Warning: the value of + * this field is ignored when comparing Symbol instances (to avoid duplicate entries in a + * SymbolTable). Therefore, this field should only contain data that can be computed from the + * other fields of this class. It contains: + * + *
    + *
  • the {@link Type#getArgumentsAndReturnSizes} of the symbol's method descriptor for {@link + * #CONSTANT_METHODREF_TAG}, {@link #CONSTANT_INTERFACE_METHODREF_TAG} and {@link + * #CONSTANT_INVOKE_DYNAMIC_TAG} symbols, + *
  • the index in the InnerClasses_attribute 'classes' array (plus one) corresponding to this + * class, for {@link #CONSTANT_CLASS_TAG} symbols, + *
  • the index (in the class' type table) of the merged type of the two source types for + * {@link #MERGED_TYPE_TAG} symbols, + *
  • 0 for the other types of symbol, or if this field has not been computed yet. + *
+ */ + int info; + + /** + * Constructs a new Symbol. This constructor can't be used directly because the Symbol class is + * abstract. Instead, use the factory methods of the {@link SymbolTable} class. + * + * @param index the symbol index in the constant pool, in the BootstrapMethods attribute, or in + * the (ASM specific) type table of a class (depending on 'tag'). + * @param tag the symbol type. Must be one of the static tag values defined in this class. + * @param owner The internal name of the symbol's owner class. Maybe {@literal null}. + * @param name The name of the symbol's corresponding class field or method. Maybe {@literal + * null}. + * @param value The string value of this symbol. Maybe {@literal null}. + * @param data The numeric value of this symbol. + */ + Symbol( + final int index, + final int tag, + final String owner, + final String name, + final String value, + final long data) { + this.index = index; + this.tag = tag; + this.owner = owner; + this.name = name; + this.value = value; + this.data = data; + } + + /** + * Returns the result {@link Type#getArgumentsAndReturnSizes} on {@link #value}. + * + * @return the result {@link Type#getArgumentsAndReturnSizes} on {@link #value} (memoized in + * {@link #info} for efficiency). This should only be used for {@link + * #CONSTANT_METHODREF_TAG}, {@link #CONSTANT_INTERFACE_METHODREF_TAG} and {@link + * #CONSTANT_INVOKE_DYNAMIC_TAG} symbols. + */ + int getArgumentsAndReturnSizes() { + if (info == 0) { + info = Type.getArgumentsAndReturnSizes(value); + } + return info; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/SymbolTable.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/SymbolTable.java new file mode 100644 index 0000000..7106610 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/SymbolTable.java @@ -0,0 +1,1304 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * The constant pool entries, the BootstrapMethods attribute entries and the (ASM specific) type + * table entries of a class. @Author Eric Bruneton + * + * @see JVMS + * 4.4 + * @see JVMS + * 4.7.23 + */ +final class SymbolTable { + + /** + * The ClassWriter to which this SymbolTable belongs. This is only used to get access to {@link + * ClassWriter#getCommonSuperClass} and to serialize custom attributes with {@link + * Attribute#write}. + */ + final ClassWriter classWriter; + + /** + * The ClassReader from which this SymbolTable was constructed, or {@literal null} if it was + * constructed from scratch. + */ + private final ClassReader sourceClassReader; + + /** The major version number of the class to which this symbol table belongs. */ + private int majorVersion; + + /** The internal name of the class to which this symbol table belongs. */ + private String className; + + /** + * The total number of {@link Entry} instances in {@link #entries}. This includes entries that are + * accessible (recursively) via {@link Entry#next}. + */ + private int entryCount; + + /** + * A hash set of all the entries in this SymbolTable (this includes the constant pool entries, the + * bootstrap method entries and the type table entries). Each {@link Entry} instance is stored at + * the array index given by its hash code modulo the array size. If several entries must be stored + * at the same array index, they are linked together via their {@link Entry#next} field. The + * factory methods of this class make sure that this table does not contain duplicated entries. + */ + private Entry[] entries; + + /** + * The number of constant pool items in {@link #constantPool}, plus 1. The first constant pool + * item has index 1, and long and double items count for two items. + */ + private int constantPoolCount; + + /** + * The content of the ClassFile's constant_pool JVMS structure corresponding to this SymbolTable. + * The ClassFile's constant_pool_count field is not included. + */ + private final ByteVector constantPool; + + /** + * The number of bootstrap methods in {@link #bootstrapMethods}. Corresponds to the + * BootstrapMethods_attribute's num_bootstrap_methods field value. + */ + private int bootstrapMethodCount; + + /** + * The content of the BootstrapMethods attribute 'bootstrap_methods' array corresponding to this + * SymbolTable. Note that the first 6 bytes of the BootstrapMethods_attribute, and its + * num_bootstrap_methods field, are not included. + */ + private ByteVector bootstrapMethods; + + /** + * The actual number of elements in {@link #typeTable}. These elements are stored from index 0 to + * typeCount (excluded). The other array entries are empty. + */ + private int typeCount; + + /** + * An ASM specific type table used to temporarily store internal names that will not necessarily + * be stored in the constant pool. This type table is used by the control flow and data flow + * analysis algorithm used to compute stack map frames from scratch. This array stores {@link + * Symbol#TYPE_TAG} and {@link Symbol#UNINITIALIZED_TYPE_TAG}) Symbol. The type symbol at index + * {@code i} has its {@link Symbol#index} equal to {@code i} (and vice versa). + */ + private Entry[] typeTable; + + /** + * Constructs a new, empty SymbolTable for the given ClassWriter. + * + * @param classWriter a ClassWriter. + */ + SymbolTable(final ClassWriter classWriter) { + this.classWriter = classWriter; + this.sourceClassReader = null; + this.entries = new Entry[256]; + this.constantPoolCount = 1; + this.constantPool = new ByteVector(); + } + + /** + * Constructs a new SymbolTable for the given ClassWriter, initialized with the constant pool and + * bootstrap methods of the given ClassReader. + * + * @param classWriter a ClassWriter. + * @param classReader the ClassReader whose constant pool and bootstrap methods must be copied to + * initialize the SymbolTable. + */ + SymbolTable(final ClassWriter classWriter, final ClassReader classReader) { + this.classWriter = classWriter; + this.sourceClassReader = classReader; + + // Copy the constant pool binary content. + byte[] inputBytes = classReader.classFileBuffer; + int constantPoolOffset = classReader.getItem(1) - 1; + int constantPoolLength = classReader.header - constantPoolOffset; + constantPoolCount = classReader.getItemCount(); + constantPool = new ByteVector(constantPoolLength); + constantPool.putByteArray(inputBytes, constantPoolOffset, constantPoolLength); + + // Add the constant pool items in the symbol table entries. Reserve enough space in 'entries' to + // avoid too many hash set collisions (entries is not dynamically resized by the addConstant* + // method calls below), and to account for bootstrap method entries. + entries = new Entry[constantPoolCount * 2]; + char[] charBuffer = new char[classReader.getMaxStringLength()]; + boolean hasBootstrapMethods = false; + int itemIndex = 1; + while (itemIndex < constantPoolCount) { + int itemOffset = classReader.getItem(itemIndex); + int itemTag = inputBytes[itemOffset - 1]; + int nameAndTypeItemOffset; + switch (itemTag) { + case Symbol.CONSTANT_FIELDREF_TAG: + case Symbol.CONSTANT_METHODREF_TAG: + case Symbol.CONSTANT_INTERFACE_METHODREF_TAG: + nameAndTypeItemOffset = + classReader.getItem(classReader.readUnsignedShort(itemOffset + 2)); + addConstantMemberReference( + itemIndex, + itemTag, + classReader.readClass(itemOffset, charBuffer), + classReader.readUTF8(nameAndTypeItemOffset, charBuffer), + classReader.readUTF8(nameAndTypeItemOffset + 2, charBuffer)); + break; + case Symbol.CONSTANT_INTEGER_TAG: + case Symbol.CONSTANT_FLOAT_TAG: + addConstantIntegerOrFloat(itemIndex, itemTag, classReader.readInt(itemOffset)); + break; + case Symbol.CONSTANT_NAME_AND_TYPE_TAG: + addConstantNameAndType( + itemIndex, + classReader.readUTF8(itemOffset, charBuffer), + classReader.readUTF8(itemOffset + 2, charBuffer)); + break; + case Symbol.CONSTANT_LONG_TAG: + case Symbol.CONSTANT_DOUBLE_TAG: + addConstantLongOrDouble(itemIndex, itemTag, classReader.readLong(itemOffset)); + break; + case Symbol.CONSTANT_UTF8_TAG: + addConstantUtf8(itemIndex, classReader.readUtf(itemIndex, charBuffer)); + break; + case Symbol.CONSTANT_METHOD_HANDLE_TAG: + int memberRefItemOffset = + classReader.getItem(classReader.readUnsignedShort(itemOffset + 1)); + nameAndTypeItemOffset = + classReader.getItem(classReader.readUnsignedShort(memberRefItemOffset + 2)); + addConstantMethodHandle( + itemIndex, + classReader.readByte(itemOffset), + classReader.readClass(memberRefItemOffset, charBuffer), + classReader.readUTF8(nameAndTypeItemOffset, charBuffer), + classReader.readUTF8(nameAndTypeItemOffset + 2, charBuffer)); + break; + case Symbol.CONSTANT_DYNAMIC_TAG: + case Symbol.CONSTANT_INVOKE_DYNAMIC_TAG: + hasBootstrapMethods = true; + nameAndTypeItemOffset = + classReader.getItem(classReader.readUnsignedShort(itemOffset + 2)); + addConstantDynamicOrInvokeDynamicReference( + itemTag, + itemIndex, + classReader.readUTF8(nameAndTypeItemOffset, charBuffer), + classReader.readUTF8(nameAndTypeItemOffset + 2, charBuffer), + classReader.readUnsignedShort(itemOffset)); + break; + case Symbol.CONSTANT_STRING_TAG: + case Symbol.CONSTANT_CLASS_TAG: + case Symbol.CONSTANT_METHOD_TYPE_TAG: + case Symbol.CONSTANT_MODULE_TAG: + case Symbol.CONSTANT_PACKAGE_TAG: + addConstantUtf8Reference( + itemIndex, itemTag, classReader.readUTF8(itemOffset, charBuffer)); + break; + default: + throw new IllegalArgumentException(); + } + itemIndex += + (itemTag == Symbol.CONSTANT_LONG_TAG || itemTag == Symbol.CONSTANT_DOUBLE_TAG) ? 2 : 1; + } + + // Copy the BootstrapMethods, if any. + if (hasBootstrapMethods) { + copyBootstrapMethods(classReader, charBuffer); + } + } + + /** + * Read the BootstrapMethods 'bootstrap_methods' array binary content and add them as entries of + * the SymbolTable. + * + * @param classReader the ClassReader whose bootstrap methods must be copied to initialize the + * SymbolTable. + * @param charBuffer a buffer used to read strings in the constant pool. + */ + private void copyBootstrapMethods(final ClassReader classReader, final char[] charBuffer) { + // Find attributOffset of the 'bootstrap_methods' array. + byte[] inputBytes = classReader.classFileBuffer; + int currentAttributeOffset = classReader.getFirstAttributeOffset(); + for (int i = classReader.readUnsignedShort(currentAttributeOffset - 2); i > 0; --i) { + String attributeName = classReader.readUTF8(currentAttributeOffset, charBuffer); + if (Constants.BOOTSTRAP_METHODS.equals(attributeName)) { + bootstrapMethodCount = classReader.readUnsignedShort(currentAttributeOffset + 6); + break; + } + currentAttributeOffset += 6 + classReader.readInt(currentAttributeOffset + 2); + } + if (bootstrapMethodCount > 0) { + // Compute the offset and the length of the BootstrapMethods 'bootstrap_methods' array. + int bootstrapMethodsOffset = currentAttributeOffset + 8; + int bootstrapMethodsLength = classReader.readInt(currentAttributeOffset + 2) - 2; + bootstrapMethods = new ByteVector(bootstrapMethodsLength); + bootstrapMethods.putByteArray(inputBytes, bootstrapMethodsOffset, bootstrapMethodsLength); + + // Add each bootstrap method in the symbol table entries. + int currentOffset = bootstrapMethodsOffset; + for (int i = 0; i < bootstrapMethodCount; i++) { + int offset = currentOffset - bootstrapMethodsOffset; + int bootstrapMethodRef = classReader.readUnsignedShort(currentOffset); + currentOffset += 2; + int numBootstrapArguments = classReader.readUnsignedShort(currentOffset); + currentOffset += 2; + int hashCode = classReader.readConst(bootstrapMethodRef, charBuffer).hashCode(); + while (numBootstrapArguments-- > 0) { + int bootstrapArgument = classReader.readUnsignedShort(currentOffset); + currentOffset += 2; + hashCode ^= classReader.readConst(bootstrapArgument, charBuffer).hashCode(); + } + add(new Entry(i, Symbol.BOOTSTRAP_METHOD_TAG, offset, hashCode & 0x7FFFFFFF)); + } + } + } + + /** + * Returns the ClassReader from which this SymbolTable was constructed. + * + * @return the ClassReader from which this SymbolTable was constructed, or {@literal null} if it + * was constructed from scratch. + */ + ClassReader getSource() { + return sourceClassReader; + } + + /** + * Returns the major version of the class to which this symbol table belongs. + * + * @return the major version of the class to which this symbol table belongs. + */ + int getMajorVersion() { + return majorVersion; + } + + /** + * Returns the internal name of the class to which this symbol table belongs. + * + * @return the internal name of the class to which this symbol table belongs. + */ + String getClassName() { + return className; + } + + /** + * Sets the major version and the name of the class to which this symbol table belongs. Also adds + * the class name to the constant pool. + * + * @param majorVersion a major ClassFile version number. + * @param className an internal class name. + * @return the constant pool index of a new or already existing Symbol with the given class name. + */ + int setMajorVersionAndClassName(final int majorVersion, final String className) { + this.majorVersion = majorVersion; + this.className = className; + return addConstantClass(className).index; + } + + /** + * Returns the number of items in this symbol table's constant_pool array (plus 1). + * + * @return the number of items in this symbol table's constant_pool array (plus 1). + */ + int getConstantPoolCount() { + return constantPoolCount; + } + + /** + * Returns the length in bytes of this symbol table's constant_pool array. + * + * @return the length in bytes of this symbol table's constant_pool array. + */ + int getConstantPoolLength() { + return constantPool.length; + } + + /** + * Puts this symbol table's constant_pool array in the given ByteVector, preceded by the + * constant_pool_count value. + * + * @param output where the JVMS ClassFile's constant_pool array must be put. + */ + void putConstantPool(final ByteVector output) { + output.putShort(constantPoolCount).putByteArray(constantPool.data, 0, constantPool.length); + } + + /** + * Returns the size in bytes of this symbol table's BootstrapMethods attribute. Also adds the + * attribute name in the constant pool. + * + * @return the size in bytes of this symbol table's BootstrapMethods attribute. + */ + int computeBootstrapMethodsSize() { + if (bootstrapMethods != null) { + addConstantUtf8(Constants.BOOTSTRAP_METHODS); + return 8 + bootstrapMethods.length; + } else { + return 0; + } + } + + /** + * Puts this symbol table's BootstrapMethods attribute in the given ByteVector. This includes the + * 6 attribute header bytes and the num_bootstrap_methods value. + * + * @param output where the JVMS BootstrapMethods attribute must be put. + */ + void putBootstrapMethods(final ByteVector output) { + if (bootstrapMethods != null) { + output + .putShort(addConstantUtf8(Constants.BOOTSTRAP_METHODS)) + .putInt(bootstrapMethods.length + 2) + .putShort(bootstrapMethodCount) + .putByteArray(bootstrapMethods.data, 0, bootstrapMethods.length); + } + } + + // ----------------------------------------------------------------------------------------------- + // Generic symbol table entries management. + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the list of entries which can potentially have the given hash code. + * + * @param hashCode a {@link Entry#hashCode} value. + * @return the list of entries which can potentially have the given hash code. The list is stored + * via the {@link Entry#next} field. + */ + private Entry get(final int hashCode) { + return entries[hashCode % entries.length]; + } + + /** + * Puts the given entry in the {@link #entries} hash set. This method does not check + * whether {@link #entries} already contains a similar entry or not. {@link #entries} is resized + * if necessary to avoid hash collisions (multiple entries needing to be stored at the same {@link + * #entries} array index) as much as possible, with reasonable memory usage. + * + * @param entry an Entry (which must not already be contained in {@link #entries}). + * @return the given entry + */ + private Entry put(final Entry entry) { + if (entryCount > (entries.length * 3) / 4) { + int currentCapacity = entries.length; + int newCapacity = currentCapacity * 2 + 1; + Entry[] newEntries = new Entry[newCapacity]; + for (int i = currentCapacity - 1; i >= 0; --i) { + Entry currentEntry = entries[i]; + while (currentEntry != null) { + int newCurrentEntryIndex = currentEntry.hashCode % newCapacity; + Entry nextEntry = currentEntry.next; + currentEntry.next = newEntries[newCurrentEntryIndex]; + newEntries[newCurrentEntryIndex] = currentEntry; + currentEntry = nextEntry; + } + } + entries = newEntries; + } + entryCount++; + int index = entry.hashCode % entries.length; + entry.next = entries[index]; + return entries[index] = entry; + } + + /** + * Adds the given entry in the {@link #entries} hash set. This method does not check + * whether {@link #entries} already contains a similar entry or not, and does not resize + * {@link #entries} if necessary. + * + * @param entry an Entry (which must not already be contained in {@link #entries}). + */ + private void add(final Entry entry) { + entryCount++; + int index = entry.hashCode % entries.length; + entry.next = entries[index]; + entries[index] = entry; + } + + // ----------------------------------------------------------------------------------------------- + // Constant pool entries management. + // ----------------------------------------------------------------------------------------------- + + /** + * Adds a number or string constant to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value the value of the constant to be added to the constant pool. This parameter must be + * an {@link Integer}, {@link Byte}, {@link Character}, {@link Short}, {@link Boolean}, {@link + * Float}, {@link Long}, {@link Double}, {@link String}, {@link Type} or {@link Handle}. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstant(final Object value) { + if (value instanceof Integer) { + return addConstantInteger(((Integer) value).intValue()); + } else if (value instanceof Byte) { + return addConstantInteger(((Byte) value).intValue()); + } else if (value instanceof Character) { + return addConstantInteger(((Character) value).charValue()); + } else if (value instanceof Short) { + return addConstantInteger(((Short) value).intValue()); + } else if (value instanceof Boolean) { + return addConstantInteger(((Boolean) value).booleanValue() ? 1 : 0); + } else if (value instanceof Float) { + return addConstantFloat(((Float) value).floatValue()); + } else if (value instanceof Long) { + return addConstantLong(((Long) value).longValue()); + } else if (value instanceof Double) { + return addConstantDouble(((Double) value).doubleValue()); + } else if (value instanceof String) { + return addConstantString((String) value); + } else if (value instanceof Type) { + Type type = (Type) value; + int typeSort = type.getSort(); + if (typeSort == Type.OBJECT) { + return addConstantClass(type.getInternalName()); + } else if (typeSort == Type.METHOD) { + return addConstantMethodType(type.getDescriptor()); + } else { // type is a primitive or array type. + return addConstantClass(type.getDescriptor()); + } + } else if (value instanceof Handle) { + Handle handle = (Handle) value; + return addConstantMethodHandle( + handle.getTag(), + handle.getOwner(), + handle.getName(), + handle.getDesc(), + handle.isInterface()); + } else if (value instanceof ConstantDynamic) { + ConstantDynamic constantDynamic = (ConstantDynamic) value; + return addConstantDynamic( + constantDynamic.getName(), + constantDynamic.getDescriptor(), + constantDynamic.getBootstrapMethod(), + constantDynamic.getBootstrapMethodArgumentsUnsafe()); + } else { + throw new IllegalArgumentException("value " + value); + } + } + + /** + * Adds a CONSTANT_Class_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value the internal name of a class. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantClass(final String value) { + return addConstantUtf8Reference(Symbol.CONSTANT_CLASS_TAG, value); + } + + /** + * Adds a CONSTANT_Fieldref_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param owner the internal name of a class. + * @param name a field name. + * @param descriptor a field descriptor. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantFieldref(final String owner, final String name, final String descriptor) { + return addConstantMemberReference(Symbol.CONSTANT_FIELDREF_TAG, owner, name, descriptor); + } + + /** + * Adds a CONSTANT_Methodref_info or CONSTANT_InterfaceMethodref_info to the constant pool of this + * symbol table. Does nothing if the constant pool already contains a similar item. + * + * @param owner the internal name of a class. + * @param name a method name. + * @param descriptor a method descriptor. + * @param isInterface whether owner is an processer or not. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantMethodref( + final String owner, final String name, final String descriptor, final boolean isInterface) { + int tag = isInterface ? Symbol.CONSTANT_INTERFACE_METHODREF_TAG : Symbol.CONSTANT_METHODREF_TAG; + return addConstantMemberReference(tag, owner, name, descriptor); + } + + /** + * Adds a CONSTANT_Fieldref_info, CONSTANT_Methodref_info or CONSTANT_InterfaceMethodref_info to + * the constant pool of this symbol table. Does nothing if the constant pool already contains a + * similar item. + * + * @param tag one of {@link Symbol#CONSTANT_FIELDREF_TAG}, {@link Symbol#CONSTANT_METHODREF_TAG} + * or {@link Symbol#CONSTANT_INTERFACE_METHODREF_TAG}. + * @param owner the internal name of a class. + * @param name a field or method name. + * @param descriptor a field or method descriptor. + * @return a new or already existing Symbol with the given value. + */ + private Entry addConstantMemberReference( + final int tag, final String owner, final String name, final String descriptor) { + int hashCode = hash(tag, owner, name, descriptor); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag + && entry.hashCode == hashCode + && entry.owner.equals(owner) + && entry.name.equals(name) + && entry.value.equals(descriptor)) { + return entry; + } + entry = entry.next; + } + constantPool.put122( + tag, addConstantClass(owner).index, addConstantNameAndType(name, descriptor)); + return put(new Entry(constantPoolCount++, tag, owner, name, descriptor, 0, hashCode)); + } + + /** + * Adds a new CONSTANT_Fieldref_info, CONSTANT_Methodref_info or CONSTANT_InterfaceMethodref_info + * to the constant pool of this symbol table. + * + * @param index the constant pool index of the new Symbol. + * @param tag one of {@link Symbol#CONSTANT_FIELDREF_TAG}, {@link Symbol#CONSTANT_METHODREF_TAG} + * or {@link Symbol#CONSTANT_INTERFACE_METHODREF_TAG}. + * @param owner the internal name of a class. + * @param name a field or method name. + * @param descriptor a field or method descriptor. + */ + private void addConstantMemberReference( + final int index, + final int tag, + final String owner, + final String name, + final String descriptor) { + add(new Entry(index, tag, owner, name, descriptor, 0, hash(tag, owner, name, descriptor))); + } + + /** + * Adds a CONSTANT_String_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value a string. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantString(final String value) { + return addConstantUtf8Reference(Symbol.CONSTANT_STRING_TAG, value); + } + + /** + * Adds a CONSTANT_Integer_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value an int. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantInteger(final int value) { + return addConstantIntegerOrFloat(Symbol.CONSTANT_INTEGER_TAG, value); + } + + /** + * Adds a CONSTANT_Float_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value a float. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantFloat(final float value) { + return addConstantIntegerOrFloat(Symbol.CONSTANT_FLOAT_TAG, Float.floatToRawIntBits(value)); + } + + /** + * Adds a CONSTANT_Integer_info or CONSTANT_Float_info to the constant pool of this symbol table. + * Does nothing if the constant pool already contains a similar item. + * + * @param tag one of {@link Symbol#CONSTANT_INTEGER_TAG} or {@link Symbol#CONSTANT_FLOAT_TAG}. + * @param value an int or float. + * @return a constant pool constant with the given tag and primitive values. + */ + private Symbol addConstantIntegerOrFloat(final int tag, final int value) { + int hashCode = hash(tag, value); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag && entry.hashCode == hashCode && entry.data == value) { + return entry; + } + entry = entry.next; + } + constantPool.putByte(tag).putInt(value); + return put(new Entry(constantPoolCount++, tag, value, hashCode)); + } + + /** + * Adds a new CONSTANT_Integer_info or CONSTANT_Float_info to the constant pool of this symbol + * table. + * + * @param index the constant pool index of the new Symbol. + * @param tag one of {@link Symbol#CONSTANT_INTEGER_TAG} or {@link Symbol#CONSTANT_FLOAT_TAG}. + * @param value an int or float. + */ + private void addConstantIntegerOrFloat(final int index, final int tag, final int value) { + add(new Entry(index, tag, value, hash(tag, value))); + } + + /** + * Adds a CONSTANT_Long_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value a long. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantLong(final long value) { + return addConstantLongOrDouble(Symbol.CONSTANT_LONG_TAG, value); + } + + /** + * Adds a CONSTANT_Double_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value a double. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantDouble(final double value) { + return addConstantLongOrDouble(Symbol.CONSTANT_DOUBLE_TAG, Double.doubleToRawLongBits(value)); + } + + /** + * Adds a CONSTANT_Long_info or CONSTANT_Double_info to the constant pool of this symbol table. + * Does nothing if the constant pool already contains a similar item. + * + * @param tag one of {@link Symbol#CONSTANT_LONG_TAG} or {@link Symbol#CONSTANT_DOUBLE_TAG}. + * @param value a long or double. + * @return a constant pool constant with the given tag and primitive values. + */ + private Symbol addConstantLongOrDouble(final int tag, final long value) { + int hashCode = hash(tag, value); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag && entry.hashCode == hashCode && entry.data == value) { + return entry; + } + entry = entry.next; + } + int index = constantPoolCount; + constantPool.putByte(tag).putLong(value); + constantPoolCount += 2; + return put(new Entry(index, tag, value, hashCode)); + } + + /** + * Adds a new CONSTANT_Long_info or CONSTANT_Double_info to the constant pool of this symbol + * table. + * + * @param index the constant pool index of the new Symbol. + * @param tag one of {@link Symbol#CONSTANT_LONG_TAG} or {@link Symbol#CONSTANT_DOUBLE_TAG}. + * @param value a long or double. + */ + private void addConstantLongOrDouble(final int index, final int tag, final long value) { + add(new Entry(index, tag, value, hash(tag, value))); + } + + /** + * Adds a CONSTANT_NameAndType_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param name a field or method name. + * @param descriptor a field or method descriptor. + * @return a new or already existing Symbol with the given value. + */ + int addConstantNameAndType(final String name, final String descriptor) { + final int tag = Symbol.CONSTANT_NAME_AND_TYPE_TAG; + int hashCode = hash(tag, name, descriptor); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag + && entry.hashCode == hashCode + && entry.name.equals(name) + && entry.value.equals(descriptor)) { + return entry.index; + } + entry = entry.next; + } + constantPool.put122(tag, addConstantUtf8(name), addConstantUtf8(descriptor)); + return put(new Entry(constantPoolCount++, tag, name, descriptor, hashCode)).index; + } + + /** + * Adds a new CONSTANT_NameAndType_info to the constant pool of this symbol table. + * + * @param index the constant pool index of the new Symbol. + * @param name a field or method name. + * @param descriptor a field or method descriptor. + */ + private void addConstantNameAndType(final int index, final String name, final String descriptor) { + final int tag = Symbol.CONSTANT_NAME_AND_TYPE_TAG; + add(new Entry(index, tag, name, descriptor, hash(tag, name, descriptor))); + } + + /** + * Adds a CONSTANT_Utf8_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value a string. + * @return a new or already existing Symbol with the given value. + */ + int addConstantUtf8(final String value) { + int hashCode = hash(Symbol.CONSTANT_UTF8_TAG, value); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == Symbol.CONSTANT_UTF8_TAG + && entry.hashCode == hashCode + && entry.value.equals(value)) { + return entry.index; + } + entry = entry.next; + } + constantPool.putByte(Symbol.CONSTANT_UTF8_TAG).putUTF8(value); + return put(new Entry(constantPoolCount++, Symbol.CONSTANT_UTF8_TAG, value, hashCode)).index; + } + + /** + * Adds a new CONSTANT_String_info to the constant pool of this symbol table. + * + * @param index the constant pool index of the new Symbol. + * @param value a string. + */ + private void addConstantUtf8(final int index, final String value) { + add(new Entry(index, Symbol.CONSTANT_UTF8_TAG, value, hash(Symbol.CONSTANT_UTF8_TAG, value))); + } + + /** + * Adds a CONSTANT_MethodHandle_info to the constant pool of this symbol table. Does nothing if + * the constant pool already contains a similar item. + * + * @param referenceKind one of {@link Opcodes#H_GETFIELD}, {@link Opcodes#H_GETSTATIC}, {@link + * Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link Opcodes#H_INVOKEVIRTUAL}, {@link + * Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, {@link + * Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. + * @param owner the internal name of a class of processer. + * @param name a field or method name. + * @param descriptor a field or method descriptor. + * @param isInterface whether owner is an processer or not. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantMethodHandle( + final int referenceKind, + final String owner, + final String name, + final String descriptor, + final boolean isInterface) { + final int tag = Symbol.CONSTANT_METHOD_HANDLE_TAG; + // Note that we don't need to include isInterface in the hash computation, because it is + // redundant with owner (we can't have the same owner with different isInterface values). + int hashCode = hash(tag, owner, name, descriptor, referenceKind); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag + && entry.hashCode == hashCode + && entry.data == referenceKind + && entry.owner.equals(owner) + && entry.name.equals(name) + && entry.value.equals(descriptor)) { + return entry; + } + entry = entry.next; + } + if (referenceKind <= Opcodes.H_PUTSTATIC) { + constantPool.put112(tag, referenceKind, addConstantFieldref(owner, name, descriptor).index); + } else { + constantPool.put112( + tag, referenceKind, addConstantMethodref(owner, name, descriptor, isInterface).index); + } + return put( + new Entry(constantPoolCount++, tag, owner, name, descriptor, referenceKind, hashCode)); + } + + /** + * Adds a new CONSTANT_MethodHandle_info to the constant pool of this symbol table. + * + * @param index the constant pool index of the new Symbol. + * @param referenceKind one of {@link Opcodes#H_GETFIELD}, {@link Opcodes#H_GETSTATIC}, {@link + * Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link Opcodes#H_INVOKEVIRTUAL}, {@link + * Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, {@link + * Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. + * @param owner the internal name of a class of processer. + * @param name a field or method name. + * @param descriptor a field or method descriptor. + */ + private void addConstantMethodHandle( + final int index, + final int referenceKind, + final String owner, + final String name, + final String descriptor) { + final int tag = Symbol.CONSTANT_METHOD_HANDLE_TAG; + int hashCode = hash(tag, owner, name, descriptor, referenceKind); + add(new Entry(index, tag, owner, name, descriptor, referenceKind, hashCode)); + } + + /** + * Adds a CONSTANT_MethodType_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param methodDescriptor a method descriptor. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantMethodType(final String methodDescriptor) { + return addConstantUtf8Reference(Symbol.CONSTANT_METHOD_TYPE_TAG, methodDescriptor); + } + + /** + * Adds a CONSTANT_Dynamic_info to the constant pool of this symbol table. Also adds the related + * bootstrap method to the BootstrapMethods of this symbol table. Does nothing if the constant + * pool already contains a similar item. + * + * @param name a method name. + * @param descriptor a field descriptor. + * @param bootstrapMethodHandle a bootstrap method handle. + * @param bootstrapMethodArguments the bootstrap method arguments. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantDynamic( + final String name, + final String descriptor, + final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + Symbol bootstrapMethod = addBootstrapMethod(bootstrapMethodHandle, bootstrapMethodArguments); + return addConstantDynamicOrInvokeDynamicReference( + Symbol.CONSTANT_DYNAMIC_TAG, name, descriptor, bootstrapMethod.index); + } + + /** + * Adds a CONSTANT_InvokeDynamic_info to the constant pool of this symbol table. Also adds the + * related bootstrap method to the BootstrapMethods of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param name a method name. + * @param descriptor a method descriptor. + * @param bootstrapMethodHandle a bootstrap method handle. + * @param bootstrapMethodArguments the bootstrap method arguments. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantInvokeDynamic( + final String name, + final String descriptor, + final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + Symbol bootstrapMethod = addBootstrapMethod(bootstrapMethodHandle, bootstrapMethodArguments); + return addConstantDynamicOrInvokeDynamicReference( + Symbol.CONSTANT_INVOKE_DYNAMIC_TAG, name, descriptor, bootstrapMethod.index); + } + + /** + * Adds a CONSTANT_Dynamic or a CONSTANT_InvokeDynamic_info to the constant pool of this symbol + * table. Does nothing if the constant pool already contains a similar item. + * + * @param tag one of {@link Symbol#CONSTANT_DYNAMIC_TAG} or {@link + * Symbol#CONSTANT_INVOKE_DYNAMIC_TAG}. + * @param name a method name. + * @param descriptor a field descriptor for CONSTANT_DYNAMIC_TAG) or a method descriptor for + * CONSTANT_INVOKE_DYNAMIC_TAG. + * @param bootstrapMethodIndex the index of a bootstrap method in the BootstrapMethods attribute. + * @return a new or already existing Symbol with the given value. + */ + private Symbol addConstantDynamicOrInvokeDynamicReference( + final int tag, final String name, final String descriptor, final int bootstrapMethodIndex) { + int hashCode = hash(tag, name, descriptor, bootstrapMethodIndex); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag + && entry.hashCode == hashCode + && entry.data == bootstrapMethodIndex + && entry.name.equals(name) + && entry.value.equals(descriptor)) { + return entry; + } + entry = entry.next; + } + constantPool.put122(tag, bootstrapMethodIndex, addConstantNameAndType(name, descriptor)); + return put( + new Entry( + constantPoolCount++, tag, null, name, descriptor, bootstrapMethodIndex, hashCode)); + } + + /** + * Adds a new CONSTANT_Dynamic_info or CONSTANT_InvokeDynamic_info to the constant pool of this + * symbol table. + * + * @param tag one of {@link Symbol#CONSTANT_DYNAMIC_TAG} or {@link + * Symbol#CONSTANT_INVOKE_DYNAMIC_TAG}. + * @param index the constant pool index of the new Symbol. + * @param name a method name. + * @param descriptor a field descriptor for CONSTANT_DYNAMIC_TAG or a method descriptor for + * CONSTANT_INVOKE_DYNAMIC_TAG. + * @param bootstrapMethodIndex the index of a bootstrap method in the BootstrapMethods attribute. + */ + private void addConstantDynamicOrInvokeDynamicReference( + final int tag, + final int index, + final String name, + final String descriptor, + final int bootstrapMethodIndex) { + int hashCode = hash(tag, name, descriptor, bootstrapMethodIndex); + add(new Entry(index, tag, null, name, descriptor, bootstrapMethodIndex, hashCode)); + } + + /** + * Adds a CONSTANT_Module_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param moduleName a fully qualified name (using dots) of a module. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantModule(final String moduleName) { + return addConstantUtf8Reference(Symbol.CONSTANT_MODULE_TAG, moduleName); + } + + /** + * Adds a CONSTANT_Package_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param packageName the internal name of a package. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantPackage(final String packageName) { + return addConstantUtf8Reference(Symbol.CONSTANT_PACKAGE_TAG, packageName); + } + + /** + * Adds a CONSTANT_Class_info, CONSTANT_String_info, CONSTANT_MethodType_info, + * CONSTANT_Module_info or CONSTANT_Package_info to the constant pool of this symbol table. Does + * nothing if the constant pool already contains a similar item. + * + * @param tag one of {@link Symbol#CONSTANT_CLASS_TAG}, {@link Symbol#CONSTANT_STRING_TAG}, {@link + * Symbol#CONSTANT_METHOD_TYPE_TAG}, {@link Symbol#CONSTANT_MODULE_TAG} or {@link + * Symbol#CONSTANT_PACKAGE_TAG}. + * @param value an internal class name, an arbitrary string, a method descriptor, a module or a + * package name, depending on tag. + * @return a new or already existing Symbol with the given value. + */ + private Symbol addConstantUtf8Reference(final int tag, final String value) { + int hashCode = hash(tag, value); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag && entry.hashCode == hashCode && entry.value.equals(value)) { + return entry; + } + entry = entry.next; + } + constantPool.put12(tag, addConstantUtf8(value)); + return put(new Entry(constantPoolCount++, tag, value, hashCode)); + } + + /** + * Adds a new CONSTANT_Class_info, CONSTANT_String_info, CONSTANT_MethodType_info, + * CONSTANT_Module_info or CONSTANT_Package_info to the constant pool of this symbol table. + * + * @param index the constant pool index of the new Symbol. + * @param tag one of {@link Symbol#CONSTANT_CLASS_TAG}, {@link Symbol#CONSTANT_STRING_TAG}, {@link + * Symbol#CONSTANT_METHOD_TYPE_TAG}, {@link Symbol#CONSTANT_MODULE_TAG} or {@link + * Symbol#CONSTANT_PACKAGE_TAG}. + * @param value an internal class name, an arbitrary string, a method descriptor, a module or a + * package name, depending on tag. + */ + private void addConstantUtf8Reference(final int index, final int tag, final String value) { + add(new Entry(index, tag, value, hash(tag, value))); + } + + // ----------------------------------------------------------------------------------------------- + // Bootstrap method entries management. + // ----------------------------------------------------------------------------------------------- + + /** + * Adds a bootstrap method to the BootstrapMethods attribute of this symbol table. Does nothing if + * the BootstrapMethods already contains a similar bootstrap method. + * + * @param bootstrapMethodHandle a bootstrap method handle. + * @param bootstrapMethodArguments the bootstrap method arguments. + * @return a new or already existing Symbol with the given value. + */ + Symbol addBootstrapMethod( + final Handle bootstrapMethodHandle, final Object... bootstrapMethodArguments) { + ByteVector bootstrapMethodsAttribute = bootstrapMethods; + if (bootstrapMethodsAttribute == null) { + bootstrapMethodsAttribute = bootstrapMethods = new ByteVector(); + } + + // The bootstrap method arguments can be Constant_Dynamic values, which reference other + // bootstrap methods. We must therefore add the bootstrap method arguments to the constant pool + // and BootstrapMethods attribute first, so that the BootstrapMethods attribute is not modified + // while adding the given bootstrap method to it, in the rest of this method. + int numBootstrapArguments = bootstrapMethodArguments.length; + int[] bootstrapMethodArgumentIndexes = new int[numBootstrapArguments]; + for (int i = 0; i < numBootstrapArguments; i++) { + bootstrapMethodArgumentIndexes[i] = addConstant(bootstrapMethodArguments[i]).index; + } + + // Write the bootstrap method in the BootstrapMethods table. This is necessary to be able to + // compare it with existing ones, and will be reverted below if there is already a similar + // bootstrap method. + int bootstrapMethodOffset = bootstrapMethodsAttribute.length; + bootstrapMethodsAttribute.putShort( + addConstantMethodHandle( + bootstrapMethodHandle.getTag(), + bootstrapMethodHandle.getOwner(), + bootstrapMethodHandle.getName(), + bootstrapMethodHandle.getDesc(), + bootstrapMethodHandle.isInterface()) + .index); + + bootstrapMethodsAttribute.putShort(numBootstrapArguments); + for (int i = 0; i < numBootstrapArguments; i++) { + bootstrapMethodsAttribute.putShort(bootstrapMethodArgumentIndexes[i]); + } + + // Compute the length and the hash code of the bootstrap method. + int bootstrapMethodlength = bootstrapMethodsAttribute.length - bootstrapMethodOffset; + int hashCode = bootstrapMethodHandle.hashCode(); + for (Object bootstrapMethodArgument : bootstrapMethodArguments) { + hashCode ^= bootstrapMethodArgument.hashCode(); + } + hashCode &= 0x7FFFFFFF; + + // Add the bootstrap method to the symbol table or revert the above changes. + return addBootstrapMethod(bootstrapMethodOffset, bootstrapMethodlength, hashCode); + } + + /** + * Adds a bootstrap method to the BootstrapMethods attribute of this symbol table. Does nothing if + * the BootstrapMethods already contains a similar bootstrap method (more precisely, reverts the + * content of {@link #bootstrapMethods} to remove the last, duplicate bootstrap method). + * + * @param offset the offset of the last bootstrap method in {@link #bootstrapMethods}, in bytes. + * @param length the length of this bootstrap method in {@link #bootstrapMethods}, in bytes. + * @param hashCode the hash code of this bootstrap method. + * @return a new or already existing Symbol with the given value. + */ + private Symbol addBootstrapMethod(final int offset, final int length, final int hashCode) { + final byte[] bootstrapMethodsData = bootstrapMethods.data; + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == Symbol.BOOTSTRAP_METHOD_TAG && entry.hashCode == hashCode) { + int otherOffset = (int) entry.data; + boolean isSameBootstrapMethod = true; + for (int i = 0; i < length; ++i) { + if (bootstrapMethodsData[offset + i] != bootstrapMethodsData[otherOffset + i]) { + isSameBootstrapMethod = false; + break; + } + } + if (isSameBootstrapMethod) { + bootstrapMethods.length = offset; // Revert to old position. + return entry; + } + } + entry = entry.next; + } + return put(new Entry(bootstrapMethodCount++, Symbol.BOOTSTRAP_METHOD_TAG, offset, hashCode)); + } + + // ----------------------------------------------------------------------------------------------- + // Type table entries management. + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the type table element whose index is given. + * + * @param typeIndex a type table index. + * @return the type table element whose index is given. + */ + Symbol getType(final int typeIndex) { + return typeTable[typeIndex]; + } + + /** + * Adds a type in the type table of this symbol table. Does nothing if the type table already + * contains a similar type. + * + * @param value an internal class name. + * @return the index of a new or already existing type Symbol with the given value. + */ + int addType(final String value) { + int hashCode = hash(Symbol.TYPE_TAG, value); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == Symbol.TYPE_TAG && entry.hashCode == hashCode && entry.value.equals(value)) { + return entry.index; + } + entry = entry.next; + } + return addTypeInternal(new Entry(typeCount, Symbol.TYPE_TAG, value, hashCode)); + } + + /** + * Adds an {@link Frame#ITEM_UNINITIALIZED} type in the type table of this symbol table. Does + * nothing if the type table already contains a similar type. + * + * @param value an internal class name. + * @param bytecodeOffset the bytecode offset of the NEW instruction that created this {@link + * Frame#ITEM_UNINITIALIZED} type value. + * @return the index of a new or already existing type Symbol with the given value. + */ + int addUninitializedType(final String value, final int bytecodeOffset) { + int hashCode = hash(Symbol.UNINITIALIZED_TYPE_TAG, value, bytecodeOffset); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == Symbol.UNINITIALIZED_TYPE_TAG + && entry.hashCode == hashCode + && entry.data == bytecodeOffset + && entry.value.equals(value)) { + return entry.index; + } + entry = entry.next; + } + return addTypeInternal( + new Entry(typeCount, Symbol.UNINITIALIZED_TYPE_TAG, value, bytecodeOffset, hashCode)); + } + + /** + * Adds a merged type in the type table of this symbol table. Does nothing if the type table + * already contains a similar type. + * + * @param typeTableIndex1 a {@link Symbol#TYPE_TAG} type, specified by its index in the type + * table. + * @param typeTableIndex2 another {@link Symbol#TYPE_TAG} type, specified by its index in the type + * table. + * @return the index of a new or already existing {@link Symbol#TYPE_TAG} type Symbol, + * corresponding to the common super class of the given types. + */ + int addMergedType(final int typeTableIndex1, final int typeTableIndex2) { + long data = + typeTableIndex1 < typeTableIndex2 + ? typeTableIndex1 | (((long) typeTableIndex2) << 32) + : typeTableIndex2 | (((long) typeTableIndex1) << 32); + int hashCode = hash(Symbol.MERGED_TYPE_TAG, typeTableIndex1 + typeTableIndex2); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == Symbol.MERGED_TYPE_TAG && entry.hashCode == hashCode && entry.data == data) { + return entry.info; + } + entry = entry.next; + } + String type1 = typeTable[typeTableIndex1].value; + String type2 = typeTable[typeTableIndex2].value; + int commonSuperTypeIndex = addType(classWriter.getCommonSuperClass(type1, type2)); + put(new Entry(typeCount, Symbol.MERGED_TYPE_TAG, data, hashCode)).info = commonSuperTypeIndex; + return commonSuperTypeIndex; + } + + /** + * Adds the given type Symbol to {@link #typeTable}. + * + * @param entry a {@link Symbol#TYPE_TAG} or {@link Symbol#UNINITIALIZED_TYPE_TAG} type symbol. + * The index of this Symbol must be equal to the current value of {@link #typeCount}. + * @return the index in {@link #typeTable} where the given type was added, which is also equal to + * entry's index by hypothesis. + */ + private int addTypeInternal(final Entry entry) { + if (typeTable == null) { + typeTable = new Entry[16]; + } + if (typeCount == typeTable.length) { + Entry[] newTypeTable = new Entry[2 * typeTable.length]; + System.arraycopy(typeTable, 0, newTypeTable, 0, typeTable.length); + typeTable = newTypeTable; + } + typeTable[typeCount++] = entry; + return put(entry).index; + } + + // ----------------------------------------------------------------------------------------------- + // Static helper methods to compute hash codes. + // ----------------------------------------------------------------------------------------------- + + private static int hash(final int tag, final int value) { + return 0x7FFFFFFF & (tag + value); + } + + private static int hash(final int tag, final long value) { + return 0x7FFFFFFF & (tag + (int) value + (int) (value >>> 32)); + } + + private static int hash(final int tag, final String value) { + return 0x7FFFFFFF & (tag + value.hashCode()); + } + + private static int hash(final int tag, final String value1, final int value2) { + return 0x7FFFFFFF & (tag + value1.hashCode() + value2); + } + + private static int hash(final int tag, final String value1, final String value2) { + return 0x7FFFFFFF & (tag + value1.hashCode() * value2.hashCode()); + } + + private static int hash( + final int tag, final String value1, final String value2, final int value3) { + return 0x7FFFFFFF & (tag + value1.hashCode() * value2.hashCode() * (value3 + 1)); + } + + private static int hash( + final int tag, final String value1, final String value2, final String value3) { + return 0x7FFFFFFF & (tag + value1.hashCode() * value2.hashCode() * value3.hashCode()); + } + + private static int hash( + final int tag, + final String value1, + final String value2, + final String value3, + final int value4) { + return 0x7FFFFFFF & (tag + value1.hashCode() * value2.hashCode() * value3.hashCode() * value4); + } + + /** + * An entry of a SymbolTable. This concrete and private subclass of {@link Symbol} adds two fields + * which are only used inside SymbolTable, to implement hash sets of symbols (in order to avoid + * duplicate symbols). See {@link #entries}. @Author Eric Bruneton + */ + private static class Entry extends Symbol { + + /** The hash code of this entry. */ + final int hashCode; + + /** + * Another entry (and so on recursively) having the same hash code (modulo the size of {@link + * #entries}) as this one. + */ + Entry next; + + Entry( + final int index, + final int tag, + final String owner, + final String name, + final String value, + final long data, + final int hashCode) { + super(index, tag, owner, name, value, data); + this.hashCode = hashCode; + } + + Entry(final int index, final int tag, final String value, final int hashCode) { + super(index, tag, /* owner= */ null, /* name= */ null, value, /* data= */ 0); + this.hashCode = hashCode; + } + + Entry(final int index, final int tag, final String value, final long data, final int hashCode) { + super(index, tag, /* owner= */ null, /* name= */ null, value, data); + this.hashCode = hashCode; + } + + Entry( + final int index, final int tag, final String name, final String value, final int hashCode) { + super(index, tag, /* owner= */ null, name, value, /* data= */ 0); + this.hashCode = hashCode; + } + + Entry(final int index, final int tag, final long data, final int hashCode) { + super(index, tag, /* owner= */ null, /* name= */ null, /* value= */ null, data); + this.hashCode = hashCode; + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Type.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Type.java new file mode 100644 index 0000000..9882a71 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/Type.java @@ -0,0 +1,877 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** + * A Java field or method type. This class can be used to make it easier to manipulate type and + * method descriptors. @Author Eric Bruneton @Author Chris Nokleberg + */ +public final class Type { + + /** The sort of the {@code void} type. See {@link #getSort}. */ + public static final int VOID = 0; + + /** The sort of the {@code boolean} type. See {@link #getSort}. */ + public static final int BOOLEAN = 1; + + /** The sort of the {@code char} type. See {@link #getSort}. */ + public static final int CHAR = 2; + + /** The sort of the {@code byte} type. See {@link #getSort}. */ + public static final int BYTE = 3; + + /** The sort of the {@code short} type. See {@link #getSort}. */ + public static final int SHORT = 4; + + /** The sort of the {@code int} type. See {@link #getSort}. */ + public static final int INT = 5; + + /** The sort of the {@code float} type. See {@link #getSort}. */ + public static final int FLOAT = 6; + + /** The sort of the {@code long} type. See {@link #getSort}. */ + public static final int LONG = 7; + + /** The sort of the {@code double} type. See {@link #getSort}. */ + public static final int DOUBLE = 8; + + /** The sort of array reference types. See {@link #getSort}. */ + public static final int ARRAY = 9; + + /** The sort of object reference types. See {@link #getSort}. */ + public static final int OBJECT = 10; + + /** The sort of method types. See {@link #getSort}. */ + public static final int METHOD = 11; + + /** The (private) sort of object reference types represented with an internal name. */ + private static final int INTERNAL = 12; + + /** The descriptors of the primitive types. */ + private static final String PRIMITIVE_DESCRIPTORS = "VZCBSIFJD"; + + /** The {@code void} type. */ + public static final Type VOID_TYPE = new Type(VOID, PRIMITIVE_DESCRIPTORS, VOID, VOID + 1); + + /** The {@code boolean} type. */ + public static final Type BOOLEAN_TYPE = + new Type(BOOLEAN, PRIMITIVE_DESCRIPTORS, BOOLEAN, BOOLEAN + 1); + + /** The {@code char} type. */ + public static final Type CHAR_TYPE = new Type(CHAR, PRIMITIVE_DESCRIPTORS, CHAR, CHAR + 1); + + /** The {@code byte} type. */ + public static final Type BYTE_TYPE = new Type(BYTE, PRIMITIVE_DESCRIPTORS, BYTE, BYTE + 1); + + /** The {@code short} type. */ + public static final Type SHORT_TYPE = new Type(SHORT, PRIMITIVE_DESCRIPTORS, SHORT, SHORT + 1); + + /** The {@code int} type. */ + public static final Type INT_TYPE = new Type(INT, PRIMITIVE_DESCRIPTORS, INT, INT + 1); + + /** The {@code float} type. */ + public static final Type FLOAT_TYPE = new Type(FLOAT, PRIMITIVE_DESCRIPTORS, FLOAT, FLOAT + 1); + + /** The {@code long} type. */ + public static final Type LONG_TYPE = new Type(LONG, PRIMITIVE_DESCRIPTORS, LONG, LONG + 1); + + /** The {@code double} type. */ + public static final Type DOUBLE_TYPE = + new Type(DOUBLE, PRIMITIVE_DESCRIPTORS, DOUBLE, DOUBLE + 1); + + // ----------------------------------------------------------------------------------------------- + // Fields + // ----------------------------------------------------------------------------------------------- + + /** + * The sort of this type. Either {@link #VOID}, {@link #BOOLEAN}, {@link #CHAR}, {@link #BYTE}, + * {@link #SHORT}, {@link #INT}, {@link #FLOAT}, {@link #LONG}, {@link #DOUBLE}, {@link #ARRAY}, + * {@link #OBJECT}, {@link #METHOD} or {@link #INTERNAL}. + */ + private final int sort; + + /** + * A buffer containing the value of this field or method type. This value is an internal name for + * {@link #OBJECT} and {@link #INTERNAL} types, and a field or method descriptor in the other + * cases. + * + *

For {@link #OBJECT} types, this field also contains the descriptor: the characters in + * [{@link #valueBegin},{@link #valueEnd}) contain the internal name, and those in [{@link + * #valueBegin} - 1, {@link #valueEnd} + 1) contain the descriptor. + */ + private final String valueBuffer; + + /** + * The beginning index, inclusive, of the value of this Java field or method type in {@link + * #valueBuffer}. This value is an internal name for {@link #OBJECT} and {@link #INTERNAL} types, + * and a field or method descriptor in the other cases. + */ + private final int valueBegin; + + /** + * The end index, exclusive, of the value of this Java field or method type in {@link + * #valueBuffer}. This value is an internal name for {@link #OBJECT} and {@link #INTERNAL} types, + * and a field or method descriptor in the other cases. + */ + private final int valueEnd; + + /** + * Constructs a reference type. + * + * @param sort the sort of this type, see {@link #sort}. + * @param valueBuffer a buffer containing the value of this field or method type. + * @param valueBegin the beginning index, inclusive, of the value of this field or method type in + * valueBuffer. + * @param valueEnd the end index, exclusive, of the value of this field or method type in + * valueBuffer. + */ + private Type(final int sort, final String valueBuffer, final int valueBegin, final int valueEnd) { + this.sort = sort; + this.valueBuffer = valueBuffer; + this.valueBegin = valueBegin; + this.valueEnd = valueEnd; + } + + // ----------------------------------------------------------------------------------------------- + // Methods to get Type(s) from a descriptor, a reflected Method or Constructor, other types, etc. + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the {@link Type} corresponding to the given type descriptor. + * + * @param typeDescriptor a field or method type descriptor. + * @return the {@link Type} corresponding to the given type descriptor. + */ + public static Type getType(final String typeDescriptor) { + return getTypeInternal(typeDescriptor, 0, typeDescriptor.length()); + } + + /** + * Returns the {@link Type} corresponding to the given class. + * + * @param clazz a class. + * @return the {@link Type} corresponding to the given class. + */ + public static Type getType(final Class clazz) { + if (clazz.isPrimitive()) { + if (clazz == Integer.TYPE) { + return INT_TYPE; + } else if (clazz == Void.TYPE) { + return VOID_TYPE; + } else if (clazz == Boolean.TYPE) { + return BOOLEAN_TYPE; + } else if (clazz == Byte.TYPE) { + return BYTE_TYPE; + } else if (clazz == Character.TYPE) { + return CHAR_TYPE; + } else if (clazz == Short.TYPE) { + return SHORT_TYPE; + } else if (clazz == Double.TYPE) { + return DOUBLE_TYPE; + } else if (clazz == Float.TYPE) { + return FLOAT_TYPE; + } else if (clazz == Long.TYPE) { + return LONG_TYPE; + } else { + throw new AssertionError(); + } + } else { + return getType(getDescriptor(clazz)); + } + } + + /** + * Returns the method {@link Type} corresponding to the given constructor. + * + * @param constructor a {@link Constructor} object. + * @return the method {@link Type} corresponding to the given constructor. + */ + public static Type getType(final Constructor constructor) { + return getType(getConstructorDescriptor(constructor)); + } + + /** + * Returns the method {@link Type} corresponding to the given method. + * + * @param method a {@link Method} object. + * @return the method {@link Type} corresponding to the given method. + */ + public static Type getType(final Method method) { + return getType(getMethodDescriptor(method)); + } + + /** + * Returns the type of the elements of this array type. This method should only be used for an + * array type. + * + * @return Returns the type of the elements of this array type. + */ + public Type getElementType() { + final int numDimensions = getDimensions(); + return getTypeInternal(valueBuffer, valueBegin + numDimensions, valueEnd); + } + + /** + * Returns the {@link Type} corresponding to the given internal name. + * + * @param internalName an internal name. + * @return the {@link Type} corresponding to the given internal name. + */ + public static Type getObjectType(final String internalName) { + return new Type( + internalName.charAt(0) == '[' ? ARRAY : INTERNAL, internalName, 0, internalName.length()); + } + + /** + * Returns the {@link Type} corresponding to the given method descriptor. Equivalent to + * Type.getType(methodDescriptor). + * + * @param methodDescriptor a method descriptor. + * @return the {@link Type} corresponding to the given method descriptor. + */ + public static Type getMethodType(final String methodDescriptor) { + return new Type(METHOD, methodDescriptor, 0, methodDescriptor.length()); + } + + /** + * Returns the method {@link Type} corresponding to the given argument and return types. + * + * @param returnType the return type of the method. + * @param argumentTypes the argument types of the method. + * @return the method {@link Type} corresponding to the given argument and return types. + */ + public static Type getMethodType(final Type returnType, final Type... argumentTypes) { + return getType(getMethodDescriptor(returnType, argumentTypes)); + } + + /** + * Returns the argument types of methods of this type. This method should only be used for method + * types. + * + * @return the argument types of methods of this type. + */ + public Type[] getArgumentTypes() { + return getArgumentTypes(getDescriptor()); + } + + /** + * Returns the {@link Type} values corresponding to the argument types of the given method + * descriptor. + * + * @param methodDescriptor a method descriptor. + * @return the {@link Type} values corresponding to the argument types of the given method + * descriptor. + */ + public static Type[] getArgumentTypes(final String methodDescriptor) { + // First step: compute the number of argument types in methodDescriptor. + int numArgumentTypes = 0; + // Skip the first character, which is always a '('. + int currentOffset = 1; + // Parse the argument types, one at a each loop iteration. + while (methodDescriptor.charAt(currentOffset) != ')') { + while (methodDescriptor.charAt(currentOffset) == '[') { + currentOffset++; + } + if (methodDescriptor.charAt(currentOffset++) == 'L') { + // Skip the argument descriptor content. + int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset); + currentOffset = Math.max(currentOffset, semiColumnOffset + 1); + } + ++numArgumentTypes; + } + + // Second step: create a Type instance for each argument type. + Type[] argumentTypes = new Type[numArgumentTypes]; + // Skip the first character, which is always a '('. + currentOffset = 1; + // Parse and create the argument types, one at each loop iteration. + int currentArgumentTypeIndex = 0; + while (methodDescriptor.charAt(currentOffset) != ')') { + final int currentArgumentTypeOffset = currentOffset; + while (methodDescriptor.charAt(currentOffset) == '[') { + currentOffset++; + } + if (methodDescriptor.charAt(currentOffset++) == 'L') { + // Skip the argument descriptor content. + int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset); + currentOffset = Math.max(currentOffset, semiColumnOffset + 1); + } + argumentTypes[currentArgumentTypeIndex++] = + getTypeInternal(methodDescriptor, currentArgumentTypeOffset, currentOffset); + } + return argumentTypes; + } + + /** + * Returns the {@link Type} values corresponding to the argument types of the given method. + * + * @param method a method. + * @return the {@link Type} values corresponding to the argument types of the given method. + */ + public static Type[] getArgumentTypes(final Method method) { + Class[] classes = method.getParameterTypes(); + Type[] types = new Type[classes.length]; + for (int i = classes.length - 1; i >= 0; --i) { + types[i] = getType(classes[i]); + } + return types; + } + + /** + * Returns the return type of methods of this type. This method should only be used for method + * types. + * + * @return the return type of methods of this type. + */ + public Type getReturnType() { + return getReturnType(getDescriptor()); + } + + /** + * Returns the {@link Type} corresponding to the return type of the given method descriptor. + * + * @param methodDescriptor a method descriptor. + * @return the {@link Type} corresponding to the return type of the given method descriptor. + */ + public static Type getReturnType(final String methodDescriptor) { + return getTypeInternal( + methodDescriptor, getReturnTypeOffset(methodDescriptor), methodDescriptor.length()); + } + + /** + * Returns the {@link Type} corresponding to the return type of the given method. + * + * @param method a method. + * @return the {@link Type} corresponding to the return type of the given method. + */ + public static Type getReturnType(final Method method) { + return getType(method.getReturnType()); + } + + /** + * Returns the start index of the return type of the given method descriptor. + * + * @param methodDescriptor a method descriptor. + * @return the start index of the return type of the given method descriptor. + */ + static int getReturnTypeOffset(final String methodDescriptor) { + // Skip the first character, which is always a '('. + int currentOffset = 1; + // Skip the argument types, one at a each loop iteration. + while (methodDescriptor.charAt(currentOffset) != ')') { + while (methodDescriptor.charAt(currentOffset) == '[') { + currentOffset++; + } + if (methodDescriptor.charAt(currentOffset++) == 'L') { + // Skip the argument descriptor content. + int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset); + currentOffset = Math.max(currentOffset, semiColumnOffset + 1); + } + } + return currentOffset + 1; + } + + /** + * Returns the {@link Type} corresponding to the given field or method descriptor. + * + * @param descriptorBuffer a buffer containing the field or method descriptor. + * @param descriptorBegin the beginning index, inclusive, of the field or method descriptor in + * descriptorBuffer. + * @param descriptorEnd the end index, exclusive, of the field or method descriptor in + * descriptorBuffer. + * @return the {@link Type} corresponding to the given type descriptor. + */ + private static Type getTypeInternal( + final String descriptorBuffer, final int descriptorBegin, final int descriptorEnd) { + switch (descriptorBuffer.charAt(descriptorBegin)) { + case 'V': + return VOID_TYPE; + case 'Z': + return BOOLEAN_TYPE; + case 'C': + return CHAR_TYPE; + case 'B': + return BYTE_TYPE; + case 'S': + return SHORT_TYPE; + case 'I': + return INT_TYPE; + case 'F': + return FLOAT_TYPE; + case 'J': + return LONG_TYPE; + case 'D': + return DOUBLE_TYPE; + case '[': + return new Type(ARRAY, descriptorBuffer, descriptorBegin, descriptorEnd); + case 'L': + return new Type(OBJECT, descriptorBuffer, descriptorBegin + 1, descriptorEnd - 1); + case '(': + return new Type(METHOD, descriptorBuffer, descriptorBegin, descriptorEnd); + default: + throw new IllegalArgumentException(); + } + } + + // ----------------------------------------------------------------------------------------------- + // Methods to get class names, internal names or descriptors. + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the binary name of the class corresponding to this type. This method must not be used + * on method types. + * + * @return the binary name of the class corresponding to this type. + */ + public String getClassName() { + switch (sort) { + case VOID: + return "void"; + case BOOLEAN: + return "boolean"; + case CHAR: + return "char"; + case BYTE: + return "byte"; + case SHORT: + return "short"; + case INT: + return "int"; + case FLOAT: + return "float"; + case LONG: + return "long"; + case DOUBLE: + return "double"; + case ARRAY: + StringBuilder stringBuilder = new StringBuilder(getElementType().getClassName()); + for (int i = getDimensions(); i > 0; --i) { + stringBuilder.append("[]"); + } + return stringBuilder.toString(); + case OBJECT: + case INTERNAL: + return valueBuffer.substring(valueBegin, valueEnd).replace('/', '.'); + default: + throw new AssertionError(); + } + } + + /** + * Returns the internal name of the class corresponding to this object or array type. The internal + * name of a class is its fully qualified name (as returned by Class.getName(), where '.' are + * replaced by '/'). This method should only be used for an object or array type. + * + * @return the internal name of the class corresponding to this object type. + */ + public String getInternalName() { + return valueBuffer.substring(valueBegin, valueEnd); + } + + /** + * Returns the internal name of the given class. The internal name of a class is its fully + * qualified name, as returned by Class.getName(), where '.' are replaced by '/'. + * + * @param clazz an object or array class. + * @return the internal name of the given class. + */ + public static String getInternalName(final Class clazz) { + return clazz.getName().replace('.', '/'); + } + + /** + * Returns the descriptor corresponding to this type. + * + * @return the descriptor corresponding to this type. + */ + public String getDescriptor() { + if (sort == OBJECT) { + return valueBuffer.substring(valueBegin - 1, valueEnd + 1); + } else if (sort == INTERNAL) { + return 'L' + valueBuffer.substring(valueBegin, valueEnd) + ';'; + } else { + return valueBuffer.substring(valueBegin, valueEnd); + } + } + + /** + * Returns the descriptor corresponding to the given class. + * + * @param clazz an object class, a primitive class or an array class. + * @return the descriptor corresponding to the given class. + */ + public static String getDescriptor(final Class clazz) { + StringBuilder stringBuilder = new StringBuilder(); + appendDescriptor(clazz, stringBuilder); + return stringBuilder.toString(); + } + + /** + * Returns the descriptor corresponding to the given constructor. + * + * @param constructor a {@link Constructor} object. + * @return the descriptor of the given constructor. + */ + public static String getConstructorDescriptor(final Constructor constructor) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append('('); + Class[] parameters = constructor.getParameterTypes(); + for (Class parameter : parameters) { + appendDescriptor(parameter, stringBuilder); + } + return stringBuilder.append(")V").toString(); + } + + /** + * Returns the descriptor corresponding to the given argument and return types. + * + * @param returnType the return type of the method. + * @param argumentTypes the argument types of the method. + * @return the descriptor corresponding to the given argument and return types. + */ + public static String getMethodDescriptor(final Type returnType, final Type... argumentTypes) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append('('); + for (Type argumentType : argumentTypes) { + argumentType.appendDescriptor(stringBuilder); + } + stringBuilder.append(')'); + returnType.appendDescriptor(stringBuilder); + return stringBuilder.toString(); + } + + /** + * Returns the descriptor corresponding to the given method. + * + * @param method a {@link Method} object. + * @return the descriptor of the given method. + */ + public static String getMethodDescriptor(final Method method) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append('('); + Class[] parameters = method.getParameterTypes(); + for (Class parameter : parameters) { + appendDescriptor(parameter, stringBuilder); + } + stringBuilder.append(')'); + appendDescriptor(method.getReturnType(), stringBuilder); + return stringBuilder.toString(); + } + + /** + * Appends the descriptor corresponding to this type to the given string buffer. + * + * @param stringBuilder the string builder to which the descriptor must be appended. + */ + private void appendDescriptor(final StringBuilder stringBuilder) { + if (sort == OBJECT) { + stringBuilder.append(valueBuffer, valueBegin - 1, valueEnd + 1); + } else if (sort == INTERNAL) { + stringBuilder.append('L').append(valueBuffer, valueBegin, valueEnd).append(';'); + } else { + stringBuilder.append(valueBuffer, valueBegin, valueEnd); + } + } + + /** + * Appends the descriptor of the given class to the given string builder. + * + * @param clazz the class whose descriptor must be computed. + * @param stringBuilder the string builder to which the descriptor must be appended. + */ + private static void appendDescriptor(final Class clazz, final StringBuilder stringBuilder) { + Class currentClass = clazz; + while (currentClass.isArray()) { + stringBuilder.append('['); + currentClass = currentClass.getComponentType(); + } + if (currentClass.isPrimitive()) { + char descriptor; + if (currentClass == Integer.TYPE) { + descriptor = 'I'; + } else if (currentClass == Void.TYPE) { + descriptor = 'V'; + } else if (currentClass == Boolean.TYPE) { + descriptor = 'Z'; + } else if (currentClass == Byte.TYPE) { + descriptor = 'B'; + } else if (currentClass == Character.TYPE) { + descriptor = 'C'; + } else if (currentClass == Short.TYPE) { + descriptor = 'S'; + } else if (currentClass == Double.TYPE) { + descriptor = 'D'; + } else if (currentClass == Float.TYPE) { + descriptor = 'F'; + } else if (currentClass == Long.TYPE) { + descriptor = 'J'; + } else { + throw new AssertionError(); + } + stringBuilder.append(descriptor); + } else { + stringBuilder.append('L').append(getInternalName(currentClass)).append(';'); + } + } + + // ----------------------------------------------------------------------------------------------- + // Methods to get the sort, dimension, size, and opcodes corresponding to a Type or descriptor. + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the sort of this type. + * + * @return {@link #VOID}, {@link #BOOLEAN}, {@link #CHAR}, {@link #BYTE}, {@link #SHORT}, {@link + * #INT}, {@link #FLOAT}, {@link #LONG}, {@link #DOUBLE}, {@link #ARRAY}, {@link #OBJECT} or + * {@link #METHOD}. + */ + public int getSort() { + return sort == INTERNAL ? OBJECT : sort; + } + + /** + * Returns the number of dimensions of this array type. This method should only be used for an + * array type. + * + * @return the number of dimensions of this array type. + */ + public int getDimensions() { + int numDimensions = 1; + while (valueBuffer.charAt(valueBegin + numDimensions) == '[') { + numDimensions++; + } + return numDimensions; + } + + /** + * Returns the size of values of this type. This method must not be used for method types. + * + * @return the size of values of this type, i.e., 2 for {@code long} and {@code double}, 0 for + * {@code void} and 1 otherwise. + */ + public int getSize() { + switch (sort) { + case VOID: + return 0; + case BOOLEAN: + case CHAR: + case BYTE: + case SHORT: + case INT: + case FLOAT: + case ARRAY: + case OBJECT: + case INTERNAL: + return 1; + case LONG: + case DOUBLE: + return 2; + default: + throw new AssertionError(); + } + } + + /** + * Returns the size of the arguments and of the return value of methods of this type. This method + * should only be used for method types. + * + * @return the size of the arguments of the method (plus one for the implicit this argument), + * argumentsSize, and the size of its return value, returnSize, packed into a single int i = + * {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code + * i >> 2}, and returnSize to {@code i & 0x03}). + */ + public int getArgumentsAndReturnSizes() { + return getArgumentsAndReturnSizes(getDescriptor()); + } + + /** + * Computes the size of the arguments and of the return value of a method. + * + * @param methodDescriptor a method descriptor. + * @return the size of the arguments of the method (plus one for the implicit this argument), + * argumentsSize, and the size of its return value, returnSize, packed into a single int i = + * {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code + * i >> 2}, and returnSize to {@code i & 0x03}). + */ + public static int getArgumentsAndReturnSizes(final String methodDescriptor) { + int argumentsSize = 1; + // Skip the first character, which is always a '('. + int currentOffset = 1; + int currentChar = methodDescriptor.charAt(currentOffset); + // Parse the argument types and compute their size, one at a each loop iteration. + while (currentChar != ')') { + if (currentChar == 'J' || currentChar == 'D') { + currentOffset++; + argumentsSize += 2; + } else { + while (methodDescriptor.charAt(currentOffset) == '[') { + currentOffset++; + } + if (methodDescriptor.charAt(currentOffset++) == 'L') { + // Skip the argument descriptor content. + int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset); + currentOffset = Math.max(currentOffset, semiColumnOffset + 1); + } + argumentsSize += 1; + } + currentChar = methodDescriptor.charAt(currentOffset); + } + currentChar = methodDescriptor.charAt(currentOffset + 1); + if (currentChar == 'V') { + return argumentsSize << 2; + } else { + int returnSize = (currentChar == 'J' || currentChar == 'D') ? 2 : 1; + return argumentsSize << 2 | returnSize; + } + } + + /** + * Returns a JVM instruction opcode adapted to this {@link Type}. This method must not be used for + * method types. + * + * @param opcode a JVM instruction opcode. This opcode must be one of ILOAD, ISTORE, IALOAD, + * IASTORE, IADD, ISUB, IMUL, IDIV, IREM, INEG, ISHL, ISHR, IUSHR, IAND, IOR, IXOR and + * IRETURN. + * @return an opcode that is similar to the given opcode, but adapted to this {@link Type}. For + * example, if this type is {@code float} and {@code opcode} is IRETURN, this method returns + * FRETURN. + */ + public int getOpcode(final int opcode) { + if (opcode == Opcodes.IALOAD || opcode == Opcodes.IASTORE) { + switch (sort) { + case BOOLEAN: + case BYTE: + return opcode + (Opcodes.BALOAD - Opcodes.IALOAD); + case CHAR: + return opcode + (Opcodes.CALOAD - Opcodes.IALOAD); + case SHORT: + return opcode + (Opcodes.SALOAD - Opcodes.IALOAD); + case INT: + return opcode; + case FLOAT: + return opcode + (Opcodes.FALOAD - Opcodes.IALOAD); + case LONG: + return opcode + (Opcodes.LALOAD - Opcodes.IALOAD); + case DOUBLE: + return opcode + (Opcodes.DALOAD - Opcodes.IALOAD); + case ARRAY: + case OBJECT: + case INTERNAL: + return opcode + (Opcodes.AALOAD - Opcodes.IALOAD); + case METHOD: + case VOID: + throw new UnsupportedOperationException(); + default: + throw new AssertionError(); + } + } else { + switch (sort) { + case VOID: + if (opcode != Opcodes.IRETURN) { + throw new UnsupportedOperationException(); + } + return Opcodes.RETURN; + case BOOLEAN: + case BYTE: + case CHAR: + case SHORT: + case INT: + return opcode; + case FLOAT: + return opcode + (Opcodes.FRETURN - Opcodes.IRETURN); + case LONG: + return opcode + (Opcodes.LRETURN - Opcodes.IRETURN); + case DOUBLE: + return opcode + (Opcodes.DRETURN - Opcodes.IRETURN); + case ARRAY: + case OBJECT: + case INTERNAL: + if (opcode != Opcodes.ILOAD && opcode != Opcodes.ISTORE && opcode != Opcodes.IRETURN) { + throw new UnsupportedOperationException(); + } + return opcode + (Opcodes.ARETURN - Opcodes.IRETURN); + case METHOD: + throw new UnsupportedOperationException(); + default: + throw new AssertionError(); + } + } + } + + // ----------------------------------------------------------------------------------------------- + // Equals, hashCode and toString. + // ----------------------------------------------------------------------------------------------- + + /** + * Tests if the given object is equal to this type. + * + * @param object the object to be compared to this type. + * @return {@literal true} if the given object is equal to this type. + */ + @Override + public boolean equals(final Object object) { + if (this == object) { + return true; + } + if (!(object instanceof Type)) { + return false; + } + Type other = (Type) object; + if ((sort == INTERNAL ? OBJECT : sort) != (other.sort == INTERNAL ? OBJECT : other.sort)) { + return false; + } + int begin = valueBegin; + int end = valueEnd; + int otherBegin = other.valueBegin; + int otherEnd = other.valueEnd; + // Compare the values. + if (end - begin != otherEnd - otherBegin) { + return false; + } + for (int i = begin, j = otherBegin; i < end; i++, j++) { + if (valueBuffer.charAt(i) != other.valueBuffer.charAt(j)) { + return false; + } + } + return true; + } + + /** + * Returns a hash code value for this type. + * + * @return a hash code value for this type. + */ + @Override + public int hashCode() { + int hashCode = 13 * (sort == INTERNAL ? OBJECT : sort); + if (sort >= ARRAY) { + for (int i = valueBegin, end = valueEnd; i < end; i++) { + hashCode = 17 * (hashCode + valueBuffer.charAt(i)); + } + } + return hashCode; + } + + /** + * Returns a string representation of this type. + * + * @return the descriptor of this type. + */ + @Override + public String toString() { + return getDescriptor(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/TypePath.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/TypePath.java new file mode 100644 index 0000000..0ece532 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/TypePath.java @@ -0,0 +1,183 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * The path to a type argument, wildcard bound, array element type, or static inner type within an + * enclosing type. @Author Eric Bruneton + */ +public final class TypePath { + + /** A type path step that steps into the element type of an array type. See {@link #getStep}. */ + public static final int ARRAY_ELEMENT = 0; + + /** A type path step that steps into the nested type of a class type. See {@link #getStep}. */ + public static final int INNER_TYPE = 1; + + /** A type path step that steps into the bound of a wildcard type. See {@link #getStep}. */ + public static final int WILDCARD_BOUND = 2; + + /** A type path step that steps into a type argument of a generic type. See {@link #getStep}. */ + public static final int TYPE_ARGUMENT = 3; + + /** + * The byte array where the 'type_path' structure - as defined in the Java Virtual Machine + * Specification (JVMS) - corresponding to this TypePath is stored. The first byte of the + * structure in this array is given by {@link #typePathOffset}. + * + * @see JVMS + * 4.7.20.2 + */ + private final byte[] typePathContainer; + + /** The offset of the first byte of the type_path JVMS structure in {@link #typePathContainer}. */ + private final int typePathOffset; + + /** + * Constructs a new TypePath. + * + * @param typePathContainer a byte array containing a type_path JVMS structure. + * @param typePathOffset the offset of the first byte of the type_path structure in + * typePathContainer. + */ + TypePath(final byte[] typePathContainer, final int typePathOffset) { + this.typePathContainer = typePathContainer; + this.typePathOffset = typePathOffset; + } + + /** + * Returns the length of this path, i.e. its number of steps. + * + * @return the length of this path. + */ + public int getLength() { + // path_length is stored in the first byte of a type_path. + return typePathContainer[typePathOffset]; + } + + /** + * Returns the value of the given step of this path. + * + * @param index an index between 0 and {@link #getLength()}, exclusive. + * @return one of {@link #ARRAY_ELEMENT}, {@link #INNER_TYPE}, {@link #WILDCARD_BOUND}, or {@link + * #TYPE_ARGUMENT}. + */ + public int getStep(final int index) { + // Returns the type_path_kind of the path element of the given index. + return typePathContainer[typePathOffset + 2 * index + 1]; + } + + /** + * Returns the index of the type argument that the given step is stepping into. This method should + * only be used for steps whose value is {@link #TYPE_ARGUMENT}. + * + * @param index an index between 0 and {@link #getLength()}, exclusive. + * @return the index of the type argument that the given step is stepping into. + */ + public int getStepArgument(final int index) { + // Returns the type_argument_index of the path element of the given index. + return typePathContainer[typePathOffset + 2 * index + 2]; + } + + /** + * Converts a type path in string form, in the format used by {@link #toString()}, into a TypePath + * object. + * + * @param typePath a type path in string form, in the format used by {@link #toString()}. May be + * {@literal null} or empty. + * @return the corresponding TypePath object, or {@literal null} if the path is empty. + */ + public static TypePath fromString(final String typePath) { + if (typePath == null || typePath.length() == 0) { + return null; + } + int typePathLength = typePath.length(); + ByteVector output = new ByteVector(typePathLength); + output.putByte(0); + int typePathIndex = 0; + while (typePathIndex < typePathLength) { + char c = typePath.charAt(typePathIndex++); + if (c == '[') { + output.put11(ARRAY_ELEMENT, 0); + } else if (c == '.') { + output.put11(INNER_TYPE, 0); + } else if (c == '*') { + output.put11(WILDCARD_BOUND, 0); + } else if (c >= '0' && c <= '9') { + int typeArg = c - '0'; + while (typePathIndex < typePathLength) { + c = typePath.charAt(typePathIndex++); + if (c >= '0' && c <= '9') { + typeArg = typeArg * 10 + c - '0'; + } else if (c == ';') { + break; + } else { + throw new IllegalArgumentException(); + } + } + output.put11(TYPE_ARGUMENT, typeArg); + } else { + throw new IllegalArgumentException(); + } + } + output.data[0] = (byte) (output.length / 2); + return new TypePath(output.data, 0); + } + + /** + * Returns a string representation of this type path. {@link #ARRAY_ELEMENT} steps are represented + * with '[', {@link #INNER_TYPE} steps with '.', {@link #WILDCARD_BOUND} steps with '*' and {@link + * #TYPE_ARGUMENT} steps with their type argument index in decimal form followed by ';'. + */ + @Override + public String toString() { + int length = getLength(); + StringBuilder result = new StringBuilder(length * 2); + for (int i = 0; i < length; ++i) { + switch (getStep(i)) { + case ARRAY_ELEMENT: + result.append('['); + break; + case INNER_TYPE: + result.append('.'); + break; + case WILDCARD_BOUND: + result.append('*'); + break; + case TYPE_ARGUMENT: + result.append(getStepArgument(i)).append(';'); + break; + default: + throw new AssertionError(); + } + } + return result.toString(); + } + + /** + * Puts the type_path JVMS structure corresponding to the given TypePath into the given + * ByteVector. + * + * @param typePath a TypePath instance, or {@literal null} for empty paths. + * @param output where the type path must be put. + */ + static void put(final TypePath typePath, final ByteVector output) { + if (typePath == null) { + output.putByte(0); + } else { + int length = typePath.typePathContainer[typePath.typePathOffset] * 2 + 1; + output.putByteArray(typePath.typePathContainer, typePath.typePathOffset, length); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/TypeReference.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/TypeReference.java new file mode 100644 index 0000000..73c93d0 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/TypeReference.java @@ -0,0 +1,418 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm; + +/** + * A reference to a type appearing in a class, field or method declaration, or on an instruction. + * Such a reference designates the part of the class where the referenced type is appearing (e.g. an + * 'extends', 'implements' or 'throws' clause, a 'new' instruction, a 'catch' clause, a type cast, a + * local variable declaration, etc). @Author Eric Bruneton + */ +public class TypeReference { + + /** + * The sort of type references that target a type parameter of a generic class. See {@link + * #getSort}. + */ + public static final int CLASS_TYPE_PARAMETER = 0x00; + + /** + * The sort of type references that target a type parameter of a generic method. See {@link + * #getSort}. + */ + public static final int METHOD_TYPE_PARAMETER = 0x01; + + /** + * The sort of type references that target the super class of a class or one of the interfaces it + * implements. See {@link #getSort}. + */ + public static final int CLASS_EXTENDS = 0x10; + + /** + * The sort of type references that target a bound of a type parameter of a generic class. See + * {@link #getSort}. + */ + public static final int CLASS_TYPE_PARAMETER_BOUND = 0x11; + + /** + * The sort of type references that target a bound of a type parameter of a generic method. See + * {@link #getSort}. + */ + public static final int METHOD_TYPE_PARAMETER_BOUND = 0x12; + + /** The sort of type references that target the type of a field. See {@link #getSort}. */ + public static final int FIELD = 0x13; + + /** The sort of type references that target the return type of a method. See {@link #getSort}. */ + public static final int METHOD_RETURN = 0x14; + + /** + * The sort of type references that target the receiver type of a method. See {@link #getSort}. + */ + public static final int METHOD_RECEIVER = 0x15; + + /** + * The sort of type references that target the type of a formal parameter of a method. See {@link + * #getSort}. + */ + public static final int METHOD_FORMAL_PARAMETER = 0x16; + + /** + * The sort of type references that target the type of an exception declared in the throws clause + * of a method. See {@link #getSort}. + */ + public static final int THROWS = 0x17; + + /** + * The sort of type references that target the type of a local variable in a method. See {@link + * #getSort}. + */ + public static final int LOCAL_VARIABLE = 0x40; + + /** + * The sort of type references that target the type of a resource variable in a method. See {@link + * #getSort}. + */ + public static final int RESOURCE_VARIABLE = 0x41; + + /** + * The sort of type references that target the type of the exception of a 'catch' clause in a + * method. See {@link #getSort}. + */ + public static final int EXCEPTION_PARAMETER = 0x42; + + /** + * The sort of type references that target the type declared in an 'instanceof' instruction. See + * {@link #getSort}. + */ + public static final int INSTANCEOF = 0x43; + + /** + * The sort of type references that target the type of the object created by a 'new' instruction. + * See {@link #getSort}. + */ + public static final int NEW = 0x44; + + /** + * The sort of type references that target the receiver type of a constructor reference. See + * {@link #getSort}. + */ + public static final int CONSTRUCTOR_REFERENCE = 0x45; + + /** + * The sort of type references that target the receiver type of a method reference. See {@link + * #getSort}. + */ + public static final int METHOD_REFERENCE = 0x46; + + /** + * The sort of type references that target the type declared in an explicit or implicit cast + * instruction. See {@link #getSort}. + */ + public static final int CAST = 0x47; + + /** + * The sort of type references that target a type parameter of a generic constructor in a + * constructor call. See {@link #getSort}. + */ + public static final int CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT = 0x48; + + /** + * The sort of type references that target a type parameter of a generic method in a method call. + * See {@link #getSort}. + */ + public static final int METHOD_INVOCATION_TYPE_ARGUMENT = 0x49; + + /** + * The sort of type references that target a type parameter of a generic constructor in a + * constructor reference. See {@link #getSort}. + */ + public static final int CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT = 0x4A; + + /** + * The sort of type references that target a type parameter of a generic method in a method + * reference. See {@link #getSort}. + */ + public static final int METHOD_REFERENCE_TYPE_ARGUMENT = 0x4B; + + /** + * The target_type and target_info structures - as defined in the Java Virtual Machine + * Specification (JVMS) - corresponding to this type reference. target_type uses one byte, and all + * the target_info union fields use up to 3 bytes (except localvar_target, handled with the + * specific method {@link MethodVisitor#visitLocalVariableAnnotation}). Thus, both structures can + * be stored in an int. + * + *

This int field stores target_type (called the TypeReference 'sort' in the public API of this + * class) in its most significant byte, followed by the target_info fields. Depending on + * target_type, 1, 2 or even 3 least significant bytes of this field are unused. target_info + * fields which reference bytecode offsets are set to 0 (these offsets are ignored in ClassReader, + * and recomputed in MethodWriter). + * + * @see JVMS + * 4.7.20 + * @see JVMS + * 4.7.20.1 + */ + private final int targetTypeAndInfo; + + /** + * Constructs a new TypeReference. + * + * @param typeRef the int encoded value of the type reference, as received in a visit method + * related to type annotations, such as {@link ClassVisitor#visitTypeAnnotation}. + */ + public TypeReference(final int typeRef) { + this.targetTypeAndInfo = typeRef; + } + + /** + * Returns a type reference of the given sort. + * + * @param sort one of {@link #FIELD}, {@link #METHOD_RETURN}, {@link #METHOD_RECEIVER}, {@link + * #LOCAL_VARIABLE}, {@link #RESOURCE_VARIABLE}, {@link #INSTANCEOF}, {@link #NEW}, {@link + * #CONSTRUCTOR_REFERENCE}, or {@link #METHOD_REFERENCE}. + * @return a type reference of the given sort. + */ + public static TypeReference newTypeReference(final int sort) { + return new TypeReference(sort << 24); + } + + /** + * Returns a reference to a type parameter of a generic class or method. + * + * @param sort one of {@link #CLASS_TYPE_PARAMETER} or {@link #METHOD_TYPE_PARAMETER}. + * @param paramIndex the type parameter index. + * @return a reference to the given generic class or method type parameter. + */ + public static TypeReference newTypeParameterReference(final int sort, final int paramIndex) { + return new TypeReference((sort << 24) | (paramIndex << 16)); + } + + /** + * Returns a reference to a type parameter bound of a generic class or method. + * + * @param sort one of {@link #CLASS_TYPE_PARAMETER} or {@link #METHOD_TYPE_PARAMETER}. + * @param paramIndex the type parameter index. + * @param boundIndex the type bound index within the above type parameters. + * @return a reference to the given generic class or method type parameter bound. + */ + public static TypeReference newTypeParameterBoundReference( + final int sort, final int paramIndex, final int boundIndex) { + return new TypeReference((sort << 24) | (paramIndex << 16) | (boundIndex << 8)); + } + + /** + * Returns a reference to the super class or to an processer of the 'implements' clause of a + * class. + * + * @param itfIndex the index of an processer in the 'implements' clause of a class, or -1 to + * reference the super class of the class. + * @return a reference to the given super type of a class. + */ + public static TypeReference newSuperTypeReference(final int itfIndex) { + return new TypeReference((CLASS_EXTENDS << 24) | ((itfIndex & 0xFFFF) << 8)); + } + + /** + * Returns a reference to the type of a formal parameter of a method. + * + * @param paramIndex the formal parameter index. + * @return a reference to the type of the given method formal parameter. + */ + public static TypeReference newFormalParameterReference(final int paramIndex) { + return new TypeReference((METHOD_FORMAL_PARAMETER << 24) | (paramIndex << 16)); + } + + /** + * Returns a reference to the type of an exception, in a 'throws' clause of a method. + * + * @param exceptionIndex the index of an exception in a 'throws' clause of a method. + * @return a reference to the type of the given exception. + */ + public static TypeReference newExceptionReference(final int exceptionIndex) { + return new TypeReference((THROWS << 24) | (exceptionIndex << 8)); + } + + /** + * Returns a reference to the type of the exception declared in a 'catch' clause of a method. + * + * @param tryCatchBlockIndex the index of a try catch block (using the order in which they are + * visited with visitTryCatchBlock). + * @return a reference to the type of the given exception. + */ + public static TypeReference newTryCatchReference(final int tryCatchBlockIndex) { + return new TypeReference((EXCEPTION_PARAMETER << 24) | (tryCatchBlockIndex << 8)); + } + + /** + * Returns a reference to the type of a type argument in a constructor or method call or + * reference. + * + * @param sort one of {@link #CAST}, {@link #CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT}, {@link + * #METHOD_INVOCATION_TYPE_ARGUMENT}, {@link #CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT}, or {@link + * #METHOD_REFERENCE_TYPE_ARGUMENT}. + * @param argIndex the type argument index. + * @return a reference to the type of the given type argument. + */ + public static TypeReference newTypeArgumentReference(final int sort, final int argIndex) { + return new TypeReference((sort << 24) | argIndex); + } + + /** + * Returns the sort of this type reference. + * + * @return one of {@link #CLASS_TYPE_PARAMETER}, {@link #METHOD_TYPE_PARAMETER}, {@link + * #CLASS_EXTENDS}, {@link #CLASS_TYPE_PARAMETER_BOUND}, {@link #METHOD_TYPE_PARAMETER_BOUND}, + * {@link #FIELD}, {@link #METHOD_RETURN}, {@link #METHOD_RECEIVER}, {@link + * #METHOD_FORMAL_PARAMETER}, {@link #THROWS}, {@link #LOCAL_VARIABLE}, {@link + * #RESOURCE_VARIABLE}, {@link #EXCEPTION_PARAMETER}, {@link #INSTANCEOF}, {@link #NEW}, + * {@link #CONSTRUCTOR_REFERENCE}, {@link #METHOD_REFERENCE}, {@link #CAST}, {@link + * #CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT}, {@link #METHOD_INVOCATION_TYPE_ARGUMENT}, {@link + * #CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT}, or {@link #METHOD_REFERENCE_TYPE_ARGUMENT}. + */ + public int getSort() { + return targetTypeAndInfo >>> 24; + } + + /** + * Returns the index of the type parameter referenced by this type reference. This method must + * only be used for type references whose sort is {@link #CLASS_TYPE_PARAMETER}, {@link + * #METHOD_TYPE_PARAMETER}, {@link #CLASS_TYPE_PARAMETER_BOUND} or {@link + * #METHOD_TYPE_PARAMETER_BOUND}. + * + * @return a type parameter index. + */ + public int getTypeParameterIndex() { + return (targetTypeAndInfo & 0x00FF0000) >> 16; + } + + /** + * Returns the index of the type parameter bound, within the type parameter {@link + * #getTypeParameterIndex}, referenced by this type reference. This method must only be used for + * type references whose sort is {@link #CLASS_TYPE_PARAMETER_BOUND} or {@link + * #METHOD_TYPE_PARAMETER_BOUND}. + * + * @return a type parameter bound index. + */ + public int getTypeParameterBoundIndex() { + return (targetTypeAndInfo & 0x0000FF00) >> 8; + } + + /** + * Returns the index of the "super type" of a class that is referenced by this type reference. + * This method must only be used for type references whose sort is {@link #CLASS_EXTENDS}. + * + * @return the index of an processer in the 'implements' clause of a class, or -1 if this type + * reference references the type of the super class. + */ + public int getSuperTypeIndex() { + return (short) ((targetTypeAndInfo & 0x00FFFF00) >> 8); + } + + /** + * Returns the index of the formal parameter whose type is referenced by this type reference. This + * method must only be used for type references whose sort is {@link #METHOD_FORMAL_PARAMETER}. + * + * @return a formal parameter index. + */ + public int getFormalParameterIndex() { + return (targetTypeAndInfo & 0x00FF0000) >> 16; + } + + /** + * Returns the index of the exception, in a 'throws' clause of a method, whose type is referenced + * by this type reference. This method must only be used for type references whose sort is {@link + * #THROWS}. + * + * @return the index of an exception in the 'throws' clause of a method. + */ + public int getExceptionIndex() { + return (targetTypeAndInfo & 0x00FFFF00) >> 8; + } + + /** + * Returns the index of the try catch block (using the order in which they are visited with + * visitTryCatchBlock), whose 'catch' type is referenced by this type reference. This method must + * only be used for type references whose sort is {@link #EXCEPTION_PARAMETER} . + * + * @return the index of an exception in the 'throws' clause of a method. + */ + public int getTryCatchBlockIndex() { + return (targetTypeAndInfo & 0x00FFFF00) >> 8; + } + + /** + * Returns the index of the type argument referenced by this type reference. This method must only + * be used for type references whose sort is {@link #CAST}, {@link + * #CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT}, {@link #METHOD_INVOCATION_TYPE_ARGUMENT}, {@link + * #CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT}, or {@link #METHOD_REFERENCE_TYPE_ARGUMENT}. + * + * @return a type parameter index. + */ + public int getTypeArgumentIndex() { + return targetTypeAndInfo & 0xFF; + } + + /** + * Returns the int encoded value of this type reference, suitable for use in visit methods related + * to type annotations, like visitTypeAnnotation. + * + * @return the int encoded value of this type reference. + */ + public int getValue() { + return targetTypeAndInfo; + } + + /** + * Puts the given target_type and target_info JVMS structures into the given ByteVector. + * + * @param targetTypeAndInfo a target_type and a target_info structures encoded as in {@link + * #targetTypeAndInfo}. LOCAL_VARIABLE and RESOURCE_VARIABLE target types are not supported. + * @param output where the type reference must be put. + */ + static void putTarget(final int targetTypeAndInfo, final ByteVector output) { + switch (targetTypeAndInfo >>> 24) { + case CLASS_TYPE_PARAMETER: + case METHOD_TYPE_PARAMETER: + case METHOD_FORMAL_PARAMETER: + output.putShort(targetTypeAndInfo >>> 16); + break; + case FIELD: + case METHOD_RETURN: + case METHOD_RECEIVER: + output.putByte(targetTypeAndInfo >>> 24); + break; + case CAST: + case CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT: + case METHOD_INVOCATION_TYPE_ARGUMENT: + case CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT: + case METHOD_REFERENCE_TYPE_ARGUMENT: + output.putInt(targetTypeAndInfo); + break; + case CLASS_EXTENDS: + case CLASS_TYPE_PARAMETER_BOUND: + case METHOD_TYPE_PARAMETER_BOUND: + case THROWS: + case EXCEPTION_PARAMETER: + case INSTANCEOF: + case NEW: + case CONSTRUCTOR_REFERENCE: + case METHOD_REFERENCE: + output.put12(targetTypeAndInfo >>> 24, (targetTypeAndInfo & 0xFFFF00) >> 8); + break; + default: + throw new IllegalArgumentException(); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/signature/SignatureReader.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/signature/SignatureReader.java new file mode 100644 index 0000000..0bacfec --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/signature/SignatureReader.java @@ -0,0 +1,237 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm.signature; + +import cn.universal.core.engine.asm.ClassVisitor; +import cn.universal.core.engine.asm.MethodVisitor; + +/** + * A parser for signature literals, as defined in the Java Virtual Machine Specification (JVMS), to + * visit them with a SignatureVisitor. @Author Thomas Hallgren @Author Eric Bruneton + * + * @see JVMS + * 4.7.9.1 + */ +public class SignatureReader { + + /** The JVMS signature to be read. */ + private final String signatureValue; + + /** + * Constructs a {@link SignatureReader} for the given signature. + * + * @param signature A JavaTypeSignature, ClassSignature or MethodSignature. + */ + public SignatureReader(final String signature) { + this.signatureValue = signature; + } + + /** + * Makes the given visitor visit the signature of this {@link SignatureReader}. This signature is + * the one specified in the constructor (see {@link #SignatureReader}). This method is intended to + * be called on a {@link SignatureReader} that was created using a ClassSignature (such as + * the signature parameter of the {@link ClassVisitor#visit} method) or a + * MethodSignature (such as the signature parameter of the {@link + * ClassVisitor#visitMethod} method). + * + * @param signatureVistor the visitor that must visit this signature. + */ + public void accept(final SignatureVisitor signatureVistor) { + String signature = this.signatureValue; + int length = signature.length(); + int offset; // Current offset in the parsed signature (parsed from left to right). + char currentChar; // The signature character at 'offset', or just before. + + // If the signature starts with '<', it starts with TypeParameters, i.e. a formal type parameter + // identifier, followed by one or more pair ':',ReferenceTypeSignature (for its class bound and + // processer bounds). + if (signature.charAt(0) == '<') { + // Invariant: offset points to the second character of a formal type parameter name at the + // beginning of each iteration of the loop below. + offset = 2; + do { + // The formal type parameter name is everything between offset - 1 and the first ':'. + int classBoundStartOffset = signature.indexOf(':', offset); + signatureVistor.visitFormalTypeParameter( + signature.substring(offset - 1, classBoundStartOffset)); + + // If the character after the ':' class bound marker is not the start of a + // ReferenceTypeSignature, it means the class bound is empty (which is a valid case). + offset = classBoundStartOffset + 1; + currentChar = signature.charAt(offset); + if (currentChar == 'L' || currentChar == '[' || currentChar == 'T') { + offset = parseType(signature, offset, signatureVistor.visitClassBound()); + } + + // While the character after the class bound or after the last parsed processer bound + // is ':', we need to parse another processer bound. + while ((currentChar = signature.charAt(offset++)) == ':') { + offset = parseType(signature, offset, signatureVistor.visitInterfaceBound()); + } + + // At this point a TypeParameter has been fully parsed, and we need to parse the next one + // (note that currentChar is now the first character of the next TypeParameter, and that + // offset points to the second character), unless the character just after this + // TypeParameter signals the end of the TypeParameters. + } while (currentChar != '>'); + } else { + offset = 0; + } + + // If the (optional) TypeParameters is followed by '(' this means we are parsing a + // MethodSignature, which has JavaTypeSignature type inside parentheses, followed by a Result + // type and optional ThrowsSignature types. + if (signature.charAt(offset) == '(') { + offset++; + while (signature.charAt(offset) != ')') { + offset = parseType(signature, offset, signatureVistor.visitParameterType()); + } + // Use offset + 1 to skip ')'. + offset = parseType(signature, offset + 1, signatureVistor.visitReturnType()); + while (offset < length) { + // Use offset + 1 to skip the first character of a ThrowsSignature, i.e. '^'. + offset = parseType(signature, offset + 1, signatureVistor.visitExceptionType()); + } + } else { + // Otherwise we are parsing a ClassSignature (by hypothesis on the method input), which has + // one or more ClassTypeSignature for the super class and the implemented interfaces. + offset = parseType(signature, offset, signatureVistor.visitSuperclass()); + while (offset < length) { + offset = parseType(signature, offset, signatureVistor.visitInterface()); + } + } + } + + /** + * Makes the given visitor visit the signature of this {@link SignatureReader}. This signature is + * the one specified in the constructor (see {@link #SignatureReader}). This method is intended to + * be called on a {@link SignatureReader} that was created using a JavaTypeSignature, such + * as the signature parameter of the {@link ClassVisitor#visitField} or {@link + * MethodVisitor#visitLocalVariable} methods. + * + * @param signatureVisitor the visitor that must visit this signature. + */ + public void acceptType(final SignatureVisitor signatureVisitor) { + parseType(signatureValue, 0, signatureVisitor); + } + + /** + * Parses a JavaTypeSignature and makes the given visitor visit it. + * + * @param signature a string containing the signature that must be parsed. + * @param startOffset index of the first character of the signature to parsed. + * @param signatureVisitor the visitor that must visit this signature. + * @return the index of the first character after the parsed signature. + */ + private static int parseType( + final String signature, final int startOffset, final SignatureVisitor signatureVisitor) { + int offset = startOffset; // Current offset in the parsed signature. + char currentChar = signature.charAt(offset++); // The signature character at 'offset'. + + // Switch based on the first character of the JavaTypeSignature, which indicates its kind. + switch (currentChar) { + case 'Z': + case 'C': + case 'B': + case 'S': + case 'I': + case 'F': + case 'J': + case 'D': + case 'V': + // Case of a BaseType or a VoidDescriptor. + signatureVisitor.visitBaseType(currentChar); + return offset; + + case '[': + // Case of an ArrayTypeSignature, a '[' followed by a JavaTypeSignature. + return parseType(signature, offset, signatureVisitor.visitArrayType()); + + case 'T': + // Case of TypeVariableSignature, an identifier between 'T' and ';'. + int endOffset = signature.indexOf(';', offset); + signatureVisitor.visitTypeVariable(signature.substring(offset, endOffset)); + return endOffset + 1; + + case 'L': + // Case of a ClassTypeSignature, which ends with ';'. + // These signatures have a main class type followed by zero or more inner class types + // (separated by '.'). Each can have type arguments, inside '<' and '>'. + int start = offset; // The start offset of the currently parsed main or inner class name. + boolean visited = false; // Whether the currently parsed class name has been visited. + boolean inner = false; // Whether we are currently parsing an inner class type. + // Parses the signature, one character at a time. + while (true) { + currentChar = signature.charAt(offset++); + if (currentChar == '.' || currentChar == ';') { + // If a '.' or ';' is encountered, this means we have fully parsed the main class name + // or an inner class name. This name may already have been visited it is was followed by + // type arguments between '<' and '>'. If not, we need to visit it here. + if (!visited) { + String name = signature.substring(start, offset - 1); + if (inner) { + signatureVisitor.visitInnerClassType(name); + } else { + signatureVisitor.visitClassType(name); + } + } + // If we reached the end of the ClassTypeSignature return, otherwise start the parsing + // of a new class name, which is necessarily an inner class name. + if (currentChar == ';') { + signatureVisitor.visitEnd(); + break; + } + start = offset; + visited = false; + inner = true; + } else if (currentChar == '<') { + // If a '<' is encountered, this means we have fully parsed the main class name or an + // inner class name, and that we now need to parse TypeArguments. First, we need to + // visit the parsed class name. + String name = signature.substring(start, offset - 1); + if (inner) { + signatureVisitor.visitInnerClassType(name); + } else { + signatureVisitor.visitClassType(name); + } + visited = true; + // Now, parse the TypeArgument(s), one at a time. + while ((currentChar = signature.charAt(offset)) != '>') { + switch (currentChar) { + case '*': + // Unbounded TypeArgument. + ++offset; + signatureVisitor.visitTypeArgument(); + break; + case '+': + case '-': + // Extends or Super TypeArgument. Use offset + 1 to skip the '+' or '-'. + offset = + parseType( + signature, offset + 1, signatureVisitor.visitTypeArgument(currentChar)); + break; + default: + // Instanceof TypeArgument. The '=' is implicit. + offset = parseType(signature, offset, signatureVisitor.visitTypeArgument('=')); + break; + } + } + } + } + return offset; + + default: + throw new IllegalArgumentException(); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/signature/SignatureVisitor.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/signature/SignatureVisitor.java new file mode 100644 index 0000000..7e20a3e --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/signature/SignatureVisitor.java @@ -0,0 +1,193 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm.signature; + +import cn.universal.core.engine.asm.Opcodes; + +/** + * A visitor to visit a generic signature. The methods of this processer must be called in one of + * the three following orders (the last one is the only valid order for a {@link SignatureVisitor} + * that is returned by a method of this processer): + * + *

    + *
  • ClassSignature = ( {@code visitFormalTypeParameter} {@code visitClassBound}? {@code + * visitInterfaceBound}* )* ({@code visitSuperclass} {@code visitInterface}* ) + *
  • MethodSignature = ( {@code visitFormalTypeParameter} {@code visitClassBound}? {@code + * visitInterfaceBound}* )* ({@code visitParameterType}* {@code visitReturnType} {@code + * visitExceptionType}* ) + *
  • TypeSignature = {@code visitBaseType} | {@code visitTypeVariable} | {@code + * visitArrayType} | ( {@code visitClassType} {@code visitTypeArgument}* ( {@code + * visitInnerClassType} {@code visitTypeArgument}* )* {@code visitEnd} ) ) + *
+ * + * @Author Thomas Hallgren @Author Eric Bruneton + */ +public abstract class SignatureVisitor { + + /** Wildcard for an "extends" type argument. */ + public static final char EXTENDS = '+'; + + /** Wildcard for a "super" type argument. */ + public static final char SUPER = '-'; + + /** Wildcard for a normal type argument. */ + public static final char INSTANCEOF = '='; + + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + protected final int api; + + /** + * Constructs a new {@link SignatureVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + public SignatureVisitor(final int api) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + this.api = api; + } + + /** + * Visits a formal type parameter. + * + * @param name the name of the formal parameter. + */ + public void visitFormalTypeParameter(final String name) {} + + /** + * Visits the class bound of the last visited formal type parameter. + * + * @return a non null visitor to visit the signature of the class bound. + */ + public SignatureVisitor visitClassBound() { + return this; + } + + /** + * Visits an processer bound of the last visited formal type parameter. + * + * @return a non null visitor to visit the signature of the processer bound. + */ + public SignatureVisitor visitInterfaceBound() { + return this; + } + + /** + * Visits the type of the super class. + * + * @return a non null visitor to visit the signature of the super class type. + */ + public SignatureVisitor visitSuperclass() { + return this; + } + + /** + * Visits the type of an processer implemented by the class. + * + * @return a non null visitor to visit the signature of the processer type. + */ + public SignatureVisitor visitInterface() { + return this; + } + + /** + * Visits the type of a method parameter. + * + * @return a non null visitor to visit the signature of the parameter type. + */ + public SignatureVisitor visitParameterType() { + return this; + } + + /** + * Visits the return type of the method. + * + * @return a non null visitor to visit the signature of the return type. + */ + public SignatureVisitor visitReturnType() { + return this; + } + + /** + * Visits the type of a method exception. + * + * @return a non null visitor to visit the signature of the exception type. + */ + public SignatureVisitor visitExceptionType() { + return this; + } + + /** + * Visits a signature corresponding to a primitive type. + * + * @param descriptor the descriptor of the primitive type, or 'V' for {@code void} . + */ + public void visitBaseType(final char descriptor) {} + + /** + * Visits a signature corresponding to a type variable. + * + * @param name the name of the type variable. + */ + public void visitTypeVariable(final String name) {} + + /** + * Visits a signature corresponding to an array type. + * + * @return a non null visitor to visit the signature of the array element type. + */ + public SignatureVisitor visitArrayType() { + return this; + } + + /** + * Starts the visit of a signature corresponding to a class or processer type. + * + * @param name the internal name of the class or processer. + */ + public void visitClassType(final String name) {} + + /** + * Visits an inner class. + * + * @param name the local name of the inner class in its enclosing class. + */ + public void visitInnerClassType(final String name) {} + + /** Visits an unbounded type argument of the last visited class or inner class type. */ + public void visitTypeArgument() {} + + /** + * Visits a type argument of the last visited class or inner class type. + * + * @param wildcard '+', '-' or '='. + * @return a non null visitor to visit the signature of the type argument. + */ + public SignatureVisitor visitTypeArgument(final char wildcard) { + return this; + } + + /** Ends the visit of a signature corresponding to a class or processer type. */ + public void visitEnd() {} +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/signature/SignatureWriter.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/signature/SignatureWriter.java new file mode 100644 index 0000000..cb1d653 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/asm/signature/SignatureWriter.java @@ -0,0 +1,223 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.asm.signature; + +import cn.universal.core.engine.asm.Opcodes; + +/** + * A SignatureVisitor that generates signature literals, as defined in the Java Virtual Machine + * Specification (JVMS). @Author Thomas Hallgren @Author Eric Bruneton + * + * @see JVMS + * 4.7.9.1 + */ +public class SignatureWriter extends SignatureVisitor { + + /** The builder used to construct the visited signature. */ + private final StringBuilder stringBuilder = new StringBuilder(); + + /** Whether the visited signature contains formal type parameters. */ + private boolean hasFormals; + + /** Whether the visited signature contains method parameter types. */ + private boolean hasParameters; + + /** + * The stack used to keep track of class types that have arguments. Each element of this stack is + * a boolean encoded in one bit. The top of the stack is the least significant bit. Pushing false + * = *2, pushing true = *2+1, popping = /2. + * + *

Class type arguments must be surrounded with '<' and '>' and, because + * + *

    + *
  1. class types can be nested (because type arguments can themselves be class types), + *
  2. SignatureWriter always returns 'this' in each visit* method (to avoid allocating new + * SignatureWriter instances), + *
+ * + *

we need a stack to properly balance these 'parentheses'. A new element is pushed on this + * stack for each new visited type, and popped when the visit of this type ends (either is + * visitEnd, or because visitInnerClassType is called). + */ + private int argumentStack; + + /** Constructs a new {@link SignatureWriter}. */ + public SignatureWriter() { + super(/* latest api =*/ Opcodes.ASM9); + } + + // ----------------------------------------------------------------------------------------------- + // Implementation of the SignatureVisitor processer + // ----------------------------------------------------------------------------------------------- + + @Override + public void visitFormalTypeParameter(final String name) { + if (!hasFormals) { + hasFormals = true; + stringBuilder.append('<'); + } + stringBuilder.append(name); + stringBuilder.append(':'); + } + + @Override + public SignatureVisitor visitClassBound() { + return this; + } + + @Override + public SignatureVisitor visitInterfaceBound() { + stringBuilder.append(':'); + return this; + } + + @Override + public SignatureVisitor visitSuperclass() { + endFormals(); + return this; + } + + @Override + public SignatureVisitor visitInterface() { + return this; + } + + @Override + public SignatureVisitor visitParameterType() { + endFormals(); + if (!hasParameters) { + hasParameters = true; + stringBuilder.append('('); + } + return this; + } + + @Override + public SignatureVisitor visitReturnType() { + endFormals(); + if (!hasParameters) { + stringBuilder.append('('); + } + stringBuilder.append(')'); + return this; + } + + @Override + public SignatureVisitor visitExceptionType() { + stringBuilder.append('^'); + return this; + } + + @Override + public void visitBaseType(final char descriptor) { + stringBuilder.append(descriptor); + } + + @Override + public void visitTypeVariable(final String name) { + stringBuilder.append('T'); + stringBuilder.append(name); + stringBuilder.append(';'); + } + + @Override + public SignatureVisitor visitArrayType() { + stringBuilder.append('['); + return this; + } + + @Override + public void visitClassType(final String name) { + stringBuilder.append('L'); + stringBuilder.append(name); + // Pushes 'false' on the stack, meaning that this type does not have type arguments (as far as + // we can tell at this point). + argumentStack *= 2; + } + + @Override + public void visitInnerClassType(final String name) { + endArguments(); + stringBuilder.append('.'); + stringBuilder.append(name); + // Pushes 'false' on the stack, meaning that this type does not have type arguments (as far as + // we can tell at this point). + argumentStack *= 2; + } + + @Override + public void visitTypeArgument() { + // If the top of the stack is 'false', this means we are visiting the first type argument of the + // currently visited type. We therefore need to append a '<', and to replace the top stack + // element with 'true' (meaning that the current type does have type arguments). + if (argumentStack % 2 == 0) { + argumentStack |= 1; + stringBuilder.append('<'); + } + stringBuilder.append('*'); + } + + @Override + public SignatureVisitor visitTypeArgument(final char wildcard) { + // If the top of the stack is 'false', this means we are visiting the first type argument of the + // currently visited type. We therefore need to append a '<', and to replace the top stack + // element with 'true' (meaning that the current type does have type arguments). + if (argumentStack % 2 == 0) { + argumentStack |= 1; + stringBuilder.append('<'); + } + if (wildcard != '=') { + stringBuilder.append(wildcard); + } + return this; + } + + @Override + public void visitEnd() { + endArguments(); + stringBuilder.append(';'); + } + + /** + * Returns the signature that was built by this signature writer. + * + * @return the signature that was built by this signature writer. + */ + @Override + public String toString() { + return stringBuilder.toString(); + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods + // ----------------------------------------------------------------------------------------------- + + /** Ends the formal type parameters section of the signature. */ + private void endFormals() { + if (hasFormals) { + hasFormals = false; + stringBuilder.append('>'); + } + } + + /** Ends the type arguments of a class or inner class type. */ + private void endArguments() { + // If the top of the stack is 'true', this means that some type arguments have been visited for + // the type whose visit is now ending. We therefore need to append a '>', and to pop one element + // from the stack. + if (argumentStack % 2 == 1) { + stringBuilder.append('>'); + } + argumentStack /= 2; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/compile/CompileCache.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/compile/CompileCache.java new file mode 100644 index 0000000..5aeb545 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/compile/CompileCache.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.compile; + +import cn.universal.core.engine.MagicScript; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; + +public class CompileCache { + + private final LinkedHashMap cacheMap; + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + public CompileCache(int capacity) { + this.cacheMap = + new LinkedHashMap((int) Math.ceil(capacity / 0.75) + 1, 0.75f, true) { + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + // lru淘汰 + return size() > capacity; + } + }; + } + + public void put(String key, MagicScript script) { + lock.writeLock().lock(); + try { + cacheMap.put(key, script); + } finally { + lock.writeLock().unlock(); + } + } + + public MagicScript get(String key) { + lock.readLock().lock(); + try { + return cacheMap.get(key); + } finally { + lock.readLock().unlock(); + } + } + + public MagicScript get(String key, Supplier value) { + MagicScript script = get(key); + if (script == null) { + script = value.get(); + put(key, script); + } + return script; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/compile/Descriptor.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/compile/Descriptor.java new file mode 100644 index 0000000..0008d64 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/compile/Descriptor.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.compile; + +import cn.universal.core.engine.asm.Type; + +public class Descriptor { + + public static String make_descriptor(Class target, String methodName, Class... args) { + try { + return Type.getMethodDescriptor(target.getMethod(methodName, args)); + } catch (NoSuchMethodException e) { + throw new MagicScriptCompileException(e); + } + } + + public static String make_descriptor(Class type, Class... args) { + int len = args.length; + Type[] types = new Type[len]; + for (int i = 0; i < len; i++) { + types[i] = Type.getType(args[i]); + } + return Type.getMethodDescriptor(Type.getType(type), types); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/compile/MagicScriptCompileException.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/compile/MagicScriptCompileException.java new file mode 100644 index 0000000..3ada139 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/compile/MagicScriptCompileException.java @@ -0,0 +1,28 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.compile; + +public class MagicScriptCompileException extends RuntimeException { + + public MagicScriptCompileException(Throwable cause) { + super(cause); + } + + public MagicScriptCompileException(String message, Throwable cause) { + super(message, cause); + } + + public MagicScriptCompileException(String message) { + super(message); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/compile/MagicScriptCompiler.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/compile/MagicScriptCompiler.java new file mode 100644 index 0000000..8e5374e --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/compile/MagicScriptCompiler.java @@ -0,0 +1,781 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.compile; + +import cn.universal.core.engine.MagicScriptContext; +import cn.universal.core.engine.asm.ClassWriter; +import cn.universal.core.engine.asm.Handle; +import cn.universal.core.engine.asm.Label; +import cn.universal.core.engine.asm.MethodVisitor; +import cn.universal.core.engine.asm.Opcodes; +import cn.universal.core.engine.asm.Type; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.VarIndex; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Node; +import cn.universal.core.engine.parsing.ast.VariableSetter; +import cn.universal.core.engine.parsing.ast.binary.AssigmentOperation; +import cn.universal.core.engine.parsing.ast.statement.VariableAccess; +import cn.universal.core.engine.runtime.MagicScriptRuntime; +import cn.universal.core.engine.runtime.RuntimeContext; +import cn.universal.core.engine.runtime.Variables; +import cn.universal.core.engine.runtime.function.MagicScriptLambdaFunction; +import cn.universal.core.engine.runtime.handle.ArithmeticHandle; +import cn.universal.core.engine.runtime.handle.BitHandle; +import cn.universal.core.engine.runtime.handle.FunctionCallHandle; +import cn.universal.core.engine.runtime.handle.OperatorHandle; +import java.lang.invoke.CallSite; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.Stack; +import java.util.concurrent.atomic.AtomicLong; + +/** 脚本编译器 */ +public class MagicScriptCompiler implements Opcodes { + + private static final AtomicLong COUNTER = new AtomicLong(1); + + private final ClassWriter classWriter; + + private final Long id = COUNTER.getAndIncrement(); + + /** < <= == === != !=== >= > 等操作符处理器 */ + private static final Handle OPERATOR_HANDLE = makeHandle(OperatorHandle.class); + + /** << >> >>> & ^ | 运算 */ + private static final Handle BIT_HANDLE = makeHandle(BitHandle.class); + + /** 方法调用、lambda调用、函数调用处理器 */ + private static final Handle FUNCTION_HANDLE = makeHandle(FunctionCallHandle.class); + + /** + - * / % 处理器 */ + private static final Handle ARITHMETIC_HANDLE = makeHandle(ArithmeticHandle.class); + + private final Stack methodVisitors = new Stack<>(); + + private final Stack> vars = new Stack<>(); + + private final Stack labelStack = new Stack<>(); + + private final Stack> finallyStack = new Stack<>(); + + private final Set varIndices; + + /** -1 ~ 5的int值 */ + private static final int[] ICONST = { + ICONST_M1, ICONST_0, ICONST_1, ICONST_2, ICONST_3, ICONST_4, ICONST_5 + }; + + private final List spans = new ArrayList<>(); + + private int functionIndex = 0; + + private int tempIndex = 4; + + private final boolean debug; + + private int lastLineNumber = -1; + + private boolean contextInited = false; + + public MagicScriptCompiler(Set varIndices, boolean debug) { + this.varIndices = varIndices; + this.debug = debug; + // 创建类并继承 MagicScriptRuntime + classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + classWriter.visit( + V1_8, + ACC_PUBLIC | ACC_SUPER, + getClassName(), + null, + getJvmType(MagicScriptRuntime.class), + null); + classWriter.visitSource(getClassName() + ".ms", null); + createMethod(ACC_PUBLIC, "", Descriptor.make_descriptor(void.class)); + this.load0() + .invoke(INVOKESPECIAL, MagicScriptRuntime.class, "", void.class) + .insn(RETURN) + .pop(); + // 创建execute方法 + createMethod( + ACC_PUBLIC, "execute", Descriptor.make_descriptor(Object.class, MagicScriptContext.class)); + initContext(); + } + + public List getSpans() { + return spans; + } + + /** + * 创建方法 + * + * @param access 访问属性 + * @param methodName 方法名 + * @param descriptor 方法描述 + */ + public MagicScriptCompiler createMethod(int access, String methodName, String descriptor) { + MethodVisitor visitor = classWriter.visitMethod(access, methodName, descriptor, null, null); + visitor.visitCode(); + methodVisitors.push(visitor); + vars.push(new ArrayList<>()); + finallyStack.push(null); + labelStack.push(new Label[2]); + return this; + } + + public int getTempIndex() { + return tempIndex++; + } + + /** 获取lambda函数下标 */ + public int getFunctionIndex() { + return ++functionIndex; + } + + /** 标识continue和break位置 */ + public MagicScriptCompiler markLabel(Label start, Label end) { + labelStack.push(new Label[] {start, end}); + return this; + } + + /** 消除标记 */ + public MagicScriptCompiler exitLabel() { + labelStack.pop(); + return this; + } + + /** 跳转到continue位置 */ + public MagicScriptCompiler start() { + return jump(GOTO, labelStack.peek()[0]); + } + + /** 跳转到break位置 */ + public MagicScriptCompiler end() { + return jump(GOTO, labelStack.peek()[1]); + } + + /** 访问AST节点 */ + public MagicScriptCompiler visit(Node node) { + // 对于赋值语句的特殊处理 + if (node instanceof AssigmentOperation) { + AssigmentOperation operation = (AssigmentOperation) node; + // 如果是a = b = 1这种情况,则需要读取a的值 + if (operation.getLeftOperand() instanceof VariableAccess) { + return compile(node, true).compile(operation.getLeftOperand()); + } + } + return compile(node, false); + } + + /** 编译AST节点 */ + public MagicScriptCompiler compile(Node node) { + return compile(node, false); + } + + public MagicScriptCompiler lineNumber(Span span) { + spans.add(span); + MethodVisitor mv = self(); + Label label = new Label(); + // 设置行号(节点序号) + mv.visitLabel(label); + mv.visitLineNumber(spans.size() - 1, label); + return this; + } + + public MagicScriptCompiler loadContext() { + if (contextInited || methodVisitors.size() > 1) { + load0() + .self() + .visitFieldInsn( + GETFIELD, getClassName(), "context", Type.getDescriptor(MagicScriptContext.class)); + return this; + } + return load1(); + } + + public MagicScriptCompiler newRuntimeContext() { + return this.typeInsn(NEW, RuntimeContext.class) + .insn(DUP) + .loadContext() + .load2() + .invoke( + INVOKESPECIAL, + RuntimeContext.class, + "", + void.class, + MagicScriptContext.class, + Variables.class); + } + + /** + * 编译AST节点 + * + * @param node AST节点 + * @param pop 是否需要弹出栈顶 + */ + public MagicScriptCompiler compile(Node node, boolean pop) { + if (node == null) { + return insn(ACONST_NULL); + } else { + lineNumber(node.getSpan()); + if (debug) { + Span.Line currentLine = node.getSpan().getLine(); + int line = currentLine.getLineNumber(); + if (lastLineNumber != line) { + this.loadContext() + .visitInt(line) + .visitInt(currentLine.getStartCol()) + .visitInt(currentLine.getEndLineNumber()) + .visitInt(currentLine.getEndCol()) + .load2() + .invoke( + INVOKEVIRTUAL, + MagicScriptContext.class, + "pause", + void.class, + int.class, + int.class, + int.class, + int.class, + Variables.class); + lastLineNumber = line; + } + } + // 编译该节点 + node.compile(this); + } + // 对于赋值语句的特殊处理,因为赋值语句有两种 + // 不带返回值的: a+=1 + // 带返回值的 xxx.xx = 1 + if (node instanceof AssigmentOperation) { + AssigmentOperation operation = (AssigmentOperation) node; + if (operation.getLeftOperand() instanceof VariableSetter + && operation.getLeftOperand() instanceof VariableAccess) { + return this; + } + } + // 对于带有返回值的表达式,且需要弹出栈顶时,追加指令POP + return pop && node instanceof Expression ? insn(POP) : this; + } + + public MagicScriptCompiler tryCatch(Label start, Label end, Label handle, Class target) { + self().visitTryCatchBlock(start, end, handle, getJvmType(target)); + return this; + } + + /** 访问 */ + public MagicScriptCompiler visit(List nodes) { + nodes.forEach(this::visit); + return this; + } + + /** 编译 */ + public MagicScriptCompiler compile(List nodes) { + nodes.forEach(it -> compile(it, true)); + return this; + } + + /** 加载this */ + public MagicScriptCompiler load0() { + self().visitVarInsn(ALOAD, 0); + return this; + } + + /** 加载context */ + public MagicScriptCompiler load1() { + self().visitVarInsn(ALOAD, 1); + return this; + } + + /** 加载context */ + public void newArrayList() { + this.typeInsn(NEW, ArrayList.class) + .insn(DUP) + .invoke(INVOKESPECIAL, ArrayList.class, "", void.class); + } + + /** 加载Variables */ + public MagicScriptCompiler load2() { + self().visitVarInsn(ALOAD, 2); + return this; + } + + /** 加载3号变量,一般指异常(临时变量) */ + public MagicScriptCompiler load3() { + self().visitVarInsn(ALOAD, 3); + return this; + } + + public MagicScriptCompiler load4() { + self().visitVarInsn(ALOAD, 4); + return this; + } + + /** 加载变量 */ + public MagicScriptCompiler load(int index) { + return load2() + .visitInt(index) + .invoke(INVOKEVIRTUAL, Variables.class, "getValue", Object.class, int.class); + } + + /** 加载变量 */ + public MagicScriptCompiler load(VarIndex varIndex) { + return load(varIndex.getIndex()); + } + + /** + * 加载变量 + * + * @param name 变量名 + */ + public MagicScriptCompiler load(String name) { + int index = vars.peek().indexOf(name) + 1; + if (index > 0) { // 如果当前栈中有,则直接使用 + return load(index); + } else { + // 从环境中获取 + this.load1() + .ldc(name) + .invoke( + INVOKEVIRTUAL, + MagicScriptContext.class, + "getEnvironmentValue", + Object.class, + String.class); + } + return this; + } + + public MagicScriptCompiler label(Label label) { + self().visitLabel(label); + return this; + } + + /** 跳转 */ + public MagicScriptCompiler jump(int opcode, Label label) { + self().visitJumpInsn(opcode, label); + return this; + } + + /** 移除变量 */ + public MagicScriptCompiler remove(VarIndex varIndex) { + if (varIndex == null) { + return this; + } + return remove(varIndex.getName()); + } + + /** 移除变量 */ + public MagicScriptCompiler remove(String name) { + List varList = vars.peek(); + int index = varList.indexOf(name); + if (index > -1) { + varList.set(index, null); + } + return this; + } + + /** 配合pre_store使用,保存至数组中 */ + public MagicScriptCompiler store() { + return invoke(INVOKEVIRTUAL, Variables.class, "setValue", void.class, int.class, Object.class); + } + + /** 配合pre_store使用,保存至数组中 */ + public MagicScriptCompiler store(VarIndex varIndex) { + return varIndex.isScoped() ? scopeStore() : store(); + } + + /** 配合pre_store使用,保存至数组中 */ + public MagicScriptCompiler scopeStore() { + return invoke( + INVOKEVIRTUAL, Variables.class, "setScopeValue", void.class, int.class, Object.class); + } + + /** 保存变量 */ + public MagicScriptCompiler store(int index) { + self().visitVarInsn(ASTORE, index); + return this; + } + + /** 保存变量 */ + public MagicScriptCompiler frame( + int type, int numLocal, Object[] local, int numStack, Object[] stack) { + self().visitFrame(type, numLocal, local, numStack, stack); + return this; + } + + /** 写变量前的准备 */ + public MagicScriptCompiler pre_store(int index) { + return load2().visitInt(index); + } + + /** 写变量前的准备 */ + public MagicScriptCompiler pre_store(VarIndex varIndex) { + return pre_store(varIndex.getIndex()); + } + + public MagicScriptCompiler bipush(int value) { + self().visitIntInsn(BIPUSH, value); + return this; + } + + public MagicScriptCompiler typeInsn(int opcode, Class target) { + self().visitTypeInsn(opcode, getJvmType(target)); + return this; + } + + /** + * 二元运算 + * + * @param methodName 运算方法 + */ + public MagicScriptCompiler operator(String methodName) { + self() + .visitInvokeDynamicInsn( + methodName, + MethodType.genericMethodType(2).toMethodDescriptorString(), + OPERATOR_HANDLE, + 2); + return this; + } + + /** + * 位运算 + * + * @param methodName 运算方法 + */ + public MagicScriptCompiler bit(String methodName) { + self() + .visitInvokeDynamicInsn( + methodName, MethodType.genericMethodType(2).toMethodDescriptorString(), BIT_HANDLE, 2); + return this; + } + + /** + * 将方法转为lambda + * + * @param methodName 方法名 + */ + public MagicScriptCompiler lambda(String methodName) { + load0(); + Handle metaFactory = + new Handle( + H_INVOKESTATIC, + getJvmType(LambdaMetafactory.class), + "metafactory", + Descriptor.make_descriptor( + CallSite.class, + MethodHandles.Lookup.class, + String.class, + MethodType.class, + MethodType.class, + MethodHandle.class, + MethodType.class), + false); + String descriptor = Descriptor.make_descriptor(Object.class, Variables.class, Object[].class); + Handle impl = new Handle(H_INVOKEVIRTUAL, getClassName(), methodName, descriptor, false); + self() + .visitInvokeDynamicInsn( + "apply", + "(L" + + getClassName() + + ";)" + + Type.getType(MagicScriptLambdaFunction.class).getDescriptor(), + metaFactory, + Type.getType(descriptor), + impl, + Type.getType(descriptor)); + load2() + .self() + .visitMethodInsn( + INVOKEVIRTUAL, + getClassName(), + "createLambda", + Descriptor.make_descriptor( + MagicScriptLambdaFunction.class, MagicScriptLambdaFunction.class, Variables.class), + false); + return this; + } + + /** + * invokedynamic调用 + * + * @param methodName 方法名 + * @param arguments 参数个数 + */ + public MagicScriptCompiler call(String methodName, int arguments) { + self() + .visitInvokeDynamicInsn( + methodName, + MethodType.genericMethodType(arguments).toMethodDescriptorString(), + FUNCTION_HANDLE, + arguments); + return this; + } + + /** + * 执行算术运算 + * + * @param methodName 方法名 + */ + public MagicScriptCompiler arithmetic(String methodName) { + self() + .visitInvokeDynamicInsn( + methodName, + MethodType.genericMethodType(2).toMethodDescriptorString(), + ARITHMETIC_HANDLE, + 2); + return this; + } + + /** 将int值装箱 */ + public MagicScriptCompiler asInteger() { + return invoke(INVOKESTATIC, Integer.class, "valueOf", Integer.class, int.class); + } + + /** 将boolean值装箱 */ + public MagicScriptCompiler asBoolean() { + return invoke(INVOKESTATIC, Boolean.class, "valueOf", Boolean.class, boolean.class); + } + + /** + * 调用方法 + * + * @param opcode 调用类型 + * @param target 目标类 + * @param method 方法名 + * @param returnType 返回值类型 + * @param argumentTypes 参数类型 + */ + public MagicScriptCompiler invoke( + int opcode, Class target, String method, Class returnType, Class... argumentTypes) { + return invoke(opcode, target, method, false, returnType, argumentTypes); + } + + /** + * 调用方法 + * + * @param opcode 调用类型 + * @param target 目标类 + * @param method 方法名 + * @param isInterface 是否是接口 + * @param returnType 返回值类型 + * @param argumentTypes 参数类型 + */ + public MagicScriptCompiler invoke( + int opcode, + Class target, + String method, + boolean isInterface, + Class returnType, + Class... argumentTypes) { + self() + .visitMethodInsn( + opcode, + getJvmType(target), + method, + Descriptor.make_descriptor(returnType, argumentTypes), + isInterface); + return this; + } + + public MagicScriptCompiler ldc(Object value) { + self().visitLdcInsn(value); + return this; + } + + public MagicScriptCompiler insn(int opcode) { + self().visitInsn(opcode); + return this; + } + + public void intInsn(int opcode, int operand) { + self().visitIntInsn(opcode, operand); + } + + /** 编译数组 */ + public MagicScriptCompiler newArray(List values) { + int size = values.size(); + visitInt(size).typeInsn(ANEWARRAY, Object.class); + for (int i = 0; i < size; i++) { + insn(DUP).visitInt(i).visit(values.get(i)).insn(AASTORE); + } + return this; + } + + /** 编译int值 */ + public MagicScriptCompiler visitInt(int value) { + if (value >= -1 && value <= 5) { + insn(ICONST[value + 1]); + } else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) { + intInsn(BIPUSH, value); + } else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) { + intInsn(SIPUSH, value); + } else { + ldc(value); + } + return this; + } + + private void initContext() { + if (!this.contextInited) { + this.load0().load1(); + self() + .visitFieldInsn( + PUTFIELD, getClassName(), "context", Type.getDescriptor(MagicScriptContext.class)); + this.contextInited = true; + // var2 = context.createVariables(this, varIndices.size()) + this.load1() + .load0() + .visitInt(varIndices.size()) + .invoke( + INVOKEVIRTUAL, + MagicScriptContext.class, + "createVariables", + Variables.class, + MagicScriptRuntime.class, + int.class) + .store(2); + } + } + + public void loadVars() { + // 对于未定义变量值的,从环境中获取 + // var2[xx] = super.getEnvironmentValue(varIndex.getName()) + this.varIndices.stream() + .filter(VarIndex::isReference) + .forEach( + varIndex -> + this.load2() + .visitInt(varIndex.getIndex()) + .load1() + .ldc(varIndex.getName()) + .invoke( + INVOKEVIRTUAL, + MagicScriptContext.class, + "getEnvironmentValue", + Object.class, + String.class) + .store()); + } + + public String visitMethod(String methodName, Runnable callback) { + return visitMethod(methodName, Collections.emptyList(), Collections.emptyList(), callback); + } + + public String visitMethod( + String methodName, List childNodes, List parameters, Runnable callback) { + childNodes.forEach(it -> it.visitMethod(this)); + int index = this.getFunctionIndex(); + methodName = methodName + "_" + index; + this.createMethod( + ACC_PRIVATE, + methodName, + Descriptor.make_descriptor(Object.class, Variables.class, Object[].class)) + .load1() // Variables + .load2() // 传入的参数 + .visitInt(index) + // 构建参数 + .visitInt(parameters.size()) + .intInsn(NEWARRAY, T_INT); // new int[parameters.size()] + for (int i = 0; i < parameters.size(); i++) { + this.insn(DUP).visitInt(i).visitInt(parameters.get(i).getIndex()).insn(IASTORE); + } + // 复制变量 + this.invoke( + INVOKEVIRTUAL, + Variables.class, + "copy", + Variables.class, + Object[].class, + int.class, + int[].class) + .store(2); + callback.run(); + this.pop(); + return methodName; + } + + public String visitMethod(String methodName, List childNodes, List parameters) { + return visitMethod(methodName, childNodes, parameters, () -> this.compile(childNodes)); + } + + public List finallyBlock() { + if (finallyStack.isEmpty()) { + return null; + } + return finallyStack.peek(); + } + + public void putHeadFinallyBlock(List finallyBlock) { + finallyStack.insertElementAt(finallyBlock, 0); + } + + public void putFinallyBlock(List finallyBlock) { + finallyStack.push(finallyBlock); + } + + public List getFinallyBlock() { + if (finallyStack.isEmpty()) { + return null; + } + return finallyStack.pop(); + } + + public MagicScriptCompiler pop() { + getFinallyBlock(); + MethodVisitor visitor = methodVisitors.pop(); + visitor.visitInsn(ACONST_NULL); + visitor.visitInsn(ARETURN); + visitor.visitMaxs(0, 0); + visitor.visitEnd(); + vars.pop(); + labelStack.pop(); + return this; + } + + public byte[] bytecode() { + pop(); + classWriter.visitEnd(); + return classWriter.toByteArray(); + } + + /** 获取类名 */ + public String getClassName() { + return "MagicScript_" + id; + } + + private MethodVisitor self() { + return methodVisitors.peek(); + } + + private static String getJvmType(Class target) { + return target == null ? null : target.getName().replace(".", "/"); + } + + private static Handle makeHandle(Class target) { + return new Handle( + H_INVOKESTATIC, + getJvmType(target), + "bootstrap", + Descriptor.make_descriptor( + CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, int.class), + false); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/constant/SafeRule.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/constant/SafeRule.java new file mode 100644 index 0000000..471d1bd --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/constant/SafeRule.java @@ -0,0 +1,45 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.constant; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @Author Aleo + * + * @version 1.0 + * @since 2023/5/19 + */ + +/** + * @version 1.0 @Author Aleo + * @since 2023/5/19 + */ +public class SafeRule { + + public static Set packages = + Stream.of( + "cn.hutool.json.JSONUtil", + "cn.hutool.service.util.HexUtil", + "cn.hutool.service.util.RandomUtil", + "cn.hutool.service.util.StrUtil") + .collect(Collectors.toSet()); + + public static boolean filter(String packageName) { + return packageName != null + && !"".equalsIgnoreCase(packageName) + && !packages.contains(packageName); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/BooleanImplicitConvert.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/BooleanImplicitConvert.java new file mode 100644 index 0000000..a0a71cd --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/BooleanImplicitConvert.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.convert; + +import cn.universal.core.engine.parsing.ast.literal.BooleanLiteral; +import cn.universal.core.engine.runtime.Variables; + +/** 任意值到boolean类型的隐式转换 */ +public class BooleanImplicitConvert implements ClassImplicitConvert { + + @Override + public boolean support(Class from, Class to) { + return to == Boolean.class || to == boolean.class; + } + + @Override + public Object convert(Variables variables, Object source, Class target) { + return BooleanLiteral.isTrue(source); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/ClassImplicitConvert.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/ClassImplicitConvert.java new file mode 100644 index 0000000..639b08c --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/ClassImplicitConvert.java @@ -0,0 +1,29 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.convert; + +import cn.universal.core.engine.runtime.Variables; + +public interface ClassImplicitConvert { + + /** 转换顺序 */ + default int sort() { + return Integer.MAX_VALUE; + } + + /** 是否支持隐式自动转换 */ + boolean support(Class from, Class to); + + /** 转换 */ + Object convert(Variables variables, Object source, Class target); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/CollectionImplicitConvert.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/CollectionImplicitConvert.java new file mode 100644 index 0000000..8dcd98b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/CollectionImplicitConvert.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.convert; + +import cn.universal.core.engine.functions.StreamExtension; +import cn.universal.core.engine.reflection.JavaReflection; +import cn.universal.core.engine.runtime.Variables; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; + +/** 集合、数组隐式转换 */ +public class CollectionImplicitConvert implements ClassImplicitConvert { + + private Class fromClazz; + + private Class toClazz; + + @Override + public boolean support(Class from, Class to) { + if ((from.isArray() + || Collection.class.isAssignableFrom(from) + || Iterator.class.isAssignableFrom(from) + || Enumeration.class.isAssignableFrom(from)) + && (to.isArray() + || Collection.class.isAssignableFrom(to) + || Iterator.class.isAssignableFrom(to) + || Enumeration.class.isAssignableFrom(to))) { + return (fromClazz = getGenericType(from)) != null + && !JavaReflection.isPrimitiveAssignableFrom(fromClazz, fromClazz) + && (toClazz = getGenericType(to)) != null + && !JavaReflection.isPrimitiveAssignableFrom(toClazz, toClazz) + && toClazz.isAssignableFrom(fromClazz); + } + return false; + } + + private Class getGenericType(Class target) { + Type type = target.getGenericSuperclass(); + if (type instanceof ParameterizedType) { + type = ((ParameterizedType) type).getActualTypeArguments()[0]; + if (type instanceof Class) { + return (Class) type; + } + } + return null; + } + + @Override + public Object convert(Variables variables, Object source, Class target) { + return StreamExtension.asBean(source, target, target.isArray()); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/FunctionalImplicitConvert.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/FunctionalImplicitConvert.java new file mode 100644 index 0000000..e9c4cd3 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/FunctionalImplicitConvert.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.convert; + +import cn.universal.core.engine.runtime.Variables; +import cn.universal.core.engine.runtime.function.MagicScriptLambdaFunction; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.function.Function; + +/** 脚本内部lambda到Java函数式的转换 */ +public class FunctionalImplicitConvert implements ClassImplicitConvert { + + private final ClassLoader classLoader = FunctionalImplicitConvert.class.getClassLoader(); + + @Override + public boolean support(Class from, Class to) { + return MagicScriptLambdaFunction.class.isAssignableFrom(from) + && to.getAnnotation(FunctionalInterface.class) != null; + } + + @Override + public Object convert(Variables variables, Object source, Class target) { + MagicScriptLambdaFunction function = (MagicScriptLambdaFunction) source; + if (target == Function.class) { + return (Function) + args -> { + Object[] param; + if (args == null) { + param = new Object[0]; + } else { + Class aClass = args.getClass(); + if (aClass.isArray() && aClass.getComponentType() == Object.class) { + param = (Object[]) args; + } else { + param = new Object[] {args}; + } + } + return function.apply(variables, param); + }; + } + return Proxy.newProxyInstance( + classLoader, + new Class[] {target}, + (proxy, method, args) -> { + if (Modifier.isAbstract(method.getModifiers())) { + return function.apply(variables, args); + } + if ("toString".equalsIgnoreCase(method.getName())) { + return "Proxy(" + source + "," + target + ")"; + } + return null; + }); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/MapImplicitConvert.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/MapImplicitConvert.java new file mode 100644 index 0000000..49bf984 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/convert/MapImplicitConvert.java @@ -0,0 +1,41 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.convert; + +import cn.universal.core.engine.functions.MapExtension; +import cn.universal.core.engine.reflection.JavaReflection; +import cn.universal.core.engine.runtime.Variables; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Map; + +/** Map到Bean的隐式转换 */ +public class MapImplicitConvert implements ClassImplicitConvert { + + @Override + public boolean support(Class from, Class to) { + return Map.class.isAssignableFrom(from) + && !JavaReflection.isPrimitiveAssignableFrom(to, to) + && !to.isArray() + && !Collection.class.isAssignableFrom(to) + && !Iterator.class.isAssignableFrom(to) + && !Enumeration.class.isAssignableFrom(to) + && !to.isInterface(); + } + + @Override + public Object convert(Variables variables, Object source, Class target) { + return MapExtension.asBean((Map) source, target); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/DebugTimeoutException.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/DebugTimeoutException.java new file mode 100644 index 0000000..edf2e7b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/DebugTimeoutException.java @@ -0,0 +1,24 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.exception; + +public class DebugTimeoutException extends RuntimeException { + + public DebugTimeoutException() { + super("debug超时"); + } + + public DebugTimeoutException(Throwable cause) { + super(cause); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/ExceptionUtils.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/ExceptionUtils.java new file mode 100644 index 0000000..3fc674b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/ExceptionUtils.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.exception; + +public class ExceptionUtils { + + public static int indexOfThrowable(Throwable root, Class clazz) { + if (root == null) { + return -1; + } + int index = 0; + do { + if (clazz.isAssignableFrom(root.getClass())) { + return index; + } + index++; + } while ((root = root.getCause()) != null); + return -1; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/MagicExitException.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/MagicExitException.java new file mode 100644 index 0000000..3824b38 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/MagicExitException.java @@ -0,0 +1,28 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.exception; + +import cn.universal.core.engine.runtime.ExitValue; + +public class MagicExitException extends RuntimeException { + + private final ExitValue exitValue; + + public MagicExitException(ExitValue exitValue) { + this.exitValue = exitValue; + } + + public ExitValue getExitValue() { + return exitValue; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/MagicScriptAssertException.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/MagicScriptAssertException.java new file mode 100644 index 0000000..46bdb6b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/MagicScriptAssertException.java @@ -0,0 +1,34 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.exception; + +public class MagicScriptAssertException extends RuntimeException { + + private final int code; + + private final String message; + + public MagicScriptAssertException(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/MagicScriptException.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/MagicScriptException.java new file mode 100644 index 0000000..7944d70 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/MagicScriptException.java @@ -0,0 +1,55 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.exception; + +import cn.universal.core.engine.parsing.Span; + +public class MagicScriptException extends RuntimeException { + + private static final long serialVersionUID = 1L; + private final String simpleMessage; + private final Span location; + + public MagicScriptException(String errorMessage, String simpleMessage, Span location) { + super(errorMessage); + this.simpleMessage = simpleMessage; + this.location = location; + } + + public MagicScriptException(String errorMessage, Span location) { + this(errorMessage, errorMessage, location); + } + + public MagicScriptException(String errorMessage) { + this(errorMessage, errorMessage, null); + } + + public MagicScriptException( + String message, String simpleMessage, Throwable cause, Span location) { + super(message, cause); + this.simpleMessage = simpleMessage; + this.location = location; + } + + public String getSimpleMessage() { + return simpleMessage; + } + + public Span getLocation() { + return location; + } + + public Span.Line getLine() { + return location == null ? null : location.getLine(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/MagicScriptRuntimeException.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/MagicScriptRuntimeException.java new file mode 100644 index 0000000..1824e46 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/MagicScriptRuntimeException.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.exception; + +public class MagicScriptRuntimeException extends RuntimeException { + + public MagicScriptRuntimeException() {} + + public MagicScriptRuntimeException(String message) { + super(message); + } + + public MagicScriptRuntimeException(Throwable cause) { + super(cause); + } + + public static MagicScriptRuntimeException create(Object target) { + if (target instanceof Throwable) { + return new MagicScriptRuntimeException((Throwable) target); + } else if (target instanceof String) { + return new MagicScriptRuntimeException(target.toString()); + } + return new MagicScriptRuntimeException(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/RegexpLiteralException.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/RegexpLiteralException.java new file mode 100644 index 0000000..17089a9 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/RegexpLiteralException.java @@ -0,0 +1,15 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.exception; + +public class RegexpLiteralException extends RuntimeException {} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/ResourceNotFoundException.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..86c1161 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/ResourceNotFoundException.java @@ -0,0 +1,20 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.exception; + +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(String module) { + super(module); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/StringLiteralException.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/StringLiteralException.java new file mode 100644 index 0000000..b70213a --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/exception/StringLiteralException.java @@ -0,0 +1,18 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.exception; + +public class StringLiteralException extends RuntimeException { + + private static final long serialVersionUID = 1L; +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ArrayFunctions.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ArrayFunctions.java new file mode 100644 index 0000000..ae8db43 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ArrayFunctions.java @@ -0,0 +1,136 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import cn.universal.core.engine.annotation.Function; +import java.lang.reflect.Array; + +public class ArrayFunctions { + + @Function + @Comment("创建`int`数组") + public int[] new_int_array(@Comment(name = "size", value = "数组大小") int size) { + return new int[size]; + } + + @Function + @Comment("创建`short`数组") + public short[] new_short_array(@Comment(name = "size", value = "数组大小") int size) { + return new short[size]; + } + + @Function + @Comment("创建`double`数组") + public double[] new_double_array(@Comment(name = "size", value = "数组大小") int size) { + return new double[size]; + } + + @Function + @Comment("创建`float`数组") + public float[] new_float_array(@Comment(name = "size", value = "数组大小") int size) { + return new float[size]; + } + + @Function + @Comment("创建`byte`数组") + public byte[] new_byte_array(@Comment(name = "size", value = "数组大小") int size) { + return new byte[size]; + } + + @Function + @Comment("创建`char`数组") + public char[] new_char_array(@Comment(name = "size", value = "数组大小") int size) { + return new char[size]; + } + + @Function + @Comment("创建`boolean`数组") + public boolean[] new_boolean_array(@Comment(name = "size", value = "数组大小") int size) { + return new boolean[size]; + } + + @Function + @Comment("创建`long`数组") + public long[] new_long_array(@Comment(name = "size", value = "数组大小") int size) { + return new long[size]; + } + + @Function + @Comment("创建`Object`数组") + public Object[] new_array(@Comment(name = "size", value = "数组大小") int size) { + return new Object[size]; + } + + @Function + @Comment("创建`Object`数组") + public T[] new_array( + @Comment(name = "componentType", value = "数组类型") Class componentType, + @Comment(name = "size", value = "数组大小") int size) { + return (T[]) Array.newInstance(componentType, size); + } + + @Function + @Comment("创建`String`数组") + public String[] new_array(@Comment(name = "values", value = "字符串") String... array) { + return array; + } + + @Function + @Comment("创建`int`数组") + public int[] new_array(@Comment(name = "size", value = "数组大小") int... array) { + return array; + } + + @Function + @Comment("创建`short`数组") + public short[] new_array(@Comment(name = "size", value = "数组大小") short... array) { + return array; + } + + @Function + @Comment("创建`double`数组") + public double[] new_array(@Comment(name = "values", value = "double数值") double... array) { + return array; + } + + @Function + @Comment("创建`float`数组") + public float[] new_array(@Comment(name = "values", value = "float数值") float... array) { + return array; + } + + @Function + @Comment("创建`char`数组") + public char[] new_array(@Comment(name = "values", value = "char值") char... array) { + return array; + } + + @Function + @Comment("创建`byte`数组") + public byte[] new_array(@Comment(name = "values", value = "byte值") byte... array) { + return array; + } + + @Function + @Comment("创建`boolean`数组") + public boolean[] new_array(@Comment(name = "values", value = "boolean值") boolean... array) { + return array; + } + + @Function + @Comment("创建`long`数组") + public long[] new_array(@Comment(name = "values", value = "long值") long... array) { + return array; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ClassExtension.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ClassExtension.java new file mode 100644 index 0000000..64967e9 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ClassExtension.java @@ -0,0 +1,92 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import cn.universal.core.engine.reflection.JavaInvoker; +import cn.universal.core.engine.reflection.JavaReflection; +import cn.universal.core.engine.runtime.RuntimeContext; +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.List; + +public class ClassExtension { + + public static Object newInstance(Class clazz) + throws IllegalAccessException, InstantiationException { + return clazz.newInstance(); + } + + public static Object newInstance( + Class clazz, + RuntimeContext runtimeContext, + @Comment(name = "values", value = "构造参数") Object... values) + throws Throwable { + if (values == null || values.length == 0) { + return newInstance(clazz); + } + Class[] parametersTypes = new Class[values.length]; + for (int i = 0; i < values.length; i++) { + Object value = values[i]; + parametersTypes[i] = value == null ? JavaReflection.Null.class : value.getClass(); + } + List> constructors = Arrays.asList(clazz.getConstructors()); + JavaInvoker invoker = + JavaReflection.findConstructorInvoker(constructors, parametersTypes); + if (invoker == null) { + throw new RuntimeException( + String.format( + "can not found constructor for [%s] with types: [%s]", + clazz, Arrays.toString(parametersTypes))); + } + return invoker.invoke0(null, runtimeContext, values); + } + + public static Object newInstance( + Object target, + RuntimeContext runtimeContext, + @Comment(name = "values", value = "构造参数") Object... values) + throws Throwable { + if (target == null) { + throw new NullPointerException("NULL不能被new"); + } + if (target instanceof Class) { + return newInstance((Class) target, runtimeContext, values); + } + return newInstance(target.getClass(), runtimeContext, values); + } + + /** + * @since 1.6.2 + */ + @Comment("获取Java类全名") + public static String getName(Class clazz) { + return clazz.getName(); + } + + /** + * @since 1.6.2 + */ + @Comment("获取Java类名") + public static String getSimpleName(Class clazz) { + return clazz.getSimpleName(); + } + + /** + * @since 1.6.2 + */ + @Comment("获取Java类规范全名") + public static String getCanonicalName(Class clazz) { + return clazz.getCanonicalName(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/CollectionFunctions.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/CollectionFunctions.java new file mode 100644 index 0000000..69164a5 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/CollectionFunctions.java @@ -0,0 +1,41 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import cn.universal.core.engine.annotation.Function; +import java.util.Iterator; + +/** 集合相关函数 */ +public class CollectionFunctions { + + @Function + @Comment("区间迭代器") + public Iterator range( + @Comment(name = "from", value = "起始编号") int from, + @Comment(name = "to", value = "结束编号") int to) { + return new Iterator() { + int idx = from; + + @Override + public boolean hasNext() { + return idx <= to; + } + + @Override + public Integer next() { + return idx++; + } + }; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/DateExtension.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/DateExtension.java new file mode 100644 index 0000000..5d961ce --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/DateExtension.java @@ -0,0 +1,33 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +/** Date扩展 */ +public class DateExtension { + + private static final ZoneId SYSTEM_ZONE_ID = ZoneId.systemDefault(); + + @Comment("日期格式化") + public static String format( + Date source, @Comment(name = "pattern", value = "格式,如yyyy-MM-dd") String pattern) { + return Instant.ofEpochMilli(source.getTime()) + .atZone(SYSTEM_ZONE_ID) + .format(DateTimeFormatter.ofPattern(pattern)); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/DynamicAttribute.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/DynamicAttribute.java new file mode 100644 index 0000000..2df5082 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/DynamicAttribute.java @@ -0,0 +1,27 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.exception.MagicScriptRuntimeException; +import java.beans.Transient; + +public interface DynamicAttribute { + + @Transient + T getDynamicAttribute(String key); + + @Transient + default R setDynamicAttribute(String key, T value) { + throw new MagicScriptRuntimeException("不支持此赋值操作"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/DynamicMethod.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/DynamicMethod.java new file mode 100644 index 0000000..a9d3130 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/DynamicMethod.java @@ -0,0 +1,20 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import java.util.List; + +public interface DynamicMethod { + + Object execute(String methodName, List parameters); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/DynamicModuleImport.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/DynamicModuleImport.java new file mode 100644 index 0000000..e1f1285 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/DynamicModuleImport.java @@ -0,0 +1,36 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.MagicScriptContext; +import java.util.function.Function; + +public class DynamicModuleImport { + + private final Class targetClass; + + private final Function finder; + + public DynamicModuleImport(Class targetClass, Function finder) { + this.targetClass = targetClass; + this.finder = finder; + } + + public Object getDynamicModule(MagicScriptContext context) { + return finder.apply(context); + } + + public Class getTargetClass() { + return targetClass; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ExtensionMethod.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ExtensionMethod.java new file mode 100644 index 0000000..622399a --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ExtensionMethod.java @@ -0,0 +1,25 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import java.util.Collections; +import java.util.List; + +public interface ExtensionMethod { + + default List> supports() { + return Collections.singletonList(support()); + } + + Class support(); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/IotRedisUtil.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/IotRedisUtil.java new file mode 100644 index 0000000..cceed27 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/IotRedisUtil.java @@ -0,0 +1,303 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.geo.Point; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class IotRedisUtil { + + private final StringRedisTemplate redisTemplate; + private static final String DEVICE_PREFIX = "univiot:device:"; + + public IotRedisUtil(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + // ==================== 基础数据结构操作(带过期时间)==================== + @Comment("设置字符串值(带过期时间)") + public void setString(String key, String value, long timeout, String unit) { + try { + redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.valueOf(unit)); + } catch (RedisConnectionFailureException e) { + log.error("Redis连接失败 - Key: {}, Error: {}", key, e.getMessage()); + } catch (RedisSystemException e) { + log.error("Redis系统异常 - Key: {}, Error: {}", key, e.getCause().getMessage()); + } catch (Exception e) { + log.error("设置字符串值异常 - Key: {}, Error: {}", key, e.getMessage()); + } + } + + @Comment("设置Hash字段值(带过期时间)") + public void setHashField(String key, String field, String value, long timeout, String unit) { + try { + redisTemplate.opsForHash().put(key, field, value); + redisTemplate.expire(key, timeout, TimeUnit.valueOf(unit)); + } catch (RedisConnectionFailureException e) { + log.error("Redis连接失败 - Key: {}, Field: {}", key, field); + } catch (Exception e) { + log.error("设置Hash字段异常 - Key: {}, Field: {}, Error: {}", key, field, e.getMessage()); + } + } + + @Comment("从左侧插入List(带过期时间)") + public Long leftPush(String key, String value, long timeout, String unit) { + try { + Long result = redisTemplate.opsForList().leftPush(key, value); + redisTemplate.expire(key, timeout, TimeUnit.valueOf(unit)); + return result; + } catch (RedisConnectionFailureException e) { + log.error("Redis连接失败 - Key: {}", key); + return 0L; + } catch (Exception e) { + log.error("List左插入异常 - Key: {}, Error: {}", key, e.getMessage()); + return 0L; + } + } + + @Comment("添加Set成员(带过期时间)") + public Long addToSet(String key, String value, long timeout, String unit) { + try { + Long result = redisTemplate.opsForSet().add(key, value); + redisTemplate.expire(key, timeout, TimeUnit.valueOf(unit)); + return result; + } catch (RedisConnectionFailureException e) { + log.error("Redis连接失败 - Key: {}", key); + return 0L; + } catch (Exception e) { + log.error("Set添加成员异常 - Key: {}, Error: {}", key, e.getMessage()); + return 0L; + } + } + + @Comment("添加ZSet成员(带过期时间)") + public Boolean addToZSet(String key, String value, double score, long timeout, String unit) { + try { + Boolean result = redisTemplate.opsForZSet().add(key, value, score); + redisTemplate.expire(key, timeout, TimeUnit.valueOf(unit)); + return result; + } catch (RedisConnectionFailureException e) { + log.error("Redis连接失败 - Key: {}", key); + return false; + } catch (Exception e) { + log.error("ZSet添加成员异常 - Key: {}, Error: {}", key, e.getMessage()); + return false; + } + } + + // ==================== 物联网场景专用方法 ==================== + @Comment("更新设备状态(自动续期)") + public void updateDeviceStatus( + String deviceId, String field, String value, long timeout, String unit) { + String key = DEVICE_PREFIX + "status:" + deviceId; + try { + redisTemplate.opsForHash().put(key, field, value); + redisTemplate.expire(key, timeout, TimeUnit.valueOf(unit)); + } catch (RedisConnectionFailureException e) { + log.error("设备状态更新失败 - Device: {}, Field: {}", deviceId, field); + } catch (Exception e) { + log.error("更新设备状态异常 - Device: {}, Field: {}, Error: {}", deviceId, field, e.getMessage()); + } + } + + @Comment("获取设备状态") + public Map getDeviceStatus(String deviceId) { + String key = DEVICE_PREFIX + "status:" + deviceId; + try { + Map rawMap = redisTemplate.opsForHash().entries(key); + return convertToStringMap(rawMap); + } catch (RedisConnectionFailureException e) { + log.error("获取设备状态失败 - Device: {}", deviceId); + return Collections.emptyMap(); + } catch (Exception e) { + log.error("获取设备状态异常 - Device: {}, Error: {}", deviceId, e.getMessage()); + return Collections.emptyMap(); + } + } + + @Comment("推送设备指令到队列(带过期时间)") + public void pushDeviceCommand( + String deviceId, String commandType, String params, long timeout, String unit) { + String key = DEVICE_PREFIX + "cmd:" + deviceId; + try { + String command = + String.format( + "{\"type\":\"%s\",\"params\":%s,\"timestamp\":%d}", + commandType, params, System.currentTimeMillis()); + + // 使用管道操作确保原子性 + redisTemplate.executePipelined( + (RedisCallback) + connection -> { + connection.rPush(key.getBytes(), command.getBytes()); + connection.expire(key.getBytes(), TimeUnit.valueOf(unit).toSeconds(timeout)); + return null; + }); + } catch (RedisConnectionFailureException e) { + log.error("指令推送失败 - Device: {}, Command: {}", deviceId, commandType); + } catch (Exception e) { + log.error( + "指令推送异常 - Device: {}, Command: {}, Error: {}", deviceId, commandType, e.getMessage()); + } + } + + @Comment("获取并移除下一个指令") + public String popNextCommand(String deviceId) { + String key = DEVICE_PREFIX + "cmd:" + deviceId; + try { + return redisTemplate.opsForList().leftPop(key); + } catch (RedisConnectionFailureException e) { + log.error("获取指令失败 - Device: {}", deviceId); + return null; + } catch (Exception e) { + log.error("获取指令异常 - Device: {}, Error: {}", deviceId, e.getMessage()); + return null; + } + } + + @Comment("记录设备位置") + public void recordDeviceLocation(String deviceId, double lng, double lat) { + try { + redisTemplate + .opsForGeo() + .add( + DEVICE_PREFIX + "geo:devices", + new RedisGeoCommands.GeoLocation<>(deviceId, new Point(lng, lat))); + } catch (RedisConnectionFailureException e) { + log.error("位置记录失败 - Device: {}", deviceId); + } catch (Exception e) { + log.error("位置记录异常 - Device: {}, Error: {}", deviceId, e.getMessage()); + } + } + + @Comment("记录设备事件") + public void recordDeviceEvent(String deviceId, String eventType, Map eventData) { + String key = DEVICE_PREFIX + "events:" + deviceId; + try { + Map fields = new HashMap<>(); + fields.put("eventType", eventType); + fields.put("timestamp", String.valueOf(System.currentTimeMillis())); + fields.putAll(eventData); + redisTemplate.opsForStream().add(key, fields); + } catch (RedisConnectionFailureException e) { + log.error("事件记录失败 - Device: {}, Event: {}", deviceId, eventType); + logToLocalStorage(eventData); // 降级到本地存储 + } catch (Exception e) { + log.error("事件记录异常 - Device: {}, Event: {}, Error: {}", deviceId, eventType, e.getMessage()); + logToLocalStorage(eventData); // 降级到本地存储 + } + } + + @Comment("缓存设备数据并记录历史") + public void cacheDeviceData( + String deviceId, String dataType, String value, long timeout, String unit) { + String currentKey = DEVICE_PREFIX + "data:current:" + deviceId + ":" + dataType; + try { + // 获取上一次值 + String lastValue = redisTemplate.opsForValue().get(currentKey); + + // 存储历史记录 + if (lastValue != null) { + String historyKey = DEVICE_PREFIX + "data:history:" + deviceId + ":" + dataType; + redisTemplate.executePipelined( + (RedisCallback) + connection -> { + connection.zAdd( + historyKey.getBytes(), System.currentTimeMillis(), lastValue.getBytes()); + connection.zRemRange(historyKey.getBytes(), 0, -101); // 保留最近100条 + return null; + }); + } + + // 更新当前值 + redisTemplate.opsForValue().set(currentKey, value, timeout, TimeUnit.valueOf(unit)); + } catch (RedisConnectionFailureException e) { + log.error("数据缓存失败 - Device: {}, Type: {}", deviceId, dataType); + } catch (Exception e) { + log.error("数据缓存异常 - Device: {}, Type: {}, Error: {}", deviceId, dataType, e.getMessage()); + } + } + + // ==================== 分布式锁(设备操作互斥)==================== + private static final String LOCK_SCRIPT = + "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " + + " redis.call('pexpire', KEYS[1], ARGV[2]) " + + " return 1 " + + "else return 0 end"; + + private static final String UNLOCK_SCRIPT = + "if redis.call('get', KEYS[1]) == ARGV[1] then " + + " return redis.call('del', KEYS[1]) " + + "else return 0 end"; + + @Comment("获取设备操作锁") + public boolean acquireDeviceLock(String deviceId, String lockId, long expireMs) { + String lockKey = DEVICE_PREFIX + "lock:" + deviceId; + try { + RedisScript script = new DefaultRedisScript<>(LOCK_SCRIPT, Boolean.class); + return Boolean.TRUE.equals( + redisTemplate.execute( + script, Collections.singletonList(lockKey), lockId, String.valueOf(expireMs))); + } catch (RedisConnectionFailureException e) { + log.error("获取锁失败 - Device: {}, LockID: {}", deviceId, lockId); + return false; + } catch (Exception e) { + log.error("获取锁异常 - Device: {}, LockID: {}, Error: {}", deviceId, lockId, e.getMessage()); + return false; + } + } + + @Comment("释放设备锁") + public boolean releaseDeviceLock(String deviceId, String lockId) { + String lockKey = DEVICE_PREFIX + "lock:" + deviceId; + try { + RedisScript script = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class); + Long result = redisTemplate.execute(script, Collections.singletonList(lockKey), lockId); + return result != null && result == 1; + } catch (RedisConnectionFailureException e) { + log.error("释放锁失败 - Device: {}, LockID: {}", deviceId, lockId); + return false; + } catch (Exception e) { + log.error("释放锁异常 - Device: {}, LockID: {}, Error: {}", deviceId, lockId, e.getMessage()); + return false; + } + } + + // ==================== 辅助方法 ==================== + private Map convertToStringMap(Map rawMap) { + Map result = new HashMap<>(); + rawMap.forEach((k, v) -> result.put(k.toString(), v.toString())); + return result; + } + + private void logToLocalStorage(Map eventData) { + // 实现本地临时存储逻辑 + log.warn("事件降级到本地存储: {}", eventData); + // 实际项目中可写入本地文件或数据库 + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/MagicScriptFunctions.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/MagicScriptFunctions.java new file mode 100644 index 0000000..1663aa6 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/MagicScriptFunctions.java @@ -0,0 +1,59 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import cn.universal.core.engine.annotation.Function; +import java.util.UUID; + +/** 脚本能力扩展 */ +public class MagicScriptFunctions { + + @Comment("生成uuid字符串,不包含`-`") + @Function + public String uuid() { + return UUID.randomUUID().toString().replace("-", ""); + } + + @Comment("判断对象是否不是`NULL`") + @Function + public boolean not_null(@Comment(name = "value", value = "目标对象") Object value) { + return value != null; + } + + @Comment("判断对象是否是`NULL`") + @Function + public boolean is_null(@Comment(name = "value", value = "目标对象") Object value) { + return value == null; + } + + @Comment("打印") + @Function + public void print(@Comment(name = "value", value = "目标对象") Object target) { + System.out.print(target); + } + + @Comment("换行打印") + @Function + public void println(@Comment(name = "value", value = "目标对象") Object target) { + System.out.println(target); + } + + @Comment("格式化打印") + @Function + public void printf( + @Comment(name = "format", value = "打印格式") String format, + @Comment(name = "args", value = "打印参数") Object... args) { + System.out.printf(format, args); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/MapExtension.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/MapExtension.java new file mode 100644 index 0000000..4dad209 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/MapExtension.java @@ -0,0 +1,221 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import cn.universal.core.engine.parsing.ast.binary.LessOperation; +import cn.universal.core.engine.reflection.JavaReflection; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +public class MapExtension { + + @Comment("Map类型对象转JavaBean") + public static Object asBean( + Map source, @Comment(name = "target", value = "目标Class") Class target) { + Object result = null; + try { + result = target.newInstance(); + Set> entries = source.entrySet(); + for (Map.Entry entry : entries) { + Object value = entry.getValue(); + String member = Objects.toString(entry.getKey(), null); + if (value != null && member != null) { + Field field = JavaReflection.getField(target, member); + setFieldValue(result, field, value); + } + } + } catch (InstantiationException | IllegalAccessException ignored) { + } + return result; + } + + @Comment("map转List") + public static List asList( + Map source, + @Comment(name = "mapping", value = "映射函数,如:(key,value,source)=>{'k' : key,'v' : value}") + Function mapping) { + List result = new ArrayList<>(); + source.forEach((key, value) -> result.add(mapping.apply(new Object[] {key, value, source}))); + return result; + } + + private static void setFieldValue(Object object, Field field, Object value) { + if (field != null) { + try { + if (ObjectTypeConditionExtension.isCollection(field.getType())) { + Type genericType = field.getGenericType(); + if (genericType instanceof ParameterizedType) { + Class type = + (Class) ((ParameterizedType) genericType).getActualTypeArguments()[0]; + JavaReflection.setFieldValue(object, field, StreamExtension.asBean(value, type)); + } + } else if (field.getType().isArray()) { + JavaReflection.setFieldValue( + object, + field, + StreamExtension.asBean(value, field.getType().getComponentType(), true)); + } else if (JavaReflection.isPrimitiveAssignableFrom(value.getClass(), field.getType()) + || field.getType().isAssignableFrom(value.getClass())) { + JavaReflection.setFieldValue(object, field, value); + } + } catch (Exception ignored) { + } + } + } + + @Comment(value = "循环Map", origin = true) + public Map each( + Map source, + @Comment(name = "function", value = "循环函数,如:(key,value,source)=>map['xx'] = key;") + Function function) { + source.forEach((key, value) -> function.apply(new Object[] {key, value, source})); + return source; + } + + @Comment("合并Map") + public Map merge( + Map source, + @Comment(name = "key", value = "key") Object key, + @Comment(name = "value", value = "value") Object value) { + source.put(key, value); + return source; + } + + @Comment("合并Map") + public Map merge( + Map source, @Comment("另一个map或多个Map") Map... targets) { + if (targets != null) { + for (int i = 0, len = targets.length; i < len; i++) { + source.putAll(targets[i]); + } + } + return source; + } + + @Comment("将Map转为String") + public String asString( + Map source, + @Comment(name = "separator", value = "key与key之间的连接符如&") String separator, + @Comment(name = "join", value = "key与value之间的连接符,如=") String join) { + Set> entries = source.entrySet(); + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : entries) { + builder.append(entry.getKey()); + builder.append(join); + builder.append(Objects.toString(entry.getValue(), "")); + builder.append(separator); + } + if (entries.size() > 0) { + return builder.substring(0, builder.length() - separator.length()); + } + return builder.toString(); + } + + @Comment("将Map转为String") + public String asString( + Map source, + @Comment(name = "separator", value = "key与value之间的连接符,如=") String separator, + @Comment(name = "mapping", value = "转换方法,如:(key,value)=>key + '=' + value || ''") + Function mapping) { + Set> entries = source.entrySet(); + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : entries) { + builder.append( + Objects.toString(mapping.apply(new Object[] {entry.getKey(), entry.getValue()}), "")); + builder.append(separator); + } + if (entries.size() > 0) { + return builder.substring(0, builder.length() - separator.length()); + } + return builder.toString(); + } + + @Comment("对Map进行排序") + public Map sort(Map source) { + Set keys = source.keySet(); + Map sortedMap = new LinkedHashMap<>(); + keys.stream() + .sorted( + (Comparator) + (o1, o2) -> { + if (Objects.equals(o1, o2)) { + return 0; + } + int value = LessOperation.compare(o1, o2); + return value == -2 ? 0 : value; + }) + .forEach( + key -> { + sortedMap.put(key, source.get(key)); + }); + return sortedMap; + } + + @Comment("对Map进行排序") + public Map sort( + Map source, + @Comment(name = "comparator", value = "比较器,如:(k1,k2,v1,v2)=>k1.compareTo(k2);") + Function comparator) { + Set keys = source.keySet(); + Map sortedMap = new LinkedHashMap<>(); + keys.stream() + .sorted( + (Comparator) + (o1, o2) -> + ObjectConvertExtension.asInt( + comparator.apply(new Object[] {o1, o2, source.get(o1), source.get(o2)}), 0)) + .forEach( + key -> { + sortedMap.put(key, source.get(key)); + }); + return sortedMap; + } + + @Comment("对Map的key进行替换") + public Map replaceKey( + Map source, + @Comment(name = "target", value = "查找字符串") String target, + @Comment(name = "replacement", value = "替换字符串") String replacement) { + return replaceKey(source, src -> src[0].toString().replace(target, replacement)); + } + + @Comment("对Map的key进行正则替换") + public Map replaceAllKey( + Map source, + @Comment(name = "pattern", value = "正则表达式") String regx, + @Comment(name = "replacement", value = "替换字符串") String replacement) { + return replaceKey(source, src -> src[0].toString().replaceAll(regx, replacement)); + } + + @Comment("对Map的key进行替换") + public Map replaceKey( + Map source, + @Comment(name = "function", value = "回调函数") Function functional) { + Map result = new LinkedHashMap<>(); + Set> entries = source.entrySet(); + for (Map.Entry entry : entries) { + result.put(functional.apply(new Object[] {entry.getKey()}), entry.getValue()); + } + return result; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/NumberExtension.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/NumberExtension.java new file mode 100644 index 0000000..9d4dbe3 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/NumberExtension.java @@ -0,0 +1,66 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** Number类型扩展 */ +public class NumberExtension { + + @Comment("四舍五入保留N位小数") + public static double round(Number number, @Comment(name = "num", value = "规定小数的位数") int num) { + return new BigDecimal("" + number.doubleValue()) + .setScale(num, RoundingMode.HALF_UP) + .doubleValue(); + } + + @Comment("向下取整") + public static Number floor(Number number) { + if (number instanceof Double || number instanceof Float) { + return fixed(Math.floor(number.floatValue())); + } else if (number instanceof BigDecimal) { + return ((BigDecimal) number).setScale(0, RoundingMode.FLOOR); + } + return number; + } + + @Comment("向上取整") + public static Number ceil(Number number) { + if (number instanceof Double || number instanceof Float) { + return fixed(Math.ceil(number.doubleValue())); + } else if (number instanceof BigDecimal) { + return ((BigDecimal) number).setScale(0, RoundingMode.UP); + } + return number; + } + + @Comment("转为百分比") + public static String asPercent(Number number, @Comment(name = "num", value = "规定小数的位数") int num) { + return new BigDecimal(number.doubleValue() * 100).setScale(num, RoundingMode.HALF_UP).toString() + + "%"; + } + + private static Number fixed(double value) { + if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) { + return (long) value; + } + return value; + } + + @Comment("四舍五入保留N位小数,仿JS的toFixed") + public String toFixed(Number number, @Comment(name = "num", value = "规定小数的位数") int num) { + return new BigDecimal("" + number.doubleValue()).setScale(num, RoundingMode.HALF_UP).toString(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ObjectConvertExtension.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ObjectConvertExtension.java new file mode 100644 index 0000000..eca6806 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ObjectConvertExtension.java @@ -0,0 +1,224 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** 类型转换 */ +public class ObjectConvertExtension { + + /** + * 转int + * + * @param defaultValue 默认值 + */ + @Comment("将值转换为int类型") + public static int asInt( + Object val, @Comment(name = "defaultValue", value = "转换失败时的默认值") int defaultValue) { + try { + return asDecimal(val).intValue(); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转double + * + * @param defaultValue 默认值 + */ + @Comment("将对象转为double类型") + public static double asDouble( + Object val, @Comment(name = "defaultValue", value = "转换失败时的默认值") double defaultValue) { + try { + return asDecimal(val).doubleValue(); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转long + * + * @param defaultValue 默认值 + */ + @Comment("将对象转为long类型") + public static long asLong( + Object val, @Comment(name = "defaultValue", value = "转换失败时的默认值") long defaultValue) { + try { + return asDecimal(val).longValue(); + } catch (Exception e) { + return defaultValue; + } + } + + /** 转String */ + @Comment("将对象转为String类型") + public static String asString(Object val) { + return asString(val, null); + } + + /** 转Date */ + @Comment("将对象转为Date类型,默认字符串格式为yyyy-MM-dd HH:mm:ss") + public static Date asDate(Object val) { + return asDate(val, "yyyy-MM-dd HH:mm:ss"); + } + + /** 转BigDecimal */ + @Comment("将对象转为BigDecimal类型") + public static BigDecimal asDecimal(Object val) { + if (val instanceof BigDecimal) { + return (BigDecimal) val; + } + return new BigDecimal(asString(val)); + } + + /** 转BigDecimal */ + @Comment("将对象转为BigDecimal类型") + public static BigDecimal asDecimal( + Object val, @Comment(name = "defaultVal", value = "转换失败时的默认值") BigDecimal defaultVal) { + if (val instanceof BigDecimal) { + return (BigDecimal) val; + } + try { + return new BigDecimal(asString(val)); + } catch (Exception e) { + return defaultVal; + } + } + + /** 转Date */ + @Comment("将对象转为Date类型,支持String、10位、13位时间戳") + public static Date asDate( + Object val, @Comment(name = "format", value = "日期格式,如yyyy-MM-dd HH:mm:ss") String format) { + if (val == null) { + return null; + } + if (val instanceof String) { + try { + return new SimpleDateFormat(format).parse(val.toString()); + } catch (ParseException e) { + long longVal = asLong(val, -1); + if (longVal > 0) { + return asDate(longVal, format); + } + } + } else if (val instanceof Date) { + return (Date) val; + } else if (val instanceof Number) { + Number number = (Number) val; + if (val.toString().length() == 10) { // 10位时间戳 + return new Date(number.longValue() * 1000L); + } else if (val.toString().length() == 13) { // 13位时间戳 + return new Date(number.longValue()); + } + } + return null; + } + + /** + * 转String + * + * @param defaultValue 默认值 + */ + @Comment("将对象转为String类型") + public static String asString( + Object val, @Comment(name = "defaultValue", value = "转换失败时的默认值") String defaultValue) { + return val == null ? defaultValue : val.toString(); + } + + /** 转int */ + @Comment("将值转换为int类型,转换失败时为0") + public int asInt(Object val) { + return asInt(val, 0); + } + + /** 转double */ + @Comment("将对象转为double类型,转换失败时为0.0") + public double asDouble(Object val) { + return asDouble(val, 0.0); + } + + /** 转long */ + @Comment("将对象转为long类型,转换失败时为0L") + public long asLong(Object val) { + return asLong(val, 0L); + } + + /** 转byte */ + @Comment("将对象转为byte类型,转换失败时默认为0") + public byte asByte(Object val) { + return asByte(val, (byte) 0); + } + + /** + * 转byte + * + * @param defaultValue 默认值 + */ + @Comment("将对象转为byte类型") + public byte asByte( + Object val, @Comment(name = "defaultValue", value = "转换失败时的默认值") byte defaultValue) { + try { + return asDecimal(val).byteValue(); + } catch (Exception e) { + return defaultValue; + } + } + + /** 转short */ + @Comment("将对象转为short类型,转换失败时默认为0") + public short asShort(Object val) { + return asShort(val, (short) 0); + } + + /** + * 转short + * + * @param defaultValue 默认值 + */ + @Comment("将对象转为short类型") + public short asShort( + Object val, @Comment(name = "defaultValue", value = "转换失败时的默认值") short defaultValue) { + try { + return asDecimal(val).shortValue(); + } catch (Exception e) { + return defaultValue; + } + } + + /** 转float */ + @Comment("将对象转为float类型,转换失败默认为0.0f") + public float asFloat(Object val) { + return asFloat(val, 0.0f); + } + + /** + * 转float + * + * @param defaultValue 默认值 + */ + @Comment("将对象转为float类型") + public float asFloat( + Object val, @Comment(name = "defaultValue", value = "转换失败时的默认值") float defaultValue) { + try { + return asDecimal(val).floatValue(); + } catch (Exception e) { + return defaultValue; + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ObjectTypeConditionExtension.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ObjectTypeConditionExtension.java new file mode 100644 index 0000000..7e529f6 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/ObjectTypeConditionExtension.java @@ -0,0 +1,170 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** 类型判断 */ +public class ObjectTypeConditionExtension { + + /** 判断是否是目标类型 */ + @Comment("判断对象是否为指定类型的对象") + public static boolean is(Object target, @Comment(name = "clazz", value = "目标类型") Class clazz) { + if (target == null || clazz == null) { + return false; + } + return clazz.isAssignableFrom(target.getClass()); + } + + /** 判断是否是数组 */ + @Comment("判断对象是否是数组") + public static boolean isArray(Object target) { + if (target instanceof Class) { + return ((Class) target).isArray(); + } + return target.getClass().isArray(); + } + + /** 判断是否是集合 */ + @Comment("判断对象是否是集合") + public static boolean isCollection(Object target) { + if (target instanceof Class) { + return Collection.class.isAssignableFrom((Class) target); + } + return Collection.class.isAssignableFrom(target.getClass()); + } + + /** 判断是否是Map */ + @Comment("判断对象是否是Map") + public static boolean isMap(Object target) { + if (target instanceof Class) { + return Map.class.isAssignableFrom((Class) target); + } + return Map.class.isAssignableFrom(target.getClass()); + } + + /** 判断是否是目标类型 */ + @Comment("判断对象是否为指定类型的对象,type为null时 返回false,支持类名缩写") + public boolean is( + Object target, + @Comment( + name = "type", + value = "类名或全类名或string、int、double、float、long、byte、short、bigdecimal、boolean") + String type) { + if (type == null) { + return false; + } + Class clazz; + if (target instanceof Class) { + clazz = (Class) target; + } else { + clazz = target.getClass(); + } + if (clazz.getName().equals(type) || clazz.getSimpleName().equals(type)) { + return true; + } + if ("string".equalsIgnoreCase(type) && clazz == String.class) { + return true; + } + if ("int".equalsIgnoreCase(type) && clazz == Integer.class) { + return true; + } + if ("double".equalsIgnoreCase(type) && clazz == Double.class) { + return true; + } + if ("float".equalsIgnoreCase(type) && clazz == Float.class) { + return true; + } + if ("long".equalsIgnoreCase(type) && clazz == Long.class) { + return true; + } + if ("byte".equalsIgnoreCase(type) && clazz == Byte.class) { + return true; + } + if ("short".equalsIgnoreCase(type) && clazz == Short.class) { + return true; + } + if ("boolean".equalsIgnoreCase(type) && clazz == Boolean.class) { + return true; + } + return "decimal".equalsIgnoreCase(type) && clazz == BigDecimal.class; + } + + /** 判断是否是String */ + @Comment("判断对象是否是String类型") + public boolean isString(Object target) { + return is(target, String.class); + } + + /** 判断是否是int */ + @Comment("判断对象是否是int类型") + public boolean isInt(Object target) { + return is(target, Integer.class); + } + + /** 判断是否是double */ + @Comment("判断对象是否是double类型") + public boolean isDouble(Object target) { + return is(target, Double.class); + } + + /** 判断是否是long */ + @Comment("判断对象是否是long类型") + public boolean isLong(Object target) { + return is(target, Long.class); + } + + /** 判断是否是byte */ + @Comment("判断对象是否是byte类型") + public boolean isByte(Object target) { + return is(target, Byte.class); + } + + /** 判断是否是short */ + @Comment("判断对象是否是short类型") + public boolean isShort(Object target) { + return is(target, Short.class); + } + + /** 判断是否是boolean */ + @Comment("判断对象是否是boolean类型") + public boolean isBoolean(Object target) { + return is(target, Boolean.class); + } + + /** 判断是否是BigDecimal */ + @Comment("判断对象是否是BigDecimal类型") + public boolean isDecimal(Object target) { + return is(target, BigDecimal.class); + } + + /** 判断是否是Date */ + @Comment("判断对象是否是Date类型") + public boolean isDate(Object target) { + return is(target, Date.class); + } + + /** 判断是否是List */ + @Comment("判断对象是否是List") + public boolean isList(Object target) { + if (target instanceof Class) { + return List.class.isAssignableFrom((Class) target); + } + return List.class.isAssignableFrom(target.getClass()); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/PatternExtension.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/PatternExtension.java new file mode 100644 index 0000000..41683bf --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/PatternExtension.java @@ -0,0 +1,25 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import java.util.regex.Pattern; + +/** Pattern 扩展 */ +public class PatternExtension { + + @Comment("校验文本是否符合正则") + public boolean test(Pattern pattern, @Comment(name = "source", value = "目标字符串") String source) { + return source != null && pattern.matcher(source).find(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/StreamExtension.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/StreamExtension.java new file mode 100644 index 0000000..4e9cc2d --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/StreamExtension.java @@ -0,0 +1,630 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import cn.universal.core.engine.exception.MagicScriptException; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.literal.BooleanLiteral; +import cn.universal.core.engine.reflection.JavaReflection; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.OptionalDouble; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public class StreamExtension { + + /** 将对象转为List */ + @Comment("将对象转为List") + public static List arrayLikeToList(Object arrayLike) { + if (arrayLike == null) { + // 不返回EmptyList + return new ArrayList<>(); + } + if (arrayLike instanceof Collection) { + return new ArrayList<>((Collection) arrayLike); + } else if (arrayLike.getClass().isArray()) { + List list = new ArrayList<>(Array.getLength(arrayLike)); + IntStream.range(0, Array.getLength(arrayLike)) + .forEach(i -> list.add(Array.get(arrayLike, i))); + return list; + } else if (arrayLike instanceof Iterator) { + List list = new ArrayList<>(); + Iterator it = (Iterator) arrayLike; + it.forEachRemaining(list::add); + return list; + } else if (arrayLike instanceof Enumeration) { + Enumeration en = (Enumeration) arrayLike; + return Collections.list(en); + } + throw new MagicScriptException("不支持的类型:" + arrayLike.getClass()); + } + + /** 将list拼接起来 */ + @Comment("将集合使用连接符拼接起来") + public static String join( + Object target, @Comment(name = "separator", value = "拼接符,如`,`") String separator) { + List objects = arrayLikeToList(target); + StringBuilder sb = new StringBuilder(); + for (int i = 0, len = objects.size(); i < len; i++) { + sb.append(objects.get(i)); + if (i + 1 < len) { + sb.append(separator); + } + } + return sb.toString(); + } + + @Comment("将集合转为JavaBean") + public static Object asBean( + Object source, @Comment(name = "target", value = "目标类型") Class target) { + return asBean(source, target, false); + } + + @Comment("将集合转为JavaBean") + public static Object asBean( + Object source, + @Comment(name = "target", value = "目标类型") Class target, + @Comment(name = "isArray", value = "是否是数组") boolean isArray) { + Class sourceClass = source.getClass(); + List objects = arrayLikeToList(source); + int size = objects.size(); + boolean isCollection = false; + boolean isMap = false; + boolean isOrigin = false; + boolean targetIsArray = false; + Class innerClass = null; + if (size > 0) { + Object first = objects.get(0); + innerClass = first.getClass(); + isCollection = ObjectTypeConditionExtension.isCollection(innerClass); + isMap = ObjectTypeConditionExtension.isMap(innerClass); + isOrigin = ObjectTypeConditionExtension.is(first, target); + isOrigin = isOrigin || JavaReflection.isPrimitiveAssignableFrom(first.getClass(), target); + targetIsArray = ObjectTypeConditionExtension.isArray(target); + } + if (isArray) { + Object result = Array.newInstance(target, objects.size()); + for (int i = 0; i < size; i++) { + Object value = objects.get(i); + if (innerClass.isArray()) { + Array.set(result, i, asBean(value, innerClass.getComponentType(), true)); + } else if (targetIsArray) { + Array.set(result, i, asBean(value, target.getComponentType(), true)); + } else if (isCollection) { + Array.set(result, i, asBean(value, innerClass, false)); + } else if (isMap) { + Array.set(result, i, MapExtension.asBean((Map) value, innerClass)); + } else if (isOrigin) { + Array.set(result, i, value); + } + } + return result; + } + Collection collection; + if (List.class.isAssignableFrom(sourceClass)) { + collection = new ArrayList<>(size); + } else if (Set.class.isAssignableFrom(sourceClass)) { + collection = new HashSet<>(size); + } else { + throw new MagicScriptException("不支持的类型:" + sourceClass); + } + for (Object value : objects) { + if (isCollection) { + collection.add(asBean(value, target)); + } else if (isMap) { + collection.add(MapExtension.asBean((Map) value, target)); + } else if (isOrigin) { + collection.add(value); + } else { + break; + } + } + return collection; + } + + private Object toOriginType(Object target, Collection results) { + if (target instanceof Collection) { + return results; + } else if (target.getClass().isArray()) { + return results.toArray(); + } else if (target instanceof Iterator) { + return results; + } else if (target instanceof Enumeration) { + return results; + } + return null; + } + + @Comment(value = "向集合中添加元素", origin = true) + public Object push(Object target, @Comment(name = "item", value = "要添加的元素") Object item) { + if (target instanceof Collection) { + if (item instanceof Collection) { + ((Collection) target).addAll((Collection) item); + } else { + ((Collection) target).add(item); + } + } else { + throw new MagicScriptException("push方法不支持类型:" + target.getClass()); + } + return target; + } + + /** + * map 函数 + * + * @param function 回调函数 + */ + @Comment(value = "将集合进行转换,并返回新集合", origin = true) + public Object map( + Object target, + @Comment(name = "function", value = "转换函数,如提取属性`(item)=>item.xxx`") + Function function) { + List objects = arrayLikeToList(target); + List results = new ArrayList<>(objects.size()); + for (int i = 0, len = objects.size(); i < len; i++) { + Object object = objects.get(i); + results.add(function.apply(new Object[] {object, i, len})); + } + return toOriginType(target, results); + } + + /** + * 对List进行过滤 + * + * @param function 回调函数 + */ + @Comment(value = "将集合进行过滤,并返回新集合", origin = true) + public Object filter( + Object target, + @Comment(name = "function", value = "过滤条件,如`(item)=>item.xxx == 1`") + Function function) { + List objects = arrayLikeToList(target); + List results = new ArrayList<>(objects.size()); + for (int i = 0, len = objects.size(); i < len; i++) { + Object object = objects.get(i); + if (BooleanLiteral.isTrue(function.apply(new Object[] {object, i, len}))) { + results.add(object); + } + } + return toOriginType(target, results); + } + + @Comment("查找元素") + public Object find( + Object target, + @Comment(name = "function", value = "匹配条件,如`(item)=>item.xxx == 1`") + Function function) { + List objects = arrayLikeToList(target); + for (int i = 0, len = objects.size(); i < len; i++) { + Object object = objects.get(i); + if (BooleanLiteral.isTrue(function.apply(new Object[] {object, i, len}))) { + return object; + } + } + return null; + } + + @Comment("查找元素索引") + public int findIndex( + Object target, + @Comment(name = "function", value = "匹配条件,如`(item)=>item.xxx == 1`") + Function function) { + List objects = arrayLikeToList(target); + for (int i = 0, len = objects.size(); i < len; i++) { + Object object = objects.get(i); + if (BooleanLiteral.isTrue(function.apply(new Object[] {object, i, len}))) { + return i; + } + } + return -1; + } + + @Comment(value = "合并多个集合, 返回新的集合", origin = true) + public Object concat(Object src, @Comment(name = "target", value = "集合") Object... target) { + List results = new ArrayList<>(arrayLikeToList(src)); + if (target != null) { + for (Object o : target) { + results.addAll(arrayLikeToList(o)); + } + } + return toOriginType(target, results); + } + + @Comment("集合转为Map") + public Map toMap( + Object src, + @Comment(name = "mappingKey", value = "key值") Function mappingKey, + @Comment(name = "mappingValue", value = "value值") Function mappingValue) { + List target = new ArrayList<>(arrayLikeToList(src)); + int size = target.size(); + Map map = new LinkedHashMap<>(size); + for (int i = 0; i < size; i++) { + Object item = target.get(i); + Object[] parameters = new Object[] {item, i, size}; + map.put(mappingKey.apply(parameters), mappingValue.apply(parameters)); + } + return map; + } + + @Comment("集合转为Map") + public Map toMap( + Object src, + @Comment(name = "mappingKey", value = "key值") Function mappingKey) { + return toMap(src, mappingKey, objects -> objects[0]); + } + + /** + * 循环List + * + * @param function 回调函数 + */ + @Comment(value = "将集合进行循环操作,并返回新集合", origin = true) + public Object each( + Object target, + @Comment(name = "function", value = "循环函数,如循环添加属性`(item)=>{item.xxx = 'newVal'}`") + Function function) { + List objects = arrayLikeToList(target); + List results = new ArrayList<>(objects.size()); + for (int i = 0, len = objects.size(); i < len; i++) { + Object object = objects.get(i); + function.apply(new Object[] {object, i, len}); + results.add(object); + } + return toOriginType(target, results); + } + + /** 排序 */ + @Comment(value = "将集合进行排序,并返回新集合", origin = true) + public Object sort( + Object target, + @Comment(name = "function", value = "排序函数,如从大到小`(a,b)=>a-b`") + Function function) { + List objects = arrayLikeToList(target); + objects.sort( + (o1, o2) -> ObjectConvertExtension.asInt(function.apply(new Object[] {o1, o2}), 0)); + return toOriginType(target, objects); + } + + /** 反转 */ + @Comment(value = "将集合进行反转操作", origin = true) + public Object reserve(Object target) { + List objects = arrayLikeToList(target); + Collections.reverse(objects); + return toOriginType(target, objects); + } + + /** 将list打乱 */ + @Comment(value = "将集合的顺序打乱", origin = true) + public Object shuffle(Object target) { + List objects = arrayLikeToList(target); + Collections.shuffle(objects); + return toOriginType(target, objects); + } + + /** + * 数组去重 + * + * @since 1.4.6 + */ + @Comment(value = "集合去重", origin = true) + public Object distinct(Object target) { + List list = arrayLikeToList(target); + List result = new ArrayList<>(list.size()); + Set sets = new LinkedHashSet<>(list.size()); + for (Object e : list) { + if (!sets.contains(e)) { + sets.add(e); + result.add(e); + } + } + return toOriginType(target, result); + } + + /** + * 数组去重 + * + * @since 1.6.2 + */ + @Comment(value = "集合去重", origin = true) + public Object distinct( + Object target, + @Comment(name = "condition", value = "去重转换器,如根据id去重 `item=>item.id`") + Function condition) { + List list = arrayLikeToList(target); + List result = new ArrayList<>(list.size()); + Set sets = new LinkedHashSet<>(list.size()); + for (Object e : list) { + Object res = condition.apply(new Object[] {e}); + if (!sets.contains(res)) { + sets.add(res); + result.add(e); + } + } + return toOriginType(target, result); + } + + /** 将list拼接起来 */ + @Comment("将集合使用`,`拼接起来") + public String join(Object target) { + return join(target, ","); + } + + /** 取最大值 */ + @Comment("取出集合最大值,如果找不到返回`null`") + public Object max(Object target) { + return arrayLikeToList(target).stream() + .filter(Objects::nonNull) + .max(BinaryOperation::compare) + .orElse(null); + } + + /** 取最小值 */ + @Comment("取出集合最小值,如果找不到返回`null`") + public Object min(Object target) { + return arrayLikeToList(target).stream() + .filter(Objects::nonNull) + .min(BinaryOperation::compare) + .orElse(null); + } + + /** 取平均值 */ + @Comment("取出集合平均值,如果无法计算返回`null`") + public Double avg(Object target) { + OptionalDouble average = + arrayLikeToList(target).stream() + .filter(v -> v instanceof Number) + .mapToDouble(value -> ((Number) value).doubleValue()) + .average(); + return average.isPresent() ? average.getAsDouble() : null; + } + + /** 累计求和 */ + @Comment("对集合进行累加操作") + public Number sum(Object target) { + return arrayLikeToList(target).stream() + .filter(value -> value instanceof Number) + .mapToDouble(value -> ((Number) value).doubleValue()) + .sum(); + } + + /** + * 分组 + * + * @param condition 分组条件 + */ + @Comment("对集合进行分组") + public Map> group( + Object target, + @Comment(name = "condition", value = "分组条件,如`item=>item.xxx + '_' + item.yyy`") + Function condition) { + return arrayLikeToList(target).stream() + .collect( + Collectors.groupingBy( + item -> condition.apply(Stream.of(item).toArray()), + LinkedHashMap::new, + Collectors.toList())); + } + + /** + * 分组 + * + * @param condition 分组条件 + * @param mapping 结果映射 + */ + @Comment("对集合进行分组并转换") + public Map group( + Object target, + @Comment(name = "condition", value = "分组条件,如`item=>item.xxx + '_' + item.yyy`") + Function condition, + @Comment(name = "mapping", value = "转换函数,如分组求和`(list)=>list.sum()`") + Function mapping) { + return arrayLikeToList(target).stream() + .collect( + Collectors.groupingBy( + item -> condition.apply(Stream.of(item).toArray()), + LinkedHashMap::new, + Collectors.collectingAndThen( + Collectors.toList(), list -> mapping.apply(Stream.of(list).toArray())))); + } + + /** + * 合并两个集合,类似sql join 操作 + * + * @param source 左表 + * @param target 右表 + * @param condition 条件 + */ + @Comment("将两个集合关联起来") + public List join( + Object source, + @Comment(name = "target", value = "另一个集合") Object target, + @Comment(name = "condition", value = "关联条件,如:`(left,right)=>left.xxx = right.xxx`") + Function condition) { + return join( + source, + target, + condition, + (args) -> { + Object left = args[0]; + Object right = args[1]; + Map map = new LinkedHashMap<>(); + if (left instanceof Map) { + map.putAll((Map) left); + } + if (right instanceof Map) { + map.putAll((Map) right); + } + return map; + }); + } + + /** + * 合并两个集合,类似 sql join 操作 + * + * @param source 左表 + * @param target 右表 + * @param condition 条件 + * @param mapping 映射 + */ + @Comment("将两个集合关联并转换") + public List join( + Object source, + @Comment(name = "target", value = "另一个集合") Object target, + @Comment(name = "condition", value = "关联条件,如:`(left,right)=>left.xxx == right.xxx`") + Function condition, + @Comment(name = "mapping", value = "映射函数,如:`(left,right)=>{xxx : left.xxx, yyy : right.yyy}`") + Function mapping) { + if (target == null) { + return null; + } + List targetList = arrayLikeToList(target); + return arrayLikeToList(source).stream() + // 将匹配结果进行映射 + .map( + left -> + mapping.apply( + Stream.of( + left, + targetList.stream() + // 匹配条件 + .filter( + right -> + Objects.equals( + true, + condition.apply(Stream.of(left, right).toArray()))) + // 只取第一条 + .findFirst() + .orElse(null)) + .toArray())) + .collect(Collectors.toList()); + } + + @Comment(value = "截取集合", origin = true) + public Object skip(Object source, @Comment(name = "value", value = "跳过的数量") int value) { + return toOriginType( + source, arrayLikeToList(source).stream().skip(value).collect(Collectors.toList())); + } + + @Comment(value = "限制集合数量", origin = true) + public Object limit(Object source, @Comment(name = "value", value = "跳过的数量") int value) { + return toOriginType( + source, arrayLikeToList(source).stream().limit(value).collect(Collectors.toList())); + } + + @Comment("判断集合是否都满足条件") + public boolean every( + Object source, + @Comment(name = "condition", value = "判断条件") Function condition) { + List objects = arrayLikeToList(source); + for (int i = 0, size = objects.size(); i < size; i++) { + if (!BooleanLiteral.isTrue(condition.apply(new Object[] {objects.get(i), i}))) { + return false; + } + } + return true; + } + + @Comment("判断集合中是否至少有一个元素满足条件") + public boolean some( + Object source, + @Comment(name = "condition", value = "判断条件") Function condition) { + List objects = arrayLikeToList(source); + for (int i = 0, size = objects.size(); i < size; i++) { + if (BooleanLiteral.isTrue(condition.apply(new Object[] {objects.get(i), i}))) { + return true; + } + } + return false; + } + + @Comment("找到集合中第一个不为null的元素") + public Object findNotNull(Object source) { + List objects = arrayLikeToList(source); + for (Object object : objects) { + if (object != null) { + return object; + } + } + return null; + } + + @Comment("循环集合通过给定的计算函数返回一个新值") + public Object reduce( + Object source, + @Comment(name = "reduceFunction", value = "处理函数,如累加计算:`(val,item)=>val + item`") + Function reduceFunction) { + List objects = arrayLikeToList(source); + if (objects.isEmpty()) { + return null; + } + int size = objects.size(); + if (size == 1) { + return objects.get(0); + } + Object result = objects.get(0); + for (int i = 1; i < size; i++) { + result = reduceFunction.apply(new Object[] {result, objects.get(i)}); + } + return result; + } + + @Comment("返回集合中的第一个元素") + public static Object first(Object source) { + List objects = arrayLikeToList(source); + return objects.size() > 0 ? objects.get(0) : null; + } + + @Comment("返回集合中的最后一个元素") + public Object last(Object source) { + List objects = arrayLikeToList(source); + return objects.size() > 0 ? objects.get(objects.size() - 1) : null; + } + + @Comment("返回集合或数组的长度") + public int size(Object arrayLike) { + if (arrayLike instanceof Collection) { + return ((Collection) arrayLike).size(); + } else if (arrayLike.getClass().isArray()) { + return Array.getLength(arrayLike); + } else if (arrayLike instanceof Iterator) { + List list = new ArrayList<>(); + Iterator it = (Iterator) arrayLike; + it.forEachRemaining(list::add); + return list.size(); + } else if (arrayLike instanceof Enumeration) { + Enumeration en = (Enumeration) arrayLike; + return Collections.list(en).size(); + } + throw new MagicScriptException("不支持的类型:" + arrayLike.getClass()); + } + + @Comment("返回集合或数组的长度") + public int getLength(Object source) { + return size(source); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/StringExtension.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/StringExtension.java new file mode 100644 index 0000000..e720b22 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/StringExtension.java @@ -0,0 +1,50 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** String 扩展 */ +public class StringExtension { + + @Comment("校验文本是否符合正则") + public boolean match(String source, @Comment(name = "pattern", value = "正则表达式") Pattern pattern) { + return pattern.matcher(source).find(); + } + + @Comment("正则匹配所有") + public List matcher( + String source, @Comment(name = "pattern", value = "正则表达式") String regex) { + // 创建 Pattern 对象 + // 现在创建 matcher 对象 + List list = new ArrayList<>(); + Pattern pattern = Pattern.compile(regex); + Matcher m = pattern.matcher(source); + while (m.find()) { + list.add(m.group().substring(1, m.group().length() - 1)); + } + return list; + } + + @Comment("正则替换字符串") + public String replace( + String source, + @Comment(name = "pattern", value = "正则表达式") Pattern pattern, + @Comment(name = "replacement", value = "替换字符串") String replacement) { + return pattern.matcher(source).replaceAll(replacement); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/StringFunctions.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/StringFunctions.java new file mode 100644 index 0000000..5b6535a --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/StringFunctions.java @@ -0,0 +1,43 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import cn.universal.core.engine.annotation.Function; + +public class StringFunctions { + + @Function + @Comment("判断字符串是否不是空") + public boolean not_blank(@Comment(name = "str", value = "目标字符串") CharSequence cs) { + return !is_blank(cs); + } + + @Function + @Comment("判断字符串是否不是空") + public boolean is_blank(@Comment(name = "str", value = "目标字符串") CharSequence cs) { + if (cs == null) { + return true; + } + int strLen = cs.length(); + if (strLen == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(cs.charAt(i))) { + return false; + } + } + return true; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/TemporalAccessorExtension.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/TemporalAccessorExtension.java new file mode 100644 index 0000000..33c3f50 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/TemporalAccessorExtension.java @@ -0,0 +1,27 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions; + +import cn.universal.core.engine.annotation.Comment; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public class TemporalAccessorExtension { + + @Comment("日期格式化") + public static String format( + TemporalAccessor source, + @Comment(name = "pattern", value = "格式,如yyyy-MM-dd") String pattern) { + return DateTimeFormatter.ofPattern(pattern).format(source); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/linq/AggregationFunctions.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/linq/AggregationFunctions.java new file mode 100644 index 0000000..66ba311 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/linq/AggregationFunctions.java @@ -0,0 +1,132 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions.linq; + +import cn.universal.core.engine.annotation.Comment; +import cn.universal.core.engine.annotation.Function; +import cn.universal.core.engine.functions.ObjectConvertExtension; +import cn.universal.core.engine.functions.StreamExtension; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import java.util.Map; +import java.util.OptionalDouble; +import java.util.function.BinaryOperator; + +/** 聚合函数 */ +public class AggregationFunctions { + + @Function + @Comment("聚合函数-count") + public int count(@Comment(name = "value", value = "集合") Object target) { + if (target == null) { + return 0; + } else if (target instanceof Map) { + return 1; + } + try { + return StreamExtension.arrayLikeToList(target).size(); + } catch (Exception e) { + return 1; + } + } + + @Function + @Comment("聚合函数-max") + public Object max(@Comment(name = "value", value = "集合") Object target) { + if (target == null) { + return null; + } else if (target instanceof Map) { + return target; + } + try { + return StreamExtension.arrayLikeToList(target).stream() + .reduce(BinaryOperator.maxBy(BinaryOperation::compare)) + .orElse(null); + } catch (Exception e) { + return null; + } + } + + @Function + @Comment("聚合函数-sum") + public Number sum(@Comment(name = "value", value = "集合") Object target) { + if (target == null) { + return null; + } else if (target instanceof Map) { + return null; + } + try { + OptionalDouble value = + StreamExtension.arrayLikeToList(target).stream() + .mapToDouble(v -> ObjectConvertExtension.asDouble(v, Double.NaN)) + .filter(v -> !Double.isNaN(v)) + .reduce(Double::sum); + return value.isPresent() ? value.getAsDouble() : null; + } catch (Exception e) { + return null; + } + } + + @Function + @Comment("聚合函数-min") + public Object min(@Comment(name = "value", value = "集合") Object target) { + if (target == null) { + return null; + } else if (target instanceof Map) { + return target; + } + try { + return StreamExtension.arrayLikeToList(target).stream() + .reduce(BinaryOperator.minBy(BinaryOperation::compare)) + .orElse(null); + } catch (Exception e) { + return null; + } + } + + @Function + @Comment("聚合函数-avg") + public Object avg(@Comment(name = "value", value = "集合") Object target) { + if (target == null) { + return null; + } else if (target instanceof Map) { + return target; + } + try { + OptionalDouble average = + StreamExtension.arrayLikeToList(target).stream() + .mapToDouble(v -> ObjectConvertExtension.asDouble(v, Double.NaN)) + .filter(v -> !Double.isNaN(v)) + .average(); + return average.isPresent() ? average.getAsDouble() : null; + } catch (Exception e) { + return null; + } + } + + @Function + @Comment("分组后按指定字符串拼接") + public String group_concat( + @Comment(name = "target", value = "列,如t.a") Object target, + @Comment(name = "separator", value = "分隔符,如`|`") String separator) { + if (target == null) { + return null; + } + return StreamExtension.join(target, separator); + } + + @Function + @Comment("分组后使用`,`拼接") + public String group_concat(@Comment(name = "target", value = "列,如t.a") Object target) { + return group_concat(target, ","); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/linq/LinqFunctions.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/linq/LinqFunctions.java new file mode 100644 index 0000000..e15cb1b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/linq/LinqFunctions.java @@ -0,0 +1,72 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions.linq; + +import cn.universal.core.engine.annotation.Comment; +import cn.universal.core.engine.annotation.Function; +import cn.universal.core.engine.functions.DateExtension; +import cn.universal.core.engine.functions.TemporalAccessorExtension; +import java.time.temporal.TemporalAccessor; +import java.util.Date; + +/** Linq中的函数 */ +public class LinqFunctions { + + @Function + @Comment("判断值是否为空") + public Object ifnull( + @Comment(name = "target", value = "目标值") Object target, + @Comment(name = "trueValue", value = "为空的值") Object trueValue) { + return target == null ? trueValue : target; + } + + @Function + @Comment("日期格式化") + public String date_format( + @Comment(name = "target", value = "目标日期") Date target, + @Comment(name = "pattern", value = "格式") String pattern) { + return target == null ? null : DateExtension.format(target, pattern); + } + + @Function + @Comment("日期格式化") + public String date_format(@Comment(name = "target", value = "目标日期") Date target) { + return target == null ? null : DateExtension.format(target, "yyyy-MM-dd HH:mm:ss"); + } + + @Function + @Comment("日期格式化") + public String date_format( + @Comment(name = "target", value = "目标日期") TemporalAccessor target, + @Comment(name = "pattern", value = "格式") String pattern) { + return target == null ? null : TemporalAccessorExtension.format(target, pattern); + } + + @Function + @Comment("取当前时间") + public Date now() { + return new Date(); + } + + @Function + @Comment("取当前时间戳(秒)") + public long current_timestamp() { + return System.currentTimeMillis() / 1000; + } + + @Function + @Comment("取当前时间戳(毫秒)") + public long current_timestamp_millis() { + return System.currentTimeMillis(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/linq/MathFunctions.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/linq/MathFunctions.java new file mode 100644 index 0000000..999e76a --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/functions/linq/MathFunctions.java @@ -0,0 +1,60 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.functions.linq; + +import cn.universal.core.engine.annotation.Comment; +import cn.universal.core.engine.annotation.Function; +import cn.universal.core.engine.functions.NumberExtension; + +public class MathFunctions { + + @Comment("四舍五入保留N为小数") + @Function + public double round( + @Comment(name = "target", value = "目标值") Number target, + @Comment(name = "len", value = "保留的小数位数") int len) { + return NumberExtension.round(target, len); + } + + @Comment("四舍五入保留N为小数") + @Function + public double round(@Comment(name = "target", value = "目标值") Number target) { + return NumberExtension.round(target, 0); + } + + @Comment("向上取整") + @Function + public Number ceil(@Comment(name = "target", value = "目标值") Number target) { + return NumberExtension.ceil(target); + } + + @Comment("向下取整") + @Function + public Number floor(@Comment(name = "target", value = "目标值") Number target) { + return NumberExtension.floor(target); + } + + @Comment("求百分比") + @Function + public String percent( + @Comment(name = "target", value = "目标值") Number target, + @Comment(name = "len", value = "保留的小数位数") int len) { + return NumberExtension.asPercent(target, len); + } + + @Comment("求百分比") + @Function + public String percent(@Comment(name = "target", value = "目标值") Number target) { + return NumberExtension.asPercent(target, 0); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/CharacterStream.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/CharacterStream.java new file mode 100644 index 0000000..dbcc78b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/CharacterStream.java @@ -0,0 +1,227 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing; + +/** 对字符串进行封装,提供匹配、跳过方法,方便词法解析。 */ +public class CharacterStream { + + private final String source; + + private final int end; + + private int index; + + private int spanStart = 0; + + public CharacterStream(String source) { + this(source, 0, source.length()); + } + + public CharacterStream(String source, int start, int end) { + if (start > end) { + throw new IllegalArgumentException("Start must be <= end."); + } + if (start < 0) { + throw new IndexOutOfBoundsException("Start must be >= 0."); + } + if (start > Math.max(0, source.length() - 1)) { + throw new IndexOutOfBoundsException("Start outside of string."); + } + if (end > source.length()) { + throw new IndexOutOfBoundsException("End outside of string."); + } + this.source = source; + this.index = start; + this.end = end; + } + + /** + * 截取字符串 + * + * @param startIndex 开始位置 + * @param endIndex 结束位置 + */ + public String substring(int startIndex, int endIndex) { + return this.source.substring(startIndex, endIndex); + } + + /** 是否有下一个字符 */ + public boolean hasMore() { + return index < end; + } + + /** 根据开始位置、结束位置返回Span */ + public Span getSpan(int start, int end) { + return new Span(this.source, start, end); + } + + /** 返回下一个字符 */ + public char consume() { + if (!hasMore()) { + throw new RuntimeException("No more characters in stream."); + } + return source.charAt(index++); + } + + /** 返回是否是以给定的字符串开头 */ + public boolean match(String needle, boolean consume) { + int needleLength = needle.length(); + if (needleLength + index > end) { + return false; + } + for (int i = 0, j = index; i < needleLength; i++, j++) { + if (index >= end) { + return false; + } + if (needle.charAt(i) != source.charAt(j)) { + return false; + } + } + if (consume) { + index += needleLength; + } + return true; + } + + /** + * 匹配任意字符串 + * + * @param strs 任意字符串 + * @return + */ + public boolean matchAny(boolean consume, String... strs) { + for (String str : strs) { + if (match(str, consume)) { + return true; + } + } + return false; + } + + /** 返回是否是数字 */ + public boolean matchDigit(boolean consume) { + if (index >= end) { + return false; + } + char c = source.charAt(index); + if (Character.isDigit(c)) { + if (consume) { + index++; + } + return true; + } + return false; + } + + /** 返回是否以标识符开头 */ + public boolean matchIdentifierStart(boolean consume) { + if (index >= end) { + return false; + } + char c = source.charAt(index); + if (Character.isJavaIdentifierStart(c) || c == '@') { + if (consume) { + index++; + } + return true; + } + return false; + } + + /** + * 返回是否是标识符部分 + * + * @param consume 是否消耗 + */ + public boolean matchIdentifierPart(boolean consume) { + if (index >= end) { + return false; + } + char c = source.charAt(index); + if (Character.isJavaIdentifierPart(c)) { + if (consume) { + index++; + } + return true; + } + return false; + } + + /** 跳过一行 */ + public void skipLine() { + while (index < end) { + if (source.charAt(index++) == '\n') { + break; + } + } + } + + /** 直到给定的字符串之前全部跳过 */ + public boolean skipUntil(String chars) { + while (index < end) { + boolean matched = true; + for (int i = 0, len = chars.length(); i < len && index + i < end; i++) { + if (chars.charAt(i) != source.charAt(index + i)) { + matched = false; + break; + } + } + this.index += matched ? chars.length() : 1; + if (matched) { + return true; + } + } + return false; + } + + /** 跳过空白字符 */ + public void skipWhiteSpace() { + while (index < end) { + char c = source.charAt(index); + if (c == ' ' || c == '\n' || c == '\r' || c == '\t') { + index++; + } else { + break; + } + } + } + + /** 记录当前位置为Span的开始位置 */ + public void startSpan() { + spanStart = index; + } + + /** 根据当前位置返回Span */ + public Span endSpan() { + return new Span(source, spanStart, index); + } + + /** 根据当前位置 - offset 返回 Span */ + public Span endSpan(int offset) { + return new Span(source, spanStart, index + offset); + } + + public Span endSpan(int start, int end) { + return new Span(source, start, end); + } + + /** 返回当前位置 */ + public int getPosition() { + return index; + } + + /** 重置当前位置 */ + public void reset(int position) { + index = position; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/GenericTokenParser.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/GenericTokenParser.java new file mode 100644 index 0000000..3d0bd15 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/GenericTokenParser.java @@ -0,0 +1,80 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing; + +import java.util.function.Function; + +public class GenericTokenParser { + + private final String open; + + private final String close; + + private final boolean skipStr; + + public GenericTokenParser(String open, String close, boolean skipStr) { + this.open = open; + this.close = close; + this.skipStr = skipStr; + } + + public String parse(String source, Function handler) { + CharacterStream stream = new CharacterStream(source); + StringBuilder builder = new StringBuilder(); + while (stream.hasMore()) { + builder.append(parseStream(stream, null, handler)); + } + return builder.toString().trim(); + } + + private String parseStream( + CharacterStream stream, String expect, Function handler) { + StringBuilder builder = new StringBuilder(); + while (stream.hasMore()) { + if (expect != null && stream.match(expect, true)) { + return builder.toString().trim(); + } + if (stream.match(open, true)) { + String value = handler.apply(parseStream(stream, close, handler)); + if (value != null) { + builder.append(value); + } + } else { + char ch = stream.consume(); + builder.append(ch); + if (skipStr && ch == '\'') { + builder.append(consumeUntil(stream, "'")); + } else if (skipStr && ch == '"') { + builder.append(consumeUntil(stream, "\"")); + } else if (ch == '{') { + builder.append(parseStream(stream, "}", handler)).append("}"); + } + } + } + return builder.toString().trim(); + } + + private String consumeUntil(CharacterStream stream, String str) { + int start = stream.getPosition(); + while (stream.hasMore()) { + if (stream.match("\\", true)) { + stream.consume(); + } + if (stream.match(str, true)) { + break; + } + stream.consume(); + } + return stream.substring(start, stream.getPosition()); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/LiteralToken.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/LiteralToken.java new file mode 100644 index 0000000..dbeb957 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/LiteralToken.java @@ -0,0 +1,28 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing; + +public class LiteralToken extends Token { + + public LiteralToken(TokenType type, Span span) { + super(type, span); + } + + public LiteralToken(TokenType type, Span span, TokenStream tokenStream) { + super(type, span, tokenStream); + } + + public LiteralToken(TokenType type, Span span, Object value) { + super(type, span, value); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/Parser.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/Parser.java new file mode 100644 index 0000000..a41dd55 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/Parser.java @@ -0,0 +1,1286 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing; + +import static cn.universal.core.engine.parsing.TokenType.And; +import static cn.universal.core.engine.parsing.TokenType.Assignment; +import static cn.universal.core.engine.parsing.TokenType.Asterisk; +import static cn.universal.core.engine.parsing.TokenType.AsteriskEqual; +import static cn.universal.core.engine.parsing.TokenType.BitAnd; +import static cn.universal.core.engine.parsing.TokenType.BitAndEqual; +import static cn.universal.core.engine.parsing.TokenType.BitNot; +import static cn.universal.core.engine.parsing.TokenType.BitOr; +import static cn.universal.core.engine.parsing.TokenType.BitOrEqual; +import static cn.universal.core.engine.parsing.TokenType.BooleanLiteral; +import static cn.universal.core.engine.parsing.TokenType.ByteLiteral; +import static cn.universal.core.engine.parsing.TokenType.Colon; +import static cn.universal.core.engine.parsing.TokenType.ColonColon; +import static cn.universal.core.engine.parsing.TokenType.Comma; +import static cn.universal.core.engine.parsing.TokenType.DecimalLiteral; +import static cn.universal.core.engine.parsing.TokenType.DoubleLiteral; +import static cn.universal.core.engine.parsing.TokenType.Equal; +import static cn.universal.core.engine.parsing.TokenType.EqualEqualEqual; +import static cn.universal.core.engine.parsing.TokenType.FloatLiteral; +import static cn.universal.core.engine.parsing.TokenType.ForwardSlash; +import static cn.universal.core.engine.parsing.TokenType.ForwardSlashEqual; +import static cn.universal.core.engine.parsing.TokenType.Greater; +import static cn.universal.core.engine.parsing.TokenType.GreaterEqual; +import static cn.universal.core.engine.parsing.TokenType.Identifier; +import static cn.universal.core.engine.parsing.TokenType.InstanceOf; +import static cn.universal.core.engine.parsing.TokenType.IntegerLiteral; +import static cn.universal.core.engine.parsing.TokenType.LShift; +import static cn.universal.core.engine.parsing.TokenType.LShiftEqual; +import static cn.universal.core.engine.parsing.TokenType.Lambda; +import static cn.universal.core.engine.parsing.TokenType.Language; +import static cn.universal.core.engine.parsing.TokenType.LeftBracket; +import static cn.universal.core.engine.parsing.TokenType.LeftCurly; +import static cn.universal.core.engine.parsing.TokenType.LeftParantheses; +import static cn.universal.core.engine.parsing.TokenType.Less; +import static cn.universal.core.engine.parsing.TokenType.LessEqual; +import static cn.universal.core.engine.parsing.TokenType.LongLiteral; +import static cn.universal.core.engine.parsing.TokenType.Minus; +import static cn.universal.core.engine.parsing.TokenType.MinusEqual; +import static cn.universal.core.engine.parsing.TokenType.MinusMinus; +import static cn.universal.core.engine.parsing.TokenType.Not; +import static cn.universal.core.engine.parsing.TokenType.NotEqual; +import static cn.universal.core.engine.parsing.TokenType.NotEqualEqual; +import static cn.universal.core.engine.parsing.TokenType.NullLiteral; +import static cn.universal.core.engine.parsing.TokenType.Or; +import static cn.universal.core.engine.parsing.TokenType.PercentEqual; +import static cn.universal.core.engine.parsing.TokenType.Percentage; +import static cn.universal.core.engine.parsing.TokenType.Period; +import static cn.universal.core.engine.parsing.TokenType.Plus; +import static cn.universal.core.engine.parsing.TokenType.PlusEqual; +import static cn.universal.core.engine.parsing.TokenType.PlusPlus; +import static cn.universal.core.engine.parsing.TokenType.QuestionMark; +import static cn.universal.core.engine.parsing.TokenType.QuestionPeriod; +import static cn.universal.core.engine.parsing.TokenType.RShift; +import static cn.universal.core.engine.parsing.TokenType.RShift2Equal; +import static cn.universal.core.engine.parsing.TokenType.RShiftEqual; +import static cn.universal.core.engine.parsing.TokenType.RegexpLiteral; +import static cn.universal.core.engine.parsing.TokenType.RightBracket; +import static cn.universal.core.engine.parsing.TokenType.RightCurly; +import static cn.universal.core.engine.parsing.TokenType.RightParantheses; +import static cn.universal.core.engine.parsing.TokenType.Rshift2; +import static cn.universal.core.engine.parsing.TokenType.ShortLiteral; +import static cn.universal.core.engine.parsing.TokenType.Spread; +import static cn.universal.core.engine.parsing.TokenType.SqlAnd; +import static cn.universal.core.engine.parsing.TokenType.SqlNotEqual; +import static cn.universal.core.engine.parsing.TokenType.SqlOr; +import static cn.universal.core.engine.parsing.TokenType.StringLiteral; +import static cn.universal.core.engine.parsing.TokenType.Xor; +import static cn.universal.core.engine.parsing.TokenType.XorEqual; + +import cn.universal.core.engine.MagicScriptError; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.LanguageExpression; +import cn.universal.core.engine.parsing.ast.Literal; +import cn.universal.core.engine.parsing.ast.Node; +import cn.universal.core.engine.parsing.ast.TernaryOperation; +import cn.universal.core.engine.parsing.ast.UnaryOperation; +import cn.universal.core.engine.parsing.ast.VariableSetter; +import cn.universal.core.engine.parsing.ast.binary.AssigmentOperation; +import cn.universal.core.engine.parsing.ast.linq.LinqExpression; +import cn.universal.core.engine.parsing.ast.linq.LinqField; +import cn.universal.core.engine.parsing.ast.linq.LinqJoin; +import cn.universal.core.engine.parsing.ast.linq.LinqOrder; +import cn.universal.core.engine.parsing.ast.linq.LinqSelect; +import cn.universal.core.engine.parsing.ast.linq.WholeLiteral; +import cn.universal.core.engine.parsing.ast.literal.BigDecimalLiteral; +import cn.universal.core.engine.parsing.ast.literal.BooleanLiteral; +import cn.universal.core.engine.parsing.ast.literal.ByteLiteral; +import cn.universal.core.engine.parsing.ast.literal.DoubleLiteral; +import cn.universal.core.engine.parsing.ast.literal.FloatLiteral; +import cn.universal.core.engine.parsing.ast.literal.IntegerLiteral; +import cn.universal.core.engine.parsing.ast.literal.ListLiteral; +import cn.universal.core.engine.parsing.ast.literal.LongLiteral; +import cn.universal.core.engine.parsing.ast.literal.MapLiteral; +import cn.universal.core.engine.parsing.ast.literal.NullLiteral; +import cn.universal.core.engine.parsing.ast.literal.RegexpLiteral; +import cn.universal.core.engine.parsing.ast.literal.ShortLiteral; +import cn.universal.core.engine.parsing.ast.literal.StringLiteral; +import cn.universal.core.engine.parsing.ast.statement.Assert; +import cn.universal.core.engine.parsing.ast.statement.AsyncCall; +import cn.universal.core.engine.parsing.ast.statement.Break; +import cn.universal.core.engine.parsing.ast.statement.ClassConverter; +import cn.universal.core.engine.parsing.ast.statement.Continue; +import cn.universal.core.engine.parsing.ast.statement.Exit; +import cn.universal.core.engine.parsing.ast.statement.ForStatement; +import cn.universal.core.engine.parsing.ast.statement.FunctionCall; +import cn.universal.core.engine.parsing.ast.statement.IfStatement; +import cn.universal.core.engine.parsing.ast.statement.Import; +import cn.universal.core.engine.parsing.ast.statement.LambdaFunction; +import cn.universal.core.engine.parsing.ast.statement.MapOrArrayAccess; +import cn.universal.core.engine.parsing.ast.statement.MemberAccess; +import cn.universal.core.engine.parsing.ast.statement.MethodCall; +import cn.universal.core.engine.parsing.ast.statement.NewStatement; +import cn.universal.core.engine.parsing.ast.statement.Return; +import cn.universal.core.engine.parsing.ast.statement.Spread; +import cn.universal.core.engine.parsing.ast.statement.Throw; +import cn.universal.core.engine.parsing.ast.statement.TryStatement; +import cn.universal.core.engine.parsing.ast.statement.VariableAccess; +import cn.universal.core.engine.parsing.ast.statement.VariableDefine; +import cn.universal.core.engine.parsing.ast.statement.VariableDestructuringDefine; +import cn.universal.core.engine.parsing.ast.statement.WhileStatement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** 语法解析器 */ +public class Parser { + + public static final String ANONYMOUS_VARIABLE = "-anonymous"; + private static final TokenType[][] BINARY_OPERATOR_PRECEDENCE = + new TokenType[][] { + new TokenType[] {Assignment}, + new TokenType[] { + RShift2Equal, + RShiftEqual, + LShiftEqual, + XorEqual, + BitOrEqual, + BitAndEqual, + PercentEqual, + ForwardSlashEqual, + AsteriskEqual, + MinusEqual, + PlusEqual + }, + new TokenType[] {Or, SqlOr}, + new TokenType[] {And, SqlAnd}, + new TokenType[] {BitOr}, + new TokenType[] {Xor}, + new TokenType[] {BitAnd}, + new TokenType[] {EqualEqualEqual, Equal, NotEqualEqual, NotEqual, SqlNotEqual}, + new TokenType[] {Less, LessEqual, Greater, GreaterEqual, InstanceOf}, + new TokenType[] {Plus, Minus}, + new TokenType[] {LShift, RShift, Rshift2}, + new TokenType[] {Asterisk, ForwardSlash, Percentage} + }; + private static final TokenType[][] LINQ_BINARY_OPERATOR_PRECEDENCE = + new TokenType[][] { + new TokenType[] { + RShift2Equal, + RShiftEqual, + LShiftEqual, + XorEqual, + BitOrEqual, + BitAndEqual, + PercentEqual, + ForwardSlashEqual, + AsteriskEqual, + MinusEqual, + PlusEqual + }, + new TokenType[] {Or, SqlOr}, + new TokenType[] {And, SqlAnd}, + new TokenType[] {BitOr}, + new TokenType[] {Xor}, + new TokenType[] {BitAnd}, + new TokenType[] {Assignment, EqualEqualEqual, Equal, NotEqualEqual, NotEqual, SqlNotEqual}, + new TokenType[] {Less, LessEqual, Greater, GreaterEqual, InstanceOf}, + new TokenType[] {Plus, Minus}, + new TokenType[] {LShift, RShift, Rshift2}, + new TokenType[] {ForwardSlash, Asterisk, Percentage} + }; + private static final TokenType[] UNARY_OPERATORS = + new TokenType[] { + MinusMinus, PlusPlus, BitNot, + Minus, Plus, Not + }; + private static final List KEYWORDS = + Arrays.asList( + "import", + "as", + "var", + "let", + "const", + "return", + "break", + "continue", + "if", + "for", + "in", + "new", + "true", + "false", + "null", + "else", + "try", + "catch", + "finally", + "async", + "while", + "exit", + "and", + "or", + "throw" /*, "assert"*/); + private static final List LINQ_KEYWORDS = + Arrays.asList( + "from", "join", "left", "group", "by", "as", "having", "and", "or", "in", "where", "on", + "limit", "offset"); + private VarScope varNames = new VarScope(); + private final VarScope rootvarNames = varNames; + private final List spans = new ArrayList<>(); + private final Set varIndices = new LinkedHashSet<>(); + private int varCount = 0; + private int linqLevel = 0; + private boolean requiredNew = true; + private TokenStream stream; + private final List defines = new ArrayList<>(); + + public Set getVarIndices() { + return varIndices; + } + + public List parse(String source) { + List nodes = new ArrayList<>(); + push(); + stream = Tokenizer.tokenize(source); + while (stream.hasMore()) { + Node node = parseStatement(); + if (node != null) { + validateNode(node); + nodes.add(node); + } + } + pop(); + return nodes; + } + + private void validateNode(Node node) { + if (node instanceof Literal) { + MagicScriptError.error("literal cannot be used alone", node.getSpan()); + } + } + + private Node parseStatement() { + return parseStatement(false); + } + + private Node parseStatement(boolean expectRightCurly) { + Node result = null; + if (stream.match("import", false)) { + result = parseImport(); + } else if (matchVarDefine()) { + result = parseVarDefine(); + } else if (stream.match("if", false)) { + result = parseIfStatement(); + } else if (stream.match("return", false)) { + result = parseReturn(); + } else if (stream.match("for", false)) { + result = parseForStatement(); + } else if (stream.match("while", false)) { + result = parseWhileStatement(); + } else if (stream.match("continue", false)) { + result = new Continue(stream.consume().getSpan()); + } else if (stream.match("async", false)) { + result = parseAsync(); + } else if (stream.match("try", false)) { + result = parseTryStatement(); + } else if (stream.match("break", false)) { + result = new Break(stream.consume().getSpan()); + } else if (stream.match("exit", false)) { + result = parseExit(); + } else if (stream.match("assert", false)) { + result = parseAssert(); + } else if (stream.match("throw", false)) { + result = parseThrow(); + } else { + int index = stream.makeIndex(); + if (matchTypeDefine()) { + stream.resetIndex(index); + result = parseVarDefine(); + } + if (result == null) { + stream.resetIndex(index); + result = parseExpression(expectRightCurly); + } + } + // consume semi-colons as statement delimiters + while (stream.match(";", true)) {} + return result; + } + + private boolean matchTypeDefine() { + boolean typeDefine = stream.match(Identifier, true); + if (!typeDefine) { + return false; + } + int index = stream.makeIndex(); + try { + if ("new".equals(stream.getPrev().getText())) { + return false; + } + if (stream.match(Identifier, false)) { + return true; + } + int end = stream.getPrev().getSpan().getEnd(); + if (stream.hasMore() && stream.consume().getSpan().getStart() == end) { + return false; + } + } finally { + stream.resetIndex(index); + } + // destructuring support + boolean isMapAccess; + if ((isMapAccess = stream.match(true, LeftCurly)) || stream.match(true, LeftBracket)) { + do { + if (!stream.match(true, Identifier)) { + return false; + } + } while (stream.match(true, Comma)); + if (isMapAccess) { + return stream.match(true, RightCurly); + } else { + return stream.match(true, RightBracket); + } + } + return false; + } + + private boolean matchVarDefine() { + return stream.match(false, "var", "let", "const"); + } + + private VarIndex add(String name) { + VarScope varIndices = varNames; + do { + for (int j = varIndices.size() - 1; j >= 0; j--) { + VarIndex varIndex = varIndices.get(j); + if (varIndex.getName().equals(name)) { + return defines.contains(name) ? varIndex.scoped() : varIndex; + } + } + } while ((varIndices = varIndices.getParent()) != null); + return add(new VarIndex(name, varCount++, true), true); + } + + private VarIndex add(VarIndex varIndex) { + return add(varIndex, false); + } + + private VarIndex add(VarIndex varIndex, boolean isRoot) { + if (defines.contains(varIndex.getName())) { + varIndex = varIndex.scoped(); + } + if (isRoot) { + rootvarNames.add(varIndex); + } else { + varNames.add(varIndex); + } + varIndices.add(varIndex); + return varIndex; + } + + private VarIndex forceAdd(String name) { + return forceAdd(name, false); + } + + private VarIndex forceAdd(String name, boolean isConst) { + return add(new VarIndex(name, varCount++, false, isConst)); + } + + private void push() { + varNames = varNames.push(); + } + + private void pop() { + varNames = varNames.pop(); + } + + private Span addSpan(Span opening, Span ending) { + addSpan(opening); + addSpan(ending); + return addSpan(new Span(opening, ending)); + } + + private Span addSpan(String source, int start, int end) { + return addSpan(new Span(source, start, end)); + } + + private Span addSpan(Span span) { + this.spans.add(span); + return span; + } + + private Node parseExit() { + Span opening = stream.expect("exit").getSpan(); + List expressionList = new ArrayList<>(); + do { + expressionList.add(parseExpression()); + } while (stream.match(Comma, true)); + return new Exit(addSpan(opening, stream.getPrev().getSpan()), expressionList); + } + + private Node parseThrow() { + Span opening = stream.consume().getSpan(); + Expression expression = parseExpression(); + return new Throw(addSpan(opening, stream.getPrev().getSpan()), expression); + } + + private Node parseAssert() { + int index = stream.makeIndex(); + try { + Span opening = stream.expect("assert").getSpan(); + Expression condition = parseExpression(); + stream.expect(Colon); + List expressionList = new ArrayList<>(); + do { + expressionList.add(parseExpression()); + } while (stream.match(Comma, true)); + return new Assert(addSpan(opening, stream.getPrev().getSpan()), condition, expressionList); + } catch (Exception e) { + stream.resetIndex(index); + return parseExpression(); + } + } + + private Expression parseAsync() { + Span opening = stream.expect("async").getSpan(); + requiredNew = false; + Expression expression = parseExpression(); + requiredNew = true; + if (expression instanceof MethodCall + || expression instanceof FunctionCall + || expression instanceof LambdaFunction) { + return new AsyncCall(addSpan(opening, stream.getPrev().getSpan()), expression); + } + MagicScriptError.error( + "Expected MethodCall or FunctionCall or LambdaFunction", stream.getPrev().getSpan()); + return null; + } + + private Import parseImport() { + Span opening = stream.expect("import").getSpan(); + if (stream.hasMore()) { + Token expected = stream.consume(); + String packageName = null; + boolean isStringLiteral = expected.getType() == StringLiteral; + if (isStringLiteral) { + packageName = createStringLiteral(expected).getValue(); + } else if (expected.getType() == Identifier) { + Span startSpan = expected.getSpan(); + packageName = startSpan.getText(); + while (stream.match(true, Period)) { + isStringLiteral = true; + if (stream.match(false, Asterisk)) { + expected = stream.consume(); + break; + } + expected = stream.expect(Identifier); + } + if (isStringLiteral) { + packageName = new Span(startSpan, expected.getSpan()).getText(); + } + } else { + MagicScriptError.error( + "Expected identifier or string, but got stream is " + expected.getType().getError(), + stream.getPrev().getSpan()); + } + String varName = packageName; + if (isStringLiteral) { + if (stream.match("as", true)) { + expected = stream.expect(Identifier); + checkKeyword(expected.getSpan()); + varName = expected.getSpan().getText(); + } else { + String temp = packageName; + if (!temp.startsWith("@")) { + int index = temp.lastIndexOf("."); + if (index != -1) { + temp = temp.substring(index + 1); + } + } else { + MagicScriptError.error("Expected as", stream); + } + varName = temp; + } + } + // 发现是危险包,则删除 + // if (SafeRule.filter(packageName)) { + // MagicScriptError.error("只允许导入[cn.hutool]的部分工具包,请查阅(https://hutool.cn/)", + // stream.getPrev().getSpan()); + // } + return new Import( + addSpan(opening, expected.getSpan()), packageName, forceAdd(varName), !isStringLiteral); + } + MagicScriptError.error( + "Expected identifier or string, but got stream is EOF", stream.getPrev().getSpan()); + return null; + } + + private TryStatement parseTryStatement() { + Token opening = stream.expect("try"); + push(); + List tryResources = new ArrayList<>(); + if (stream.match("(", true)) { + if (stream.match(")", false)) { + // 空的 try-with-resource + } else { + while (!stream.match(")", false)) { + if (stream.match(";", true)) { + continue; + } + VariableDefine result = null; + if (matchVarDefine()) { + result = parseVarDefine(); + } else { + if (stream.match(false, KEYWORDS.toArray(new String[0]))) { + MagicScriptError.error("try 括号中只允许写赋值语句", stream.consume().getSpan()); + } + int index = stream.makeIndex(); + if (matchTypeDefine()) { + stream.resetIndex(index); + result = parseVarDefine(); + } + if (result == null) { + stream.resetIndex(index); + MagicScriptError.error("try 括号中只允许写赋值语句", stream.consume().getSpan()); + } + } + tryResources.add(result); + } + } + stream.expect(")"); + } + List tryBlocks = parseFunctionBody(); + pop(); + List catchBlocks = new ArrayList<>(); + List finallyBlocks = new ArrayList<>(); + VarIndex exceptionVarNode = null; + if (stream.match("catch", true)) { + push(); + if (stream.match("(", true)) { + exceptionVarNode = add(stream.expect(Identifier).getText()); + defines.add(exceptionVarNode.getName()); + stream.expect(")"); + } + catchBlocks.addAll(parseFunctionBody()); + pop(); + } + if (stream.match("finally", true)) { + push(); + finallyBlocks.addAll(parseFunctionBody()); + pop(); + } + return new TryStatement( + addSpan(opening.getSpan(), stream.getPrev().getSpan()), + exceptionVarNode, + tryBlocks, + tryResources, + catchBlocks, + finallyBlocks); + } + + private List parseFunctionBody() { + stream.expect("{"); + List blocks = new ArrayList<>(); + while (stream.hasMore() && !stream.match("}", false)) { + Node node = parseStatement(true); + if (node != null) { + validateNode(node); + blocks.add(node); + } + } + expectCloseing(); + return blocks; + } + + private Expression parseNewExpression(Span opening) { + Expression expression = parseAccessOrCall(Identifier, true); + if (expression instanceof MethodCall) { + MethodCall call = (MethodCall) expression; + Span span = + addSpan(opening.getSource(), opening.getStart(), stream.getPrev().getSpan().getEnd()); + return parseAccessOrCall(new NewStatement(span, call.getMethod(), call.getArguments())); + } else if (expression instanceof FunctionCall) { + FunctionCall call = (FunctionCall) expression; + Span span = + addSpan(opening.getSource(), opening.getStart(), stream.getPrev().getSpan().getEnd()); + return parseAccessOrCall(new NewStatement(span, call.getFunction(), call.getArguments())); + } + MagicScriptError.error( + "Expected MethodCall or FunctionCall or LambdaFunction", stream.getPrev().getSpan()); + return null; + } + + private VariableDefine parseVarDefine() { + Span opening = stream.consume().getSpan(); + boolean isMapAccess; + if ((isMapAccess = stream.match(false, LeftCurly)) || stream.match(false, LeftBracket)) { + stream.expect(LeftCurly, LeftBracket); + List tokens = new ArrayList<>(); + do { + Token token = stream.expect(Identifier); + tokens.add(token); + } while (stream.match(true, Comma)); + if (isMapAccess) { + stream.expect(RightCurly); + } else { + stream.expect(RightBracket); + } + stream.match(Assignment, true); + boolean isConst = "const".equals(opening.getText()); + VariableDestructuringDefine destructuring = + new VariableDestructuringDefine( + addSpan(opening, stream.getPrev().getSpan()), + tokens.size(), + parseExpression(), + isMapAccess); + for (Token token : tokens) { + String variableName = token.getSpan().getText(); + VarIndex varIndex = forceAdd(variableName, isConst); + defines.add(variableName); + VariableDefine variableDefine = + new VariableDefine( + addSpan(token.getSpan(), stream.getPrev().getSpan()), varIndex, null); + destructuring.add(variableDefine); + } + return destructuring; + } + Token token = stream.expect(Identifier); + boolean isConst = "const".equals(opening.getText()); + checkKeyword(token.getSpan()); + String variableName = token.getSpan().getText(); + if (stream.match(Assignment, true)) { + VarIndex varIndex = forceAdd(variableName, isConst); + defines.add(variableName); + return new VariableDefine( + addSpan(opening, stream.getPrev().getSpan()), varIndex, parseExpression()); + } else if (isConst) { + MagicScriptError.error("const修饰的变量需要给初始值", stream.getPrev().getSpan()); + } + return new VariableDefine( + addSpan(opening, stream.getPrev().getSpan()), forceAdd(variableName), null); + } + + private void checkKeyword(Span span) { + if (KEYWORDS.contains(span.getText())) { + MagicScriptError.error("变量名不能定义为关键字", span); + } + } + + private WhileStatement parseWhileStatement() { + Span openingWhile = stream.expect("while").getSpan(); + requiredNew = false; + Expression condition = parseExpression(); + requiredNew = true; + push(); + List trueBlock = parseFunctionBody(); + Span closingEnd = stream.getPrev().getSpan(); + pop(); + return new WhileStatement(addSpan(openingWhile, closingEnd), condition, trueBlock); + } + + private ForStatement parseForStatement() { + Span openingFor = stream.expect("for").getSpan(); + stream.expect("("); + push(); + Span index = null; + Span value = stream.expect(Identifier).getSpan(); + checkKeyword(value); + defines.add(value.getText()); + if (stream.match(Comma, true)) { + index = value; + value = stream.expect(Identifier).getSpan(); + defines.add(value.getText()); + checkKeyword(value); + } + VarIndex indexOrKeyNode = null; + if (index != null) { + indexOrKeyNode = forceAdd(index.getText()); + } + VarIndex valueNode = forceAdd(value.getText()); + stream.expect("in"); + Expression mapOrArray = parseExpression(); + stream.expect(")"); + List body = parseFunctionBody(); + VarIndex anonymous = forceAdd(ANONYMOUS_VARIABLE); + pop(); + return new ForStatement( + addSpan(openingFor, stream.getPrev().getSpan()), + indexOrKeyNode, + valueNode, + anonymous, + mapOrArray, + body); + } + + private Span expectCloseing() { + if (!stream.hasMore()) { + MagicScriptError.error("Did not find closing }.", stream.prev().getSpan()); + } + return stream.expect("}").getSpan(); + } + + private Node parseIfStatement() { + Span openingIf = stream.expect("if").getSpan(); + requiredNew = false; + Expression condition = parseExpression(); + requiredNew = true; + push(); + List trueBlock = parseFunctionBody(); + pop(); + List elseIfs = new ArrayList<>(); + List falseBlock = new ArrayList<>(); + while (stream.hasMore() && stream.match("else", true)) { + if (stream.hasMore() && stream.match("if", false)) { + Span elseIfOpening = stream.expect("if").getSpan(); + Expression elseIfCondition = parseExpression(); + push(); + List elseIfBlock = parseFunctionBody(); + Span elseIfSpan = + addSpan( + elseIfOpening, + elseIfBlock.size() > 0 + ? elseIfBlock.get(elseIfBlock.size() - 1).getSpan() + : elseIfOpening); + pop(); + elseIfs.add( + new IfStatement( + elseIfSpan, elseIfCondition, elseIfBlock, new ArrayList<>(), new ArrayList<>())); + } else { + push(); + falseBlock.addAll(parseFunctionBody()); + pop(); + break; + } + } + Span closingEnd = stream.getPrev().getSpan(); + + return new IfStatement( + addSpan(openingIf, closingEnd), condition, trueBlock, elseIfs, falseBlock); + } + + private Node parseReturn() { + Span returnSpan = stream.expect("return").getSpan(); + if (stream.match(false, ";", "}")) { + return new Return(returnSpan, null); + } + Expression returnValue = parseExpression(); + return new Return(addSpan(returnSpan, returnValue.getSpan()), returnValue); + } + + public Expression parseExpression() { + return parseTernaryOperator(); + } + + public Expression parseExpression(boolean expectRightCurly) { + return parseTernaryOperator(expectRightCurly); + } + + private Expression parseTernaryOperator(boolean expectRightCurly) { + Expression condition = parseBinaryOperator(0, expectRightCurly); + if (stream.match(QuestionMark, true)) { + Expression trueExpression = parseTernaryOperator(expectRightCurly); + stream.expect(Colon); + Expression falseExpression = parseTernaryOperator(expectRightCurly); + if (condition instanceof AssigmentOperation) { + AssigmentOperation operation = (AssigmentOperation) condition; + operation.setRightOperand( + new TernaryOperation(operation.getRightOperand(), trueExpression, falseExpression)); + return operation; + } + return new TernaryOperation(condition, trueExpression, falseExpression); + } else { + return condition; + } + } + + private Expression parseTernaryOperator() { + return parseTernaryOperator(false); + } + + private Expression parseBinaryOperator( + TokenType[][] precedence, int level, boolean expectRightCurly) { + int nextLevel = level + 1; + Expression left = + nextLevel == precedence.length + ? parseUnaryOperator(expectRightCurly) + : parseBinaryOperator(nextLevel, expectRightCurly); + + TokenType[] operators = precedence[level]; + while (stream.hasMore() && stream.match(false, operators)) { + Token operator = stream.consume(); + if (operator.getType().isInLinq() && linqLevel == 0) { + MagicScriptError.error(operator.getText() + " 只能在Linq中使用", stream); + } + Expression right = + nextLevel == precedence.length + ? parseUnaryOperator(expectRightCurly) + : parseBinaryOperator(nextLevel, expectRightCurly); + left = BinaryOperation.create(left, operator, right, linqLevel); + } + addSpan(left.getSpan()); + return left; + } + + private Expression parseBinaryOperator(int level, boolean expectRightCurly) { + if (linqLevel > 0) { + return parseBinaryOperator(LINQ_BINARY_OPERATOR_PRECEDENCE, level, expectRightCurly); + } + return parseBinaryOperator(BINARY_OPERATOR_PRECEDENCE, level, expectRightCurly); + } + + private Expression parseUnaryOperator(boolean expectRightCurly) { + if (stream.match(false, UNARY_OPERATORS)) { + return new UnaryOperation(stream.consume(), parseUnaryOperator(expectRightCurly)); + } else { + if (stream.match(LeftParantheses, false)) { // ( + Span openSpan = stream.expect(LeftParantheses).getSpan(); + int index = stream.makeIndex(); + List parameters = new ArrayList<>(); + push(); + try { + while (stream.match(Identifier, false)) { + Token identifier = stream.expect(Identifier); + checkKeyword(identifier.getSpan()); + if (requiredNew) { + parameters.add(forceAdd(identifier.getSpan().getText())); + } else { + parameters.add(add(identifier.getSpan().getText())); + } + if (stream.match(Comma, true)) { // , + continue; + } + if (stream.match(RightParantheses, true)) { // ) + if (stream.match(Lambda, true)) { // => + return parseLambdaBody(openSpan, parameters); + } + break; + } + } + if (stream.match(RightParantheses, true) && stream.match(Lambda, true)) { + return parseLambdaBody(openSpan, parameters); + } + } finally { + pop(); + } + stream.resetIndex(index); + Expression expression = parseExpression(); + stream.expect(RightParantheses); + return parseAccessOrCall(expression); + } else { + Expression expression = parseAccessOrCallOrLiteral(expectRightCurly); + if (expression instanceof VariableSetter) { + if (stream.match(false, PlusPlus, MinusMinus)) { + return new UnaryOperation(stream.consume(), expression, true); + } + } + + return expression; + } + } + } + + private Expression parseLambdaBody(Span openSpan, List parameters) { + defines.clear(); + int index = stream.makeIndex(); + List childNodes = new ArrayList<>(); + try { + Expression expression = parseExpression(); + childNodes.add(new Return(new Span("return", 0, 6), expression)); + return new LambdaFunction(addSpan(openSpan, expression.getSpan()), parameters, childNodes); + } catch (Exception e) { + stream.resetIndex(index); + if (stream.match(LeftCurly, true)) { + while (stream.hasMore() && !stream.match(false, "}")) { + Node node = parseStatement(true); + validateNode(node); + childNodes.add(node); + } + Span closeSpan = expectCloseing(); + return new LambdaFunction(addSpan(openSpan, closeSpan), parameters, childNodes); + } else { + Node node = parseStatement(); + childNodes.add(new Return(addSpan("return", 0, 6), node)); + return new LambdaFunction(addSpan(openSpan, node.getSpan()), parameters, childNodes); + } + } finally { + defines.clear(); + } + } + + private Expression parseSpreadAccess(Token spread) { + Expression target = parseExpression(); + return new Spread(addSpan(spread.getSpan(), target.getSpan()), target); + } + + private Expression parseSpreadAccess() { + Token spread = stream.expect(Spread); + return parseSpreadAccess(spread); + } + + private Expression parseSelect() { + Span opeing = stream.expect("select", true).getSpan(); + linqLevel++; + List fields = parseLinqFields(); + stream.expect("from", true); + LinqField from = parseLinqField(); + List joins = parseLinqJoins(); + LinqExpression where = null; + if (stream.match("where", true, true)) { + where = new LinqExpression(parseExpression()); + } + List groups = parseGroup(); + LinqExpression having = null; + if (stream.match("having", true, true)) { + having = new LinqExpression(parseExpression()); + } + List orders = parseLinqOrders(); + linqLevel--; + Expression limit = null; + Expression offset = null; + if (stream.match("limit", true, true)) { + limit = parseExpression(); + if (stream.match("offset", true, true)) { + offset = parseExpression(); + } + } + Span close = stream.getPrev().getSpan(); + return new LinqSelect( + addSpan(opeing, close), fields, from, joins, where, groups, having, orders, limit, offset); + } + + private List parseGroup() { + List groups = new ArrayList<>(); + if (stream.match("group", true, true)) { + stream.expect("by", true); + do { + Expression expression = parseExpression(); + groups.add(new LinqField(expression.getSpan(), expression, null)); + } while (stream.match(Comma, true)); + } + return groups; + } + + private List parseLinqOrders() { + List orders = new ArrayList<>(); + if (stream.match("order", true, true)) { + stream.expect("by", true); + do { + Expression expression = parseExpression(); + int order = 1; + if (stream.match(false, true, "desc", "asc")) { + if ("desc".equalsIgnoreCase(stream.consume().getText())) { + order = -1; + } + } + orders.add( + new LinqOrder( + addSpan(expression.getSpan(), stream.getPrev().getSpan()), + expression, + null, + order)); + } while (stream.match(Comma, true)); + } + return orders; + } + + private List parseLinqFields() { + List fields = new ArrayList<>(); + do { + Expression expression = parseExpression(); + + if (stream.match(Identifier, false) && !stream.match(LINQ_KEYWORDS, false, true)) { + if (expression instanceof WholeLiteral) { + MagicScriptError.error("* 后边不能跟别名", stream); + } else if (expression instanceof MemberAccess && ((MemberAccess) expression).isWhole()) { + MagicScriptError.error(expression.getSpan().getText() + " 后边不能跟别名", stream); + } + Span alias = stream.consume().getSpan(); + fields.add( + new LinqField(addSpan(expression.getSpan(), alias), expression, add(alias.getText()))); + } else { + fields.add(new LinqField(expression.getSpan(), expression, null)); + } + } while (stream.match(Comma, true)); // , + if (fields.isEmpty()) { + MagicScriptError.error("至少要查询一个字段", stream); + } + return fields; + } + + private List parseLinqJoins() { + List joins = new ArrayList<>(); + do { + boolean isLeft = stream.match("left", false, true); + Span opeing = isLeft ? stream.consume().getSpan() : null; + if (stream.match("join", true, true)) { + opeing = isLeft ? opeing : stream.getPrev().getSpan(); + LinqField target = parseLinqField(); + stream.expect("on", true); + Expression condition = parseExpression(); + joins.add( + new LinqJoin(addSpan(opeing, stream.getPrev().getSpan()), isLeft, target, condition)); + } + } while (stream.match(false, true, "left", "join")); + return joins; + } + + private LinqField parseLinqField() { + Expression expression = parseExpression(); + if (stream.match(Identifier, false) && !stream.match(LINQ_KEYWORDS, false, true)) { + Span alias = stream.expect(Identifier).getSpan(); + return new LinqField(addSpan(expression.getSpan(), alias), expression, add(alias.getText())); + } + return new LinqField(expression.getSpan(), expression, null); + } + + private Expression parseAccessOrCallOrLiteral(boolean expectRightCurly) { + Expression expression = null; + if (expectRightCurly && stream.match("}", false)) { + return null; + } else if (stream.match(Spread, false)) { + expression = parseSpreadAccess(); + } else if (stream.match(Identifier, false)) { + if (stream.match("async", false)) { + expression = parseAsync(); + } else if (stream.match("select", false, true)) { + expression = parseSelect(); + } else { + expression = parseAccessOrCall(TokenType.Identifier, false); + } + } else if (stream.match(LeftCurly, false)) { + expression = parseMapLiteral(); + } else if (stream.match(LeftBracket, false)) { + expression = parseListLiteral(); + } else if (stream.match(StringLiteral, false)) { + expression = createStringLiteral(stream.expect(StringLiteral)); + } else if (stream.match(BooleanLiteral, false)) { + expression = new BooleanLiteral(stream.expect(BooleanLiteral).getSpan()); + } else if (stream.match(DoubleLiteral, false)) { + expression = new DoubleLiteral(stream.expect(DoubleLiteral).getSpan()); + } else if (stream.match(FloatLiteral, false)) { + expression = new FloatLiteral(stream.expect(FloatLiteral).getSpan()); + } else if (stream.match(ByteLiteral, false)) { + Token token = stream.expect(ByteLiteral); + expression = + token.getValue() != null + ? new ByteLiteral(token.getSpan(), token.getValue()) + : new ByteLiteral(token.getSpan()); + } else if (stream.match(ShortLiteral, false)) { + expression = new ShortLiteral(stream.expect(ShortLiteral).getSpan()); + } else if (stream.match(IntegerLiteral, false)) { + Token token = stream.expect(IntegerLiteral); + expression = + token.getValue() != null + ? new IntegerLiteral(token.getSpan(), token.getValue()) + : new IntegerLiteral(token.getSpan()); + } else if (stream.match(LongLiteral, false)) { + Token token = stream.expect(LongLiteral); + expression = + token.getValue() != null + ? new LongLiteral(token.getSpan(), token.getValue()) + : new LongLiteral(token.getSpan()); + } else if (stream.match(DecimalLiteral, false)) { + expression = new BigDecimalLiteral(stream.expect(DecimalLiteral).getSpan()); + } else if (stream.match(RegexpLiteral, false)) { + Token token = stream.expect(RegexpLiteral); + expression = new RegexpLiteral(token.getSpan(), token); + } else if (stream.match(NullLiteral, false)) { + expression = new NullLiteral(stream.expect(NullLiteral).getSpan()); + } else if (linqLevel > 0 && stream.match(Asterisk, false)) { + expression = new WholeLiteral(stream.expect(Asterisk).getSpan()); + } else if (stream.match(Language, false)) { + expression = new LanguageExpression(stream.consume().getSpan(), stream.consume().getSpan()); + } + if (expression == null) { + MagicScriptError.error( + "Expected a variable, field, map, array, function or method call, or literal.", stream); + } + return parseAccessOrCall(expression); + } + + private StringLiteral createStringLiteral(Token token) { + if (token.getTokenStream() == null) { + return new StringLiteral(token); + } + TokenStream tempStream = this.stream; + this.stream = token.getTokenStream(); + List expressionList = new ArrayList<>(); + while (this.stream.hasMore()) { + expressionList.add(parseExpression()); + } + this.stream = tempStream; + return new StringLiteral(token, expressionList); + } + + private Expression parseMapLiteral() { + Span openCurly = stream.expect(LeftCurly).getSpan(); + + List keys = new ArrayList<>(); + List values = new ArrayList<>(); + while (stream.hasMore() && !stream.match("}", false)) { + Expression key; + if (stream.hasPrev()) { + Token prev = stream.getPrev(); + if (stream.match(Spread, false) + && (prev.getType() == LeftCurly || prev.getType() == Comma)) { + Token spread = stream.expect(Spread); + keys.add(null); + values.add(parseSpreadAccess(spread)); + if (stream.match(false, Comma, RightCurly)) { + stream.match(Comma, true); + } + continue; + } + } + boolean isStringKey; + if (isStringKey = stream.match(StringLiteral, false)) { // "key" 'key' """key""" + key = createStringLiteral(stream.expect(StringLiteral)); + } else if (stream.match(LeftBracket, true)) { // [key] + key = parseExpression(); + stream.expect(RightBracket); + } else { // key + key = createStringLiteral(stream.expect(Identifier)); + } + keys.add(key); + if (stream.match(false, Comma, RightCurly)) { + stream.match(Comma, true); + if (!isStringKey && !(key instanceof VariableAccess)) { + values.add(new VariableAccess(key.getSpan(), add(key.getSpan().getText()))); + } else { + values.add(key); + } + } else { + stream.expect(":"); + values.add(parseExpression()); + if (!stream.match("}", false)) { + stream.expect(Comma); + } + } + } + Span closeCurly = stream.expect("}").getSpan(); + return new MapLiteral(addSpan(openCurly, closeCurly), keys, values); + } + + private Expression parseListLiteral() { + Span openBracket = stream.expect(LeftBracket).getSpan(); + + List values = new ArrayList<>(); + while (stream.hasMore() && !stream.match(RightBracket, false)) { + values.add(parseExpression()); + if (!stream.match(RightBracket, false)) { + stream.expect(Comma); + } + } + + Span closeBracket = stream.expect(RightBracket).getSpan(); + return new ListLiteral(addSpan(openBracket, closeBracket), values); + } + + private Expression parseAccessOrCall(TokenType tokenType, boolean isNew) { + Token token = stream.expect(tokenType); + Span identifier = token.getSpan(); + if (tokenType == Identifier && "new".equals(identifier.getText())) { + return parseNewExpression(identifier); + } + if (tokenType == Identifier && stream.match(Lambda, true)) { + push(); + String name = identifier.getText(); + Expression expression = + parseLambdaBody( + identifier, Collections.singletonList(requiredNew ? forceAdd(name) : add(name))); + pop(); + return expression; + } + Expression result = + tokenType == StringLiteral + ? createStringLiteral(token) + : new VariableAccess(identifier, add(identifier.getText())); + return parseAccessOrCall(result, isNew); + } + + private Expression parseAccessOrCall(Expression target) { + return parseAccessOrCall(target, false); + } + + private Expression parseAccessOrCall(Expression target, boolean isNew) { + while (stream.hasMore() + && stream.match(false, LeftParantheses, LeftBracket, Period, QuestionPeriod, ColonColon)) { + if (stream.match(ColonColon, false)) { + Span open = stream.consume().getSpan(); + List arguments = Collections.emptyList(); + Token identifier = stream.expect(Identifier); + Span closing = identifier.getSpan(); + if (stream.match(LeftParantheses, false)) { + arguments = parseArguments(); + closing = stream.expect(RightParantheses).getSpan(); + } + target = + new ClassConverter(addSpan(open, closing), identifier.getText(), target, arguments); + } + // function or method call + else if (stream.match(LeftParantheses, false)) { + List arguments = parseArguments(); + Span closingSpan = stream.expect(RightParantheses).getSpan(); + if (target instanceof VariableAccess || target instanceof MapOrArrayAccess) { + target = + new FunctionCall( + addSpan(target.getSpan(), closingSpan), target, arguments, linqLevel > 0); + } else if (target instanceof MemberAccess) { + target = + new MethodCall( + addSpan(target.getSpan(), closingSpan), + (MemberAccess) target, + arguments, + linqLevel > 0); + } else { + MagicScriptError.error("Expected a variable, field or method.", stream); + } + if (isNew) { + break; + } + } + + // map or array access + else if (stream.match(LeftBracket, true)) { + Expression keyOrIndex = parseExpression(); + Span closingSpan = stream.expect(RightBracket).getSpan(); + target = new MapOrArrayAccess(addSpan(target.getSpan(), closingSpan), target, keyOrIndex); + } + + // field or method access + else if (stream.match(false, Period, QuestionPeriod)) { + boolean optional = stream.consume().getType() == QuestionPeriod; + if (linqLevel > 0 && stream.match(Asterisk, false)) { + target = new MemberAccess(target, optional, stream.expect(Asterisk).getSpan(), true); + } else { + target = + new MemberAccess( + target, optional, stream.expect(Identifier, SqlAnd, SqlOr).getSpan(), false); + } + } + } + return target; + } + + /** Does not consume the closing parentheses. */ + private List parseArguments() { + stream.expect(LeftParantheses); + List arguments = new ArrayList(); + while (stream.hasMore() && !stream.match(RightParantheses, false)) { + arguments.add(parseExpression()); + if (!stream.match(RightParantheses, false)) { + stream.expect(Comma); + } + } + return arguments; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/RegexpToken.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/RegexpToken.java new file mode 100644 index 0000000..a41c30e --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/RegexpToken.java @@ -0,0 +1,27 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing; + +public class RegexpToken extends Token { + + private final int flag; + + public RegexpToken(TokenType type, Span span, int regFlag) { + super(type, span); + this.flag = regFlag; + } + + public int getFlag() { + return flag; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/Span.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/Span.java new file mode 100644 index 0000000..01b90c4 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/Span.java @@ -0,0 +1,250 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing; + +import java.util.Objects; + +/** + * A span within a source string denoted by start and end index, with the latter being exclusive. + */ +public class Span { + + /** the source string this span refers to */ + private final String source; + + /** Cached String instance to reduce pressure on GC */ + private final String cachedText; + + /** start index in source string, starting at 0 */ + private final int start; + + /** end index in source string, exclusive, starting at 0 */ + private final int end; + + private Line line; + + public Span(String source) { + this(source, 0, source.length()); + } + + public Span(String source, int start, int end) { + if (start > end) { + throw new IllegalArgumentException("Start must be <= end."); + } + if (start < 0) { + throw new IndexOutOfBoundsException("Start must be >= 0."); + } + // if (start > source.length() - 1) { + // throw new IndexOutOfBoundsException("Start outside of string."); + // } + if (end > source.length()) { + throw new IndexOutOfBoundsException("End outside of string."); + } + + this.source = source; + this.start = start; + this.end = end; + this.cachedText = source.substring(start, end); + } + + public Span(Span start, Span end) { + if (!start.source.equals(end.source)) { + throw new IllegalArgumentException("The two spans do not reference the same source."); + } + if (start.start > end.end) { + throw new IllegalArgumentException("Start must be <= end."); + } + if (start.start < 0) { + throw new IndexOutOfBoundsException("Start must be >= 0."); + } + if (start.start > start.source.length() - 1) { + throw new IndexOutOfBoundsException("Start outside of string."); + } + if (end.end > start.source.length()) { + throw new IndexOutOfBoundsException("End outside of string."); + } + + this.source = start.source; + this.start = start.start; + this.end = end.end; + this.cachedText = source.substring(this.start, this.end); + } + + /** Returns the text referenced by this span */ + public String getText() { + return cachedText; + } + + /** Returns the index of the first character of this span. */ + public int getStart() { + return start; + } + + /** Returns the index of the last character of this span plus 1. */ + public int getEnd() { + return end; + } + + /** Returns the source string this span references. */ + public String getSource() { + return source; + } + + @Override + public String toString() { + return "Span [text=" + getText() + ", start=" + start + ", end=" + end + "]"; + } + + /** + * Returns the line this span is on. Does not return a correct result for spans across multiple + * lines. + */ + public Line getLine() { + if (this.line != null) { + return this.line; + } + int lineStart = start; + while (lineStart < end) { + if (lineStart < 0) { + break; + } + char c = source.charAt(lineStart); + if (c == '\n') { + lineStart = lineStart + 1; + break; + } + lineStart--; + } + if (lineStart < 0) { + lineStart = 0; + } + + int lineEnd = end; + while (true) { + if (lineEnd > source.length() - 1) { + break; + } + char c = source.charAt(lineEnd); + if (c == '\n') { + break; + } + lineEnd++; + } + + int lineNumber = 0; + int idx = lineStart; + while (idx > 0 && idx < end) { + char c = source.charAt(idx); + if (c == '\n') { + lineNumber++; + } + idx--; + } + lineNumber++; + idx = lineStart + 1; + int endLineNumber = lineNumber; + while (idx < lineEnd) { + char c = source.charAt(idx); + if (c == '\n') { + endLineNumber++; + } + idx++; + } + int startCol = this.start - lineStart + 1; + int endCol = startCol + this.end - this.start - 1; + this.line = new Line(source, lineStart, lineEnd, lineNumber, endLineNumber, startCol, endCol); + return this.line; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Span)) { + return false; + } + Span span = (Span) o; + return start == span.start + && end == span.end + && Objects.equals(source, span.source) + && Objects.equals(cachedText, span.cachedText) + && Objects.equals(line, span.line); + } + + @Override + public int hashCode() { + return Objects.hash(source, cachedText, start, end, line); + } + + /** A line within a Source */ + public static class Line { + + private final String source; + private final int start; + private final int end; + private final int lineNumber; + private final int endLineNumber; + private final int startCol; + private final int endCol; + + public Line( + String source, + int start, + int end, + int lineNumber, + int endLineNumber, + int startCol, + int endCol) { + this.source = source; + this.start = start; + this.end = end; + this.lineNumber = lineNumber; + this.endLineNumber = endLineNumber; + this.startCol = startCol; + this.endCol = endCol; + } + + public int getStartCol() { + return startCol; + } + + public int getEndCol() { + return endCol; + } + + public String getSource() { + return source; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + + public int getEndLineNumber() { + return endLineNumber; + } + + public int getLineNumber() { + return lineNumber; + } + + public String getText() { + return source.substring(start, end); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/Token.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/Token.java new file mode 100644 index 0000000..21eb01c --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/Token.java @@ -0,0 +1,66 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing; + +public class Token { + + private final TokenType type; + + private final Span span; + private final Object value; + private TokenStream tokenStream; + + public Token(TokenType type, Span span) { + this.type = type; + this.span = span; + this.value = null; + } + + public Token(TokenType type, Span span, Object value) { + this.type = type; + this.span = span; + this.value = value; + } + + public Token(TokenType type, Span span, TokenStream tokenStream) { + this.type = type; + this.span = span; + this.tokenStream = tokenStream; + this.value = null; + } + + public TokenType getType() { + return type; + } + + public Span getSpan() { + return span; + } + + public String getText() { + return span.getText(); + } + + public Object getValue() { + return value; + } + + @Override + public String toString() { + return "Token [type=" + type + ", span=" + span + "]"; + } + + public TokenStream getTokenStream() { + return tokenStream; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/TokenStream.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/TokenStream.java new file mode 100644 index 0000000..454adc5 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/TokenStream.java @@ -0,0 +1,276 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing; + +import cn.universal.core.engine.MagicScriptError; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** 对List进行封装,提供匹配相关方法,方便语法解析 */ +public class TokenStream { + + private final List tokens; + + private final int end; + + private int index; + + public TokenStream(List tokens) { + this.tokens = tokens; + this.index = 0; + this.end = tokens.size(); + } + + /** 当前是否可读 */ + public boolean hasMore() { + return index < end; + } + + /** 是否有下一个Token */ + public boolean hasNext() { + return index + 1 < end; + } + + /** 是否有前一个Token */ + public boolean hasPrev() { + return index > 0; + } + + /** 标记当前位置,和resetIndex搭配使用。 */ + public int makeIndex() { + return index; + } + + /** 重置当前位置,和makeIndex搭配使用 */ + public void resetIndex(int index) { + this.index = index; + } + + /** 无条件消耗掉当前Token */ + public Token consume() { + if (!hasMore()) { + throw new RuntimeException("流已经遍历完毕"); + } + return tokens.get(index++); + } + + /** 获取下一个Token并改变当前位置 */ + public Token next() { + if (!hasMore()) { + throw new RuntimeException("流已经遍历完毕"); + } + return tokens.get(++index); + } + + /** 获取前一个Token并改变当前位置 */ + public Token prev() { + if (index == 0) { + throw new RuntimeException("流已经遍历完毕"); + } + return tokens.get(--index); + } + + /** 获取前一个Token,不改变当前位置 */ + public Token getPrev() { + if (index == 0) { + throw new RuntimeException("流已经遍历完毕"); + } + return tokens.get(index - 1); + } + + /** 期待下一个Token是给定的类型中之一 */ + public Token expect(TokenType... types) { + if (!match(true, types)) { + Token token = index < tokens.size() ? tokens.get(index) : null; + Span span = token != null ? token.getSpan() : null; + if (span == null) { + MagicScriptError.error( + "期待 '" + + Stream.of(types).map(TokenType::getError).collect(Collectors.joining("','")) + + "', 但是流已经遍历完毕", + this); + } else { + MagicScriptError.error( + "期待 '" + + Stream.of(types).map(TokenType::getError).collect(Collectors.joining("','")) + + "', 获得 '" + + token.getText() + + "'", + span); + } + return null; // 执行不到这里 + } else { + return tokens.get(index - 1); + } + } + + /** 获取全部注释 */ + public List comments() { + return tokens.stream() + .filter(it -> it.getType() == TokenType.Comment) + .map(Token::getSpan) + .collect(Collectors.toList()); + } + + /** 期待下一个Token为指定类型 */ + public Token expect(TokenType type) { + if (!match(type, true)) { + Token token = index < tokens.size() ? tokens.get(index) : null; + Span span = token != null ? token.getSpan() : null; + if (span == null) { + MagicScriptError.error("期待 '" + type.getError() + "', 但是流已经遍历完毕", this); + } else { + MagicScriptError.error("期待 '" + type.getError() + "', 获得 '" + token.getText() + "'", span); + } + return null; // 执行不到这里 + } else { + return tokens.get(index - 1); + } + } + + /** 期待匹配字符串 */ + public Token expect(String text) { + return expect(text, false); + } + + /** + * 期待匹配字符串 + * + * @param ignoreCase 是否忽略大小写 + */ + public Token expect(String text, boolean ignoreCase) { + boolean result = match(text, true, ignoreCase); + if (!result) { + Token token = index < tokens.size() ? tokens.get(index) : null; + Span span = token != null ? token.getSpan() : null; + if (span == null) { + MagicScriptError.error("期待 '" + text + "', 但是流已经遍历完毕", this); + } else { + MagicScriptError.error("期待 '" + text + "', 获得 '" + token.getText() + "'", span); + } + return null; // never reached + } else { + return tokens.get(index - 1); + } + } + + /** + * 匹配指定类型Token + * + * @param consume 匹配成功后是否改变当前位置 + */ + public boolean match(TokenType type, boolean consume) { + if (index >= end) { + return false; + } + if (tokens.get(index).getType() == type) { + if (consume) { + index++; + } + return true; + } + return false; + } + + /** + * 匹配指定类型Token + * + * @param consume 匹配成功后是否改变当前位置 + */ + public boolean match(List texts, boolean consume) { + return match(texts, consume, false); + } + + /** + * 匹配指定字符串 + * + * @param consume 匹配成功后是否改变当前位置 + * @param ignoreCase 是否忽略大小写 + */ + public boolean match(List texts, boolean consume, boolean ignoreCase) { + for (String text : texts) { + if (match(text, consume, ignoreCase)) { + return true; + } + } + return false; + } + + /** + * 匹配指定字符串 + * + * @param consume 匹配成功后是否改变当前位置 + * @param ignoreCase 是否忽略大小写 + */ + public boolean match(String text, boolean consume, boolean ignoreCase) { + if (index >= end) { + return false; + } + String matchText = tokens.get(index).getText(); + if (ignoreCase ? matchText.equalsIgnoreCase(text) : matchText.equals(text)) { + if (consume) { + index++; + } + return true; + } + return false; + } + + /** + * 匹配指定字符串 + * + * @param consume 匹配成功后是否改变当前位置 + */ + public boolean match(String text, boolean consume) { + return match(text, consume, false); + } + + /** + * 匹配指定Token + * + * @param consume 匹配成功后是否改变当前位置 + */ + public boolean match(boolean consume, TokenType... types) { + for (TokenType type : types) { + if (match(type, consume)) { + return true; + } + } + return false; + } + + /** + * 匹配指定字符串 + * + * @param consume 匹配成功后是否改变当前位置 + */ + public boolean match(boolean consume, String... tokenTexts) { + return match(consume, false, tokenTexts); + } + + /** + * 匹配指定字符串 + * + * @param consume 匹配成功后是否改变当前位置 + * @param ignoreCase 是否忽略大小写 + */ + public boolean match(boolean consume, boolean ignoreCase, String... tokenTexts) { + for (String text : tokenTexts) { + if (match(text, consume, ignoreCase)) { + return true; + } + } + return false; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/TokenType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/TokenType.java new file mode 100644 index 0000000..94418f9 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/TokenType.java @@ -0,0 +1,178 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing; + +import java.util.Arrays; + +/** Token类型 */ +public enum TokenType { + // @off + Spread("...", "..."), + Period(".", "."), + QuestionPeriod("?.", "?."), + Comma(",", ","), + Semicolon(";", ";"), + Colon(":", ":"), + Plus("+", "+"), + Minus("-", "-"), + Asterisk("*", "*"), + ForwardSlash("/", "/"), + PostSlash("\\", "\\"), + Percentage("%", "%"), + LeftParantheses("(", ")"), + RightParantheses(")", ")"), + LeftBracket("[", "["), + RightBracket("]", "]"), + LeftCurly("{", "{"), + RightCurly("}", "{"), + Less("<", "<"), + Greater(">", ">"), + LessEqual("<=", "<="), + GreaterEqual(">=", ">="), + Equal("==", "=="), + NotEqual("!=", "!="), + Assignment(true, "=", "="), + // 1.3.0 + PlusPlus(true, "++", "++"), + MinusMinus(true, "--", "--"), + PlusEqual(true, "+=", "+="), + MinusEqual(true, "-=", "-="), + AsteriskEqual(true, "*=", "*="), + ForwardSlashEqual(true, "/=", "/="), + PercentEqual(true, "%=", "%="), + // 1.3.0 end + // 1.3.9 + ColonColon("::", "::"), + EqualEqualEqual("===", "==="), + NotEqualEqual("!==", "!=="), + // 1.3.9 end + And("&&", "&&"), + Or("||", "||"), + Xor("^", "^"), + Not("!", "!"), + // 1.5.0 start + BitAnd("&", "&"), + BitOr("|", "|"), + BitNot("~", "~"), + LShift("<<", "<<"), + RShift(">>", ">>"), + Rshift2(">>>", ">>>"), + + XorEqual(true, "^=", "^="), + BitAndEqual(true, "&=", "&="), + BitOrEqual(true, "|=", "|="), + LShiftEqual(true, "<<=", "<<="), + RShiftEqual(true, ">>=", ">>="), + RShift2Equal(true, ">>>=", ">>>="), + // 1.5.0 end + SqlAnd("and", "and"), + SqlOr("or", "or"), + SqlNotEqual("<>", "<>", true), + // 1.8.0 start + InstanceOf("instanceof", "instanceof"), + // 1.8.0 end + + QuestionMark("?", "?"), + DoubleQuote("\"", "\""), + TripleQuote("\"\"\"", "\"\"\""), + SingleQuote("'", "'"), + Lambda("=>"), + RegexpLiteral("a regexp"), + BooleanLiteral("true or false"), + DoubleLiteral("a double floating point number"), + DecimalLiteral("a decimal point number"), + FloatLiteral("a floating point number"), + LongLiteral("a long integer number"), + IntegerLiteral("an integer number"), + ShortLiteral("a short integer number"), + ByteLiteral("a byte integer number"), + CharacterLiteral("a character"), + StringLiteral("a string"), + NullLiteral("null"), + Language("language"), + Comment("comment"), + Identifier("an identifier"); + // @on + + private static final TokenType[] VALUES; + + static { + VALUES = TokenType.values(); + // 根据字符长度排序 + Arrays.sort( + VALUES, + (o1, o2) -> { + if (o1.literal == null && o2.literal == null) { + return 0; + } + if (o1.literal == null && o2.literal != null) { + return 1; + } + if (o1.literal != null && o2.literal == null) { + return -1; + } + return o2.literal.length() - o1.literal.length(); + }); + } + + private final String literal; + + private final String error; + + private final boolean inLinq; + + private final boolean modifiable; + + TokenType(String error) { + this(null, error); + } + + TokenType(String literal, String error) { + this(literal, error, false); + } + + TokenType(boolean modifiable, String literal, String error) { + this(literal, error, false, modifiable); + } + + TokenType(String literal, String error, boolean inLinq) { + this(literal, error, inLinq, false); + } + + TokenType(String literal, String error, boolean inLinq, boolean modifiable) { + this.literal = literal; + this.error = error; + this.inLinq = inLinq; + this.modifiable = modifiable; + } + + public static TokenType[] getSortedValues() { + return VALUES; + } + + public boolean isInLinq() { + return inLinq; + } + + public boolean isModifiable() { + return modifiable; + } + + public String getLiteral() { + return literal; + } + + public String getError() { + return error; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/Tokenizer.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/Tokenizer.java new file mode 100644 index 0000000..54e3c57 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/Tokenizer.java @@ -0,0 +1,445 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing; + +import cn.universal.core.engine.MagicScriptError; +import cn.universal.core.engine.exception.StringLiteralException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class Tokenizer { + + public static TokenStream tokenize(String source) { + return tokenize(source, false); + } + + public static TokenStream tokenize(String source, boolean matchComment) { + CharacterStream stream = new CharacterStream(source, 0, source.length()); + List tokens = new ArrayList<>(); + tokenizer(stream, tokens, matchComment, null); + return new TokenStream(tokens); + } + + private static List tokenizer( + CharacterStream stream, List tokens, boolean matchComment, String except) { + int leftCount = 0; + int rightCount = 0; + outer: + while (stream.hasMore()) { + stream.skipWhiteSpace(); + stream.startSpan(); + if (except != null && stream.match(except, true)) { + return tokens; + } + // // /* */ + if (tokenizerComment(stream, tokens, matchComment)) { + continue; + } + // int double long float byte decimal + if (tokenizerNumber(stream, tokens)) { + continue; + } + // '' """ """ "" + if (tokenizerString(stream, TokenType.SingleQuote, tokens) + || tokenizerString(stream, TokenType.TripleQuote, tokens) + || tokenizerString(stream, TokenType.DoubleQuote, tokens)) { + continue; + } + + // regexp + if (regexpToken(stream, tokens)) { + continue; + } + // ``` ``` + if (tokenizerLanguage(stream, tokens)) { + continue; + } + // template string + if (tokenizerTemplateString(stream, tokens, matchComment)) { + continue; + } + // Identifier, keyword, boolean literal, or null literal + if (tokenizerIdentifier(stream, tokens)) { + continue; + } + // lambda + if (stream.match("=>", true) || stream.match("->", true)) { + tokens.add( + new Token( + TokenType.Lambda, stream.getSpan(stream.getPosition() - 2, stream.getPosition()))); + continue; + } + // Simple tokens + for (TokenType t : TokenType.getSortedValues()) { + if (t.getLiteral() != null) { + if (stream.match(t.getLiteral(), true)) { + if (t == TokenType.LeftCurly) { + leftCount++; + } + tokens.add( + new Token( + t, + stream.getSpan( + stream.getPosition() - t.getLiteral().length(), stream.getPosition()))); + continue outer; + } + } + } + if (leftCount != rightCount && stream.match("}", true)) { + rightCount++; + tokens.add( + new Token( + TokenType.RightCurly, + stream.getSpan(stream.getPosition() - 1, stream.getPosition()))); + continue outer; + } + if (stream.hasMore()) { + MagicScriptError.error( + "Unknown token", stream.getSpan(stream.getPosition(), stream.getPosition() + 1)); + } + } + return tokens; + } + + private static boolean tokenizerLanguage(CharacterStream stream, List tokens) { + // TODO exception + if (stream.match("```", true)) { + stream.startSpan(); + if (stream.matchIdentifierStart(true)) { + while (stream.matchIdentifierPart(true)) {} + Span language = stream.endSpan(); + tokens.add(new Token(TokenType.Language, language)); + stream.startSpan(); + if (!stream.skipUntil("```")) { + MagicScriptError.error("```需要以```结尾", stream.endSpan(), new StringLiteralException()); + } + tokens.add(new Token(TokenType.Language, stream.endSpan(-3))); + return true; + } else { + MagicScriptError.error("```后需要标识语言类型", stream.endSpan(), new StringLiteralException()); + } + } + return false; + } + + private static boolean tokenizerTemplateString( + CharacterStream stream, List tokens, boolean matchComment) { + if (stream.match("`", true)) { + int begin = stream.getPosition(); + int start = begin; + boolean matchedEndQuote = false; + List subTokens = new ArrayList<>(); + while (stream.hasMore()) { + if (stream.match("\\", true)) { + stream.consume(); + continue; + } + if (stream.match("`", true)) { + matchedEndQuote = true; + break; + } + if (stream.match("${", true)) { + int end = stream.getPosition(); + if (start < end - 2) { + subTokens.add( + new LiteralToken(TokenType.StringLiteral, stream.endSpan(start, end - 2))); + } + subTokens.addAll(tokenizer(stream, new ArrayList<>(), matchComment, "}")); + start = stream.getPosition(); + continue; + } + stream.consume(); + } + if (!matchedEndQuote) { + MagicScriptError.error("模板字符串没有结束符`", stream.endSpan(), new StringLiteralException()); + } + Span stringSpan = stream.endSpan(begin, stream.getPosition()); + int end = stream.getPosition() - 1; + if (end - start > 0) { + subTokens.add(new LiteralToken(TokenType.StringLiteral, stream.endSpan(start, end))); + } + stringSpan = stream.getSpan(stringSpan.getStart() - 1, stringSpan.getEnd()); + tokens.add(new LiteralToken(TokenType.StringLiteral, stringSpan, new TokenStream(subTokens))); + return true; + } + return false; + } + + private static boolean tokenizerIdentifier(CharacterStream stream, List tokens) { + if (stream.matchIdentifierStart(true)) { + stream.startSpan(); + while (stream.matchIdentifierPart(true)) {} + Span identifierSpan = stream.endSpan(); + identifierSpan = stream.getSpan(identifierSpan.getStart() - 1, identifierSpan.getEnd()); + if ("true".equals(identifierSpan.getText()) || "false".equals(identifierSpan.getText())) { + tokens.add(new LiteralToken(TokenType.BooleanLiteral, identifierSpan)); + } else if ("null".equals(identifierSpan.getText())) { + tokens.add(new LiteralToken(TokenType.NullLiteral, identifierSpan)); + } else if ("instanceof".equals(identifierSpan.getText())) { + tokens.add(new Token(TokenType.InstanceOf, identifierSpan)); + } else if (TokenType.SqlAnd.getLiteral().equalsIgnoreCase(identifierSpan.getText())) { + tokens.add(new Token(TokenType.SqlAnd, identifierSpan)); + } else if (TokenType.SqlOr.getLiteral().equalsIgnoreCase(identifierSpan.getText())) { + tokens.add(new Token(TokenType.SqlOr, identifierSpan)); + } else { + tokens.add(new Token(TokenType.Identifier, identifierSpan)); + } + return true; + } + return false; + } + + private static boolean tokenizerComment( + CharacterStream stream, List tokens, boolean matchComment) { + if (stream.match("//", true)) { // 注释 + stream.skipLine(); + if (matchComment) { + tokens.add(new Token(TokenType.Comment, stream.endSpan())); + } + return true; + } + stream.startSpan(); + if (stream.match("/*", true)) { // 多行注释 + stream.skipUntil("*/"); + if (matchComment) { + tokens.add(new Token(TokenType.Comment, stream.endSpan())); + } + return true; + } + return false; + } + + private static boolean tokenizerNumber(CharacterStream stream, List tokens) { + if (stream.match("0", false)) { + int index = stream.getPosition(); + stream.startSpan(); + stream.consume(); + if (stream.matchAny(true, "x", "X")) { // 0x 16进制 + while (stream.matchDigit(true) + || stream.matchAny( + true, "A", "B", "C", "D", "E", "F", "a", "b", "c", "d", "e", "f", "_")) {} + if (stream.matchAny(true, "L", "l")) { + Span span = stream.endSpan(); + String text = span.getText(); + tokens.add( + new LiteralToken( + TokenType.LongLiteral, + span, + Long.parseLong(text.substring(2, text.length() - 1).replace("_", ""), 16))); + return true; + } + tokens.add(autoNumberType(stream.endSpan(), 16)); + return true; + } else if (stream.matchAny(true, "b", "B")) { // 二进制 + while (stream.matchAny(true, "0", "1", "_")) {} + if (stream.matchAny(true, "L", "l")) { + Span span = stream.endSpan(); + String text = span.getText(); + tokens.add( + new LiteralToken( + TokenType.LongLiteral, + span, + Long.parseLong(text.substring(2, text.length() - 1).replace("_", ""), 2))); + return true; + } + tokens.add(autoNumberType(stream.endSpan(), 2)); + return true; + } + stream.reset(index); + } + if (stream.matchDigit(false)) { + TokenType type = TokenType.IntegerLiteral; + stream.startSpan(); + while (stream.matchDigit(true) || stream.match("_", true)) {} + if (stream.match(TokenType.Period.getLiteral(), true)) { + type = TokenType.DoubleLiteral; + while (stream.matchDigit(true) || stream.match("_", true)) {} + } + if (stream.matchAny(true, "b", "B")) { + if (type == TokenType.DoubleLiteral) { + MagicScriptError.error("Byte literal can not have a decimal point.", stream.endSpan()); + } + type = TokenType.ByteLiteral; + } else if (stream.matchAny(true, "s", "S")) { + if (type == TokenType.DoubleLiteral) { + MagicScriptError.error("Short literal can not have a decimal point.", stream.endSpan()); + } + type = TokenType.ShortLiteral; + } else if (stream.matchAny(true, "L", "l")) { + if (type == TokenType.DoubleLiteral) { + MagicScriptError.error("Long literal can not have a decimal point.", stream.endSpan()); + } + type = TokenType.LongLiteral; + } else if (stream.matchAny(true, "f", "F")) { + type = TokenType.FloatLiteral; + } else if (stream.matchAny(true, "d", "D")) { + type = TokenType.DoubleLiteral; + } else if (stream.matchAny(true, "m", "M")) { + type = TokenType.DecimalLiteral; + } + Span numberSpan = stream.endSpan(); + tokens.add(new LiteralToken(type, numberSpan)); + return true; + } + return false; + } + + private static LiteralToken autoNumberType(Span span, int radix) { + long value = Long.parseLong(span.getText().substring(2).replace("_", ""), radix); + if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) { + return new LiteralToken(TokenType.LongLiteral, span, value); + } else if (value > Byte.MAX_VALUE || value < Byte.MIN_VALUE) { + return new LiteralToken(TokenType.IntegerLiteral, span, (int) value); + } else { + return new LiteralToken(TokenType.ByteLiteral, span, (byte) value); + } + } + + private static boolean tokenizerString( + CharacterStream stream, TokenType tokenType, List tokens) { + // String literal + if (stream.match(tokenType.getLiteral(), true)) { + stream.startSpan(); + boolean matchedEndQuote = false; + while (stream.hasMore()) { + // Note: escape sequences like \n are parsed in StringLiteral + if (stream.match("\\", true)) { + stream.consume(); + continue; + } + if (stream.match(tokenType.getLiteral(), true)) { + matchedEndQuote = true; + break; + } + char ch = stream.consume(); + if (tokenType != TokenType.TripleQuote && (ch == '\r' || ch == '\n')) { + MagicScriptError.error( + tokenType.getError() + tokenType.getError() + "定义的字符串不能换行", + stream.endSpan(), + new StringLiteralException()); + } + } + if (!matchedEndQuote) { + MagicScriptError.error( + "字符串没有结束符" + tokenType.getError(), stream.endSpan(), new StringLiteralException()); + } + Span stringSpan = stream.endSpan(); + stringSpan = + stream.getSpan( + stringSpan.getStart(), stringSpan.getEnd() - tokenType.getLiteral().length()); + tokens.add(new LiteralToken(TokenType.StringLiteral, stringSpan)); + return true; + } + return false; + } + + private static boolean regexpToken(CharacterStream stream, List tokens) { + if (tokens.size() > 0) { + Token token = tokens.get(tokens.size() - 1); + if (token instanceof LiteralToken) { + return false; + } + switch (token.getType()) { + case Comma: // , + case Semicolon: // ; + case Colon: // : + case RightCurly: // } + case LeftBracket: // [ + case LeftParantheses: // ( + case Assignment: // = + case NotEqual: // != + case EqualEqualEqual: // === + case NotEqualEqual: // !== + case Equal: // == + case And: // && + case Or: // || + case SqlAnd: // and + case SqlOr: // or + case SqlNotEqual: // <> + case QuestionMark: // ? + case Lambda: // => -> + case Not: // ! + break; + default: + return false; + } + } + if (stream.match("/", false)) { + int mark = stream.getPosition(); + stream.consume(); + stream.startSpan(); + boolean matchedEndQuote = false; + int deep = 0; + int expFlag = 0; + int maybeMissForwardSlash = 0; + int maybeMissForwardSlashEnd = 0; + while (stream.hasMore()) { + // Note: escape sequences like \n are parsed in StringLiteral + if (stream.match("\\", true)) { + stream.consume(); + continue; + } + if (stream.match("[", false)) { + deep++; + maybeMissForwardSlash = stream.getPosition(); + } else if (deep > 0 && stream.match("]", false)) { + deep--; + } else if (stream.match(TokenType.ForwardSlash.getLiteral(), true)) { + if (deep == 0) { + if (stream.match("g", true)) { + expFlag |= 1; + } + if (stream.match("i", true)) { + expFlag |= Pattern.CASE_INSENSITIVE; + } + if (stream.match("m", true)) { + expFlag |= Pattern.MULTILINE; + } + if (stream.match("s", true)) { + expFlag |= Pattern.DOTALL; + } + if (stream.match("u", true)) { + expFlag |= Pattern.UNICODE_CHARACTER_CLASS; + } + if (stream.match("y", true)) { + expFlag |= 16; + } + matchedEndQuote = true; + break; + } else { + maybeMissForwardSlashEnd = stream.getPosition(); + } + } + char ch = stream.consume(); + if (ch == '\r' || ch == '\n') { + stream.reset(mark); + return false; + } + } + if (deep != 0) { + MagicScriptError.error( + "Missing ']'", stream.getSpan(maybeMissForwardSlash, maybeMissForwardSlashEnd - 1)); + } + if (!matchedEndQuote) { + stream.reset(mark); + return false; + } + Span regexpSpan = stream.endSpan(); + regexpSpan = stream.getSpan(regexpSpan.getStart() - 1, regexpSpan.getEnd()); + tokens.add(new RegexpToken(TokenType.RegexpLiteral, regexpSpan, expFlag)); + return true; + } + return false; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/VarIndex.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/VarIndex.java new file mode 100644 index 0000000..fe3bb38 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/VarIndex.java @@ -0,0 +1,87 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing; + +import java.util.Objects; + +public class VarIndex { + + private final String name; + + private final int index; + + private final boolean reference; + + private final boolean readonly; + + private final boolean scoped; + + VarIndex(String name, int index, boolean reference) { + this(name, index, reference, false); + } + + public VarIndex(String name, int index, boolean reference, boolean readonly) { + this(name, index, reference, readonly, false); + } + + public VarIndex(String name, int index, boolean reference, boolean readonly, boolean scoped) { + this.name = name; + this.index = index; + this.reference = reference; + this.readonly = readonly; + this.scoped = scoped; + } + + public String getName() { + return name; + } + + public int getIndex() { + return index; + } + + public boolean isReference() { + return reference; + } + + public boolean isReadonly() { + return readonly; + } + + public boolean isScoped() { + return scoped; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof VarIndex)) { + return false; + } + VarIndex varIndex = (VarIndex) o; + return index == varIndex.index + && reference == varIndex.reference + && Objects.equals(name, varIndex.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, index, reference); + } + + public VarIndex scoped() { + return new VarIndex(this.name, this.index, this.reference, this.readonly, true); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/VarScope.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/VarScope.java new file mode 100644 index 0000000..dd26bde --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/VarScope.java @@ -0,0 +1,38 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing; + +import java.util.ArrayList; + +public class VarScope extends ArrayList { + + private VarScope parent; + + public VarScope(VarScope parent) { + this.parent = parent; + } + + public VarScope() {} + + public VarScope push() { + return new VarScope(this); + } + + public VarScope getParent() { + return parent; + } + + public VarScope pop() { + return parent; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/BinaryOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/BinaryOperation.java new file mode 100644 index 0000000..0523525 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/BinaryOperation.java @@ -0,0 +1,241 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast; + +import cn.universal.core.engine.MagicScriptError; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.functions.ObjectConvertExtension; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.Token; +import cn.universal.core.engine.parsing.ast.binary.AddOperation; +import cn.universal.core.engine.parsing.ast.binary.AndOperation; +import cn.universal.core.engine.parsing.ast.binary.AssigmentOperation; +import cn.universal.core.engine.parsing.ast.binary.BitAndOperation; +import cn.universal.core.engine.parsing.ast.binary.BitOrOperation; +import cn.universal.core.engine.parsing.ast.binary.DivisionOperation; +import cn.universal.core.engine.parsing.ast.binary.EqualOperation; +import cn.universal.core.engine.parsing.ast.binary.GreaterEqualOperation; +import cn.universal.core.engine.parsing.ast.binary.GreaterOperation; +import cn.universal.core.engine.parsing.ast.binary.InstanceofOperation; +import cn.universal.core.engine.parsing.ast.binary.LShiftOperation; +import cn.universal.core.engine.parsing.ast.binary.LessEqualOperation; +import cn.universal.core.engine.parsing.ast.binary.LessOperation; +import cn.universal.core.engine.parsing.ast.binary.ModuloOperation; +import cn.universal.core.engine.parsing.ast.binary.MultiplicationOperation; +import cn.universal.core.engine.parsing.ast.binary.NotEqualOperation; +import cn.universal.core.engine.parsing.ast.binary.OrOperation; +import cn.universal.core.engine.parsing.ast.binary.RShift2Operation; +import cn.universal.core.engine.parsing.ast.binary.RShiftOperation; +import cn.universal.core.engine.parsing.ast.binary.SubtractionOperation; +import cn.universal.core.engine.parsing.ast.binary.XorOperation; +import cn.universal.core.engine.parsing.ast.statement.VariableAccess; +import java.math.BigDecimal; +import java.util.Date; + +public abstract class BinaryOperation extends Expression { + + private Expression leftOperand; + private Expression rightOperand; + + public BinaryOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(span); + this.leftOperand = leftOperand; + this.rightOperand = rightOperand; + } + + public static Expression create( + Expression left, Token operator, Expression right, int linqLevel) { + if (operator.getType().isModifiable() + && left instanceof VariableAccess + && ((VariableAccess) left).getVarIndex().isReadonly()) { + MagicScriptError.error("const定义的变量不能被修改", new Span(left.getSpan(), right.getSpan())); + } + Expression expression = null; + Span span = new Span(left.getSpan(), right.getSpan()); + switch (operator.getType()) { + case Assignment: + expression = + linqLevel == 0 + ? new AssigmentOperation(left, span, right) + : new EqualOperation(left, span, right, false); + break; + case Plus: + expression = new AddOperation(left, span, right); + break; + case Minus: + expression = new SubtractionOperation(left, span, right); + break; + case Asterisk: + expression = new MultiplicationOperation(left, span, right); + break; + case ForwardSlash: + expression = new DivisionOperation(left, span, right); + break; + case Percentage: + expression = new ModuloOperation(left, span, right); + break; + case PlusEqual: + expression = new AssigmentOperation(left, span, new AddOperation(left, span, right)); + break; + case MinusEqual: + expression = + new AssigmentOperation(left, span, new SubtractionOperation(left, span, right)); + break; + case AsteriskEqual: + expression = + new AssigmentOperation(left, span, new MultiplicationOperation(left, span, right)); + break; + case ForwardSlashEqual: + expression = new AssigmentOperation(left, span, new DivisionOperation(left, span, right)); + break; + case PercentEqual: + expression = new AssigmentOperation(left, span, new ModuloOperation(left, span, right)); + break; + case Less: + expression = new LessOperation(left, span, right); + break; + case LessEqual: + expression = new LessEqualOperation(left, span, right); + break; + case Greater: + expression = new GreaterOperation(left, span, right); + break; + case GreaterEqual: + expression = new GreaterEqualOperation(left, span, right); + break; + case Equal: + expression = new EqualOperation(left, span, right, false); + break; + case EqualEqualEqual: + expression = new EqualOperation(left, span, right, true); + break; + case SqlNotEqual: + case NotEqual: + expression = new NotEqualOperation(left, span, right, false); + break; + case NotEqualEqual: + expression = new NotEqualOperation(left, span, right, true); + break; + case SqlAnd: + case And: + expression = new AndOperation(left, span, right); + break; + case SqlOr: + case Or: + expression = new OrOperation(left, span, right); + break; + case LShift: + expression = new LShiftOperation(left, span, right); + break; + case RShift: + expression = new RShiftOperation(left, span, right); + break; + case Rshift2: + expression = new RShift2Operation(left, span, right); + break; + case Xor: + expression = new XorOperation(left, span, right); + break; + case BitAnd: + expression = new BitAndOperation(left, span, right); + break; + case BitOr: + expression = new BitOrOperation(left, span, right); + break; + case LShiftEqual: + expression = new AssigmentOperation(left, span, new LShiftOperation(left, span, right)); + break; + case RShiftEqual: + expression = new AssigmentOperation(left, span, new RShiftOperation(left, span, right)); + break; + case RShift2Equal: + expression = new AssigmentOperation(left, span, new RShift2Operation(left, span, right)); + break; + case XorEqual: + expression = new AssigmentOperation(left, span, new XorOperation(left, span, right)); + break; + case BitAndEqual: + expression = new AssigmentOperation(left, span, new BitAndOperation(left, span, right)); + break; + case BitOrEqual: + expression = new AssigmentOperation(left, span, new BitOrOperation(left, span, right)); + break; + case InstanceOf: + expression = new InstanceofOperation(left, span, right); + break; + default: + MagicScriptError.error("[" + operator.getText() + "]操作符未实现", span); + } + return expression; + } + + /** 比较两个值 1 左边大 0 相等 -1 右边大 -2 无法比较 */ + public static int compare(Object left, Object right) { + if (left == null && right == null) { + return -2; + } + if (left == null) { + return -1; + } + if (right == null) { + return 1; + } + if (left instanceof Number && right instanceof Number) { + if (left instanceof BigDecimal || right instanceof BigDecimal) { + return ObjectConvertExtension.asDecimal(left) + .compareTo(ObjectConvertExtension.asDecimal(right)); + } else if (left instanceof Double || right instanceof Double) { + return Double.compare(((Number) left).doubleValue(), ((Number) right).doubleValue()); + } else if (left instanceof Float || right instanceof Float) { + return Float.compare(((Number) left).floatValue(), ((Number) right).floatValue()); + } else if (left instanceof Long || right instanceof Long) { + return Long.compare(((Number) left).longValue(), ((Number) right).longValue()); + } else if (left instanceof Integer || right instanceof Integer) { + return Integer.compare(((Number) left).intValue(), ((Number) right).intValue()); + } else if (left instanceof Short || right instanceof Short) { + return Short.compare(((Number) left).shortValue(), ((Number) right).shortValue()); + } else if (left instanceof Byte || right instanceof Byte) { + return Byte.compare(((Number) left).byteValue(), ((Number) right).byteValue()); + } + } + if (left instanceof Date && right instanceof Date) { + return ((Date) left).compareTo((Date) right); + } + if (left instanceof String && right instanceof String) { + return ((String) left).compareTo((String) right); + } + return -2; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + leftOperand.visitMethod(compiler); + rightOperand.visitMethod(compiler); + } + + public Expression getLeftOperand() { + return leftOperand; + } + + public void setLeftOperand(Expression leftOperand) { + this.leftOperand = leftOperand; + } + + public Expression getRightOperand() { + return rightOperand; + } + + public void setRightOperand(Expression rightOperand) { + this.rightOperand = rightOperand; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/Expression.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/Expression.java new file mode 100644 index 0000000..df8a087 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/Expression.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast; + +import cn.universal.core.engine.parsing.Span; + +/** 表达式 */ +public abstract class Expression extends Node { + + public Expression(Span span) { + super(span); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/LanguageExpression.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/LanguageExpression.java new file mode 100644 index 0000000..6acfd34 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/LanguageExpression.java @@ -0,0 +1,50 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast; + +import cn.universal.core.engine.MagicScriptContext; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.runtime.function.MagicScriptLanguageFunction; + +public class LanguageExpression extends Expression { + + private final String language; + + private final String content; + + public LanguageExpression(Span language, Span content) { + super(new Span(language, content)); + this.language = language.getText(); + this.content = content.getText(); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + // new MagicScriptLanguageFunction(language, content) + compiler + .typeInsn(NEW, MagicScriptLanguageFunction.class) + .insn(DUP) + .loadContext() + .ldc(this.language) + .ldc(this.content) + .invoke( + INVOKESPECIAL, + MagicScriptLanguageFunction.class, + "", + void.class, + MagicScriptContext.class, + String.class, + String.class); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/Literal.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/Literal.java new file mode 100644 index 0000000..ae1ac05 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/Literal.java @@ -0,0 +1,40 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; + +/** 常量 */ +public abstract class Literal extends Expression { + + protected Object value = null; + + public Literal(Span span) { + super(span); + } + + public Literal(Span span, Object value) { + super(span); + this.value = value; + } + + public void setValue(Object value) { + this.value = value; + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler.ldc(value); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/Node.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/Node.java new file mode 100644 index 0000000..4674921 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/Node.java @@ -0,0 +1,50 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast; + +import cn.universal.core.engine.asm.Opcodes; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; + +/** 节点 */ +public abstract class Node implements Opcodes { + + /** 对应的文本 */ + private final Span span; + + /** 在Linq中 */ + private boolean inLinq; + + public Node(Span span) { + this.span = span; + } + + public Span getSpan() { + return span; + } + + public boolean isInLinq() { + return inLinq; + } + + @Override + public String toString() { + return getClass().getSimpleName() + ":" + span.getText(); + } + + public void visitMethod(MagicScriptCompiler compiler) {} + + public void compile(MagicScriptCompiler compiler) { + throw new UnsupportedOperationException(this.getClass().getSimpleName() + "不支持编译"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/TernaryOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/TernaryOperation.java new file mode 100644 index 0000000..066b225 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/TernaryOperation.java @@ -0,0 +1,57 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast; + +import cn.universal.core.engine.asm.Label; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.runtime.handle.OperatorHandle; + +public class TernaryOperation extends Expression { + + private final Expression condition; + private final Expression trueExpression; + private final Expression falseExpression; + + public TernaryOperation( + Expression condition, Expression trueExpression, Expression falseExpression) { + super(new Span(condition.getSpan(), falseExpression.getSpan())); + this.condition = condition; + this.trueExpression = trueExpression; + this.falseExpression = falseExpression; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + condition.visitMethod(compiler); + trueExpression.visitMethod(compiler); + falseExpression.visitMethod(compiler); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + Label end = new Label(); + Label falseValue = new Label(); + // condition ? trueExpr : falseExpr + compiler + .compile(condition) // 访问表达式 + .invoke( + INVOKESTATIC, OperatorHandle.class, "isTrue", boolean.class, Object.class) // 判断是否为true + .jump(IFEQ, falseValue) // 为false时跳转 + .visit(trueExpression) // 访问true表达式 + .jump(GOTO, end) // 跳转至结束 + .label(falseValue) + .visit(falseExpression) // 访问false表达式 + .label(end); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/UnaryOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/UnaryOperation.java new file mode 100644 index 0000000..0dbb417 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/UnaryOperation.java @@ -0,0 +1,164 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast; + +import cn.universal.core.engine.MagicScriptError; +import cn.universal.core.engine.compile.MagicScriptCompileException; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Token; +import cn.universal.core.engine.parsing.TokenType; +import cn.universal.core.engine.parsing.ast.statement.VariableAccess; +import cn.universal.core.engine.runtime.handle.BitHandle; +import cn.universal.core.engine.runtime.handle.OperatorHandle; +import java.util.function.Supplier; + +/** 一元操作符 */ +public class UnaryOperation extends Expression { + + private final UnaryOperator operator; + private final Expression operand; + private final boolean atAfter; + + public UnaryOperation(Token operator, Expression operand) { + this(operator, operand, false); + } + + public UnaryOperation(Token operator, Expression operand, boolean atAfter) { + super(operator.getSpan()); + if (operator.getType().isModifiable() + && operand instanceof VariableAccess + && ((VariableAccess) operand).getVarIndex().isReadonly()) { + MagicScriptError.error("const修饰的变量不能被修改", getSpan()); + } + this.operator = UnaryOperator.getOperator(operator); + this.operand = operand; + this.atAfter = atAfter; + } + + public UnaryOperator getOperator() { + return operator; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + operand.visitMethod(compiler); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + switch (getOperator()) { + case Not: + compiler + .compile(operand) + .invoke(INVOKESTATIC, OperatorHandle.class, "isFalse", boolean.class, Object.class) + .asBoolean(); + break; + case BitNot: + compiler + .compile(operand) + .invoke(INVOKESTATIC, BitHandle.class, "not", Object.class, Object.class); + break; + case PlusPlus: + case MinusMinus: + if (operand instanceof VariableSetter) { + boolean access = operand instanceof VariableAccess; + Supplier plus = + () -> + compiler + .visit(operand) // 访问变量 + // 执行 ± 1 操作 + .visitInt(operator == UnaryOperator.PlusPlus ? 1 : -1) + .asInteger() + .arithmetic("plus"); + if (atAfter) { // ++ -- 在后 + if (access) { // a++ a-- + // 执行 ± 操作 + compiler.compile(operand).pre_store(((VariableAccess) operand).getVarIndex()); + plus.get().store(((VariableAccess) operand).getVarIndex()); + } else { // map.key++ map.key-- + compiler.compile(operand); // 先访问变量,后续返回使用 + compiler.newRuntimeContext(); + ((VariableSetter) operand).compile_visit_variable(compiler); // 赋值前准备 + plus.get() + .call("set_variable_value", 4) // 赋值操作 + .insn(POP); // 抛弃 ++ -- 的返回值。 + } + } else { // ++ -- 在前 + if (access) { // ++a --a + compiler.pre_store(((VariableAccess) operand).getVarIndex()); + // 执行 ± 操作 + plus.get() + .store(((VariableAccess) operand).getVarIndex()) // 结果存入到变量中 + .visit(operand); + } else { // ++map.key --map.key + compiler.newRuntimeContext(); + // 赋值前准备 + ((VariableSetter) operand).compile_visit_variable(compiler); + // 将执行结果赋值给变量。 + plus.get().call("set_variable_value", 4); + } + } + break; + } + throw new MagicScriptCompileException("此处不支持++/--操作"); + case Negate: + compiler.visit(operand).visitInt(-1).asInteger().arithmetic("mul"); + break; + default: + break; + } + } + + public enum UnaryOperator { + /** ! */ + Not, + + /** - */ + Negate, + + /** + */ + Positive, + + /** ++ */ + PlusPlus, + + /** -- */ + MinusMinus, + + /** ~ */ + BitNot; + + public static UnaryOperator getOperator(Token op) { + if (op.getType() == TokenType.Not) { + return UnaryOperator.Not; + } + if (op.getType() == TokenType.Plus) { + return UnaryOperator.Positive; + } + if (op.getType() == TokenType.Minus) { + return UnaryOperator.Negate; + } + if (op.getType() == TokenType.PlusPlus) { + return UnaryOperator.PlusPlus; + } + if (op.getType() == TokenType.MinusMinus) { + return UnaryOperator.MinusMinus; + } + if (op.getType() == TokenType.BitNot) { + return UnaryOperator.BitNot; + } + MagicScriptError.error("不支持的一元操作符:" + op, op.getSpan()); + return null; // not reached + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/VariableSetter.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/VariableSetter.java new file mode 100644 index 0000000..98405e5 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/VariableSetter.java @@ -0,0 +1,22 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast; + +import cn.universal.core.engine.compile.MagicScriptCompiler; + +public interface VariableSetter { + + default void compile_visit_variable(MagicScriptCompiler compiler) { + throw new UnsupportedOperationException("暂不支持编译" + this.getClass().getSimpleName()); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/AddOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/AddOperation.java new file mode 100644 index 0000000..6c796b3 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/AddOperation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** + 运算 */ +public class AddOperation extends BinaryOperation { + + public AddOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .arithmetic("plus"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/AndOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/AndOperation.java new file mode 100644 index 0000000..1438d5d --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/AndOperation.java @@ -0,0 +1,41 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.asm.Label; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.runtime.handle.OperatorHandle; + +/** && 操作 */ +public class AndOperation extends BinaryOperation { + + public AndOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + Label end = new Label(); + compiler + .visit(getLeftOperand()) + .insn(DUP) + .invoke(INVOKESTATIC, OperatorHandle.class, "isTrue", boolean.class, Object.class) + .jump(IFEQ, end) + .insn(POP) + .visit(getRightOperand()) + .label(end); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/AssigmentOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/AssigmentOperation.java new file mode 100644 index 0000000..a508420 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/AssigmentOperation.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.MagicScriptError; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.VariableSetter; +import cn.universal.core.engine.parsing.ast.statement.VariableAccess; + +/** = 操作 */ +public class AssigmentOperation extends BinaryOperation { + + public AssigmentOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + if (getLeftOperand() instanceof VariableAccess) { + compiler + .pre_store(((VariableAccess) getLeftOperand()).getVarIndex()) + .compile(getRightOperand()); + if (getRightOperand() instanceof AssigmentOperation) { + compiler.visit(((AssigmentOperation) getRightOperand()).getLeftOperand()); + } + compiler.store(((VariableAccess) getLeftOperand()).getVarIndex()); + } else if (getLeftOperand() instanceof VariableSetter) { + compiler.newRuntimeContext(); + ((VariableSetter) getLeftOperand()).compile_visit_variable(compiler); + compiler.compile(getRightOperand()).call("set_variable_value", 4); + } else { + MagicScriptError.error("赋值目标应为变量", getLeftOperand().getSpan()); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/BitAndOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/BitAndOperation.java new file mode 100644 index 0000000..8927db3 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/BitAndOperation.java @@ -0,0 +1,31 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** & */ +public class BitAndOperation extends BinaryOperation { + + public BitAndOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler.visit(getLeftOperand()).visit(getRightOperand()).lineNumber(getSpan()).bit("and"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/BitOrOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/BitOrOperation.java new file mode 100644 index 0000000..b378819 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/BitOrOperation.java @@ -0,0 +1,31 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** | */ +public class BitOrOperation extends BinaryOperation { + + public BitOrOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler.visit(getLeftOperand()).visit(getRightOperand()).lineNumber(getSpan()).bit("or"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/DivisionOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/DivisionOperation.java new file mode 100644 index 0000000..009bc26 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/DivisionOperation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** / 操作 */ +public class DivisionOperation extends BinaryOperation { + + public DivisionOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .arithmetic("divide"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/EqualOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/EqualOperation.java new file mode 100644 index 0000000..b2bf3de --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/EqualOperation.java @@ -0,0 +1,39 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** ==、===操作 */ +public class EqualOperation extends BinaryOperation { + + protected final boolean accurate; + + public EqualOperation( + Expression leftOperand, Span span, Expression rightOperand, boolean accurate) { + super(leftOperand, span, rightOperand); + this.accurate = accurate; + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .operator(accurate ? "accurate_equals" : "equals"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/GreaterEqualOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/GreaterEqualOperation.java new file mode 100644 index 0000000..3b20770 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/GreaterEqualOperation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** >=操作 */ +public class GreaterEqualOperation extends BinaryOperation { + + public GreaterEqualOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .operator("greater_equals"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/GreaterOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/GreaterOperation.java new file mode 100644 index 0000000..4cf76c8 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/GreaterOperation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** > 运算 */ +public class GreaterOperation extends BinaryOperation { + + public GreaterOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .operator("greater"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/InstanceofOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/InstanceofOperation.java new file mode 100644 index 0000000..748d055 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/InstanceofOperation.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.functions.ObjectTypeConditionExtension; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** instanceof */ +public class InstanceofOperation extends BinaryOperation { + + public InstanceofOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .typeInsn(CHECKCAST, Class.class) + .lineNumber(getSpan()) + .invoke( + INVOKESTATIC, + ObjectTypeConditionExtension.class, + "is", + boolean.class, + Object.class, + Class.class) + .asBoolean(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/LShiftOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/LShiftOperation.java new file mode 100644 index 0000000..bc0ccfb --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/LShiftOperation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** << */ +public class LShiftOperation extends BinaryOperation { + + public LShiftOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .bit("left_shift"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/LessEqualOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/LessEqualOperation.java new file mode 100644 index 0000000..be69b7a --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/LessEqualOperation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** <= 运算 */ +public class LessEqualOperation extends BinaryOperation { + + public LessEqualOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .operator("less_equals"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/LessOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/LessOperation.java new file mode 100644 index 0000000..730bcab --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/LessOperation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** < */ +public class LessOperation extends BinaryOperation { + + public LessOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .operator("less"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/ModuloOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/ModuloOperation.java new file mode 100644 index 0000000..50f243f --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/ModuloOperation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** % 运算 */ +public class ModuloOperation extends BinaryOperation { + + public ModuloOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .arithmetic("divideAndRemainder"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/MultiplicationOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/MultiplicationOperation.java new file mode 100644 index 0000000..ddc4ec4 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/MultiplicationOperation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** * 运算 */ +public class MultiplicationOperation extends BinaryOperation { + + public MultiplicationOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .arithmetic("mul"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/NotEqualOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/NotEqualOperation.java new file mode 100644 index 0000000..0e8ab64 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/NotEqualOperation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; + +/** !=、!==操作 */ +public class NotEqualOperation extends EqualOperation { + + public NotEqualOperation( + Expression leftOperand, Span span, Expression rightOperand, boolean accurate) { + super(leftOperand, span, rightOperand, accurate); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .operator(accurate ? "not_accurate_equals" : "not_equals"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/OrOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/OrOperation.java new file mode 100644 index 0000000..3b6b67a --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/OrOperation.java @@ -0,0 +1,41 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.asm.Label; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.runtime.handle.OperatorHandle; + +/** || 操作 */ +public class OrOperation extends BinaryOperation { + + public OrOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + Label end = new Label(); + compiler + .visit(getLeftOperand()) + .insn(DUP) + .invoke(INVOKESTATIC, OperatorHandle.class, "isTrue", boolean.class, Object.class) + .jump(IFNE, end) + .insn(POP) + .visit(getRightOperand()) + .label(end); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/RShift2Operation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/RShift2Operation.java new file mode 100644 index 0000000..a6aff96 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/RShift2Operation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** >>> */ +public class RShift2Operation extends BinaryOperation { + + public RShift2Operation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .bit("right_shift2"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/RShiftOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/RShiftOperation.java new file mode 100644 index 0000000..190d12b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/RShiftOperation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** >> */ +public class RShiftOperation extends BinaryOperation { + + public RShiftOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .bit("right_shift"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/SubtractionOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/SubtractionOperation.java new file mode 100644 index 0000000..1a45a5e --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/SubtractionOperation.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** - 操作 */ +public class SubtractionOperation extends BinaryOperation { + + public SubtractionOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(getLeftOperand()) + .visit(getRightOperand()) + .lineNumber(getSpan()) + .arithmetic("minus"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/XorOperation.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/XorOperation.java new file mode 100644 index 0000000..f475dca --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/binary/XorOperation.java @@ -0,0 +1,31 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.binary; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import cn.universal.core.engine.parsing.ast.Expression; + +/** ^ */ +public class XorOperation extends BinaryOperation { + + public XorOperation(Expression leftOperand, Span span, Expression rightOperand) { + super(leftOperand, span, rightOperand); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler.visit(getLeftOperand()).visit(getRightOperand()).lineNumber(getSpan()).bit("xor"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqExpression.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqExpression.java new file mode 100644 index 0000000..e98915b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqExpression.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.linq; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.statement.MemberAccess; + +public class LinqExpression extends Expression { + + private final Expression expression; + + private String methodName; + + public LinqExpression(Expression expression) { + this(expression.getSpan(), expression); + } + + public LinqExpression(Span span, Expression expression) { + super(span); + this.expression = expression; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + expression.visitMethod(compiler); + if (!(expression instanceof WholeLiteral)) { + this.methodName = + compiler.visitMethod( + "linq_expression", + () -> + compiler + .compile( + expression instanceof MemberAccess + && ((MemberAccess) expression).isWhole() + ? ((MemberAccess) expression).getObject() + : expression) + .insn(ARETURN)); + } + } + + @Override + public void compile(MagicScriptCompiler compiler) { + if (methodName != null) { + compiler.load0().lambda(methodName); + } else { + compiler.insn(ACONST_NULL); + } + } + + public Expression getExpression() { + return expression; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqField.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqField.java new file mode 100644 index 0000000..0096f4c --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqField.java @@ -0,0 +1,39 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.linq; + +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.VarIndex; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.VariableSetter; + +public class LinqField extends LinqExpression implements VariableSetter { + + private final String aliasName; + + private final VarIndex varIndex; + + public LinqField(Span span, Expression expression, VarIndex alias) { + super(span, expression); + this.aliasName = alias != null ? alias.getName() : expression.getSpan().getText(); + this.varIndex = alias; + } + + public VarIndex getVarIndex() { + return varIndex; + } + + public String getAlias() { + return aliasName; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqJoin.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqJoin.java new file mode 100644 index 0000000..f4ab798 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqJoin.java @@ -0,0 +1,67 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.linq; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.runtime.function.MagicScriptLambdaFunction; +import cn.universal.core.engine.runtime.linq.LinQBuilder; + +public class LinqJoin extends Expression { + + private final LinqField target; + + private final Expression condition; + + private final boolean leftJoin; + + private String methodName; + + public LinqJoin(Span span, boolean leftJoin, LinqField target, Expression condition) { + super(span); + this.leftJoin = leftJoin; + this.target = target; + this.condition = condition; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + this.methodName = + compiler.visitMethod( + "linq_join_condition", () -> compiler.compile(condition).insn(ARETURN)); + } + + public LinqField getTarget() { + return target; + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .load0() + .lambda(methodName) + .visit(target.getExpression()) + .insn(leftJoin ? ICONST_1 : ICONST_0) + .visitInt(target.getVarIndex() == null ? -1 : target.getVarIndex().getIndex()) + .invoke( + INVOKEVIRTUAL, + LinQBuilder.class, + "join", + LinQBuilder.class, + MagicScriptLambdaFunction.class, + Object.class, + boolean.class, + int.class); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqOrder.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqOrder.java new file mode 100644 index 0000000..69c7ce6 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqOrder.java @@ -0,0 +1,32 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.linq; + +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.VarIndex; +import cn.universal.core.engine.parsing.ast.Expression; + +public class LinqOrder extends LinqField { + + /** 1 正序 -1 倒序 */ + private final int order; + + public LinqOrder(Span span, Expression expression, VarIndex alias, int order) { + super(span, expression, alias); + this.order = order; + } + + public int getOrder() { + return order; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqSelect.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqSelect.java new file mode 100644 index 0000000..2a511d0 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/LinqSelect.java @@ -0,0 +1,174 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.linq; + +import cn.universal.core.engine.MagicScriptError; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Literal; +import cn.universal.core.engine.runtime.Variables; +import cn.universal.core.engine.runtime.function.MagicScriptLambdaFunction; +import cn.universal.core.engine.runtime.linq.LinQBuilder; +import java.util.List; + +public class LinqSelect extends Expression { + + private final List fields; + + private final LinqField from; + + private final List joins; + + private final LinqExpression where; + + private final List groups; + + private final LinqExpression having; + + private final List orders; + + private final Expression limit; + + private final Expression offset; + + public LinqSelect( + Span span, + List fields, + LinqField from, + List joins, + LinqExpression where, + List groups, + LinqExpression having, + List orders, + Expression limit, + Expression offset) { + super(span); + this.fields = fields; + this.from = from; + this.joins = joins; + this.where = where; + this.groups = groups; + this.having = having; + this.orders = orders; + this.limit = limit; + this.offset = offset; + if (from.getVarIndex() == null) { + MagicScriptError.error("LINQ中,别名是必须的", from.getSpan()); + } + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + from.visitMethod(compiler); + fields.forEach(it -> it.visitMethod(compiler)); + joins.forEach(it -> it.visitMethod(compiler)); + groups.forEach(it -> it.visitMethod(compiler)); + orders.forEach(it -> it.visitMethod(compiler)); + if (where != null) { + where.visitMethod(compiler); + } + if (having != null) { + having.visitMethod(compiler); + } + if (limit != null) { + limit.visitMethod(compiler); + } + if (offset != null) { + offset.visitMethod(compiler); + } + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .load2() + .invoke(INVOKESTATIC, LinQBuilder.class, "create", LinQBuilder.class, Variables.class) + .visit(from.getExpression()) + .visitInt(from.getVarIndex().getIndex()) + .invoke( + INVOKEVIRTUAL, LinQBuilder.class, "from", LinQBuilder.class, Object.class, int.class); + if (where != null) { + compiler + .visit(where) + .invoke( + INVOKEVIRTUAL, + LinQBuilder.class, + "where", + LinQBuilder.class, + MagicScriptLambdaFunction.class); + } + if (having != null) { + compiler + .visit(having) + .invoke( + INVOKEVIRTUAL, + LinQBuilder.class, + "having", + LinQBuilder.class, + MagicScriptLambdaFunction.class); + } + groups.forEach( + group -> + compiler + .visit(group) + .invoke( + INVOKEVIRTUAL, + LinQBuilder.class, + "group", + LinQBuilder.class, + MagicScriptLambdaFunction.class)); + joins.forEach(compiler::visit); + fields.forEach( + field -> + compiler + .visit(field) + .ldc(field.getAlias()) + .visitInt(field.getVarIndex() == null ? -1 : field.getVarIndex().getIndex()) + .invoke( + INVOKEVIRTUAL, + LinQBuilder.class, + "select", + LinQBuilder.class, + MagicScriptLambdaFunction.class, + String.class, + int.class)); + orders.forEach( + order -> + compiler + .visit(order) + .visitInt(order.getOrder()) + .invoke( + INVOKEVIRTUAL, + LinQBuilder.class, + "order", + LinQBuilder.class, + MagicScriptLambdaFunction.class, + int.class)); + if (limit != null) { + if (offset == null && limit instanceof Literal && "1".equals(limit.getSpan().getText())) { + compiler.invoke(INVOKEVIRTUAL, LinQBuilder.class, "executeAndFetchFirst", Object.class); + return; + } + compiler + .visit(limit) + .invoke(INVOKEVIRTUAL, LinQBuilder.class, "limit", LinQBuilder.class, Object.class); + if (offset != null) { + compiler + .visit(offset) + .invoke(INVOKEVIRTUAL, LinQBuilder.class, "offset", LinQBuilder.class, Object.class); + } + } + compiler.invoke(INVOKEVIRTUAL, LinQBuilder.class, "execute", List.class); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/WholeLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/WholeLiteral.java new file mode 100644 index 0000000..44717fe --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/linq/WholeLiteral.java @@ -0,0 +1,33 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.linq; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Literal; + +public class WholeLiteral extends Literal { + + public WholeLiteral(Span span) { + super(span); + } + + public WholeLiteral(Span span, Object value) { + super(span, value); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler.load2(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/BigDecimalLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/BigDecimalLiteral.java new file mode 100644 index 0000000..da316e2 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/BigDecimalLiteral.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Literal; +import java.math.BigDecimal; + +/** int常量 */ +public class BigDecimalLiteral extends Literal { + + public BigDecimalLiteral(Span literal) { + super(literal, literal.getText().substring(0, literal.getText().length() - 1).replace("_", "")); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .typeInsn(NEW, BigDecimal.class) + .insn(DUP) + .ldc(value) + .invoke(INVOKESPECIAL, BigDecimal.class, "", void.class, String.class); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/BooleanLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/BooleanLiteral.java new file mode 100644 index 0000000..492b887 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/BooleanLiteral.java @@ -0,0 +1,59 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Literal; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Map; + +/** boolean常量 */ +public class BooleanLiteral extends Literal { + + public BooleanLiteral(Span literal) { + super(literal, Boolean.parseBoolean(literal.getText())); + } + + public static boolean isTrue(Object object) { + if (object == null) { + return false; + } + if (object instanceof Boolean) { + return (Boolean) object; + } + if (object instanceof CharSequence) { // 非空字符串 + return ((CharSequence) object).length() != 0; + } + if (object instanceof Number) { // 非0 为 true + return ((Number) object).doubleValue() != 0; + } + if (object instanceof Collection) { // 非空集合 + return !((Collection) object).isEmpty(); + } + if (object.getClass().isArray()) { // 非空数组 + return Array.getLength(object) > 0; + } + if (object instanceof Map) { // 非空Map + return !((Map) object).isEmpty(); + } + // 其它情况全视作为true + return true; + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler.insn((Boolean) this.value ? ICONST_1 : ICONST_0).asBoolean(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/ByteLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/ByteLiteral.java new file mode 100644 index 0000000..1b93aae --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/ByteLiteral.java @@ -0,0 +1,45 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.MagicScriptError; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Literal; + +/** byte常量 */ +public class ByteLiteral extends Literal { + + private byte value; + + public ByteLiteral(Span literal) { + super(literal); + try { + this.value = + Byte.parseByte( + literal.getText().substring(0, literal.getText().length() - 1).replace("_", "")); + } catch (NumberFormatException e) { + MagicScriptError.error("定义byte变量值不合法", literal, e); + } + } + + public ByteLiteral(Span span, Object value) { + super(span, value); + this.value = ((Number) value).byteValue(); + } + + @Override + public void compile(MagicScriptCompiler context) { + context.bipush(this.value).invoke(INVOKESTATIC, Byte.class, "valueOf", Byte.class, byte.class); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/DoubleLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/DoubleLiteral.java new file mode 100644 index 0000000..c2d9a48 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/DoubleLiteral.java @@ -0,0 +1,36 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.MagicScriptError; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Literal; + +/** double常量 */ +public class DoubleLiteral extends Literal { + + public DoubleLiteral(Span literal) { + super(literal); + try { + setValue(Double.parseDouble(literal.getText().replace("_", ""))); + } catch (NumberFormatException e) { + MagicScriptError.error("定义double变量值不合法", literal, e); + } + } + + @Override + public void compile(MagicScriptCompiler context) { + context.ldc(value).invoke(INVOKESTATIC, Double.class, "valueOf", Double.class, double.class); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/FloatLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/FloatLiteral.java new file mode 100644 index 0000000..8fee9f7 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/FloatLiteral.java @@ -0,0 +1,38 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.MagicScriptError; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Literal; + +/** float常量 */ +public class FloatLiteral extends Literal { + + public FloatLiteral(Span literal) { + super(literal); + try { + setValue( + Float.parseFloat( + literal.getText().substring(0, literal.getText().length() - 1).replace("_", ""))); + } catch (NumberFormatException e) { + MagicScriptError.error("定义float变量值不合法", literal, e); + } + } + + @Override + public void compile(MagicScriptCompiler context) { + context.ldc(value).invoke(INVOKESTATIC, Float.class, "valueOf", Float.class, float.class); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/IntegerLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/IntegerLiteral.java new file mode 100644 index 0000000..1d5e229 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/IntegerLiteral.java @@ -0,0 +1,42 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.MagicScriptError; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Literal; + +/** int常量 */ +public class IntegerLiteral extends Literal { + + public IntegerLiteral(Span literal) { + super(literal); + try { + setValue(Integer.parseInt(literal.getText().replace("_", ""))); + } catch (NumberFormatException e) { + MagicScriptError.error("定义int变量值不合法", literal, e); + } + } + + public IntegerLiteral(Span span, Object value) { + super(span, value); + } + + @Override + public void compile(MagicScriptCompiler context) { + context + .visitInt((Integer) value) + .invoke(INVOKESTATIC, Integer.class, "valueOf", Integer.class, int.class); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/ListLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/ListLiteral.java new file mode 100644 index 0000000..edde25c --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/ListLiteral.java @@ -0,0 +1,50 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Literal; +import cn.universal.core.engine.parsing.ast.statement.Spread; +import java.util.List; + +/** List常量 */ +public class ListLiteral extends Literal { + + public final List values; + + public ListLiteral(Span span, List values) { + super(span); + this.values = values; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + values.forEach(expr -> expr.visitMethod(compiler)); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + int size = values.size(); + if (size == 0) { + compiler.newArrayList(); + } else { + compiler + .insn(values.stream().anyMatch(it -> it instanceof Spread) ? ICONST_1 : ICONST_0) + .asBoolean() + .newArray(values) + .call("newArrayList", 2); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/LongLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/LongLiteral.java new file mode 100644 index 0000000..296a8ad --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/LongLiteral.java @@ -0,0 +1,38 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Literal; + +/** long 常量 */ +public class LongLiteral extends Literal { + + public LongLiteral(Span literal) { + this( + literal, + Long.parseLong( + literal.getText().substring(0, literal.getText().length() - 1).replace("_", ""))); + } + + public LongLiteral(Span span, Object value) { + super(span); + this.value = value; + } + + @Override + public void compile(MagicScriptCompiler context) { + context.ldc(value).invoke(INVOKESTATIC, Long.class, "valueOf", Long.class, long.class); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/MapLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/MapLiteral.java new file mode 100644 index 0000000..5e91afb --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/MapLiteral.java @@ -0,0 +1,62 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Literal; +import cn.universal.core.engine.parsing.ast.statement.Spread; +import java.util.List; +import java.util.Objects; + +/** map常量 */ +public class MapLiteral extends Literal { + + private final List keys; + private final List values; + + public MapLiteral(Span span, List keys, List values) { + super(span); + this.keys = keys; + this.values = values; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + values.forEach(it -> it.visitMethod(compiler)); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + int size = keys.size(); + compiler + .insn(values.stream().anyMatch(it -> it instanceof Spread) ? ICONST_1 : ICONST_0) + .asBoolean() + .visitInt((int) (size * 2 - keys.stream().filter(Objects::isNull).count())) + .typeInsn(ANEWARRAY, Object.class); + int index = 0; + for (int i = 0; i < size; i++) { + Expression key = keys.get(i); + Expression expression = values.get(i); + compiler.insn(DUP).visitInt(index++); + if (expression instanceof Spread) { + compiler.visit(expression); + } else { + compiler.visit(key).insn(AASTORE).insn(DUP).visitInt(index++).visit(expression); + } + compiler.insn(AASTORE); + } + compiler.call("newLinkedHashMap", 2); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/NullLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/NullLiteral.java new file mode 100644 index 0000000..1c99c95 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/NullLiteral.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Literal; + +/** null 常量 */ +public class NullLiteral extends Literal { + + public NullLiteral(Span span) { + super(span); + } + + @Override + public void compile(MagicScriptCompiler context) { + context.insn(ACONST_NULL); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/RegexpLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/RegexpLiteral.java new file mode 100644 index 0000000..9418a1f --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/RegexpLiteral.java @@ -0,0 +1,52 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.RegexpToken; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Literal; +import java.util.regex.Pattern; + +/** 正则常量 */ +public class RegexpLiteral extends Literal { + + private int flag; + + public RegexpLiteral(Span span, Object value) { + super(span); + this.flag = ((RegexpToken) value).getFlag(); + int i = flag & Pattern.CASE_INSENSITIVE; + int m = flag & Pattern.MULTILINE; + int s = flag & Pattern.DOTALL; + int u = flag & Pattern.UNICODE_CHARACTER_CLASS; + + int f = 0; + f |= i; + f |= m; + f |= s; + f |= u; + this.flag = f; + setValue( + Pattern.compile(span.getText().replaceAll("^/", "").replaceAll("/[gismuy]*?$", ""), f)); + } + + @Override + public void compile(MagicScriptCompiler context) { + String regex = getSpan().getText().replaceAll("^/", "").replaceAll("/[gismuy]*?$", ""); + context + .ldc(regex) + .visitInt(this.flag) + .invoke(INVOKESTATIC, Pattern.class, "compile", Pattern.class, String.class, int.class); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/ShortLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/ShortLiteral.java new file mode 100644 index 0000000..4a13bee --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/ShortLiteral.java @@ -0,0 +1,38 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.MagicScriptError; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Literal; + +/** short 常量 */ +public class ShortLiteral extends Literal { + + public ShortLiteral(Span literal) { + super(literal); + try { + setValue( + Short.parseShort( + literal.getText().substring(0, literal.getText().length() - 1).replace("_", ""))); + } catch (NumberFormatException e) { + MagicScriptError.error("定义short变量值不合法", literal, e); + } + } + + @Override + public void compile(MagicScriptCompiler context) { + context.ldc(value).invoke(INVOKESTATIC, Short.class, "valueOf", Short.class, short.class); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/StringLiteral.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/StringLiteral.java new file mode 100644 index 0000000..5eff5af --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/literal/StringLiteral.java @@ -0,0 +1,120 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.literal; + +import cn.universal.core.engine.MagicScriptError; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.CharacterStream; +import cn.universal.core.engine.parsing.Token; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Literal; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + +/** String 常量 */ +public class StringLiteral extends Literal { + + private final Token token; + + private final List expressionList; + + public StringLiteral(Token token) { + this(token, null); + } + + public StringLiteral(Token token, List expressionList) { + super(token.getSpan()); + this.token = token; + this.expressionList = expressionList; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + if (expressionList != null) { + expressionList.forEach(expr -> expr.visitMethod(compiler)); + } + } + + @Override + public void compile(MagicScriptCompiler compiler) { + if (isTemplateString()) { + if (expressionList.isEmpty()) { + compiler.ldc(""); + } else { + compiler + .typeInsn(NEW, StringJoiner.class) + .insn(DUP) + .ldc("") + .invoke(INVOKESPECIAL, StringJoiner.class, "", void.class, CharSequence.class); + expressionList.forEach( + expression -> + compiler + .visit(expression) + .ldc("") + .invoke( + INVOKESTATIC, + Objects.class, + "toString", + String.class, + Object.class, + String.class) + .invoke( + INVOKEVIRTUAL, + StringJoiner.class, + "add", + StringJoiner.class, + CharSequence.class)); + compiler.invoke(INVOKEVIRTUAL, StringJoiner.class, "toString", String.class); + } + } else { + compiler.ldc(getValue()); + } + } + + public boolean isTemplateString() { + return expressionList != null; + } + + private String transferString(String text) { + StringBuilder builder = new StringBuilder(); + + CharacterStream stream = new CharacterStream(text); + // 处理转义符 + while (stream.hasMore()) { + if (stream.match("\\\\", true)) { + builder.append('\\'); + } else if (stream.match("\\n", true)) { + builder.append('\n'); + } else if (stream.match("\\r", true)) { + builder.append('\r'); + } else if (stream.match("\\t", true)) { + builder.append('\t'); + } else if (stream.match("\\\"", true)) { + builder.append('"'); + } else if (stream.match("\\'", true)) { + builder.append("'"); + } else { + builder.append(stream.consume()); + } + } + return builder.toString(); + } + + public String getValue() { + if (token.getTokenStream() != null) { + MagicScriptError.error("此处不支持模板字符串", getSpan()); + } + return transferString(getSpan().getText()); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Assert.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Assert.java new file mode 100644 index 0000000..a603946 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Assert.java @@ -0,0 +1,52 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.asm.Label; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Node; +import cn.universal.core.engine.runtime.handle.OperatorHandle; +import java.util.List; + +/** assert expr : expr[,expr][,expr][,expr] */ +public class Assert extends Node { + + private final Expression condition; + + private final List expressions; + + public Assert(Span span, Expression condition, List expressions) { + super(span); + this.condition = condition; + this.expressions = expressions; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + condition.visitMethod(compiler); + expressions.forEach(it -> it.visitMethod(compiler)); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + Label end = new Label(); + compiler + .visit(condition) + .invoke(INVOKESTATIC, OperatorHandle.class, "isFalse", boolean.class, Object.class) + .jump(IFEQ, end) + .compile(new Exit(getSpan(), expressions)) + .label(end); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/AsyncCall.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/AsyncCall.java new file mode 100644 index 0000000..4cac49f --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/AsyncCall.java @@ -0,0 +1,111 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.VarIndex; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.runtime.Variables; +import cn.universal.core.engine.runtime.function.MagicScriptLambdaFunction; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** 异步调用 */ +public class AsyncCall extends Expression { + + /** 默认线程池大小(CPU核心数 * 2) */ + private static ThreadPoolExecutor threadPoolExecutor = + setThreadPoolExecutorSize(Runtime.getRuntime().availableProcessors() * 2); + + private final LambdaFunction expression; + + public AsyncCall(Span span, Expression expression) { + super(span); + if (expression instanceof LambdaFunction) { + this.expression = (LambdaFunction) expression; + } else { + this.expression = + new LambdaFunction( + span, + Collections.emptyList(), + Collections.singletonList(new Return(span, expression))); + } + this.expression.setAsync(true); + } + + public static ThreadPoolExecutor setThreadPoolExecutorSize(int size) { + if (size > 0) { + threadPoolExecutor = + new ThreadPoolExecutor( + size, + size, + 0L, + TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(size * 2), + new AsyncThreadFactory(), + new ThreadPoolExecutor.CallerRunsPolicy()); + } + return threadPoolExecutor; + } + + public static FutureTask execute( + MagicScriptLambdaFunction function, Variables variables, Object[] args) { + FutureTask futureTask = new FutureTask<>(() -> function.apply(variables, args)); + // 判断当前是否在线程池中,如果是的话,则直接运行,防止线程嵌套造成的"死锁" + if (Thread.currentThread().getThreadGroup() == AsyncThreadFactory.ASYNC_THREAD_GROUP) { + futureTask.run(); + } else { + threadPoolExecutor.submit(futureTask); + } + return futureTask; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + expression.visitMethod(compiler); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + List parameters = expression.getParameters(); + compiler.compile(expression).load2(); + parameters.forEach(compiler::load); + compiler.call("call_async", parameters.size() + 2); + } + + static class AsyncThreadFactory implements ThreadFactory { + + private static final ThreadGroup ASYNC_THREAD_GROUP = new ThreadGroup("magic-async-group"); + private final AtomicLong threadNumber = new AtomicLong(1); + private final String namePrefix = "magic-async-"; + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(ASYNC_THREAD_GROUP, r, namePrefix + threadNumber.getAndIncrement(), 0); + if (t.isDaemon()) { + t.setDaemon(false); + } + if (t.getPriority() != Thread.NORM_PRIORITY) { + t.setPriority(Thread.NORM_PRIORITY); + } + return t; + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Break.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Break.java new file mode 100644 index 0000000..969c97c --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Break.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Node; + +/** break 语句 */ +public class Break extends Node { + + public Break(Span span) { + super(span); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler.end(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/ClassConverter.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/ClassConverter.java new file mode 100644 index 0000000..4f4d201 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/ClassConverter.java @@ -0,0 +1,118 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.exception.MagicScriptException; +import cn.universal.core.engine.functions.ObjectConvertExtension; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +public class ClassConverter extends Expression { + + private static final Map> CONVERTERS = + new HashMap<>(); + + static { + register("int", BigDecimal::intValue); + register("double", BigDecimal::doubleValue); + register("long", BigDecimal::longValue); + register("byte", BigDecimal::byteValue); + register("float", BigDecimal::floatValue); + register("short", BigDecimal::shortValue); + register("string", (target, params) -> process(target::toString, params)); + register( + "date", + (target, params) -> { + if (params.length == 0) { + throw new IllegalArgumentException("::date需要日期格式,如::date('yyyy-mm-dd')"); + } + try { + return ObjectConvertExtension.asDate(target, params[0].toString()); + } catch (Exception e) { + return null; + } + }); + } + + private final Expression target; + private final String convert; + private final List arguments; + + public ClassConverter(Span span, String convert, Expression target, List arguments) { + super(span); + this.convert = convert; + this.target = target; + this.arguments = arguments; + } + + private static void register(String target, Function converter) { + register( + target, + (value, params) -> { + try { + BigDecimal decimal; + if (value instanceof BigDecimal) { + decimal = (BigDecimal) value; + } else { + decimal = new BigDecimal(value.toString().trim()); + } + return converter.apply(decimal); + } catch (Exception e) { + return params != null && params.length > 0 ? params[0] : null; + } + }); + } + + private static Object process(Supplier callback, Object[] params) { + try { + return callback.get(); + } catch (Exception e) { + return params != null && params.length > 0 ? params[0] : null; + } + } + + public static Object process(Object object, String target, Object[] params) { + try { + BiFunction function = CONVERTERS.get(target); + if (function == null) { + throw new MagicScriptException(String.format("找不到转换器[%s]", target)); + } + return function.apply(object, params); + } catch (Exception e) { + return params != null && params.length > 0 ? params[0] : null; + } + } + + public static void register(String target, BiFunction converter) { + CONVERTERS.put(target, converter); + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + target.visitMethod(compiler); + arguments.forEach(it -> it.visitMethod(compiler)); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler.visit(target).ldc(convert).visit(arguments).call("type_cast", arguments.size() + 2); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Continue.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Continue.java new file mode 100644 index 0000000..4293d3f --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Continue.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Node; + +/** continue语句 */ +public class Continue extends Node { + + public Continue(Span span) { + super(span); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler.start(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Exit.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Exit.java new file mode 100644 index 0000000..44064ea --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Exit.java @@ -0,0 +1,55 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.exception.MagicExitException; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Node; +import cn.universal.core.engine.runtime.ExitValue; +import java.util.List; + +public class Exit extends Node { + + private final List expressions; + + public Exit(Span span, List expressions) { + super(span); + this.expressions = expressions; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + expressions.forEach(it -> it.visitMethod(compiler)); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .typeInsn(NEW, MagicExitException.class) + .insn(DUP) + .typeInsn(NEW, ExitValue.class) + .insn(DUP); + if (expressions == null) { + compiler.invoke(INVOKESPECIAL, ExitValue.class, "", void.class); + } else { + compiler + .newArray(expressions) + .invoke(INVOKESPECIAL, ExitValue.class, "", void.class, Object[].class); + } + compiler + .invoke(INVOKESPECIAL, MagicExitException.class, "", void.class, ExitValue.class) + .insn(ATHROW); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/ForStatement.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/ForStatement.java new file mode 100644 index 0000000..abd533b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/ForStatement.java @@ -0,0 +1,104 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.asm.Label; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.VarIndex; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Node; +import cn.universal.core.engine.runtime.handle.FunctionCallHandle; +import cn.universal.core.engine.runtime.lang.KeyIterator; +import java.util.Iterator; +import java.util.List; + +public class ForStatement extends Node { + + private final VarIndex indexOrKey; + private final VarIndex value; + private final VarIndex anonymousVariable; + private final Expression mapOrArray; + private final List body; + + public ForStatement( + Span span, + VarIndex indexOrKey, + VarIndex value, + VarIndex anonymousVariable, + Expression mapOrArray, + List body) { + super(span); + this.indexOrKey = indexOrKey; + this.anonymousVariable = anonymousVariable; + this.value = value; + this.mapOrArray = mapOrArray; + this.body = body; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + mapOrArray.visitMethod(compiler); + body.forEach(it -> it.visitMethod(compiler)); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + Label start = new Label(); + Label end = new Label(); + compiler + .markLabel(start, end) // 标记 continue 、 break位置 + .pre_store(anonymousVariable) + // 初始化 iterator + .compile(mapOrArray) + .invoke( + INVOKESTATIC, + FunctionCallHandle.class, + indexOrKey == null ? "newValueIterator" : "newKeyValueIterator", + Iterator.class, + Object.class) + // 保存至临时变量 + .store(anonymousVariable) + .label(start) + // 判断是否有值 + .load(anonymousVariable) + .invoke(INVOKEINTERFACE, Iterator.class, "hasNext", true, boolean.class) + // 值为false时,跳出循环 + .jump(IFEQ, end) + .pre_store(value) + // 获取当前 value + .load(anonymousVariable) + .invoke(INVOKEINTERFACE, Iterator.class, "next", true, Object.class) + // 存入到 value 中 + .store(value); + if (indexOrKey != null) { + // 获取当前 key + compiler + .pre_store(indexOrKey) + .load(anonymousVariable) + .invoke(INVOKEINTERFACE, KeyIterator.class, "getKey", true, Object.class) + // 存入到 key 中 + .store(indexOrKey); + } + compiler + .compile(body) // 执行循环体 + // 执行完毕后跳转到循环起始位置 + .jump(GOTO, start) + .label(end) + // 移除 key 变量 + .remove(indexOrKey) + // 移除 value 变量 + .remove(value) + .exitLabel(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/FunctionCall.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/FunctionCall.java new file mode 100644 index 0000000..9a6f5e0 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/FunctionCall.java @@ -0,0 +1,73 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import java.util.List; + +public class FunctionCall extends Expression { + + private final Expression function; + private final List arguments; + + private final boolean inLinq; + + public FunctionCall(Span span, Expression function, List arguments, boolean inLinq) { + super(span); + this.function = function; + this.arguments = arguments; + this.inLinq = inLinq; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + function.visitMethod(compiler); + arguments.forEach(it -> it.visitMethod(compiler)); + } + + public Expression getFunction() { + return function; + } + + public List getArguments() { + return arguments; + } + + @Override + public void compile(MagicScriptCompiler compiler) { + int size = arguments.size(); + compiler + .newRuntimeContext() + .ldc(getFunction().getSpan().getText()) // 函数名 + .insn(arguments.stream().anyMatch(it -> it instanceof Spread) ? ICONST_1 : ICONST_0) + .asBoolean() // 是否有扩展参数(...xxx) + .visitInt(size) + .typeInsn(ANEWARRAY, Object.class); + for (int i = 0; i < size; i++) { + Expression argument = arguments.get(i); + compiler.insn(DUP).visitInt(i); + if (inLinq && argument instanceof MemberAccess) { + ((MemberAccess) argument).compileLinq(compiler); + } else { + compiler.visit(argument); + } + compiler.insn(AASTORE); + } + compiler + .visit(function) // 访问函数 + .lineNumber(getSpan()) + .call("invoke_function", 5); // 调用函数 + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/IfStatement.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/IfStatement.java new file mode 100644 index 0000000..8a63b10 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/IfStatement.java @@ -0,0 +1,85 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.asm.Label; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Node; +import cn.universal.core.engine.runtime.handle.OperatorHandle; +import java.util.List; + +public class IfStatement extends Node { + + private final Expression condition; + private final List trueBlock; + private final List elseIfs; + private final List falseBlock; + + public IfStatement( + Span span, + Expression condition, + List trueBlock, + List elseIfs, + List falseBlock) { + super(span); + this.condition = condition; + this.trueBlock = trueBlock; + this.elseIfs = elseIfs; + this.falseBlock = falseBlock; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + condition.visitMethod(compiler); + trueBlock.forEach(it -> it.visitMethod(compiler)); + elseIfs.forEach(it -> it.visitMethod(compiler)); + falseBlock.forEach(it -> it.visitMethod(compiler)); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + Label end = new Label(); + Label next = new Label(); + compiler + .visit(condition) // 访问表达式 + .invoke( + INVOKESTATIC, OperatorHandle.class, "isTrue", boolean.class, Object.class) // 判断是否为true + .jump(IFEQ, next) + .compile(trueBlock) // 编译true代码块 + .jump(GOTO, end); // 跳转至结束 + for (IfStatement elseIf : elseIfs) { + compiler + .label(next) + .visit(elseIf.condition) // 访问else if 表达式 + .invoke( + INVOKESTATIC, + OperatorHandle.class, + "isTrue", + boolean.class, + Object.class); // 判断是否为true + next = new Label(); + compiler + .jump(IFEQ, next) + .compile(elseIf.trueBlock) // 编译 else if true代码块 + .jump(GOTO, end); + } + compiler.label(next); + if (!falseBlock.isEmpty()) { + // 编译 false 代码块 + compiler.compile(falseBlock); + } + compiler.label(end); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Import.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Import.java new file mode 100644 index 0000000..7a52700 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Import.java @@ -0,0 +1,95 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.MagicResourceLoader; +import cn.universal.core.engine.MagicScriptContext; +import cn.universal.core.engine.asm.Label; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.functions.DynamicModuleImport; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.VarIndex; +import cn.universal.core.engine.parsing.ast.Node; + +public class Import extends Node { + + private final VarIndex varIndex; + private final boolean module; + private String packageName; + private boolean function; + + public Import(Span span, String packageName, VarIndex varIndex, boolean module) { + super(span); + this.packageName = packageName; + this.varIndex = varIndex; + this.module = module; + if (!module && packageName.startsWith("@")) { + function = true; + this.packageName = packageName.substring(1); + } + } + + public boolean isImportPackage() { + /** 不允许批量导入包 */ + return false; + // return packageName.endsWith(".*"); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + if (isImportPackage()) { + compiler + .loadContext() + .ldc(packageName.substring(0, packageName.length() - 1)) + .invoke(INVOKEVIRTUAL, MagicScriptContext.class, "addImport", void.class, String.class); + } else { + String methodName = "loadClass"; + if (this.module) { + methodName = "loadModule"; + } else if (this.function) { + methodName = "loadFunction"; + } + compiler + .pre_store(varIndex) // 保存变量前的准备 + .loadContext() + .ldc(packageName) // 包名&函数名 + .invoke( + INVOKESTATIC, + MagicResourceLoader.class, + methodName, + Object.class, + MagicScriptContext.class, + String.class); // 加载资源 + if (this.module) { + // if(module instanceof DynamicModuleImport){ module = + // ((DynamicModuleImport)module).getDynamicModule + // (context); } + Label end = new Label(); + compiler + .insn(DUP) + .typeInsn(INSTANCEOF, DynamicModuleImport.class) + .jump(IFEQ, end) + .typeInsn(CHECKCAST, DynamicModuleImport.class) + .loadContext() + .invoke( + INVOKEVIRTUAL, + DynamicModuleImport.class, + "getDynamicModule", + Object.class, + MagicScriptContext.class) + .label(end); + } + compiler.store(varIndex); // 保存变量 + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/LambdaFunction.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/LambdaFunction.java new file mode 100644 index 0000000..c4d91ac --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/LambdaFunction.java @@ -0,0 +1,59 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.VarIndex; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Node; +import java.util.List; + +public class LambdaFunction extends Expression { + + private final List parameters; + private final List childNodes; + private String methodName; + + private boolean async; + + public LambdaFunction(Span span, List parameters, List childNodes) { + super(span); + this.parameters = parameters; + this.childNodes = childNodes; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + this.methodName = + compiler.visitMethod((async ? "async_" : "") + "lambda", childNodes, parameters); + } + + public void setAsync(boolean async) { + this.async = async; + } + + public List getParameters() { + return parameters; + } + + /** 访问lambda方法 */ + private void compileMethod(MagicScriptCompiler compiler) { + compiler.load0().lambda(methodName); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compileMethod(compiler); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/MapOrArrayAccess.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/MapOrArrayAccess.java new file mode 100644 index 0000000..3e898c5 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/MapOrArrayAccess.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.VariableSetter; + +public class MapOrArrayAccess extends Expression implements VariableSetter { + + private final Expression mapOrArray; + private final Expression keyOrIndex; + + public MapOrArrayAccess(Span span, Expression mapOrArray, Expression keyOrIndex) { + super(span); + this.mapOrArray = mapOrArray; + this.keyOrIndex = keyOrIndex; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + mapOrArray.visitMethod(compiler); + keyOrIndex.visitMethod(compiler); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler.visit(mapOrArray).visit(keyOrIndex).operator("map_or_array_access"); + } + + @Override + public void compile_visit_variable(MagicScriptCompiler compiler) { + compiler.visit(mapOrArray).visit(keyOrIndex); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/MemberAccess.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/MemberAccess.java new file mode 100644 index 0000000..76493a1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/MemberAccess.java @@ -0,0 +1,87 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.VariableSetter; + +public class MemberAccess extends Expression implements VariableSetter { + + private final Expression object; + private final Span name; + private final boolean optional; + private final boolean whole; + + public MemberAccess(Expression object, boolean optional, Span name, boolean whole) { + super(new Span(object.getSpan(), name)); + this.object = object; + this.name = name; + this.optional = optional; + this.whole = whole; + } + + public boolean isWhole() { + return whole; + } + + public boolean isOptional() { + return optional || (object instanceof MemberAccess && ((MemberAccess) object).isOptional()); + } + + public Expression getObject() { + return object; + } + + public Span getName() { + return name; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + object.visitMethod(compiler); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .newRuntimeContext() + .visit(object) // 访问目标对象 + .ldc(name.getText()) // 成员名 + .insn(isOptional() ? ICONST_1 : ICONST_0) // 是否可空调用 ?. + .asBoolean() + .insn(ICONST_0) // 非linq中 + .asBoolean() + .lineNumber(name) + .call("member_access", 5); + } + + public void compileLinq(MagicScriptCompiler compiler) { + compiler + .newRuntimeContext() + .visit(object) // 访问目标对象 + .ldc(name.getText()) // 成员名 + .insn(optional ? ICONST_1 : ICONST_0) // 是否可空调用 ?. + .asBoolean() + .insn(ICONST_1) // linq中 + .asBoolean() + .lineNumber(name) + .call("member_access", 5); + } + + @Override + public void compile_visit_variable(MagicScriptCompiler compiler) { + compiler.compile(object).ldc(name.getText()); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/MethodCall.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/MethodCall.java new file mode 100644 index 0000000..4631c83 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/MethodCall.java @@ -0,0 +1,78 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import java.util.List; + +public class MethodCall extends Expression { + + private final MemberAccess method; + private final List arguments; + private final boolean inLinq; + + public MethodCall(Span span, MemberAccess method, List arguments, boolean inLinq) { + super(span); + this.method = method; + this.arguments = arguments; + this.inLinq = inLinq; + } + + public MemberAccess getMethod() { + return method; + } + + public List getArguments() { + return arguments; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + method.visitMethod(compiler); + arguments.forEach(it -> it.visitMethod(compiler)); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + int size = arguments.size(); + compiler + .newRuntimeContext() + .ldc(method.getName().getText()) // 方法名 + .insn( + arguments.stream().anyMatch(it -> it instanceof Spread) + ? ICONST_1 + : ICONST_0) // 是否是 (...xxx) + .asBoolean() + .insn(method.isOptional() ? ICONST_1 : ICONST_0) // 是否允许可空调用 + .asBoolean() + .visitInt(size) + .typeInsn(ANEWARRAY, Object.class); + for (int i = 0; i < size; i++) { + Expression argument = arguments.get(i); + compiler.insn(DUP).visitInt(i); + if (inLinq && argument instanceof MemberAccess) { + ((MemberAccess) argument).compileLinq(compiler); + } else { + compiler.visit(argument); + } + compiler.insn(AASTORE); + } + compiler + .visit(method.getObject()) // 访问目标对象 + .lineNumber( + new Span(getSpan().getSource(), method.getName().getStart(), getSpan().getEnd())) + .call("invoke_method", 6); // 调用方法 + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/NewStatement.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/NewStatement.java new file mode 100644 index 0000000..bb10a81 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/NewStatement.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import java.util.List; + +public class NewStatement extends Expression { + + private final List arguments; + + private final Expression target; + + public NewStatement(Span span, Expression target, List arguments) { + super(span); + this.target = target; + this.arguments = arguments; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + target.visitMethod(compiler); + arguments.forEach(it -> it.visitMethod(compiler)); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .newRuntimeContext() + .visit(target) // 访问目标 + .newArray(arguments) // 访问参数 + .lineNumber(getSpan()) + .call("invoke_new_instance", 3); // 执行new操作 + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Return.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Return.java new file mode 100644 index 0000000..569d7fe --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Return.java @@ -0,0 +1,62 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Node; +import java.util.List; + +public class Return extends Node { + + private final Node returnValue; + + public Return(Span span, Node returnValue) { + super(span); + this.returnValue = returnValue; + } + + public Node getReturnValue() { + return returnValue; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + if (returnValue != null) { + returnValue.visitMethod(compiler); + } + } + + @Override + public void compile(MagicScriptCompiler compiler) { + List block = compiler.getFinallyBlock(); + if (returnValue == null) { // return + if (block != null) { // 如果有finally块 + compiler.compile(block); // 执行finally块 + } + compiler.insn(ACONST_NULL); // 压入 NULL + } else { // return expr; + compiler.visit(returnValue); + if (block != null) { // 如果有finally块 + compiler + .store(3) // 保存返回结果 + .compile(block) // 执行 finally + .load3(); // 加载返回结果 + } + } + if (block != null) { + compiler.putFinallyBlock(block); + } + compiler.insn(ARETURN); // 返回 + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Spread.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Spread.java new file mode 100644 index 0000000..383f5da --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Spread.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.runtime.SpreadValue; + +/** 展开语法 Spread syntax (...) */ +public class Spread extends Expression { + + private final Expression target; + + public Spread(Span span, Expression target) { + super(span); + this.target = target; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + target.visitMethod(compiler); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + // 对于...xxx 的参数 统一转换为 new SpreadValue(object) + compiler + .typeInsn(NEW, SpreadValue.class) + .insn(DUP) + .visit(target) + .invoke(INVOKESPECIAL, SpreadValue.class, "", void.class, Object.class); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Throw.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Throw.java new file mode 100644 index 0000000..5fcfe67 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/Throw.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.exception.MagicScriptRuntimeException; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Node; + +public class Throw extends Node { + + private final Expression expression; + + public Throw(Span span, Expression expression) { + super(span); + this.expression = expression; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + expression.visitMethod(compiler); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .visit(expression) + .invoke( + INVOKESTATIC, + MagicScriptRuntimeException.class, + "create", + MagicScriptRuntimeException.class, + Object.class) + .insn(ATHROW); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/TryStatement.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/TryStatement.java new file mode 100644 index 0000000..b67f458 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/TryStatement.java @@ -0,0 +1,138 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.asm.Label; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.exception.MagicExitException; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.VarIndex; +import cn.universal.core.engine.parsing.ast.Node; +import java.util.Collections; +import java.util.List; + +public class TryStatement extends Node { + + private final VarIndex exceptionVarNode; + private final List tryBlock; + private final List tryResources; + private final List catchBlock; + private final List finallyBlock; + + public TryStatement( + Span span, + VarIndex exceptionVarNode, + List tryBlock, + List tryResources, + List catchBlock, + List finallyBlock) { + super(span); + this.exceptionVarNode = exceptionVarNode; + this.tryBlock = tryBlock; + this.tryResources = tryResources; + Collections.reverse(this.tryResources); + this.catchBlock = catchBlock; + this.finallyBlock = finallyBlock; + this.finallyBlock.add( + 0, + new Node(new Span("auto close")) { + @Override + public void visitMethod(MagicScriptCompiler compiler) { + tryResources.forEach(it -> it.visitMethod(compiler)); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + tryResources.forEach( + it -> + compiler + .load(it.getVarIndex()) + .invoke( + INVOKESTATIC, + TryStatement.class, + "autoClose", + void.class, + Object.class)); + } + }); + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + tryBlock.forEach(it -> it.visitMethod(compiler)); + catchBlock.forEach(it -> it.visitMethod(compiler)); + finallyBlock.forEach(it -> it.visitMethod(compiler)); + } + + public static void autoClose(Object object) { + if (object instanceof AutoCloseable) { + try { + ((AutoCloseable) object).close(); + } catch (Exception ignored) { + } + } + } + + @Override + public void compile(MagicScriptCompiler compiler) { + Label l0 = new Label(); + Label l1 = new Label(); + Label l2 = new Label(); + Label l3 = new Label(); + Label l4 = new Label(); + Label l5 = new Label(); + Label end = new Label(); + boolean hasCatch = exceptionVarNode != null; + boolean hasFinally = !finallyBlock.isEmpty(); + if (hasFinally) { // try + catch + finally + compiler.putFinallyBlock(finallyBlock); + } + compiler + .label(l0) + .compile(tryResources) + .compile(tryBlock) // try + .label(l1); + if (hasFinally) { + compiler.compile(finallyBlock).jump(GOTO, end); // 跳转至结束 + } + compiler + .label(l2) // catch MagicExitException + .store(3) + // .compile(this.autoClose) + .load3() + .insn(ATHROW); // throw e + if (hasCatch) { + compiler + .label(l3) + .store(3) + .pre_store(exceptionVarNode) // 保存异常变量前准备 + .load3() + .store(exceptionVarNode) + .compile(catchBlock); // 编译catch代码块 + } + if (hasFinally) { + compiler.label(l5).compile(finallyBlock).jump(GOTO, end); // 跳转至结束 + compiler.label(l4).store(3).compile(finallyBlock).load3().insn(ATHROW); + compiler.getFinallyBlock(); + } + compiler.label(end); + compiler.tryCatch(l0, l1, l2, MagicExitException.class); + if (hasCatch) { + compiler.tryCatch(l0, l1, l3, Throwable.class); + } + if (hasFinally) { // try + catch + finally + compiler.tryCatch(l0, l1, l4, null); + compiler.tryCatch(l2, l5, l4, null); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/VariableAccess.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/VariableAccess.java new file mode 100644 index 0000000..5a4ac53 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/VariableAccess.java @@ -0,0 +1,38 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.VarIndex; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.VariableSetter; + +public class VariableAccess extends Expression implements VariableSetter { + + private final VarIndex varIndex; + + public VariableAccess(Span name, VarIndex varIndex) { + super(name); + this.varIndex = varIndex; + } + + public VarIndex getVarIndex() { + return varIndex; + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler.load(varIndex); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/VariableDefine.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/VariableDefine.java new file mode 100644 index 0000000..0dffdd5 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/VariableDefine.java @@ -0,0 +1,51 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.VarIndex; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Node; + +public class VariableDefine extends Node { + + private final Expression right; + + private final VarIndex varIndex; + + public VariableDefine(Span span, VarIndex varIndex, Expression right) { + super(span); + this.varIndex = varIndex; + this.right = right; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + if (right != null) { + right.visitMethod(compiler); + } + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler + .pre_store(varIndex) + .visit(right) // 读取变量值 + .scopeStore(); // 保存变量 + } + + public VarIndex getVarIndex() { + return varIndex; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/VariableDestructuringDefine.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/VariableDestructuringDefine.java new file mode 100644 index 0000000..851f37c --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/VariableDestructuringDefine.java @@ -0,0 +1,67 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import java.util.ArrayList; +import java.util.List; + +public class VariableDestructuringDefine extends VariableDefine { + + private final List defines; + private final Expression right; + private final boolean mapAccess; + + public VariableDestructuringDefine(Span span, int size, Expression right, boolean mapAccess) { + super(span, null, null); + this.defines = new ArrayList<>(size); + this.right = right; + this.mapAccess = mapAccess; + } + + public void add(VariableDefine variableDefine) { + defines.add(variableDefine); + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + if (right != null) { + right.visitMethod(compiler); + } + } + + @Override + public void compile(MagicScriptCompiler compiler) { + compiler.visit(this.right).store(4); + for (int i = 0, size = this.defines.size(); i < size; i++) { + VariableDefine define = this.defines.get(i); + compiler.pre_store(define.getVarIndex()); + if (this.mapAccess) { + compiler + .newRuntimeContext() + .load4() + .ldc(define.getVarIndex().getName()) // 成员名 + .insn(ICONST_1) // 是否可空调用 ?. + .asBoolean() + .insn(ICONST_0) // 非linq中 + .asBoolean() + .call("member_access", 5); + } else { + compiler.load4().visitInt(i).asInteger().operator("map_or_array_access"); + } + compiler.scopeStore(); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/WhileStatement.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/WhileStatement.java new file mode 100644 index 0000000..24b15db --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/parsing/ast/statement/WhileStatement.java @@ -0,0 +1,59 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.parsing.ast.statement; + +import cn.universal.core.engine.asm.Label; +import cn.universal.core.engine.compile.MagicScriptCompiler; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.parsing.ast.Expression; +import cn.universal.core.engine.parsing.ast.Node; +import cn.universal.core.engine.runtime.handle.OperatorHandle; +import java.util.List; + +public class WhileStatement extends Node { + + private final Expression condition; + private final List trueBlock; + + public WhileStatement(Span span, Expression condition, List trueBlock) { + super(span); + this.condition = condition; + this.trueBlock = trueBlock; + } + + @Override + public void visitMethod(MagicScriptCompiler compiler) { + condition.visitMethod(compiler); + trueBlock.forEach(it -> it.visitMethod(compiler)); + } + + @Override + public void compile(MagicScriptCompiler compiler) { + Label start = new Label(); + Label end = new Label(); + compiler + .markLabel(start, end) // 标记 continue 和 break 位置 + .label(start) + // 判断是否为true + .visit(condition) + .invoke(INVOKESTATIC, OperatorHandle.class, "isTrue", boolean.class, Object.class) + // 值为false时,跳出循环 + .jump(IFEQ, end) + // 执行循环体 + .compile(trueBlock) + // 执行完毕后跳转到循环起始位置 + .jump(GOTO, start) + .label(end) + .exitLabel(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/reflection/ConstructorInvoker.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/reflection/ConstructorInvoker.java new file mode 100644 index 0000000..ff43646 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/reflection/ConstructorInvoker.java @@ -0,0 +1,38 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.reflection; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +class ConstructorInvoker extends JavaInvoker { + + ConstructorInvoker(Constructor constructor) { + super(constructor); + } + + ConstructorInvoker(JavaInvoker invoker) { + super(invoker); + } + + @Override + public ConstructorInvoker copy() { + return new ConstructorInvoker(this); + } + + @Override + Object invoke(Object target, Object... args) + throws InvocationTargetException, IllegalAccessException, InstantiationException { + return getExecutable().newInstance(args); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/reflection/JavaInvoker.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/reflection/JavaInvoker.java new file mode 100644 index 0000000..7fd6460 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/reflection/JavaInvoker.java @@ -0,0 +1,212 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.reflection; + +import cn.universal.core.engine.convert.ClassImplicitConvert; +import cn.universal.core.engine.runtime.RuntimeContext; +import cn.universal.core.engine.runtime.Variables; +import java.lang.reflect.Array; +import java.lang.reflect.Executable; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +public class JavaInvoker { + + private final Map converts = new HashMap<>(); + private final T executable; + private final Class[] parameterTypes; + private final Class[] fixedParameterTypes; + private boolean implicit = false; + private boolean extension = false; + private Object defaultTarget; + private final boolean hasRuntimeContext; + + JavaInvoker(T executable) { + this.executable = executable; + this.executable.setAccessible(true); + this.parameterTypes = this.executable.getParameterTypes(); + this.hasRuntimeContext = + this.parameterTypes != null + && Stream.of(this.parameterTypes).anyMatch(RuntimeContext.class::isAssignableFrom); + if (this.hasRuntimeContext) { + this.fixedParameterTypes = + Stream.of(this.parameterTypes) + .filter(it -> !RuntimeContext.class.isAssignableFrom(it)) + .toArray(Class[]::new); + } else { + this.fixedParameterTypes = this.parameterTypes; + } + } + + JavaInvoker(JavaInvoker invoker) { + this.executable = invoker.executable; + this.parameterTypes = invoker.parameterTypes; + this.fixedParameterTypes = invoker.fixedParameterTypes; + this.hasRuntimeContext = invoker.hasRuntimeContext; + this.implicit = invoker.implicit; + this.extension = invoker.extension; + this.defaultTarget = invoker.defaultTarget; + } + + public JavaInvoker copy() { + throw new UnsupportedOperationException(); + } + + public boolean isImplicit() { + return implicit; + } + + public void setImplicit(boolean implicit) { + this.implicit = implicit; + } + + public boolean isExtension() { + return extension; + } + + public void setExtension(boolean extension) { + this.extension = extension; + } + + public Object getDefaultTarget() { + return defaultTarget; + } + + public void setDefaultTarget(Object defaultTarget) { + this.defaultTarget = defaultTarget; + } + + public T getExecutable() { + return this.executable; + } + + public Class[] getParameterTypes() { + return fixedParameterTypes; + } + + public boolean isVarArgs() { + return this.executable.isVarArgs(); + } + + public boolean hasRuntimeContext() { + return hasRuntimeContext; + } + + public Object invoke0(Object target, RuntimeContext runtimeContext, Object[] arguments) + throws Throwable { + try { + if (hasRuntimeContext) { + arguments = insertArgument(arguments, runtimeContext); + } + if (extension) { + arguments = insertArgument(arguments, target); + if (target.getClass().isArray()) { + Object[] objs = new Object[Array.getLength(target)]; + for (int i = 0, len = objs.length; i < len; i++) { + Array.set(objs, i, Array.get(target, i)); + } + arguments[0] = objs; + } + } else if (isVarArgs() + && parameterTypes.length == 1 + && arguments.length == 1 + && (arguments[0] == null || arguments[0].getClass().isArray())) { + return invoke(target, arguments); + } + return invoke( + target, + processArguments( + runtimeContext == null ? null : runtimeContext.getVariables(), arguments)); + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } catch (IllegalAccessException e1) { + throw e1; + } + } + + Object invoke(Object target, Object... arguments) + throws InvocationTargetException, IllegalAccessException, InstantiationException { + throw new UnsupportedOperationException(); + } + + protected Object[] insertArgument(Object[] arguments, Object value) { + int argumentLength = arguments == null ? 0 : arguments.length; + Object[] dest = new Object[argumentLength + 1]; + System.arraycopy(arguments, 0, dest, 1, argumentLength); + dest[0] = value; + return dest; + } + + /** + * 给参数设置隐式转换方法 + * + * @param index 索引 + * @param classImplicitConvert 转换方法 + */ + protected void addClassImplicitConvert(int index, ClassImplicitConvert classImplicitConvert) { + converts.put(index, classImplicitConvert); + } + + /** 预处理参数,用来实现隐式转换 */ + protected Object[] processArguments(Variables variables, Object[] arguments) { + int count = this.executable.getParameterCount(); + int maxIndex = Integer.MAX_VALUE; + Class componentType = null; + if (isVarArgs()) { + componentType = this.executable.getParameterTypes()[count - 1].getComponentType(); + maxIndex = count - 1; + } + if (arguments != null) { + for (Map.Entry entry : converts.entrySet()) { + int index = entry.getKey(); + arguments[index] = + entry + .getValue() + .convert( + variables, + arguments[index], + index < maxIndex ? parameterTypes[index] : componentType); + } + } + if (isVarArgs()) { + Object[] args = new Object[count]; + if (arguments != null) { + if (count - 1 >= 0) { + System.arraycopy(arguments, 0, args, 0, count - 1); + } + int len = arguments.length - count + 1; + Object varArgs = null; + if (len == 1) { + Object target = arguments[arguments.length - 1]; + if (target != null + && target.getClass() == this.executable.getParameterTypes()[count - 1]) { + varArgs = target; + } + } + if (varArgs == null) { + varArgs = Array.newInstance(componentType, len); + if (len > 0) { + for (int i = 0; i < len; i++) { + Array.set(varArgs, i, arguments[count - 1 + i]); + } + } + } + args[count - 1] = varArgs; + } + arguments = args; + } + return arguments; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/reflection/JavaReflection.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/reflection/JavaReflection.java new file mode 100644 index 0000000..c392b34 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/reflection/JavaReflection.java @@ -0,0 +1,792 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.reflection; + +import cn.universal.core.engine.convert.BooleanImplicitConvert; +import cn.universal.core.engine.convert.ClassImplicitConvert; +import cn.universal.core.engine.convert.CollectionImplicitConvert; +import cn.universal.core.engine.convert.FunctionalImplicitConvert; +import cn.universal.core.engine.convert.MapImplicitConvert; +import cn.universal.core.engine.functions.ArrayFunctions; +import cn.universal.core.engine.functions.ClassExtension; +import cn.universal.core.engine.functions.CollectionFunctions; +import cn.universal.core.engine.functions.DateExtension; +import cn.universal.core.engine.functions.MagicScriptFunctions; +import cn.universal.core.engine.functions.MapExtension; +import cn.universal.core.engine.functions.NumberExtension; +import cn.universal.core.engine.functions.ObjectConvertExtension; +import cn.universal.core.engine.functions.ObjectTypeConditionExtension; +import cn.universal.core.engine.functions.PatternExtension; +import cn.universal.core.engine.functions.StreamExtension; +import cn.universal.core.engine.functions.StringExtension; +import cn.universal.core.engine.functions.StringFunctions; +import cn.universal.core.engine.functions.TemporalAccessorExtension; +import cn.universal.core.engine.functions.linq.AggregationFunctions; +import cn.universal.core.engine.functions.linq.LinqFunctions; +import cn.universal.core.engine.functions.linq.MathFunctions; +import cn.universal.core.engine.runtime.RuntimeContext; +import java.beans.Transient; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class JavaReflection { + + private static final Map, Map> FIELD_CACHE = new ConcurrentHashMap<>(); + private static final List CONVERTS = new ArrayList<>(); + private static final Map, Map>>> + EXTENSION_METHOD_CACHE = new ConcurrentHashMap<>(); + private static final Map, Map>> METHOD_CACHE = + new ConcurrentHashMap<>(); + private static final Map, List>> EXTENSION_MAP = new ConcurrentHashMap<>(); + private static final List> FUNCTIONS = new ArrayList<>(); + + static { + registerMethodExtension(Class.class, new ClassExtension()); + StreamExtension streamExtension = new StreamExtension(); + registerMethodExtension(Collection.class, streamExtension); + registerMethodExtension(Object[].class, streamExtension); + registerMethodExtension(Enumeration.class, streamExtension); + registerMethodExtension(Iterator.class, streamExtension); + registerMethodExtension(Object.class, new ObjectConvertExtension()); + registerMethodExtension(Object.class, new ObjectTypeConditionExtension()); + registerMethodExtension(Map.class, new MapExtension()); + registerMethodExtension(Date.class, new DateExtension()); + registerMethodExtension(TemporalAccessorExtension.class, new TemporalAccessorExtension()); + registerMethodExtension(Number.class, new NumberExtension()); + registerMethodExtension(Pattern.class, new PatternExtension()); + registerMethodExtension(String.class, new StringExtension()); + // Map 到 Bean 隐式转换 + registerImplicitConvert(new MapImplicitConvert()); + // 集合 到 List 的转换 + registerImplicitConvert(new CollectionImplicitConvert()); + // lambda 到 functional 的转换 + registerImplicitConvert(new FunctionalImplicitConvert()); + // 任意值 到 Boolean 的转换 + registerImplicitConvert(new BooleanImplicitConvert()); + + registerFunction(new AggregationFunctions()); + registerFunction(new LinqFunctions()); + registerFunction(new CollectionFunctions()); + registerFunction(new MathFunctions()); + registerFunction(new StringFunctions()); + registerFunction(new MagicScriptFunctions()); + registerFunction(new ArrayFunctions()); + /** 功能扩展 */ + } + + public static void registerFunction(Object target) { + Stream.of(target.getClass().getMethods()) + .filter( + method -> + method.getAnnotation(cn.universal.core.engine.annotation.Function.class) != null) + .map(MethodInvoker::new) + .forEach( + it -> { + it.setDefaultTarget(target); + FUNCTIONS.add(it); + }); + } + + public static Map, List>> getExtensionMap() { + return EXTENSION_MAP; + } + + public static List> getFunctions() { + return FUNCTIONS; + } + + private static MethodInvoker findApply(Class cls) { + for (Method method : cls.getDeclaredMethods()) { + if ("apply".equals(method.getName())) { + return new MethodInvoker(method); + } + } + return null; + } + + private static int calcToObjectDistanceWithInterface( + Class[] interfaces, int distance, int score) { + if (interfaces == null) { + return distance; + } + return Arrays.stream(interfaces) + .mapToInt( + i -> { + int v = calcToObjectDistanceWithInterface(i.getInterfaces(), distance, score + 2); + return v + distance + score; + }) + .sum(); + } + + private static int calcToObjectDistance(Class clazz) { + return calcToObjectDistance(clazz, 0); + } + + private static int calcToObjectDistance(Class clazz, int distance) { + if (clazz == null) { + return distance + 3; + } + if (Object.class.equals(clazz)) { + return distance; + } + int interfaceScore = calcToObjectDistanceWithInterface(clazz.getInterfaces(), distance + 2, 0); + if (clazz.isInterface()) { + return interfaceScore; + } + int classScore = calcToObjectDistance(clazz.getSuperclass(), distance + 3); + return classScore + interfaceScore; + } + + private static boolean isImplicitConvert(Class from, Class to) { + if (isPrimitiveAssignableFrom(from, from) || isPrimitiveAssignableFrom(to, to)) { + return false; + } else if (Collection.class.isAssignableFrom(to) + || Iterator.class.isAssignableFrom(to) + || Enumeration.class.isAssignableFrom(to) + || to.isArray()) { + Class toClazz = getGenericType(to); + return toClazz != null && (!isPrimitiveAssignableFrom(toClazz, toClazz)); + } + return Map.class.isAssignableFrom(from); + } + + private static int matchTypes( + JavaInvoker invoker, + Class[] parameterTypes, + Class[] otherTypes, + boolean matchCount) { + if (matchCount && parameterTypes.length != otherTypes.length) { + return -1; + } + int score = 0; + for (int ii = 0, nn = parameterTypes.length; ii < nn; ii++) { + Class type = parameterTypes[ii]; + Class otherType = otherTypes[ii]; + if (RuntimeContext.class.isAssignableFrom(otherType)) { + score += 1000; + } else if (Null.class.equals(type)) { + if (otherType.isPrimitive()) { + score = -1; + break; + } + score += 1000; + } else if (!isPrimitiveAssignableFrom(type, otherType)) { + score += 1000; + if (!otherType.isAssignableFrom(type)) { + score += 1000; + if (!isCoercible(type, otherType)) { + score += 2000; + boolean found = false; + for (ClassImplicitConvert convert : CONVERTS) { + if (convert.support(type, otherType)) { + invoker.addClassImplicitConvert(ii, convert); + found = true; + break; + } + } + invoker.setImplicit(found); + if (!found) { + return -1; + } + } + } + } + } + return score; + } + + private static Class getGenericType(Class target) { + Type type = target.getGenericSuperclass(); + if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getActualTypeArguments()[0]; + } + return null; + } + + public static JavaInvoker findMethodInvoker( + List> methods, Class[] parameterTypes) { + return findInvoker(methods, parameterTypes); + } + + public static JavaInvoker findConstructorInvoker( + List> constructors, Class[] parameterTypes) { + return findInvoker( + constructors.stream().map(ConstructorInvoker::new).collect(Collectors.toList()), + parameterTypes); + } + + public static JavaInvoker findInvoker( + List> executables, Class[] parameterTypes) { + JavaInvoker foundInvoker = null; + int foundScore = 0; + List> executableWithVarArgs = new ArrayList<>(); + for (JavaInvoker invoker : executables) { + // Check if the types match. + Class[] otherTypes = invoker.getParameterTypes(); + invoker = invoker.copy(); + int score = matchTypes(invoker, parameterTypes, otherTypes, true); + if (score > -1) { + if (foundInvoker == null) { + foundInvoker = invoker; + foundScore = score; + } else { + if (score < foundScore) { + foundScore = score; + foundInvoker = invoker; + } + } + } else if (invoker.isVarArgs()) { + executableWithVarArgs.add(invoker); + } + } + if (foundInvoker == null) { + for (JavaInvoker invoker : executableWithVarArgs) { + Class[] otherTypes = invoker.getParameterTypes(); + int score = -1; + int fixedParaLength = otherTypes.length - 1; + if (parameterTypes.length >= fixedParaLength) { + Class[] argTypes = new Class[fixedParaLength]; + System.arraycopy(parameterTypes, 0, argTypes, 0, fixedParaLength); + invoker = invoker.copy(); + score = matchTypes(invoker, argTypes, otherTypes, false); + if (score > -1) { + Class target = otherTypes[fixedParaLength].getComponentType(); + for (int i = fixedParaLength; i < parameterTypes.length; i++) { + Class type = parameterTypes[i]; + if (RuntimeContext.class.isAssignableFrom(type)) { + score++; + } else if (Null.class.equals(type)) { + if (!target.isPrimitive()) { + score++; + } else { + score = -1; + break; + } + } else if (!isPrimitiveAssignableFrom(type, target)) { + score++; + if (!target.isAssignableFrom(type)) { + score++; + if (!isCoercible(type, target)) { + boolean found = false; + for (ClassImplicitConvert convert : CONVERTS) { + if (convert.support(type, target)) { + invoker.addClassImplicitConvert(i, convert); + found = true; + } + } + invoker.setImplicit(found); + if (!found) { + score = -1; + break; + } + score++; + } else { + score++; + } + } + } + } + } + } + if (score > -1) { + if (foundInvoker == null) { + foundInvoker = invoker; + foundScore = score; + } else { + if (score < foundScore) { + foundScore = score; + foundInvoker = invoker; + } + } + } + } + } + return foundInvoker; + } + + /** Returns the method best matching the given signature, including type coercion, or null. */ + public static JavaInvoker findInvoker( + Class cls, String name, Class[] parameterTypes) { + List> methodList = new ArrayList<>(); + Method[] methods = cls.getMethods(); + for (int i = 0, n = methods.length; i < n; i++) { + Method method = methods[i]; + if (!method.getName().equals(name)) { + continue; + } + if (method.getAnnotation(Transient.class) != null) { + continue; + } + if (Modifier.isPublic(method.getModifiers())) { + methodList.add(new MethodInvoker(method)); + } + } + return findMethodInvoker(methodList, parameterTypes); + } + + public static JavaInvoker findInvoker(Class cls, String name) { + return findInvoker(cls, name, new Class[0]); + } + + /** 是否可以自动装修拆箱 */ + public static boolean isPrimitiveAssignableFrom(Class from, Class to) { + if ((from == Boolean.class || from == boolean.class) + && (to == boolean.class || to == Boolean.class)) { + return true; + } + if ((from == Integer.class || from == int.class) && (to == int.class || to == Integer.class)) { + return true; + } + if ((from == Float.class || from == float.class) && (to == float.class || to == Float.class)) { + return true; + } + if ((from == Double.class || from == double.class) + && (to == double.class || to == Double.class)) { + return true; + } + if ((from == Byte.class || from == byte.class) && (to == byte.class || to == Byte.class)) { + return true; + } + if ((from == Short.class || from == short.class) && (to == short.class || to == Short.class)) { + return true; + } + if ((from == Long.class || from == long.class) && (to == long.class || to == Long.class)) { + return true; + } + return (from == Character.class || from == char.class) + && (to == char.class || to == Character.class); + } + + /** 获取String类型的参数描述 */ + public static String[] getStringTypes(Object[] objects) { + String[] parameterTypes = new String[objects == null ? 0 : objects.length]; + if (objects != null) { + for (int i = 0, len = objects.length; i < len; i++) { + Object value = objects[i]; + parameterTypes[i] = value == null ? "null" : value.getClass().getSimpleName(); + } + } + return parameterTypes; + } + + /** 是否可以自动隐式转换 https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html */ + private static boolean isCoercible(Class from, Class to) { + if (from == Integer.class || from == int.class) { + return to == float.class + || to == Float.class + || to == double.class + || to == Double.class + || to == long.class + || to == Long.class; + } + + if (from == Float.class || from == float.class) { + return to == double.class || to == Double.class; + } + + if (from == Double.class || from == double.class) { + return false; + } + + if (from == Character.class || from == char.class) { + return to == int.class + || to == Integer.class + || to == float.class + || to == Float.class + || to == double.class + || to == Double.class + || to == long.class + || to == Long.class; + } + + if (from == Byte.class || from == byte.class) { + return to == int.class + || to == Integer.class + || to == float.class + || to == Float.class + || to == double.class + || to == Double.class + || to == long.class + || to == Long.class + || to == short.class + || to == Short.class; + } + + if (from == Short.class || from == short.class) { + return to == int.class + || to == Integer.class + || to == float.class + || to == Float.class + || to == double.class + || to == Double.class + || to == long.class + || to == Long.class; + } + + if (from == Long.class || from == long.class) { + return to == float.class || to == Float.class || to == double.class || to == Double.class; + } + + if (from == int[].class || from == Integer[].class) { + return to == Object[].class + || to == float[].class + || to == Float[].class + || to == double[].class + || to == Double[].class + || to == long[].class + || to == Long[].class; + } + + return false; + } + + /** + * 获取内部类 + * + * @param obj 目标对象,可以是实例,可以是Class + * @param name 内部类名称 + */ + public static Object getInnerClass(Object obj, String name) { + Class cls = obj instanceof Class ? (Class) obj : obj.getClass(); + Class[] classes = cls.getDeclaredClasses(); + for (int i = 0, len = classes.length; i < len; i++) { + Class clazz = classes[i]; + if (name.equalsIgnoreCase(clazz.getSimpleName())) { + return clazz; + } + } + return null; + } + + /** + * 获取字段 + * + * @param obj 目标对象可以是实例,可以是Class + * @param name 字段名称 + * @return + */ + @SuppressWarnings("rawtypes") + public static Field getField(Object obj, String name) { + Class cls = obj instanceof Class ? (Class) obj : obj.getClass(); + Map fields = FIELD_CACHE.get(cls); + if (fields == null) { + fields = new ConcurrentHashMap<>(); + FIELD_CACHE.put(cls, fields); + } + + Field field = fields.get(name); + if (field == null) { + try { + field = cls.getDeclaredField(name); + if (field.getAnnotation(Transient.class) != null) { + field = null; + } else { + field.setAccessible(true); + fields.put(name, field); + } + } catch (Throwable t) { + // fall through, try super classes + } + + if (field == null) { + Class parentClass = cls.getSuperclass(); + while (parentClass != Object.class && parentClass != null) { + try { + field = parentClass.getDeclaredField(name); + if (field.getAnnotation(Transient.class) != null) { + field = null; + } else { + field.setAccessible(true); + fields.put(name, field); + } + } catch (NoSuchFieldException e) { + // fall through + } + parentClass = parentClass.getSuperclass(); + } + } + } + + return field; + } + + /** 注册隐式转换器 */ + public static void registerImplicitConvert(ClassImplicitConvert classImplicitConvert) { + CONVERTS.add(classImplicitConvert); + } + + /** + * 注册扩展方法 + * + * @param target 目标类 + * @param extensionObject 实现类 + */ + public static void registerMethodExtension(Class target, Object extensionObject) { + List> classList = EXTENSION_MAP.get(target); + if (classList == null) { + classList = new ArrayList<>(); + EXTENSION_MAP.put(target, classList); + } + Class clazz = extensionObject.getClass(); + classList.add(clazz); + Method[] methods = clazz.getDeclaredMethods(); + if (methods != null) { + Map>> cachedMethodMap = EXTENSION_METHOD_CACHE.get(target); + if (cachedMethodMap == null) { + cachedMethodMap = new HashMap<>(); + EXTENSION_METHOD_CACHE.put(target, cachedMethodMap); + } + for (Method method : methods) { + if (Modifier.isPublic(method.getModifiers()) + && method.getParameterCount() > 0 + && method.getAnnotation(Transient.class) == null) { + List> cachedList = cachedMethodMap.get(method.getName()); + if (cachedList == null) { + cachedList = new ArrayList<>(); + cachedMethodMap.put(method.getName(), cachedList); + } + cachedList.add(new MethodInvoker(method, extensionObject)); + } + } + Collection>> methodsValues = cachedMethodMap.values(); + for (List> methodList : methodsValues) { + methodList.sort( + (m1, m2) -> { + int sum1 = + Arrays.stream(m1.getParameterTypes()) + .mapToInt(JavaReflection::calcToObjectDistance) + .sum(); + int sum2 = + Arrays.stream(m2.getParameterTypes()) + .mapToInt(JavaReflection::calcToObjectDistance) + .sum(); + return sum2 - sum1; + }); + } + } + } + + public static Object getFieldValue(Object obj, Field field) { + try { + return field.get(obj); + } catch (Throwable e) { + throw new RuntimeException( + "Couldn't get value of field '" + + field.getName() + + "' from object of type '" + + obj.getClass().getSimpleName() + + "'"); + } + } + + public static void setFieldValue(Object obj, Field field, Object value) { + try { + field.set(obj, value); + } catch (Throwable e) { + throw new RuntimeException( + "Couldn't set value of field '" + + field.getName() + + "' from object of type '" + + obj.getClass().getSimpleName() + + "'"); + } + } + + public static JavaInvoker getExtensionMethod( + Object obj, String name, Object... arguments) { + boolean isClass = obj instanceof Class; + Class cls = isClass ? Class.class : obj.getClass(); + if (cls.isArray()) { + cls = Object[].class; + } + return getExtensionMethod(cls, name, arguments); + } + + private static Class[] getParameterTypes(Class cls, Object... arguments) { + int begin = cls == null ? 0 : 1; + Class[] parameterTypes = new Class[arguments.length + begin]; + if (begin > 0) { + parameterTypes[0] = cls; + } + for (int i = 0; i < arguments.length; i++) { + parameterTypes[i + begin] = arguments[i] == null ? Null.class : arguments[i].getClass(); + } + return parameterTypes; + } + + private static JavaInvoker getExtensionMethod( + Class cls, String name, Object... arguments) { + if (cls == null) { + cls = Object.class; + } + Map>> methodMap = EXTENSION_METHOD_CACHE.get(cls); + if (methodMap != null) { + List> methodList = methodMap.get(name); + if (methodList != null) { + return findMethodInvoker(methodList, getParameterTypes(cls, arguments)); + } + } + if (cls != Object.class) { + Class[] interfaces = cls.getInterfaces(); + if (interfaces != null) { + for (Class clazz : interfaces) { + JavaInvoker invoker = getExtensionMethod(clazz, name, arguments); + if (invoker != null) { + return invoker; + } + } + } + return getExtensionMethod(cls.getSuperclass(), name, arguments); + } + return null; + } + + public static JavaInvoker getMethod(Object obj, String name, Object... arguments) { + boolean isClass = obj instanceof Class; + Class cls = + isClass ? (Class) obj : (obj instanceof Function ? Function.class : obj.getClass()); + Map> methods = METHOD_CACHE.get(cls); + if (methods == null) { + methods = new ConcurrentHashMap<>(); + METHOD_CACHE.put(cls, methods); + } + + Class[] parameterTypes = getParameterTypes(null, arguments); + MethodSignature signature = new MethodSignature(name, parameterTypes); + JavaInvoker invoker = methods.get(signature); + + if (invoker == null) { + try { + if (name == null) { + invoker = findApply(cls); + } else { + invoker = findInvoker(cls, name, parameterTypes); + // if (invoker == null) { + // invoker = findInvoker(cls, name, new Class[]{Object[].class}); + // } + } + } catch (Throwable e) { + // fall through + } + + if (invoker == null) { + Class parentClass = cls.getSuperclass(); + while (parentClass != null) { + try { + if (name == null) { + invoker = findApply(parentClass); + } else { + invoker = findInvoker(parentClass, name, parameterTypes); + } + if (invoker != null) { + break; + } + } catch (Throwable e) { + // fall through + } + parentClass = parentClass.getSuperclass(); + } + } + } + if (invoker == null || invoker.isImplicit()) { + JavaInvoker extensionInvoker = getExtensionMethod(obj, name, arguments); + if (extensionInvoker != null) { + extensionInvoker.setExtension(true); + invoker = extensionInvoker; + } + } + if (invoker != null) { + methods.put(signature, invoker); + } + return invoker; + } + + public static JavaInvoker getFunction(String name, Object... arguments) { + List> methodList = + FUNCTIONS.stream() + .filter(it -> it.getExecutable().getName().equals(name)) + .collect(Collectors.toList()); + return findMethodInvoker(methodList, getParameterTypes(null, arguments)); + } + + /** NULL值 */ + public static final class Null {} + + /** 方法签名 */ + private static class MethodSignature { + + private final String name; + + @SuppressWarnings("rawtypes") + private final Class[] parameters; + + private final int hashCode; + + @SuppressWarnings("rawtypes") + MethodSignature(String name, Class[] parameters) { + this.name = name; + this.parameters = parameters; + final int prime = 31; + int hash = 1; + hash = prime * hash + ((name == null) ? 0 : name.hashCode()); + hash = prime * hash + Arrays.hashCode(parameters); + hashCode = hash; + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MethodSignature other = (MethodSignature) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return Arrays.equals(parameters, other.parameters); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/reflection/MethodInvoker.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/reflection/MethodInvoker.java new file mode 100644 index 0000000..2ac7d6e --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/reflection/MethodInvoker.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.reflection; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class MethodInvoker extends JavaInvoker { + + public MethodInvoker(Method method) { + super(method); + } + + public MethodInvoker(MethodInvoker invoker) { + super(invoker); + } + + public MethodInvoker(Method method, Object defaultTarget) { + super(method); + setDefaultTarget(defaultTarget); + } + + @Override + public MethodInvoker copy() { + return new MethodInvoker(this); + } + + @Override + Object invoke(Object target, Object... args) + throws InvocationTargetException, IllegalAccessException { + Object defaultTarget = getDefaultTarget(); + return getExecutable().invoke(defaultTarget == null ? target : defaultTarget, args); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/ExitValue.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/ExitValue.java new file mode 100644 index 0000000..44c28ce --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/ExitValue.java @@ -0,0 +1,34 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime; + +public class ExitValue { + + private final Object[] values; + + public ExitValue() { + this(new Object[0]); + } + + public ExitValue(Object[] values) { + this.values = values; + } + + public Object[] getValues() { + return values; + } + + public int getLength() { + return values.length; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/MagicScriptClassLoader.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/MagicScriptClassLoader.java new file mode 100644 index 0000000..11c6805 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/MagicScriptClassLoader.java @@ -0,0 +1,26 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime; + +public class MagicScriptClassLoader extends ClassLoader { + + public MagicScriptClassLoader(ClassLoader parent) { + super(parent); + } + + public synchronized Class load(String className, byte[] bytecode) + throws ClassNotFoundException { + defineClass(className, bytecode, 0, bytecode.length); + return (Class) loadClass(className); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/MagicScriptRuntime.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/MagicScriptRuntime.java new file mode 100644 index 0000000..d11e21b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/MagicScriptRuntime.java @@ -0,0 +1,54 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime; + +import cn.universal.core.engine.MagicScriptContext; +import cn.universal.core.engine.parsing.Span; +import cn.universal.core.engine.runtime.function.MagicScriptLambdaFunction; +import java.util.List; + +public abstract class MagicScriptRuntime { + + protected MagicScriptContext context; + + private String[] varNames; + + private List spans; + + public abstract Object execute(MagicScriptContext context); + + public String[] getVarNames() { + return varNames; + } + + public void setVarNames(String[] varNames) { + this.varNames = varNames; + } + + public void setSpans(List spans) { + this.spans = spans; + } + + public Span getSpan(int index) { + return spans.get(index); + } + + public List getSpans() { + return spans; + } + + protected MagicScriptLambdaFunction createLambda( + MagicScriptLambdaFunction function, Variables variables) { + return (var, arguments) -> function.apply(variables, arguments); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/MagicScriptVariableAccessRuntime.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/MagicScriptVariableAccessRuntime.java new file mode 100644 index 0000000..8268479 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/MagicScriptVariableAccessRuntime.java @@ -0,0 +1,29 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime; + +import cn.universal.core.engine.MagicScriptContext; + +public class MagicScriptVariableAccessRuntime extends MagicScriptRuntime { + + private final String varName; + + public MagicScriptVariableAccessRuntime(String varName) { + this.varName = varName; + } + + @Override + public Object execute(MagicScriptContext context) { + return context.get(varName); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/RuntimeContext.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/RuntimeContext.java new file mode 100644 index 0000000..7551899 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/RuntimeContext.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime; + +import cn.universal.core.engine.MagicScriptContext; +import java.util.Map; + +public class RuntimeContext { + + private final MagicScriptContext context; + + private final Variables variables; + + public RuntimeContext(MagicScriptContext context, Variables variables) { + this.context = context; + this.variables = variables; + } + + public Variables getVariables() { + return variables; + } + + public Map getVarMap() { + return variables.getVariables(context); + } + + public MagicScriptContext getScriptContext() { + return context; + } + + public Object eval(String script) { + return this.context.eval(this, script); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/SpreadValue.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/SpreadValue.java new file mode 100644 index 0000000..8de04ec --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/SpreadValue.java @@ -0,0 +1,26 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime; + +public class SpreadValue { + + private final Object value; + + public SpreadValue(Object value) { + this.value = value; + } + + public Object getValue() { + return value; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/Variables.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/Variables.java new file mode 100644 index 0000000..2595fbc --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/Variables.java @@ -0,0 +1,107 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime; + +import cn.universal.core.engine.MagicScriptContext; +import cn.universal.core.engine.parsing.Parser; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Variables { + + private final Object[] elements; + + private Variables parent; + + private int[] args; + + private int argLength = -1; + + private int scopeIndex = -1; + + public Variables(int size) { + this.elements = new Object[size]; + } + + public Variables(Object[] elements, int scopeIndex, int[] args) { + int len = elements.length; + this.scopeIndex = scopeIndex; + this.elements = new Object[len]; + this.args = args; + this.argLength = args.length; + } + + public Object getValue(int index) { + Object value = elements[index]; + if (value == null) { + return parent == null ? null : parent.getValue(index); + } + return value; + } + + public void setScopeValue(int index, Object value) { + if (index > -1) { + this.elements[index] = value; + } + } + + public void setValue(int index, Object value) { + if (index > -1) { + this.elements[index] = value; + Variables target = parent; + while (target != null && argLength > -1 && target.scopeIndex != this.scopeIndex) { + for (int i = 0; i < argLength; i++) { + if (index == this.args[i]) { + return; + } + } + target.elements[index] = value; + target = target.parent; + } + } + } + + public Variables copy(Object[] target, int scopeIndex, int... args) { + Variables variables = new Variables(this.elements, scopeIndex, args); + variables.parent = this; + for (int i = 0, len = target == null ? 0 : Math.min(args.length, target.length); i < len; i++) { + variables.setValue(args[i], target[i]); + } + return variables; + } + + public Map getVariables(MagicScriptContext context) { + Map variables = new LinkedHashMap<>(context.getRootVariables()); + Variables p = parent; + List elements = new ArrayList<>(); + elements.add(this.elements); + while (p != null) { + elements.add(p.elements); + p = p.parent; + } + String[] varNames = context.getVarNames(); + for (int j = elements.size() - 1; j >= 0; j--) { + Object[] element = elements.get(j); + for (int i = 0, len = element.length; i < len; i++) { + Object value = element[i]; + if (value != null) { + variables.put(varNames[i], value); + } + } + } + variables.remove(Parser.ANONYMOUS_VARIABLE); + return variables; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/function/MagicScriptLambdaFunction.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/function/MagicScriptLambdaFunction.java new file mode 100644 index 0000000..bd374eb --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/function/MagicScriptLambdaFunction.java @@ -0,0 +1,21 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.function; + +import cn.universal.core.engine.runtime.Variables; + +@FunctionalInterface +public interface MagicScriptLambdaFunction { + + Object apply(Variables variables, Object[] args); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/function/MagicScriptLanguageFunction.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/function/MagicScriptLanguageFunction.java new file mode 100644 index 0000000..62fc3ca --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/function/MagicScriptLanguageFunction.java @@ -0,0 +1,40 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.function; + +import cn.universal.core.engine.MagicResourceLoader; +import cn.universal.core.engine.MagicScriptContext; +import cn.universal.core.engine.runtime.Variables; +import java.util.Map; +import java.util.function.BiFunction; + +public class MagicScriptLanguageFunction implements MagicScriptLambdaFunction { + + private final BiFunction, String, Object> function; + + private final String content; + + private final MagicScriptContext context; + + public MagicScriptLanguageFunction(MagicScriptContext context, String language, String content) { + this.context = context; + this.function = MagicResourceLoader.loadScriptLanguage(language); + this.content = content; + } + + @Override + public Object apply(Variables variables, Object[] args) { + Map vars = variables.getVariables(this.context); + return function.apply(vars, this.content); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/ArithmeticHandle.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/ArithmeticHandle.java new file mode 100644 index 0000000..eb8f51a --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/ArithmeticHandle.java @@ -0,0 +1,1571 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.handle; + +import static java.lang.invoke.MethodHandles.catchException; +import static java.lang.invoke.MethodHandles.dropArguments; +import static java.lang.invoke.MethodType.methodType; + +import java.lang.invoke.CallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; + +public class ArithmeticHandle { + + private static final MethodHandle FALLBACK; + + private static final Map OPERATORS = new HashMap<>(5); + + static { + try { + OPERATORS.put("plus", "+"); + OPERATORS.put("minus", "-"); + OPERATORS.put("mul", "*"); + OPERATORS.put("divide", "/"); + OPERATORS.put("divideAndRemainder", "%"); + MethodHandles.Lookup lookup = MethodHandles.lookup(); + FALLBACK = + lookup.findStatic( + ArithmeticHandle.class, + "fallback", + methodType(Object.class, MethodCallSite.class, Object[].class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new Error("ArithmeticHandle初始化失败", e); + } + } + + public static CallSite bootstrap( + MethodHandles.Lookup caller, String name, MethodType type, int count) { + MethodCallSite callSite = new MethodCallSite(caller, name, type, ArithmeticHandle.class); + MethodHandle fallback = + FALLBACK.bindTo(callSite).asCollector(Object[].class, type.parameterCount()).asType(type); + callSite.setTarget(fallback); + callSite.fallback = fallback; + return callSite; + } + + public static Object fallback(MethodCallSite callSite, Object[] args) throws Throwable { + Class arg1Class = (args[0] == null) ? Object.class : args[0].getClass(); + Class arg2Class = (args[1] == null) ? Object.class : args[1].getClass(); + MethodHandle target; + try { + target = callSite.findStatic(methodType(Object.class, arg1Class, arg2Class)); + } catch (Throwable ignored) { + try { + target = + callSite.findStatic( + callSite.methodName + "_fallback", + methodType(Object.class, Object.class, Object.class)); + } catch (Throwable t) { + return reject( + args[0], args[1], OPERATORS.getOrDefault(callSite.methodName, callSite.methodName)); + } + } + target = target.asType(methodType(Object.class, Object.class, Object.class)); + target = + catchException( + target, + ClassCastException.class, + dropArguments(callSite.fallback, 0, ClassCastException.class)); + callSite.setTarget(target); + return target.invokeWithArguments(args); + } + + /* byte + 操作实现 */ + public static Object plus(Byte a, Byte b) { + return a + b; + } + + public static Object plus(Byte a, Short b) { + return a + b; + } + + public static Object plus(Byte a, Integer b) { + return a + b; + } + + public static Object plus(Byte a, Float b) { + return a + b; + } + + public static Object plus(Byte a, Double b) { + return a + b; + } + + public static Object plus(Byte a, Long b) { + return a + b; + } + + public static Object plus(Byte a, BigDecimal b) { + return b.add(new BigDecimal(a)); + } + + public static Object plus(Byte a, BigInteger b) { + return b.add(BigInteger.valueOf(a)); + } + + public static Object plus(Byte a, String b) { + return a + b; + } + + /* byte + 操作实现 */ + + /* short + 操作实现 */ + public static Object plus(Short a, Byte b) { + return a + b; + } + + public static Object plus(Short a, Short b) { + return a + b; + } + + public static Object plus(Short a, Integer b) { + return a + b; + } + + public static Object plus(Short a, Float b) { + return a + b; + } + + public static Object plus(Short a, Double b) { + return a + b; + } + + public static Object plus(Short a, Long b) { + return a + b; + } + + public static Object plus(Short a, BigDecimal b) { + return b.add(new BigDecimal(a)); + } + + public static Object plus(Short a, BigInteger b) { + return b.add(BigInteger.valueOf(a)); + } + + public static Object plus(Short a, String b) { + return a + b; + } + + /* short + 操作实现 */ + + /* int + 操作实现 */ + public static Object plus(Integer a, Byte b) { + return a + b; + } + + public static Object plus(Integer a, Short b) { + return a + b; + } + + public static Object plus(Integer a, Integer b) { + return a + b; + } + + public static Object plus(Integer a, Float b) { + return a + b; + } + + public static Object plus(Integer a, Double b) { + return a + b; + } + + public static Object plus(Integer a, Long b) { + return a + b; + } + + public static Object plus(Integer a, BigDecimal b) { + return b.add(new BigDecimal(a)); + } + + public static Object plus(Integer a, BigInteger b) { + return b.add(BigInteger.valueOf(a)); + } + + public static Object plus(Integer a, String b) { + return a + b; + } + + /* int + 操作实现 */ + + /* float + 操作实现 */ + public static Object plus(Float a, Byte b) { + return a + b; + } + + public static Object plus(Float a, Short b) { + return a + b; + } + + public static Object plus(Float a, Integer b) { + return a + b; + } + + public static Object plus(Float a, Float b) { + return a + b; + } + + public static Object plus(Float a, Double b) { + return a + b; + } + + public static Object plus(Float a, Long b) { + return a + b; + } + + public static Object plus(Float a, BigDecimal b) { + return b.add(new BigDecimal(a.toString())); + } + + public static Object plus(Float a, BigInteger b) { + return new BigDecimal(b).add(new BigDecimal(a.toString())); + } + + public static Object plus(Float a, String b) { + return a + b; + } + + /* float + 操作实现 */ + + /* double + 操作实现 */ + public static Object plus(Double a, Byte b) { + return a + b; + } + + public static Object plus(Double a, Short b) { + return a + b; + } + + public static Object plus(Double a, Integer b) { + return a + b; + } + + public static Object plus(Double a, Float b) { + return a + b; + } + + public static Object plus(Double a, Double b) { + return a + b; + } + + public static Object plus(Double a, Long b) { + return a + b; + } + + public static Object plus(Double a, BigDecimal b) { + return b.add(new BigDecimal(a.toString())); + } + + public static Object plus(Double a, BigInteger b) { + return new BigDecimal(b).add(new BigDecimal(a.toString())); + } + + public static Object plus(Double a, String b) { + return a + b; + } + + /* double + 操作实现 */ + + /* long + 操作实现 */ + public static Object plus(Long a, Byte b) { + return a + b; + } + + public static Object plus(Long a, Short b) { + return a + b; + } + + public static Object plus(Long a, Integer b) { + return a + b; + } + + public static Object plus(Long a, Float b) { + return a + b; + } + + public static Object plus(Long a, Double b) { + return a + b; + } + + public static Object plus(Long a, Long b) { + return a + b; + } + + public static Object plus(Long a, BigDecimal b) { + return b.add(new BigDecimal(a)); + } + + public static Object plus(Long a, BigInteger b) { + return b.add(BigInteger.valueOf(a)); + } + + public static Object plus(Long a, String b) { + return a + b; + } + + /* long + 操作实现 */ + + /* bigecimal + 操作实现 */ + public static Object plus(BigDecimal a, Byte b) { + return a.add(new BigDecimal(b)); + } + + public static Object plus(BigDecimal a, Short b) { + return a.add(new BigDecimal(b)); + } + + public static Object plus(BigDecimal a, Integer b) { + return a.add(new BigDecimal(b)); + } + + public static Object plus(BigDecimal a, Float b) { + return a.add(new BigDecimal(b.toString())); + } + + public static Object plus(BigDecimal a, Double b) { + return a.add(new BigDecimal(b.toString())); + } + + public static Object plus(BigDecimal a, Long b) { + return a.add(new BigDecimal(b)); + } + + public static Object plus(BigDecimal a, BigDecimal b) { + return a.add(b); + } + + public static Object plus(BigDecimal a, BigInteger b) { + return a.add(new BigDecimal(b)); + } + + public static Object plus(BigDecimal a, String b) { + return a + b; + } + + /* bigdecimal + 操作实现 */ + + /* biginteger + 操作实现 */ + public static Object plus(BigInteger a, Byte b) { + return a.add(BigInteger.valueOf(b)); + } + + public static Object plus(BigInteger a, Short b) { + return a.add(BigInteger.valueOf(b)); + } + + public static Object plus(BigInteger a, Integer b) { + return a.add(BigInteger.valueOf(b)); + } + + public static Object plus(BigInteger a, Float b) { + return new BigDecimal(a).add(new BigDecimal(b.toString())); + } + + public static Object plus(BigInteger a, Double b) { + return new BigDecimal(a).add(new BigDecimal(b.toString())); + } + + public static Object plus(BigInteger a, Long b) { + return a.add(BigInteger.valueOf(b)); + } + + public static Object plus(BigInteger a, BigDecimal b) { + return new BigDecimal(a).add(b); + } + + public static Object plus(BigInteger a, BigInteger b) { + return a.add(b); + } + + public static Object plus(BigInteger a, String b) { + return a + b; + } + + /* biginteger + 操作实现 */ + + /* string + 操作实现 */ + public static Object plus(String a, Byte b) { + return a + b; + } + + public static Object plus(String a, Short b) { + return a + b; + } + + public static Object plus(String a, Integer b) { + return a + b; + } + + public static Object plus(String a, Float b) { + return a + b; + } + + public static Object plus(String a, Double b) { + return a + b; + } + + public static Object plus(String a, Long b) { + return a + b; + } + + public static Object plus(String a, BigDecimal b) { + return a + b; + } + + public static Object plus(String a, BigInteger b) { + return a + b; + } + + public static Object plus(String a, String b) { + return a + b; + } + + public static Object plus(String a, Object b) { + return a + b; + } + + /* string + 操作实现 */ + + public static Object plus_fallback(Object a, Object b) { + if (a == null && b == null) { + return null; + } + return "" + a + b; + } + + /* byte - 操作实现 */ + public static Object minus(Byte a, Byte b) { + return a - b; + } + + public static Object minus(Byte a, Short b) { + return a - b; + } + + public static Object minus(Byte a, Integer b) { + return a - b; + } + + public static Object minus(Byte a, Float b) { + return a - b; + } + + public static Object minus(Byte a, Double b) { + return a - b; + } + + public static Object minus(Byte a, Long b) { + return a - b; + } + + public static Object minus(Byte a, BigDecimal b) { + return new BigDecimal(a).subtract(b); + } + + public static Object minus(Byte a, BigInteger b) { + return BigInteger.valueOf(a).subtract(b); + } + + /* byte - 操作实现 */ + + /* short - 操作实现 */ + public static Object minus(Short a, Byte b) { + return a - b; + } + + public static Object minus(Short a, Short b) { + return a - b; + } + + public static Object minus(Short a, Integer b) { + return a - b; + } + + public static Object minus(Short a, Float b) { + return a - b; + } + + public static Object minus(Short a, Double b) { + return a - b; + } + + public static Object minus(Short a, Long b) { + return a - b; + } + + public static Object minus(Short a, BigDecimal b) { + return new BigDecimal(a).subtract(b); + } + + public static Object minus(Short a, BigInteger b) { + return BigInteger.valueOf(a).subtract(b); + } + + /* short - 操作实现 */ + + /* int - 操作实现 */ + public static Object minus(Integer a, Byte b) { + return a - b; + } + + public static Object minus(Integer a, Short b) { + return a - b; + } + + public static Object minus(Integer a, Integer b) { + return a - b; + } + + public static Object minus(Integer a, Float b) { + return a - b; + } + + public static Object minus(Integer a, Double b) { + return a - b; + } + + public static Object minus(Integer a, Long b) { + return a - b; + } + + public static Object minus(Integer a, BigDecimal b) { + return new BigDecimal(a).subtract(b); + } + + public static Object minus(Integer a, BigInteger b) { + return BigInteger.valueOf(a).subtract(b); + } + + /* int - 操作实现 */ + + /* float - 操作实现 */ + public static Object minus(Float a, Byte b) { + return a - b; + } + + public static Object minus(Float a, Short b) { + return a - b; + } + + public static Object minus(Float a, Integer b) { + return a - b; + } + + public static Object minus(Float a, Float b) { + return a - b; + } + + public static Object minus(Float a, Double b) { + return a - b; + } + + public static Object minus(Float a, Long b) { + return a - b; + } + + public static Object minus(Float a, BigDecimal b) { + return new BigDecimal(a.toString()).subtract(b); + } + + public static Object minus(Float a, BigInteger b) { + return new BigDecimal(b).subtract(new BigDecimal(a.toString())); + } + + /* float - 操作实现 */ + + /* double - 操作实现 */ + public static Object minus(Double a, Byte b) { + return a - b; + } + + public static Object minus(Double a, Short b) { + return a - b; + } + + public static Object minus(Double a, Integer b) { + return a - b; + } + + public static Object minus(Double a, Float b) { + return a - b; + } + + public static Object minus(Double a, Double b) { + return a - b; + } + + public static Object minus(Double a, Long b) { + return a - b; + } + + public static Object minus(Double a, BigDecimal b) { + return new BigDecimal(a.toString()).subtract(b); + } + + public static Object minus(Double a, BigInteger b) { + return new BigDecimal(a.toString()).subtract(new BigDecimal(b)); + } + + /* double - 操作实现 */ + + /* long - 操作实现 */ + public static Object minus(Long a, Byte b) { + return a - b; + } + + public static Object minus(Long a, Short b) { + return a - b; + } + + public static Object minus(Long a, Integer b) { + return a - b; + } + + public static Object minus(Long a, Float b) { + return a - b; + } + + public static Object minus(Long a, Double b) { + return a - b; + } + + public static Object minus(Long a, Long b) { + return a - b; + } + + public static Object minus(Long a, BigDecimal b) { + return new BigDecimal(a).subtract(b); + } + + /* long - 操作实现 */ + + /* bigecimal - 操作实现 */ + public static Object minus(BigDecimal a, Byte b) { + return a.subtract(new BigDecimal(b)); + } + + public static Object minus(BigDecimal a, Short b) { + return a.subtract(new BigDecimal(b)); + } + + public static Object minus(BigDecimal a, Integer b) { + return a.subtract(new BigDecimal(b)); + } + + public static Object minus(BigDecimal a, Float b) { + return a.subtract(new BigDecimal(b.toString())); + } + + public static Object minus(BigDecimal a, Double b) { + return a.subtract(new BigDecimal(b.toString())); + } + + public static Object minus(BigDecimal a, Long b) { + return a.subtract(new BigDecimal(b)); + } + + public static Object minus(BigDecimal a, BigDecimal b) { + return a.subtract(b); + } + + public static Object minus(BigDecimal a, BigInteger b) { + return a.subtract(new BigDecimal(b)); + } + + /* bigdecimal - 操作实现 */ + + /* biginteger - 操作实现 */ + public static Object minus(BigInteger a, Byte b) { + return a.subtract(BigInteger.valueOf(b)); + } + + public static Object minus(BigInteger a, Short b) { + return a.subtract(BigInteger.valueOf(b)); + } + + public static Object minus(BigInteger a, Integer b) { + return a.subtract(BigInteger.valueOf(b)); + } + + public static Object minus(BigInteger a, Float b) { + return new BigDecimal(a).subtract(new BigDecimal(b.toString())); + } + + public static Object minus(BigInteger a, Double b) { + return new BigDecimal(a).subtract(new BigDecimal(b.toString())); + } + + public static Object minus(BigInteger a, Long b) { + return a.subtract(BigInteger.valueOf(b)); + } + + public static Object minus(BigInteger a, BigDecimal b) { + return new BigDecimal(a).subtract(b); + } + + public static Object minus(BigInteger a, BigInteger b) { + return a.subtract(b); + } + + /* biginteger - 操作实现 */ + + /* byte * 操作实现 */ + public static Object mul(Byte a, Byte b) { + return a * b; + } + + public static Object mul(Byte a, Short b) { + return a * b; + } + + public static Object mul(Byte a, Integer b) { + return a * b; + } + + public static Object mul(Byte a, Float b) { + return a * b; + } + + public static Object mul(Byte a, Double b) { + return a * b; + } + + public static Object mul(Byte a, Long b) { + return a * b; + } + + public static Object mul(Byte a, BigDecimal b) { + return new BigDecimal(a).multiply(b); + } + + public static Object mul(Byte a, BigInteger b) { + return BigInteger.valueOf(a).multiply(b); + } + + /* byte * 操作实现 */ + + /* short * 操作实现 */ + public static Object mul(Short a, Byte b) { + return a * b; + } + + public static Object mul(Short a, Short b) { + return a * b; + } + + public static Object mul(Short a, Integer b) { + return a * b; + } + + public static Object mul(Short a, Float b) { + return a * b; + } + + public static Object mul(Short a, Double b) { + return a * b; + } + + public static Object mul(Short a, Long b) { + return a * b; + } + + public static Object mul(Short a, BigDecimal b) { + return new BigDecimal(a).multiply(b); + } + + public static Object mul(Short a, BigInteger b) { + return BigInteger.valueOf(a).multiply(b); + } + + /* short * 操作实现 */ + + /* int * 操作实现 */ + public static Object mul(Integer a, Byte b) { + return a * b; + } + + public static Object mul(Integer a, Short b) { + return a * b; + } + + public static Object mul(Integer a, Integer b) { + return a * b; + } + + public static Object mul(Integer a, Float b) { + return a * b; + } + + public static Object mul(Integer a, Double b) { + return a * b; + } + + public static Object mul(Integer a, Long b) { + return a * b; + } + + public static Object mul(Integer a, BigDecimal b) { + return new BigDecimal(a).multiply(b); + } + + public static Object mul(Integer a, BigInteger b) { + return BigInteger.valueOf(a).multiply(b); + } + + /* int * 操作实现 */ + + /* float * 操作实现 */ + public static Object mul(Float a, Byte b) { + return a * b; + } + + public static Object mul(Float a, Short b) { + return a * b; + } + + public static Object mul(Float a, Integer b) { + return a * b; + } + + public static Object mul(Float a, Float b) { + return a * b; + } + + public static Object mul(Float a, Double b) { + return a * b; + } + + public static Object mul(Float a, Long b) { + return a * b; + } + + public static Object mul(Float a, BigDecimal b) { + return b.multiply(new BigDecimal(a.toString())); + } + + public static Object mul(Float a, BigInteger b) { + return new BigDecimal(b).multiply(new BigDecimal(a.toString())); + } + + /* float * 操作实现 */ + + /* double * 操作实现 */ + public static Object mul(Double a, Byte b) { + return a * b; + } + + public static Object mul(Double a, Short b) { + return a * b; + } + + public static Object mul(Double a, Integer b) { + return a * b; + } + + public static Object mul(Double a, Float b) { + return a * b; + } + + public static Object mul(Double a, Double b) { + return a * b; + } + + public static Object mul(Double a, Long b) { + return a * b; + } + + public static Object mul(Double a, BigDecimal b) { + return new BigDecimal(a.toString()).multiply(b); + } + + public static Object mul(Double a, BigInteger b) { + return new BigDecimal(a.toString()).multiply(new BigDecimal(b)); + } + + /* double * 操作实现 */ + + /* long * 操作实现 */ + public static Object mul(Long a, Byte b) { + return a * b; + } + + public static Object mul(Long a, Short b) { + return a * b; + } + + public static Object mul(Long a, Integer b) { + return a * b; + } + + public static Object mul(Long a, Float b) { + return a * b; + } + + public static Object mul(Long a, Double b) { + return a * b; + } + + public static Object mul(Long a, Long b) { + return a * b; + } + + public static Object mul(Long a, BigDecimal b) { + return new BigDecimal(a).multiply(b); + } + + public static Object mul(Long a, BigInteger b) { + return BigInteger.valueOf(a).multiply(b); + } + + /* long * 操作实现 */ + + /* bigecimal * 操作实现 */ + public static Object mul(BigDecimal a, Byte b) { + return a.multiply(new BigDecimal(b)); + } + + public static Object mul(BigDecimal a, Short b) { + return a.multiply(new BigDecimal(b)); + } + + public static Object mul(BigDecimal a, Integer b) { + return a.multiply(new BigDecimal(b)); + } + + public static Object mul(BigDecimal a, Float b) { + return a.multiply(new BigDecimal(b.toString())); + } + + public static Object mul(BigDecimal a, Double b) { + return a.multiply(new BigDecimal(b.toString())); + } + + public static Object mul(BigDecimal a, Long b) { + return a.multiply(new BigDecimal(b)); + } + + public static Object mul(BigDecimal a, BigDecimal b) { + return a.multiply(b); + } + + public static Object mul(BigDecimal a, BigInteger b) { + return a.multiply(new BigDecimal(b)); + } + + /* bigdecimal * 操作实现 */ + + /* biginteger * 操作实现 */ + public static Object mul(BigInteger a, Byte b) { + return a.multiply(BigInteger.valueOf(b)); + } + + public static Object mul(BigInteger a, Short b) { + return a.multiply(BigInteger.valueOf(b)); + } + + public static Object mul(BigInteger a, Integer b) { + return a.multiply(BigInteger.valueOf(b)); + } + + public static Object mul(BigInteger a, Float b) { + return new BigDecimal(a).multiply(new BigDecimal(b.toString())); + } + + public static Object mul(BigInteger a, Double b) { + return new BigDecimal(a).multiply(new BigDecimal(b.toString())); + } + + public static Object mul(BigInteger a, Long b) { + return a.multiply(BigInteger.valueOf(b)); + } + + public static Object mul(BigInteger a, BigDecimal b) { + return new BigDecimal(a).multiply(b); + } + + public static Object mul(BigInteger a, BigInteger b) { + return a.multiply(b); + } + + /* biginteger * 操作实现 */ + + /* byte / 操作实现 */ + public static Object divide(Byte a, Byte b) { + return a / b; + } + + public static Object divide(Byte a, Short b) { + return a / b; + } + + public static Object divide(Byte a, Integer b) { + return a / b; + } + + public static Object divide(Byte a, Float b) { + return a / b; + } + + public static Object divide(Byte a, Double b) { + return a / b; + } + + public static Object divide(Byte a, Long b) { + return a / b; + } + + public static Object divide(Byte a, BigDecimal b) { + return new BigDecimal(a).divide(b); + } + + public static Object divide(Byte a, BigInteger b) { + return BigInteger.valueOf(a).divide(b); + } + + /* byte / 操作实现 */ + + /* short / 操作实现 */ + public static Object divide(Short a, Byte b) { + return a / b; + } + + public static Object divide(Short a, Short b) { + return a / b; + } + + public static Object divide(Short a, Integer b) { + return a / b; + } + + public static Object divide(Short a, Float b) { + return a / b; + } + + public static Object divide(Short a, Double b) { + return a / b; + } + + public static Object divide(Short a, Long b) { + return a / b; + } + + public static Object divide(Short a, BigDecimal b) { + return new BigDecimal(a).divide(b); + } + + public static Object divide(Short a, BigInteger b) { + return BigInteger.valueOf(a).divide(b); + } + + /* short / 操作实现 */ + + /* int / 操作实现 */ + public static Object divide(Integer a, Byte b) { + return a / b; + } + + public static Object divide(Integer a, Short b) { + return a / b; + } + + public static Object divide(Integer a, Integer b) { + return a / b; + } + + public static Object divide(Integer a, Float b) { + return a / b; + } + + public static Object divide(Integer a, Double b) { + return a / b; + } + + public static Object divide(Integer a, Long b) { + return a / b; + } + + public static Object divide(Integer a, BigDecimal b) { + return new BigDecimal(a).divide(b); + } + + public static Object divide(Integer a, BigInteger b) { + return BigInteger.valueOf(a).divide(b); + } + + /* int / 操作实现 */ + + /* float / 操作实现 */ + public static Object divide(Float a, Byte b) { + return a / b; + } + + public static Object divide(Float a, Short b) { + return a / b; + } + + public static Object divide(Float a, Integer b) { + return a / b; + } + + public static Object divide(Float a, Float b) { + return a / b; + } + + public static Object divide(Float a, Double b) { + return a / b; + } + + public static Object divide(Float a, Long b) { + return a / b; + } + + public static Object divide(Float a, BigDecimal b) { + return new BigDecimal(a.toString()).divide(b); + } + + public static Object divide(Float a, BigInteger b) { + return new BigDecimal(a.toString()).divide(new BigDecimal(b)); + } + + /* float / 操作实现 */ + + /* double / 操作实现 */ + public static Object divide(Double a, Byte b) { + return a / b; + } + + public static Object divide(Double a, Short b) { + return a / b; + } + + public static Object divide(Double a, Integer b) { + return a / b; + } + + public static Object divide(Double a, Float b) { + return a / b; + } + + public static Object divide(Double a, Double b) { + return a / b; + } + + public static Object divide(Double a, Long b) { + return a / b; + } + + public static Object divide(Double a, BigDecimal b) { + return new BigDecimal(a.toString()).divide(b); + } + + public static Object divide(Double a, BigInteger b) { + return new BigDecimal(a.toString()).divide(new BigDecimal(b)); + } + + /* double / 操作实现 */ + + /* long / 操作实现 */ + public static Object divide(Long a, Byte b) { + return a / b; + } + + public static Object divide(Long a, Short b) { + return a / b; + } + + public static Object divide(Long a, Integer b) { + return a / b; + } + + public static Object divide(Long a, Float b) { + return a / b; + } + + public static Object divide(Long a, Double b) { + return a / b; + } + + public static Object divide(Long a, Long b) { + return a / b; + } + + public static Object divide(Long a, BigDecimal b) { + return new BigDecimal(a).divide(b); + } + + public static Object divide(Long a, BigInteger b) { + return BigInteger.valueOf(a).divide(b); + } + + /* long / 操作实现 */ + + /* bigecimal / 操作实现 */ + public static Object divide(BigDecimal a, Byte b) { + return a.divide(new BigDecimal(b)); + } + + public static Object divide(BigDecimal a, Short b) { + return a.divide(new BigDecimal(b)); + } + + public static Object divide(BigDecimal a, Integer b) { + return a.divide(new BigDecimal(b)); + } + + public static Object divide(BigDecimal a, Float b) { + return a.divide(new BigDecimal(b.toString())); + } + + public static Object divide(BigDecimal a, Double b) { + return a.divide(new BigDecimal(b.toString())); + } + + public static Object divide(BigDecimal a, Long b) { + return a.divide(new BigDecimal(b)); + } + + public static Object divide(BigDecimal a, BigDecimal b) { + return a.divide(b); + } + + public static Object divide(BigDecimal a, BigInteger b) { + return a.divide(new BigDecimal(b)); + } + + /* bigdecimal / 操作实现 */ + + /* biginteger / 操作实现 */ + public static Object divide(BigInteger a, Byte b) { + return a.divide(BigInteger.valueOf(b)); + } + + public static Object divide(BigInteger a, Short b) { + return a.divide(BigInteger.valueOf(b)); + } + + public static Object divide(BigInteger a, Integer b) { + return a.divide(BigInteger.valueOf(b)); + } + + public static Object divide(BigInteger a, Float b) { + return new BigDecimal(a).divide(new BigDecimal(b.toString())); + } + + public static Object divide(BigInteger a, Double b) { + return new BigDecimal(a).divide(new BigDecimal(b.toString())); + } + + public static Object divide(BigInteger a, Long b) { + return a.divide(BigInteger.valueOf(b)); + } + + public static Object divide(BigInteger a, BigDecimal b) { + return new BigDecimal(a).divide(b); + } + + public static Object divide(BigInteger a, BigInteger b) { + return a.divide(b); + } + + /* biginteger / 操作实现 */ + + /* byte % 操作实现 */ + public static Object divideAndRemainder(Byte a, Byte b) { + return a % b; + } + + public static Object divideAndRemainder(Byte a, Short b) { + return a % b; + } + + public static Object divideAndRemainder(Byte a, Integer b) { + return a % b; + } + + public static Object divideAndRemainder(Byte a, Float b) { + return a % b; + } + + public static Object divideAndRemainder(Byte a, Double b) { + return a % b; + } + + public static Object divideAndRemainder(Byte a, Long b) { + return a % b; + } + + public static Object divideAndRemainder(Byte a, BigDecimal b) { + return new BigDecimal(a).divideAndRemainder(b); + } + + /* byte % 操作实现 */ + + /* short % 操作实现 */ + public static Object divideAndRemainder(Short a, Byte b) { + return a % b; + } + + public static Object divideAndRemainder(Short a, Short b) { + return a % b; + } + + public static Object divideAndRemainder(Short a, Integer b) { + return a % b; + } + + public static Object divideAndRemainder(Short a, Float b) { + return a % b; + } + + public static Object divideAndRemainder(Short a, Double b) { + return a % b; + } + + public static Object divideAndRemainder(Short a, Long b) { + return a % b; + } + + public static Object divideAndRemainder(Short a, BigDecimal b) { + return new BigDecimal(a).divideAndRemainder(b); + } + + public static Object divideAndRemainder(Short a, BigInteger b) { + return BigInteger.valueOf(a).divideAndRemainder(b); + } + + /* short % 操作实现 */ + + /* int % 操作实现 */ + public static Object divideAndRemainder(Integer a, Byte b) { + return a % b; + } + + public static Object divideAndRemainder(Integer a, Short b) { + return a % b; + } + + public static Object divideAndRemainder(Integer a, Integer b) { + return a % b; + } + + public static Object divideAndRemainder(Integer a, Float b) { + return a % b; + } + + public static Object divideAndRemainder(Integer a, Double b) { + return a % b; + } + + public static Object divideAndRemainder(Integer a, Long b) { + return a % b; + } + + public static Object divideAndRemainder(Integer a, BigDecimal b) { + return new BigDecimal(a).divideAndRemainder(b); + } + + public static Object divideAndRemainder(Integer a, BigInteger b) { + return BigInteger.valueOf(a).divideAndRemainder(b); + } + + /* int % 操作实现 */ + + /* float % 操作实现 */ + public static Object divideAndRemainder(Float a, Byte b) { + return a % b; + } + + public static Object divideAndRemainder(Float a, Short b) { + return a % b; + } + + public static Object divideAndRemainder(Float a, Integer b) { + return a % b; + } + + public static Object divideAndRemainder(Float a, Float b) { + return a % b; + } + + public static Object divideAndRemainder(Float a, Double b) { + return a % b; + } + + public static Object divideAndRemainder(Float a, Long b) { + return a % b; + } + + public static Object divideAndRemainder(Float a, BigDecimal b) { + return new BigDecimal(a.toString()).divideAndRemainder(b); + } + + public static Object divideAndRemainder(Float a, BigInteger b) { + return new BigDecimal(a.toString()).divideAndRemainder(new BigDecimal(b)); + } + + /* float % 操作实现 */ + + /* double % 操作实现 */ + public static Object divideAndRemainder(Double a, Byte b) { + return a % b; + } + + public static Object divideAndRemainder(Double a, Short b) { + return a % b; + } + + public static Object divideAndRemainder(Double a, Integer b) { + return a % b; + } + + public static Object divideAndRemainder(Double a, Float b) { + return a % b; + } + + public static Object divideAndRemainder(Double a, Double b) { + return a % b; + } + + public static Object divideAndRemainder(Double a, Long b) { + return a % b; + } + + public static Object divideAndRemainder(Double a, BigDecimal b) { + return new BigDecimal(a.toString()).divideAndRemainder(b); + } + + public static Object divideAndRemainder(Double a, BigInteger b) { + return new BigDecimal(a.toString()).divideAndRemainder(new BigDecimal(b)); + } + + /* double % 操作实现 */ + + /* long % 操作实现 */ + public static Object divideAndRemainder(Long a, Byte b) { + return a % b; + } + + public static Object divideAndRemainder(Long a, Short b) { + return a % b; + } + + public static Object divideAndRemainder(Long a, Integer b) { + return a % b; + } + + public static Object divideAndRemainder(Long a, Float b) { + return a % b; + } + + public static Object divideAndRemainder(Long a, Double b) { + return a % b; + } + + public static Object divideAndRemainder(Long a, Long b) { + return a % b; + } + + public static Object divideAndRemainder(Long a, BigDecimal b) { + return new BigDecimal(a).divideAndRemainder(b); + } + + public static Object divideAndRemainder(Long a, BigInteger b) { + return BigInteger.valueOf(a).divideAndRemainder(b); + } + + /* long % 操作实现 */ + + /* bigecimal % 操作实现 */ + public static Object divideAndRemainder(BigDecimal a, Byte b) { + return a.divideAndRemainder(new BigDecimal(b)); + } + + public static Object divideAndRemainder(BigDecimal a, Short b) { + return a.divideAndRemainder(new BigDecimal(b)); + } + + public static Object divideAndRemainder(BigDecimal a, Integer b) { + return a.divideAndRemainder(new BigDecimal(b)); + } + + public static Object divideAndRemainder(BigDecimal a, Float b) { + return a.divideAndRemainder(new BigDecimal(b.toString())); + } + + public static Object divideAndRemainder(BigDecimal a, Double b) { + return a.divideAndRemainder(new BigDecimal(b.toString())); + } + + public static Object divideAndRemainder(BigDecimal a, Long b) { + return a.divideAndRemainder(new BigDecimal(b)); + } + + public static Object divideAndRemainder(BigDecimal a, BigDecimal b) { + return a.divideAndRemainder(b); + } + + public static Object divideAndRemainder(BigDecimal a, BigInteger b) { + return a.divideAndRemainder(new BigDecimal(b)); + } + + /* bigdecimal % 操作实现 */ + + /* biginteger % 操作实现 */ + public static Object divideAndRemainder(BigInteger a, Byte b) { + return a.divideAndRemainder(BigInteger.valueOf(b)); + } + + public static Object divideAndRemainder(BigInteger a, Short b) { + return a.divideAndRemainder(BigInteger.valueOf(b)); + } + + public static Object divideAndRemainder(BigInteger a, Integer b) { + return a.divideAndRemainder(BigInteger.valueOf(b)); + } + + public static Object divideAndRemainder(BigInteger a, Float b) { + return new BigDecimal(a).divideAndRemainder(new BigDecimal(b.toString())); + } + + public static Object divideAndRemainder(BigInteger a, Double b) { + return new BigDecimal(a).divideAndRemainder(new BigDecimal(b.toString())); + } + + public static Object divideAndRemainder(BigInteger a, Long b) { + return a.divideAndRemainder(BigInteger.valueOf(b)); + } + + public static Object divideAndRemainder(BigInteger a, BigDecimal b) { + return new BigDecimal(a).divideAndRemainder(b); + } + + public static Object divideAndRemainder(BigInteger a, BigInteger b) { + return a.divideAndRemainder(b); + } + + /* biginteger % 操作实现 */ + + private static Object reject(Object a, Object b, String symbol) throws IllegalArgumentException { + throw new IllegalArgumentException( + String.format( + "`%s` 运算不支持 (%s,%s) 类型", + symbol, + a == null ? "null" : a.getClass().getName(), + b == null ? "null" : b.getClass().getName())); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/BitHandle.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/BitHandle.java new file mode 100644 index 0000000..2ab24c1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/BitHandle.java @@ -0,0 +1,814 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.handle; + +import static java.lang.invoke.MethodHandles.catchException; +import static java.lang.invoke.MethodHandles.dropArguments; +import static java.lang.invoke.MethodType.methodType; + +import java.lang.invoke.CallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +public class BitHandle { + + private static final MethodHandle FALLBACK; + + private static final Map OPERATORS = new HashMap<>(5); + + static { + try { + OPERATORS.put("and", "&"); + OPERATORS.put("or", "|"); + OPERATORS.put("left_shift", "<<"); + OPERATORS.put("right_shift2", ">>>"); + OPERATORS.put("right_shift", ">>"); + OPERATORS.put("xor", "^"); + MethodHandles.Lookup lookup = MethodHandles.lookup(); + FALLBACK = + lookup.findStatic( + BitHandle.class, + "fallback", + methodType(Object.class, MethodCallSite.class, Object[].class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new Error("BitHandle初始化失败", e); + } + } + + public static CallSite bootstrap( + MethodHandles.Lookup caller, String name, MethodType type, int flags) { + MethodCallSite callSite = new MethodCallSite(caller, name, type, BitHandle.class); + MethodHandle fallback = + FALLBACK.bindTo(callSite).asCollector(Object[].class, type.parameterCount()).asType(type); + callSite.setTarget(fallback); + callSite.fallback = fallback; + return callSite; + } + + public static Object fallback(MethodCallSite callSite, Object[] args) throws Throwable { + Class arg0Class = args[0] == null ? Object.class : args[0].getClass(); + Class arg1Class = args[1] == null ? Object.class : args[1].getClass(); + MethodHandle target; + try { + target = callSite.findStatic(methodType(Object.class, arg0Class, arg1Class)); + } catch (Throwable ignored) { + try { + target = + callSite.findStatic( + callSite.methodName + "_fallback", + methodType(Object.class, Object.class, Object.class)); + } catch (Throwable t) { + return reject( + args[0], args[1], OPERATORS.getOrDefault(callSite.methodName, callSite.methodName)); + } + } + target = target.asType(methodType(Object.class, Object.class, Object.class)); + target = + catchException( + target, + ClassCastException.class, + dropArguments(callSite.fallback, 0, ClassCastException.class)); + callSite.setTarget(target); + return target.invokeWithArguments(args); + } + + /* byte << 操作实现 */ + public static Object left_shift(Byte a, Byte b) { + return a << b; + } + + public static Object left_shift(Byte a, Short b) { + return a << b; + } + + public static Object left_shift(Byte a, Integer b) { + return a << b; + } + + public static Object left_shift(Byte a, Long b) { + return a << b; + } + + public static Object left_shift(Byte a, BigDecimal b) { + return a << b.longValue(); + } + + /* byte << 操作实现 */ + + /* short << 操作实现 */ + public static Object left_shift(Short a, Byte b) { + return a << b; + } + + public static Object left_shift(Short a, Short b) { + return a << b; + } + + public static Object left_shift(Short a, Integer b) { + return a << b; + } + + public static Object left_shift(Short a, Long b) { + return a << b; + } + + public static Object left_shift(Short a, BigDecimal b) { + return a << b.longValue(); + } + + /* short << 操作实现 */ + + /* int << 操作实现 */ + public static Object left_shift(Integer a, Byte b) { + return a << b; + } + + public static Object left_shift(Integer a, Short b) { + return a << b; + } + + public static Object left_shift(Integer a, Integer b) { + return a << b; + } + + public static Object left_shift(Integer a, Long b) { + return a << b; + } + + public static Object left_shift(Integer a, BigDecimal b) { + return a << b.longValue(); + } + + /* int << 操作实现 */ + + /* long < 操作实现 */ + public static Object left_shift(Long a, Byte b) { + return a << b; + } + + public static Object left_shift(Long a, Short b) { + return a << b; + } + + public static Object left_shift(Long a, Integer b) { + return a << b; + } + + public static Object left_shift(Long a, Long b) { + return a << b; + } + + public static Object left_shift(Long a, BigDecimal b) { + return a << b.longValue(); + } + + /* long < 操作实现 */ + + /* bigecimal < 操作实现 */ + public static Object left_shift(BigDecimal a, Byte b) { + return a.longValue() << b; + } + + public static Object left_shift(BigDecimal a, Short b) { + return a.longValue() << b; + } + + public static Object left_shift(BigDecimal a, Integer b) { + return a.longValue() << b; + } + + public static Object left_shift(BigDecimal a, Long b) { + return a.longValue() << b; + } + + public static Object left_shift(BigDecimal a, BigDecimal b) { + return a.longValue() << b.longValue(); + } + + /* bigdecimal < 操作实现 */ + + /* byte >> 操作实现 */ + public static Object right_shift(Byte a, Byte b) { + return a >> b; + } + + public static Object right_shift(Byte a, Short b) { + return a >> b; + } + + public static Object right_shift(Byte a, Integer b) { + return a >> b; + } + + public static Object right_shift(Byte a, Long b) { + return a >> b; + } + + public static Object right_shift(Byte a, BigDecimal b) { + return a >> b.longValue(); + } + + /* byte >> 操作实现 */ + + /* short >> 操作实现 */ + public static Object right_shift(Short a, Byte b) { + return a >> b; + } + + public static Object right_shift(Short a, Short b) { + return a >> b; + } + + public static Object right_shift(Short a, Integer b) { + return a >> b; + } + + public static Object right_shift(Short a, Long b) { + return a >> b; + } + + public static Object right_shift(Short a, BigDecimal b) { + return a >> b.longValue(); + } + + /* short >> 操作实现 */ + + /* int >> 操作实现 */ + public static Object right_shift(Integer a, Byte b) { + return a >> b; + } + + public static Object right_shift(Integer a, Short b) { + return a >> b; + } + + public static Object right_shift(Integer a, Integer b) { + return a >> b; + } + + public static Object right_shift(Integer a, Long b) { + return a >> b; + } + + public static Object right_shift(Integer a, BigDecimal b) { + return a >> b.longValue(); + } + + /* int >> 操作实现 */ + + /* long >> 操作实现 */ + public static Object right_shift(Long a, Byte b) { + return a >> b; + } + + public static Object right_shift(Long a, Short b) { + return a >> b; + } + + public static Object right_shift(Long a, Integer b) { + return a >> b; + } + + public static Object right_shift(Long a, Long b) { + return a >> b; + } + + public static Object right_shift(Long a, BigDecimal b) { + return a >> b.longValue(); + } + + /* long >> 操作实现 */ + + /* bigecimal >> 操作实现 */ + public static Object right_shift(BigDecimal a, Byte b) { + return a.longValue() >> b; + } + + public static Object right_shift(BigDecimal a, Short b) { + return a.longValue() >> b; + } + + public static Object right_shift(BigDecimal a, Integer b) { + return a.longValue() >> b; + } + + public static Object right_shift(BigDecimal a, Long b) { + return a.longValue() >> b; + } + + public static Object right_shift(BigDecimal a, BigDecimal b) { + return a.longValue() >> b.longValue(); + } + + /* bigdecimal > 操作实现 */ + + /* byte >>> 操作实现 */ + public static Object right_shift2(Byte a, Byte b) { + return a >>> b; + } + + public static Object right_shift2(Byte a, Short b) { + return a >>> b; + } + + public static Object right_shift2(Byte a, Integer b) { + return a >>> b; + } + + public static Object right_shift2(Byte a, Long b) { + return a >>> b; + } + + public static Object right_shift2(Byte a, BigDecimal b) { + return a >>> b.longValue(); + } + + /* byte >>> 操作实现 */ + + /* short >>> 操作实现 */ + public static Object right_shift2(Short a, Byte b) { + return a >>> b; + } + + public static Object right_shift2(Short a, Short b) { + return a >>> b; + } + + public static Object right_shift2(Short a, Integer b) { + return a >>> b; + } + + public static Object right_shift2(Short a, Long b) { + return a >>> b; + } + + public static Object right_shift2(Short a, BigDecimal b) { + return a >>> b.longValue(); + } + + /* short >>> 操作实现 */ + + /* int >>> 操作实现 */ + public static Object right_shift2(Integer a, Byte b) { + return a >>> b; + } + + public static Object right_shift2(Integer a, Short b) { + return a >>> b; + } + + public static Object right_shift2(Integer a, Integer b) { + return a >>> b; + } + + public static Object right_shift2(Integer a, Long b) { + return a >>> b; + } + + public static Object right_shift2(Integer a, BigDecimal b) { + return a >>> b.longValue(); + } + + /* int >>> 操作实现 */ + + /* long >>> 操作实现 */ + public static Object right_shift2(Long a, Byte b) { + return a >>> b; + } + + public static Object right_shift2(Long a, Short b) { + return a >>> b; + } + + public static Object right_shift2(Long a, Integer b) { + return a >>> b; + } + + public static Object right_shift2(Long a, Long b) { + return a >>> b; + } + + public static Object right_shift2(Long a, BigDecimal b) { + return a >>> b.longValue(); + } + + /* long >>> 操作实现 */ + + /* bigecimal >>> 操作实现 */ + public static Object right_shift2(BigDecimal a, Byte b) { + return a.longValue() >>> b; + } + + public static Object right_shift2(BigDecimal a, Short b) { + return a.longValue() >>> b; + } + + public static Object right_shift2(BigDecimal a, Integer b) { + return a.longValue() >>> b; + } + + public static Object right_shift2(BigDecimal a, Long b) { + return a.longValue() >>> b; + } + + public static Object right_shift2(BigDecimal a, BigDecimal b) { + return a.longValue() >>> b.longValue(); + } + + /* bigdecimal > 操作实现 */ + + /* byte & 操作实现 */ + public static Object and(Byte a, Byte b) { + return a & b; + } + + public static Object and(Byte a, Short b) { + return a & b; + } + + public static Object and(Byte a, Integer b) { + return a & b; + } + + public static Object and(Byte a, Long b) { + return a & b; + } + + public static Object and(Byte a, BigDecimal b) { + return a & b.longValue(); + } + + /* byte & 操作实现 */ + + /* short & 操作实现 */ + public static Object and(Short a, Byte b) { + return a & b; + } + + public static Object and(Short a, Short b) { + return a & b; + } + + public static Object and(Short a, Integer b) { + return a & b; + } + + public static Object and(Short a, Long b) { + return a & b; + } + + public static Object and(Short a, BigDecimal b) { + return a & b.longValue(); + } + + /* short & 操作实现 */ + + /* int & 操作实现 */ + public static Object and(Integer a, Byte b) { + return a & b; + } + + public static Object and(Integer a, Short b) { + return a & b; + } + + public static Object and(Integer a, Integer b) { + return a & b; + } + + public static Object and(Integer a, Long b) { + return a & b; + } + + public static Object and(Integer a, BigDecimal b) { + return a & b.longValue(); + } + + /* int & 操作实现 */ + + /* long & 操作实现 */ + public static Object and(Long a, Byte b) { + return a & b; + } + + public static Object and(Long a, Short b) { + return a & b; + } + + public static Object and(Long a, Integer b) { + return a & b; + } + + public static Object and(Long a, Long b) { + return a & b; + } + + public static Object and(Long a, BigDecimal b) { + return a & b.longValue(); + } + + /* long & 操作实现 */ + + /* bigecimal & 操作实现 */ + public static Object and(BigDecimal a, Byte b) { + return a.longValue() & b; + } + + public static Object and(BigDecimal a, Short b) { + return a.longValue() & b; + } + + public static Object and(BigDecimal a, Integer b) { + return a.longValue() & b; + } + + public static Object and(BigDecimal a, Long b) { + return a.longValue() & b; + } + + public static Object and(BigDecimal a, BigDecimal b) { + return a.longValue() & b.longValue(); + } + + /* bigdecimal & 操作实现 */ + + public static Object and(Boolean a, Boolean b) { + return a & b; + } + + /* byte | 操作实现 */ + public static Object or(Byte a, Byte b) { + return a | b; + } + + public static Object or(Byte a, Short b) { + return a | b; + } + + public static Object or(Byte a, Integer b) { + return a | b; + } + + public static Object or(Byte a, Long b) { + return a | b; + } + + public static Object or(Byte a, BigDecimal b) { + return a | b.longValue(); + } + + /* byte | 操作实现 */ + + /* short | 操作实现 */ + public static Object or(Short a, Byte b) { + return a | b; + } + + public static Object or(Short a, Short b) { + return a | b; + } + + public static Object or(Short a, Integer b) { + return a | b; + } + + public static Object or(Short a, Long b) { + return a | b; + } + + public static Object or(Short a, BigDecimal b) { + return a | b.longValue(); + } + + /* short | 操作实现 */ + + /* int | 操作实现 */ + public static Object or(Integer a, Byte b) { + return a | b; + } + + public static Object or(Integer a, Short b) { + return a | b; + } + + public static Object or(Integer a, Integer b) { + return a | b; + } + + public static Object or(Integer a, Long b) { + return a | b; + } + + public static Object or(Integer a, BigDecimal b) { + return a | b.longValue(); + } + + /* int | 操作实现 */ + + /* long | 操作实现 */ + public static Object or(Long a, Byte b) { + return a | b; + } + + public static Object or(Long a, Short b) { + return a | b; + } + + public static Object or(Long a, Integer b) { + return a | b; + } + + public static Object or(Long a, Long b) { + return a | b; + } + + public static Object or(Long a, BigDecimal b) { + return a | b.longValue(); + } + + /* long | 操作实现 */ + + /* bigecimal | 操作实现 */ + public static Object or(BigDecimal a, Byte b) { + return a.longValue() | b; + } + + public static Object or(BigDecimal a, Short b) { + return a.longValue() | b; + } + + public static Object or(BigDecimal a, Integer b) { + return a.longValue() | b; + } + + public static Object or(BigDecimal a, Long b) { + return a.longValue() | b; + } + + public static Object or(BigDecimal a, BigDecimal b) { + return a.longValue() | b.longValue(); + } + + /* bigdecimal | 操作实现 */ + + public static Object or(Boolean a, Boolean b) { + return a | b; + } + + /* byte ^ 操作实现 */ + public static Object xor(Byte a, Byte b) { + return a ^ b; + } + + public static Object xor(Byte a, Short b) { + return a ^ b; + } + + public static Object xor(Byte a, Integer b) { + return a ^ b; + } + + public static Object xor(Byte a, Long b) { + return a ^ b; + } + + public static Object xor(Byte a, BigDecimal b) { + return a ^ b.longValue(); + } + + /* byte ^ 操作实现 */ + + /* short ^ 操作实现 */ + public static Object xor(Short a, Byte b) { + return a ^ b; + } + + public static Object xor(Short a, Short b) { + return a ^ b; + } + + public static Object xor(Short a, Integer b) { + return a ^ b; + } + + public static Object xor(Short a, Long b) { + return a ^ b; + } + + public static Object xor(Short a, BigDecimal b) { + return a ^ b.longValue(); + } + + /* short ^ 操作实现 */ + + /* int ^ 操作实现 */ + public static Object xor(Integer a, Byte b) { + return a ^ b; + } + + public static Object xor(Integer a, Short b) { + return a ^ b; + } + + public static Object xor(Integer a, Integer b) { + return a ^ b; + } + + public static Object xor(Integer a, Long b) { + return a ^ b; + } + + public static Object xor(Integer a, BigDecimal b) { + return a ^ b.longValue(); + } + + /* int ^ 操作实现 */ + + /* long ^ 操作实现 */ + public static Object xor(Long a, Byte b) { + return a ^ b; + } + + public static Object xor(Long a, Short b) { + return a ^ b; + } + + public static Object xor(Long a, Integer b) { + return a ^ b; + } + + public static Object xor(Long a, Long b) { + return a ^ b; + } + + public static Object xor(Long a, BigDecimal b) { + return a ^ b.longValue(); + } + + /* long ^ 操作实现 */ + + /* bigecimal ^ 操作实现 */ + public static Object xor(BigDecimal a, Byte b) { + return a.longValue() ^ b; + } + + public static Object xor(BigDecimal a, Short b) { + return a.longValue() ^ b; + } + + public static Object xor(BigDecimal a, Integer b) { + return a.longValue() ^ b; + } + + public static Object xor(BigDecimal a, Long b) { + return a.longValue() ^ b; + } + + public static Object xor(BigDecimal a, BigDecimal b) { + return a.longValue() ^ b.longValue(); + } + + /* bigdecimal ^ 操作实现 */ + + public static Object xor(Boolean a, Boolean b) { + return a ^ b; + } + + public static Object not(Object object) { + if (object == null) { + return null; + } else if (object instanceof Byte) { + return ~(Byte) object; + } else if (object instanceof Short) { + return ~(Short) object; + } else if (object instanceof Integer) { + return ~(Integer) object; + } else if (object instanceof Long) { + return ~(Long) object; + } else if (object instanceof BigDecimal) { + return ~((BigDecimal) object).longValue(); + } + throw new IllegalArgumentException( + String.format("操作符 `~` 不支持 (%s) 类型", object.getClass().getName())); + } + + private static Object reject(Object a, Object b, String symbol) throws IllegalArgumentException { + throw new IllegalArgumentException( + String.format( + "操作符 `%s` 不支持 (%s,%s) 类型", symbol, a.getClass().getName(), b.getClass().getName())); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/FunctionCallHandle.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/FunctionCallHandle.java new file mode 100644 index 0000000..59036e9 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/FunctionCallHandle.java @@ -0,0 +1,485 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.handle; + +import static java.lang.invoke.MethodType.methodType; + +import cn.universal.core.engine.exception.MagicScriptRuntimeException; +import cn.universal.core.engine.functions.ClassExtension; +import cn.universal.core.engine.functions.DynamicAttribute; +import cn.universal.core.engine.functions.DynamicMethod; +import cn.universal.core.engine.functions.StreamExtension; +import cn.universal.core.engine.parsing.ast.statement.AsyncCall; +import cn.universal.core.engine.parsing.ast.statement.ClassConverter; +import cn.universal.core.engine.reflection.JavaInvoker; +import cn.universal.core.engine.reflection.JavaReflection; +import cn.universal.core.engine.reflection.MethodInvoker; +import cn.universal.core.engine.runtime.RuntimeContext; +import cn.universal.core.engine.runtime.SpreadValue; +import cn.universal.core.engine.runtime.Variables; +import cn.universal.core.engine.runtime.function.MagicScriptLambdaFunction; +import cn.universal.core.engine.runtime.lang.ArrayKeyValueIterator; +import cn.universal.core.engine.runtime.lang.ArrayValueIterator; +import cn.universal.core.engine.runtime.lang.KeyValueIterator; +import cn.universal.core.engine.runtime.lang.MapKeyValueIterator; +import java.lang.invoke.CallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class FunctionCallHandle { + + private static final MethodHandle FALLBACK; + + private static final MethodHandle INVOKE_METHOD; + + private static final MethodHandle INVOKE_FUNCTION; + + private static final MethodHandle MEMBER_ACCESS; + + private static final MethodHandle INVOKE_NEW_INSTANCE; + + private static final MethodHandle SET_VARIABLE_VALUE; + + private static final Object[] EMPTY_ARGS = new Object[0]; + + static { + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + FALLBACK = + lookup.findStatic( + FunctionCallHandle.class, + "fallback", + methodType(Object.class, MethodCallSite.class, Object[].class)); + INVOKE_METHOD = + lookup.findStatic( + FunctionCallHandle.class, + "invoke_method", + methodType( + Object.class, + RuntimeContext.class, + String.class, + boolean.class, + boolean.class, + Object[].class, + Object.class)); + INVOKE_FUNCTION = + lookup.findStatic( + FunctionCallHandle.class, + "invoke_function", + methodType( + Object.class, + RuntimeContext.class, + String.class, + boolean.class, + Object[].class, + Object.class)); + MEMBER_ACCESS = + lookup.findStatic( + FunctionCallHandle.class, + "member_access", + methodType( + Object.class, + RuntimeContext.class, + Object.class, + String.class, + boolean.class, + boolean.class)); + INVOKE_NEW_INSTANCE = + lookup.findStatic( + FunctionCallHandle.class, + "invoke_new_instance", + methodType(Object.class, RuntimeContext.class, Object.class, Object[].class)); + SET_VARIABLE_VALUE = + lookup.findStatic( + FunctionCallHandle.class, + "set_variable_value", + methodType( + Object.class, RuntimeContext.class, Object.class, Object.class, Object.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new Error("FunctionCallHandle初始化失败", e); + } + } + + public static CallSite bootstrap( + MethodHandles.Lookup caller, String name, MethodType type, int flag) { + MethodCallSite callSite = new MethodCallSite(caller, name, type, FunctionCallHandle.class); + MethodHandle fallback = null; + switch (name) { + case "invoke_method": + fallback = INVOKE_METHOD; + break; + case "invoke_function": + fallback = INVOKE_FUNCTION; + break; + case "member_access": + fallback = MEMBER_ACCESS; + break; + case "invoke_new_instance": + fallback = INVOKE_NEW_INSTANCE; + break; + case "set_variable_value": + fallback = SET_VARIABLE_VALUE; + break; + } + if (fallback != null) { + fallback = fallback.asType(type); + callSite.setTarget(fallback); + callSite.fallback = fallback; + } else { + fallback = + FALLBACK.bindTo(callSite).asCollector(Object[].class, type.parameterCount()).asType(type); + callSite.setTarget(fallback); + callSite.fallback = fallback; + } + return callSite; + } + + public static Object fallback(MethodCallSite callSite, Object[] args) throws Throwable { + JavaInvoker method = + JavaReflection.getMethod(FunctionCallHandle.class, callSite.methodName, args); + if (method != null) { + return method.invoke0(null, null, args); + } + return null; + } + + public static Object invoke_function( + RuntimeContext runtimeContext, String name, boolean hasSpread, Object[] args, Object target) + throws Throwable { + JavaInvoker method = null; + if (hasSpread) { + args = do_spread(args); + } + if (args != null) { + for (int i = 0, len = args.length; i < len; i++) { + if (args[i] instanceof MagicScriptLambdaFunction) { + MagicScriptLambdaFunction function = (MagicScriptLambdaFunction) args[i]; + args[i] = + (Function) + objects -> function.apply(runtimeContext.getVariables(), objects); + } + } + } + if (target == null) { + method = JavaReflection.getFunction(name, args); + } else if (target instanceof MagicScriptLambdaFunction) { + return ((MagicScriptLambdaFunction) target).apply(runtimeContext.getVariables(), args); + } else if (target instanceof Function) { + return ((Function) target).apply(args); + } + if (method != null) { + return method.invoke0(target, runtimeContext, args); + } + throw new NoSuchMethodException( + String.format("找不到函数%s(%s)", name, String.join(",", JavaReflection.getStringTypes(args)))); + } + + public static Object invoke_method( + RuntimeContext runtimeContext, + String name, + boolean hasSpread, + boolean optional, + Object[] args, + Object target) + throws Throwable { + if (target == null && optional) { + return null; + } + JavaInvoker method; + if (hasSpread) { + args = do_spread(args); + } + if (target == null) { + throw new NullPointerException("对象为空"); + } else if (target instanceof MagicScriptLambdaFunction) { + return ((MagicScriptLambdaFunction) target).apply(runtimeContext.getVariables(), args); + } else if (target instanceof Function) { + return ((Function) target).apply(args); + } else { + method = JavaReflection.getMethod(target, name, args); + } + if (method != null) { + return method.invoke0(target, runtimeContext, args); + } + if (target instanceof DynamicMethod) { + MethodInvoker invoker = + new MethodInvoker( + DynamicMethod.class.getDeclaredMethod("execute", String.class, List.class)); + Object[] newArgumentValues = new Object[] {name, Arrays.asList(args)}; + return invoker.invoke0(target, runtimeContext, newArgumentValues); + } + try { + Object function = member_access(runtimeContext, target, name, optional, false); + if (function != null) { + return invoke_function(runtimeContext, name, false, args, function); + } + } catch (Exception ignored) { + } + throw new NoSuchMethodException( + String.format( + "在%s中找不到方法%s(%s)", + target.getClass().getName(), + name, + String.join(",", JavaReflection.getStringTypes(args)))); + } + + public static Object spread(List source, List target) { + source.addAll(target); + return source; + } + + public static Object spread(Map source, Map target) { + source.putAll(target); + return source; + } + + public static Object member_access( + RuntimeContext runtimeContext, Object target, String name, boolean optional, boolean inLinq) { + if (target == null) { + if (optional) { + return null; + } + throw new NullPointerException("target is null"); + } + if ("class".equals(name)) { + return target instanceof Class ? target : target.getClass(); + } else if (target instanceof Map) { + return ((Map) target).get(name); + } else if (target instanceof DynamicAttribute) { + return ((DynamicAttribute) target).getDynamicAttribute(name); + } + String methodName; + Field field = JavaReflection.getField(target, name); + if (field != null) { + return JavaReflection.getFieldValue(target, field); + } else { + Object innerClass = JavaReflection.getInnerClass(target, name); + if (innerClass != null) { + return innerClass; + } + if (name.length() > 1) { + methodName = name.substring(0, 1).toUpperCase() + name.substring(1); + } else { + methodName = name.toUpperCase(); + } + JavaInvoker invoker = + JavaReflection.getMethod(target, "get" + methodName, EMPTY_ARGS); + try { + if (invoker != null) { + return invoker.invoke0(target, runtimeContext, EMPTY_ARGS); + } else if ((invoker = JavaReflection.getMethod(target, "is" + methodName, EMPTY_ARGS)) + != null) { + return invoker.invoke0(target, runtimeContext, EMPTY_ARGS); + } + } catch (Throwable throwable) { + throw new MagicScriptRuntimeException(throwable); + } + } + if (target instanceof List) { + List list = (List) target; + if (inLinq) { + return list.stream() + .map(it -> member_access(runtimeContext, it, name, optional, inLinq)) + .collect(Collectors.toList()); + } else if (list.size() > 0) { + return member_access(runtimeContext, list.get(0), name, optional, false); + } + return null; + } + if (optional) { + return null; + } + throw new MagicScriptRuntimeException( + String.format( + "在%s中找不到属性%s或者方法get%s、方法is%s,内部类%s", target, name, methodName, methodName, name)); + } + + public static Object call_async( + MagicScriptLambdaFunction function, Variables variables, Object... args) { + return AsyncCall.execute(function, variables, args); + } + + public static Object invoke_new_instance( + RuntimeContext runtimeContext, Object target, Object[] args) throws Throwable { + return ClassExtension.newInstance(target, runtimeContext, args); + } + + public static Object newArrayList(boolean hasSpread, Object[] args) { + if (!hasSpread) { + return new ArrayList<>(Arrays.asList(args)); + } + List list = new ArrayList<>(args.length); + for (Object item : args) { + if (item instanceof SpreadValue) { + Object res = ((SpreadValue) item).getValue(); + if (res == null) { + // 其实是因为该变量未定义 + } else if (res instanceof Collection) { + list.addAll(((Collection) res)); + } else if (res instanceof Map) { + throw new MagicScriptRuntimeException("不能在list中展开map"); + } else { + throw new MagicScriptRuntimeException("不能展开的类型:" + res.getClass()); + } + } else { + list.add(item); + } + } + return list; + } + + public static Iterator newValueIterator(Object target) { + if (target instanceof Iterable) { + return ((Iterable) target).iterator(); + } else if (target instanceof Iterator) { + return (Iterator) target; + } else if (target instanceof Map) { + return ((Map) target).values().iterator(); + } else if (target.getClass().isArray()) { + return new ArrayValueIterator(target); + } else { + throw new MagicScriptRuntimeException("不支持循环" + target.getClass()); + } + } + + public static Iterator newKeyValueIterator(Object target) { + if (target instanceof Iterable) { + return new KeyValueIterator(((Iterable) target).iterator()); + } else if (target instanceof Iterator) { + return new KeyValueIterator((Iterator) target); + } else if (target instanceof Map) { + return new MapKeyValueIterator((Map) target); + } else if (target.getClass().isArray()) { + return new ArrayKeyValueIterator(target); + } else { + throw new MagicScriptRuntimeException("不支持循环" + target.getClass()); + } + } + + public static Object newLinkedHashMap(Boolean hasSpread, Object[] args) { + Map map = new LinkedHashMap<>(); + if (args != null) { + for (int i = 0, len = args.length; i < len; ) { + Object key = args[i++]; + if (hasSpread && key instanceof SpreadValue) { + Object res = ((SpreadValue) key).getValue(); + if (res == null) { + // 其实是因为该变量未定义 + } else if (res instanceof Map) { + // 可能导致 map put 入非String 的 key + map.putAll((Map) res); + } else if (res instanceof Collection) { + int index = 0; + for (Object obj : ((Collection) res)) { + map.put(String.valueOf(index++), obj); + } + } else { + throw new MagicScriptRuntimeException("不能展开的类型:" + res.getClass()); + } + continue; + } + map.put(key, args[i++]); + } + } + return map; + } + + public static Object set_variable_value( + RuntimeContext runtimeContext, Object target, Object name, Object value) throws Throwable { + if (target == null) { + throw new NullPointerException("target is null"); + } + if (name == null) { + throw new NullPointerException("key is null"); + } + if (target instanceof Map) { + ((Map) target).put(name, value); + } else if (target instanceof DynamicAttribute) { + ((DynamicAttribute) target).setDynamicAttribute(Objects.toString(name, null), value); + } else if (target instanceof Collection) { + if (name instanceof Number) { + // TODO NEW EXCEPTION + ((List) target).set(((Number) name).intValue(), value); + } else { + throw new MagicScriptRuntimeException("不支持此赋值操作"); + } + } else if (target.getClass().isArray()) { + if (name instanceof Number) { + // TODO NEW EXCEPTION + Array.set(target, ((Number) name).intValue(), value); + } else { + throw new MagicScriptRuntimeException("不支持此赋值操作"); + } + } else { + String text = name.toString(); + Field field = JavaReflection.getField(target, text); + if (field != null) { + JavaReflection.setFieldValue(target, field, value); + } else { + String methodName; + if (text.length() > 1) { + methodName = text.substring(0, 1).toUpperCase() + text.substring(1); + } else { + methodName = text.toUpperCase(); + } + JavaInvoker invoker = JavaReflection.getMethod(target, "set" + methodName, value); + if (invoker == null) { + throw new MagicScriptRuntimeException( + String.format("在%s中找不到属性%s或者方法set%s", target.getClass(), name, methodName)); + } + invoker.invoke0(target, runtimeContext, new Object[] {value}); + } + } + return value; + } + + public static Object type_cast(Object object, String target, Object... args) { + return ClassConverter.process(object, target, args); + } + + private static Object[] do_spread(Object[] args) { + Object[] dest = new Object[args.length]; + for (int i = 0, n = args.length, pIndex = 0; i < n; i++) { + Object item = args[i]; + if (item instanceof SpreadValue) { + Object value = ((SpreadValue) item).getValue(); + Object[] spreadValues = StreamExtension.arrayLikeToList(value).toArray(); + int spreadLength = spreadValues.length; + if (spreadLength > 0) { + Object[] valTemp = dest; + dest = new Object[dest.length + spreadLength - 1]; + System.arraycopy(valTemp, 0, dest, 0, valTemp.length); + System.arraycopy(spreadValues, 0, dest, pIndex, spreadLength); + pIndex += spreadLength; + } + } else { + dest[pIndex++] = item; + } + } + return dest; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/MethodCallSite.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/MethodCallSite.java new file mode 100644 index 0000000..5152b83 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/MethodCallSite.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.handle; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.MutableCallSite; + +public class MethodCallSite extends MutableCallSite { + + MethodHandles.Lookup caller; + + String methodName; + + Class targetClass; + + MethodHandle fallback; + + public MethodCallSite( + MethodHandles.Lookup caller, String methodName, MethodType type, Class targetClass) { + super(type); + this.caller = caller; + this.methodName = methodName; + this.targetClass = targetClass; + } + + public MethodHandle findStatic(MethodType methodType) + throws NoSuchMethodException, IllegalAccessException { + return findStatic(this.methodName, methodType); + } + + public MethodHandle findStatic(String name, MethodType methodType) + throws NoSuchMethodException, IllegalAccessException { + return this.caller.findStatic(this.targetClass, name, methodType); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/OperatorHandle.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/OperatorHandle.java new file mode 100644 index 0000000..d45f6b4 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/handle/OperatorHandle.java @@ -0,0 +1,1351 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.handle; + +import static java.lang.invoke.MethodHandles.catchException; +import static java.lang.invoke.MethodHandles.dropArguments; +import static java.lang.invoke.MethodType.methodType; + +import cn.universal.core.engine.functions.DynamicAttribute; +import cn.universal.core.engine.functions.ObjectConvertExtension; +import cn.universal.core.engine.parsing.ast.literal.BooleanLiteral; +import java.lang.invoke.CallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class OperatorHandle { + + private static final MethodHandle FALLBACK; + + static { + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + FALLBACK = + lookup.findStatic( + OperatorHandle.class, + "fallback", + methodType(Object.class, MethodCallSite.class, Object[].class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new Error("OperatorHandle初始化失败", e); + } + } + + public static CallSite bootstrap( + MethodHandles.Lookup caller, String name, MethodType type, int flags) { + MethodCallSite callSite = new MethodCallSite(caller, name, type, OperatorHandle.class); + MethodHandle fallback = + FALLBACK.bindTo(callSite).asCollector(Object[].class, type.parameterCount()).asType(type); + callSite.setTarget(fallback); + callSite.fallback = fallback; + return callSite; + } + + public static Object fallback(MethodCallSite callSite, Object[] args) throws Throwable { + Class arg0Class = args[0] == null ? Object.class : args[0].getClass(); + Class arg1Class = args[1] == null ? Object.class : args[1].getClass(); + MethodHandle target; + try { + target = callSite.findStatic(methodType(Object.class, arg0Class, arg1Class)); + } catch (Throwable ignored) { + try { + target = + callSite.findStatic( + callSite.methodName + "_fallback", + methodType(Object.class, Object.class, Object.class)); + } catch (Throwable t) { + return reject(args[0], args[1], callSite.methodName); + } + } + target = target.asType(methodType(Object.class, Object.class, Object.class)); + target = + catchException( + target, + ClassCastException.class, + dropArguments(callSite.fallback, 0, ClassCastException.class)); + callSite.setTarget(target); + return target.invokeWithArguments(args); + } + + /* byte < 操作实现 */ + public static Object less(Byte a, Byte b) { + return a < b; + } + + public static Object less(Byte a, Short b) { + return a < b; + } + + public static Object less(Byte a, Integer b) { + return a < b; + } + + public static Object less(Byte a, Float b) { + return a < b; + } + + public static Object less(Byte a, Double b) { + return a < b; + } + + public static Object less(Byte a, Long b) { + return a < b; + } + + public static Object less(Byte a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) < 0; + } + + public static Object less(Byte a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) < 0; + } + + /* byte < 操作实现 */ + + /* short < 操作实现 */ + public static Object less(Short a, Byte b) { + return a < b; + } + + public static Object less(Short a, Short b) { + return a < b; + } + + public static Object less(Short a, Integer b) { + return a < b; + } + + public static Object less(Short a, Float b) { + return a < b; + } + + public static Object less(Short a, Double b) { + return a < b; + } + + public static Object less(Short a, Long b) { + return a < b; + } + + public static Object less(Short a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) < 0; + } + + public static Object less(Short a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) < 0; + } + + /* short < 操作实现 */ + + /* int < 操作实现 */ + public static Object less(Integer a, Byte b) { + return a < b; + } + + public static Object less(Integer a, Short b) { + return a < b; + } + + public static Object less(Integer a, Integer b) { + return a < b; + } + + public static Object less(Integer a, Float b) { + return a < b; + } + + public static Object less(Integer a, Double b) { + return a < b; + } + + public static Object less(Integer a, Long b) { + return a < b; + } + + public static Object less(Integer a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) < 0; + } + + public static Object less(Integer a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) < 0; + } + + /* int < 操作实现 */ + + /* float < 操作实现 */ + public static Object less(Float a, Byte b) { + return a < b; + } + + public static Object less(Float a, Short b) { + return a < b; + } + + public static Object less(Float a, Integer b) { + return a < b; + } + + public static Object less(Float a, Float b) { + return a < b; + } + + public static Object less(Float a, Double b) { + return a < b; + } + + public static Object less(Float a, Long b) { + return a < b; + } + + public static Object less(Float a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) < 0; + } + + public static Object less(Float a, BigInteger b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) < 0; + } + + /* float < 操作实现 */ + + /* double < 操作实现 */ + public static Object less(Double a, Byte b) { + return a < b; + } + + public static Object less(Double a, Short b) { + return a < b; + } + + public static Object less(Double a, Integer b) { + return a < b; + } + + public static Object less(Double a, Float b) { + return a < b; + } + + public static Object less(Double a, Double b) { + return a < b; + } + + public static Object less(Double a, Long b) { + return a < b; + } + + public static Object less(Double a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) < 0; + } + + public static Object less(Double a, BigInteger b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) < 0; + } + + /* double < 操作实现 */ + + /* long < 操作实现 */ + public static Object less(Long a, Byte b) { + return a < b; + } + + public static Object less(Long a, Short b) { + return a < b; + } + + public static Object less(Long a, Integer b) { + return a < b; + } + + public static Object less(Long a, Float b) { + return a < b; + } + + public static Object less(Long a, Double b) { + return a < b; + } + + public static Object less(Long a, Long b) { + return a < b; + } + + public static Object less(Long a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) < 0; + } + + public static Object less(Long a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) < 0; + } + + /* long < 操作实现 */ + + /* bigecimal < 操作实现 */ + public static Object less(BigDecimal a, Byte b) { + return a.compareTo(new BigDecimal(b)) < 0; + } + + public static Object less(BigDecimal a, Short b) { + return a.compareTo(new BigDecimal(b)) < 0; + } + + public static Object less(BigDecimal a, Integer b) { + return a.compareTo(new BigDecimal(b)) < 0; + } + + public static Object less(BigDecimal a, Float b) { + return a.compareTo(new BigDecimal(b)) < 0; + } + + public static Object less(BigDecimal a, Double b) { + return a.compareTo(new BigDecimal(b)) < 0; + } + + public static Object less(BigDecimal a, Long b) { + return a.compareTo(new BigDecimal(b)) < 0; + } + + public static Object less(BigDecimal a, BigDecimal b) { + return a.compareTo(b) < 0; + } + + public static Object less(BigDecimal a, BigInteger b) { + return a.compareTo(new BigDecimal(b)) < 0; + } + + /* bigdecimal < 操作实现 */ + + /* biginteger < 操作实现 */ + public static Object less(BigInteger a, Byte b) { + return a.compareTo(BigInteger.valueOf(b)) < 0; + } + + public static Object less(BigInteger a, Short b) { + return a.compareTo(BigInteger.valueOf(b)) < 0; + } + + public static Object less(BigInteger a, Integer b) { + return a.compareTo(BigInteger.valueOf(b)) < 0; + } + + public static Object less(BigInteger a, Float b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) < 0; + } + + public static Object less(BigInteger a, Double b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) < 0; + } + + public static Object less(BigInteger a, Long b) { + return a.compareTo(BigInteger.valueOf(b)) < 0; + } + + public static Object less(BigInteger a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) < 0; + } + + public static Object less(BigInteger a, BigInteger b) { + return a.compareTo(b) < 0; + } + + /* biginteger < 操作实现 */ + + public static Object less_fallback(Object a, Object b) { + if (a == null || b == null) { + return false; + } + if (a.getClass() == b.getClass() && a instanceof Comparable) { + return ((Comparable) (a)).compareTo(b) < 0; + } + return reject(a, b, "<"); + } + + /* byte <= 操作实现 */ + public static Object less_equals(Byte a, Byte b) { + return a <= b; + } + + public static Object less_equals(Byte a, Short b) { + return a <= b; + } + + public static Object less_equals(Byte a, Integer b) { + return a <= b; + } + + public static Object less_equals(Byte a, Float b) { + return a <= b; + } + + public static Object less_equals(Byte a, Double b) { + return a <= b; + } + + public static Object less_equals(Byte a, Long b) { + return a <= b; + } + + public static Object less_equals(Byte a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) <= 0; + } + + public static Object less_equals(Byte a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) <= 0; + } + + /* byte <= 操作实现 */ + + /* short <= 操作实现 */ + public static Object less_equals(Short a, Byte b) { + return a <= b; + } + + public static Object less_equals(Short a, Short b) { + return a <= b; + } + + public static Object less_equals(Short a, Integer b) { + return a <= b; + } + + public static Object less_equals(Short a, Float b) { + return a <= b; + } + + public static Object less_equals(Short a, Double b) { + return a <= b; + } + + public static Object less_equals(Short a, Long b) { + return a <= b; + } + + public static Object less_equals(Short a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) <= 0; + } + + public static Object less_equals(Short a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) <= 0; + } + + /* short <= 操作实现 */ + + /* int <= 操作实现 */ + public static Object less_equals(Integer a, Byte b) { + return a <= b; + } + + public static Object less_equals(Integer a, Short b) { + return a <= b; + } + + public static Object less_equals(Integer a, Integer b) { + return a <= b; + } + + public static Object less_equals(Integer a, Float b) { + return a <= b; + } + + public static Object less_equals(Integer a, Double b) { + return a <= b; + } + + public static Object less_equals(Integer a, Long b) { + return a <= b; + } + + public static Object less_equals(Integer a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) <= 0; + } + + public static Object less_equals(Integer a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) <= 0; + } + + /* int <= 操作实现 */ + + /* float <= 操作实现 */ + public static Object less_equals(Float a, Byte b) { + return a <= b; + } + + public static Object less_equals(Float a, Short b) { + return a <= b; + } + + public static Object less_equals(Float a, Integer b) { + return a <= b; + } + + public static Object less_equals(Float a, Float b) { + return a <= b; + } + + public static Object less_equals(Float a, Double b) { + return a <= b; + } + + public static Object less_equals(Float a, Long b) { + return a <= b; + } + + public static Object less_equals(Float a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) <= 0; + } + + public static Object less_equals(Float a, BigInteger b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) <= 0; + } + + /* float <= 操作实现 */ + + /* double <= 操作实现 */ + public static Object less_equals(Double a, Byte b) { + return a <= b; + } + + public static Object less_equals(Double a, Short b) { + return a <= b; + } + + public static Object less_equals(Double a, Integer b) { + return a <= b; + } + + public static Object less_equals(Double a, Float b) { + return a <= b; + } + + public static Object less_equals(Double a, Double b) { + return a <= b; + } + + public static Object less_equals(Double a, Long b) { + return a <= b; + } + + public static Object less_equals(Double a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) <= 0; + } + + public static Object less_equals(Double a, BigInteger b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) <= 0; + } + + /* double <= 操作实现 */ + + /* long <= 操作实现 */ + public static Object less_equals(Long a, Byte b) { + return a <= b; + } + + public static Object less_equals(Long a, Short b) { + return a <= b; + } + + public static Object less_equals(Long a, Integer b) { + return a <= b; + } + + public static Object less_equals(Long a, Float b) { + return a <= b; + } + + public static Object less_equals(Long a, Double b) { + return a <= b; + } + + public static Object less_equals(Long a, Long b) { + return a <= b; + } + + public static Object less_equals(Long a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) <= 0; + } + + public static Object less_equals(Long a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) <= 0; + } + + /* long <= 操作实现 */ + + /* bigecimal <= 操作实现 */ + public static Object less_equals(BigDecimal a, Byte b) { + return a.compareTo(new BigDecimal(b)) <= 0; + } + + public static Object less_equals(BigDecimal a, Short b) { + return a.compareTo(new BigDecimal(b)) <= 0; + } + + public static Object less_equals(BigDecimal a, Integer b) { + return a.compareTo(new BigDecimal(b)) <= 0; + } + + public static Object less_equals(BigDecimal a, Float b) { + return a.compareTo(new BigDecimal(b)) <= 0; + } + + public static Object less_equals(BigDecimal a, Double b) { + return a.compareTo(new BigDecimal(b)) <= 0; + } + + public static Object less_equals(BigDecimal a, Long b) { + return a.compareTo(new BigDecimal(b)) <= 0; + } + + public static Object less_equals(BigDecimal a, BigDecimal b) { + return a.compareTo(b) <= 0; + } + + public static Object less_equals(BigDecimal a, BigInteger b) { + return a.compareTo(new BigDecimal(b)) <= 0; + } + + /* bigdecimal <= 操作实现 */ + + /* biginteger <= 操作实现 */ + public static Object less_equals(BigInteger a, Byte b) { + return a.compareTo(BigInteger.valueOf(b)) <= 0; + } + + public static Object less_equals(BigInteger a, Short b) { + return a.compareTo(BigInteger.valueOf(b)) <= 0; + } + + public static Object less_equals(BigInteger a, Integer b) { + return a.compareTo(BigInteger.valueOf(b)) <= 0; + } + + public static Object less_equals(BigInteger a, Float b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) <= 0; + } + + public static Object less_equals(BigInteger a, Double b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) <= 0; + } + + public static Object less_equals(BigInteger a, Long b) { + return a.compareTo(BigInteger.valueOf(b)) <= 0; + } + + public static Object less_equals(BigInteger a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) <= 0; + } + + public static Object less_equals(BigInteger a, BigInteger b) { + return a.compareTo(b) <= 0; + } + + /* biginteger <= 操作实现 */ + + public static Object less_equals_fallback(Object a, Object b) { + if (a == null || b == null) { + return false; + } + if (a.getClass() == b.getClass() && a instanceof Comparable) { + return ((Comparable) (a)).compareTo(b) <= 0; + } + return reject(a, b, "<="); + } + + /* == 操作实现 */ + public static Object equals_fallback(Object a, Object b) { + if (Objects.equals(a, b)) { + return true; + } + if (a == null || b == null) { + return false; + } + if (a instanceof Number || b instanceof Number) { + BigDecimal left = ObjectConvertExtension.asDecimal(a, null); + BigDecimal right = ObjectConvertExtension.asDecimal(b, null); + return left != null && right != null && left.compareTo(right) == 0; + } + return false; + } + + /* === 操作实现 */ + public static Object accurate_equals_fallback(Object a, Object b) { + return Objects.equals(a, b); + } + + /* != 操作实现 */ + public static Object not_equals_fallback(Object a, Object b) { + return !(boolean) equals_fallback(a, b); + } + + /* !== 操作实现 */ + public static Object not_accurate_equals_fallback(Object a, Object b) { + return !Objects.equals(a, b); + } + + /* byte > 操作实现 */ + public static Object greater(Byte a, Byte b) { + return a > b; + } + + public static Object greater(Byte a, Short b) { + return a > b; + } + + public static Object greater(Byte a, Integer b) { + return a > b; + } + + public static Object greater(Byte a, Float b) { + return a > b; + } + + public static Object greater(Byte a, Double b) { + return a > b; + } + + public static Object greater(Byte a, Long b) { + return a > b; + } + + public static Object greater(Byte a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) > 0; + } + + public static Object greater(Byte a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) > 0; + } + + /* byte > 操作实现 */ + + /* short > 操作实现 */ + public static Object greater(Short a, Byte b) { + return a > b; + } + + public static Object greater(Short a, Short b) { + return a > b; + } + + public static Object greater(Short a, Integer b) { + return a > b; + } + + public static Object greater(Short a, Float b) { + return a > b; + } + + public static Object greater(Short a, Double b) { + return a > b; + } + + public static Object greater(Short a, Long b) { + return a > b; + } + + public static Object greater(Short a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) > 0; + } + + public static Object greater(Short a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) > 0; + } + + /* short > 操作实现 */ + + /* int > 操作实现 */ + public static Object greater(Integer a, Byte b) { + return a > b; + } + + public static Object greater(Integer a, Short b) { + return a > b; + } + + public static Object greater(Integer a, Integer b) { + return a > b; + } + + public static Object greater(Integer a, Float b) { + return a > b; + } + + public static Object greater(Integer a, Double b) { + return a > b; + } + + public static Object greater(Integer a, Long b) { + return a > b; + } + + public static Object greater(Integer a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) > 0; + } + + public static Object greater(Integer a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) > 0; + } + + /* int > 操作实现 */ + + /* float > 操作实现 */ + public static Object greater(Float a, Byte b) { + return a > b; + } + + public static Object greater(Float a, Short b) { + return a > b; + } + + public static Object greater(Float a, Integer b) { + return a > b; + } + + public static Object greater(Float a, Float b) { + return a > b; + } + + public static Object greater(Float a, Double b) { + return a > b; + } + + public static Object greater(Float a, Long b) { + return a > b; + } + + public static Object greater(Float a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) > 0; + } + + public static Object greater(Float a, BigInteger b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) > 0; + } + + /* float > 操作实现 */ + + /* double > 操作实现 */ + public static Object greater(Double a, Byte b) { + return a > b; + } + + public static Object greater(Double a, Short b) { + return a > b; + } + + public static Object greater(Double a, Integer b) { + return a > b; + } + + public static Object greater(Double a, Float b) { + return a > b; + } + + public static Object greater(Double a, Double b) { + return a > b; + } + + public static Object greater(Double a, Long b) { + return a > b; + } + + public static Object greater(Double a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) > 0; + } + + public static Object greater(Double a, BigInteger b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) > 0; + } + + /* double > 操作实现 */ + + /* long > 操作实现 */ + public static Object greater(Long a, Byte b) { + return a > b; + } + + public static Object greater(Long a, Short b) { + return a > b; + } + + public static Object greater(Long a, Integer b) { + return a > b; + } + + public static Object greater(Long a, Float b) { + return a > b; + } + + public static Object greater(Long a, Double b) { + return a > b; + } + + public static Object greater(Long a, Long b) { + return a > b; + } + + public static Object greater(Long a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) > 0; + } + + public static Object greater(Long a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) > 0; + } + + /* long > 操作实现 */ + + /* bigecimal > 操作实现 */ + public static Object greater(BigDecimal a, Byte b) { + return a.compareTo(new BigDecimal(b)) > 0; + } + + public static Object greater(BigDecimal a, Short b) { + return a.compareTo(new BigDecimal(b)) > 0; + } + + public static Object greater(BigDecimal a, Integer b) { + return a.compareTo(new BigDecimal(b)) > 0; + } + + public static Object greater(BigDecimal a, Float b) { + return a.compareTo(new BigDecimal(b)) > 0; + } + + public static Object greater(BigDecimal a, Double b) { + return a.compareTo(new BigDecimal(b)) > 0; + } + + public static Object greater(BigDecimal a, Long b) { + return a.compareTo(new BigDecimal(b)) > 0; + } + + public static Object greater(BigDecimal a, BigDecimal b) { + return a.compareTo(b) > 0; + } + + public static Object greater(BigDecimal a, BigInteger b) { + return a.compareTo(new BigDecimal(b)) > 0; + } + + /* bigdecimal > 操作实现 */ + + /* biginteger > 操作实现 */ + public static Object greater(BigInteger a, Byte b) { + return a.compareTo(BigInteger.valueOf(b)) > 0; + } + + public static Object greater(BigInteger a, Short b) { + return a.compareTo(BigInteger.valueOf(b)) > 0; + } + + public static Object greater(BigInteger a, Integer b) { + return a.compareTo(BigInteger.valueOf(b)) > 0; + } + + public static Object greater(BigInteger a, Float b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) > 0; + } + + public static Object greater(BigInteger a, Double b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) > 0; + } + + public static Object greater(BigInteger a, Long b) { + return a.compareTo(BigInteger.valueOf(b)) > 0; + } + + public static Object greater(BigInteger a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) > 0; + } + + public static Object greater(BigInteger a, BigInteger b) { + return a.compareTo(b) > 0; + } + + /* biginteger > 操作实现 */ + + public static Object greater_fallback(Object a, Object b) { + if (a == null || b == null) { + return false; + } + if (a.getClass() == b.getClass() && a instanceof Comparable) { + return ((Comparable) (a)).compareTo(b) > 0; + } + return reject(a, b, ">"); + } + + /* byte >= 操作实现 */ + public static Object greater_equals(Byte a, Byte b) { + return a >= b; + } + + public static Object greater_equals(Byte a, Short b) { + return a >= b; + } + + public static Object greater_equals(Byte a, Integer b) { + return a >= b; + } + + public static Object greater_equals(Byte a, Float b) { + return a >= b; + } + + public static Object greater_equals(Byte a, Double b) { + return a >= b; + } + + public static Object greater_equals(Byte a, Long b) { + return a >= b; + } + + public static Object greater_equals(Byte a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) >= 0; + } + + public static Object greater_equals(Byte a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) >= 0; + } + + /* byte >= 操作实现 */ + + /* short >= 操作实现 */ + public static Object greater_equals(Short a, Byte b) { + return a >= b; + } + + public static Object greater_equals(Short a, Short b) { + return a >= b; + } + + public static Object greater_equals(Short a, Integer b) { + return a >= b; + } + + public static Object greater_equals(Short a, Float b) { + return a >= b; + } + + public static Object greater_equals(Short a, Double b) { + return a >= b; + } + + public static Object greater_equals(Short a, Long b) { + return a >= b; + } + + public static Object greater_equals(Short a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) >= 0; + } + + public static Object greater_equals(Short a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) >= 0; + } + + /* short >= 操作实现 */ + + /* int >= 操作实现 */ + public static Object greater_equals(Integer a, Byte b) { + return a >= b; + } + + public static Object greater_equals(Integer a, Short b) { + return a >= b; + } + + public static Object greater_equals(Integer a, Integer b) { + return a >= b; + } + + public static Object greater_equals(Integer a, Float b) { + return a >= b; + } + + public static Object greater_equals(Integer a, Double b) { + return a >= b; + } + + public static Object greater_equals(Integer a, Long b) { + return a >= b; + } + + public static Object greater_equals(Integer a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) >= 0; + } + + public static Object greater_equals(Integer a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) >= 0; + } + + /* int >= 操作实现 */ + + /* float >= 操作实现 */ + public static Object greater_equals(Float a, Byte b) { + return a >= b; + } + + public static Object greater_equals(Float a, Short b) { + return a >= b; + } + + public static Object greater_equals(Float a, Integer b) { + return a >= b; + } + + public static Object greater_equals(Float a, Float b) { + return a >= b; + } + + public static Object greater_equals(Float a, Double b) { + return a >= b; + } + + public static Object greater_equals(Float a, Long b) { + return a >= b; + } + + public static Object greater_equals(Float a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) >= 0; + } + + public static Object greater_equals(Float a, BigInteger b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) >= 0; + } + + /* float >= 操作实现 */ + + /* double >= 操作实现 */ + public static Object greater_equals(Double a, Byte b) { + return a >= b; + } + + public static Object greater_equals(Double a, Short b) { + return a >= b; + } + + public static Object greater_equals(Double a, Integer b) { + return a >= b; + } + + public static Object greater_equals(Double a, Float b) { + return a >= b; + } + + public static Object greater_equals(Double a, Double b) { + return a >= b; + } + + public static Object greater_equals(Double a, Long b) { + return a >= b; + } + + public static Object greater_equals(Double a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) >= 0; + } + + public static Object greater_equals(Double a, BigInteger b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) >= 0; + } + + /* double >= 操作实现 */ + + /* long >= 操作实现 */ + public static Object greater_equals(Long a, Byte b) { + return a >= b; + } + + public static Object greater_equals(Long a, Short b) { + return a >= b; + } + + public static Object greater_equals(Long a, Integer b) { + return a >= b; + } + + public static Object greater_equals(Long a, Float b) { + return a >= b; + } + + public static Object greater_equals(Long a, Double b) { + return a >= b; + } + + public static Object greater_equals(Long a, Long b) { + return a >= b; + } + + public static Object greater_equals(Long a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) >= 0; + } + + public static Object greater_equals(Long a, BigInteger b) { + return BigInteger.valueOf(a).compareTo(b) >= 0; + } + + /* long >= 操作实现 */ + + /* bigdecimal >= 操作实现 */ + public static Object greater_equals(BigDecimal a, Byte b) { + return a.compareTo(new BigDecimal(b)) >= 0; + } + + public static Object greater_equals(BigDecimal a, Short b) { + return a.compareTo(new BigDecimal(b)) >= 0; + } + + public static Object greater_equals(BigDecimal a, Integer b) { + return a.compareTo(new BigDecimal(b)) >= 0; + } + + public static Object greater_equals(BigDecimal a, Float b) { + return a.compareTo(new BigDecimal(b)) >= 0; + } + + public static Object greater_equals(BigDecimal a, Double b) { + return a.compareTo(new BigDecimal(b)) >= 0; + } + + public static Object greater_equals(BigDecimal a, Long b) { + return a.compareTo(new BigDecimal(b)) >= 0; + } + + public static Object greater_equals(BigDecimal a, BigDecimal b) { + return a.compareTo(b) >= 0; + } + + public static Object greater_equals(BigDecimal a, BigInteger b) { + return a.compareTo(new BigDecimal(b)) >= 0; + } + + /* bigdecimal >= 操作实现 */ + + /* biginteger > 操作实现 */ + public static Object greater_equals(BigInteger a, Byte b) { + return a.compareTo(BigInteger.valueOf(b)) >= 0; + } + + public static Object greater_equals(BigInteger a, Short b) { + return a.compareTo(BigInteger.valueOf(b)) >= 0; + } + + public static Object greater_equals(BigInteger a, Integer b) { + return a.compareTo(BigInteger.valueOf(b)) >= 0; + } + + public static Object greater_equals(BigInteger a, Float b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) >= 0; + } + + public static Object greater_equals(BigInteger a, Double b) { + return new BigDecimal(a).compareTo(new BigDecimal(b)) >= 0; + } + + public static Object greater_equals(BigInteger a, Long b) { + return a.compareTo(BigInteger.valueOf(b)) >= 0; + } + + public static Object greater_equals(BigInteger a, BigDecimal b) { + return new BigDecimal(a).compareTo(b) >= 0; + } + + public static Object greater_equals(BigInteger a, BigInteger b) { + return a.compareTo(b) >= 0; + } + + /* biginteger >= 操作实现 */ + + public static Object greater_equals_fallback(Object a, Object b) { + if (a == null || b == null) { + return false; + } + if (a.getClass() == b.getClass() && a instanceof Comparable) { + return ((Comparable) (a)).compareTo(b) >= 0; + } + return reject(a, b, ">="); + } + + public static boolean isFalse(Object object) { + return !isTrue(object); + } + + public static boolean isTrue(Object object) { + return BooleanLiteral.isTrue(object); + } + + private static Object reject(Object a, Object b, String symbol) throws IllegalArgumentException { + throw new IllegalArgumentException( + String.format( + "操作符 `%s` 不支持 (%s,%s) 类型", symbol, a.getClass().getName(), b.getClass().getName())); + } + + public static Object map_or_array_access(int[] target, Number key) { + return key.intValue() < target.length ? target[key.intValue()] : null; + } + + public static Object map_or_array_access(byte[] target, Number key) { + return key.intValue() < target.length ? target[key.intValue()] : null; + } + + public static Object map_or_array_access(short[] target, Number key) { + return key.intValue() < target.length ? target[key.intValue()] : null; + } + + public static Object map_or_array_access(float[] target, Number key) { + return key.intValue() < target.length ? target[key.intValue()] : null; + } + + public static Object map_or_array_access(double[] target, Number key) { + return key.intValue() < target.length ? target[key.intValue()] : null; + } + + public static Object map_or_array_access(long[] target, Number key) { + return key.intValue() < target.length ? target[key.intValue()] : null; + } + + public static Object map_or_array_access(char[] target, Number key) { + return key.intValue() < target.length ? target[key.intValue()] : null; + } + + public static Object map_or_array_access(boolean[] target, Number key) { + return key.intValue() < target.length ? target[key.intValue()] : null; + } + + public static Object map_or_array_access(Object[] target, Number key) { + return key.intValue() < target.length ? target[key.intValue()] : null; + } + + public static Object map_or_array_access(String target, Number key) { + return key.intValue() < target.length() ? target.charAt(key.intValue()) : null; + } + + public static Object map_or_array_access_fallback(Object target, Object key) { + if (target == null) { + return null; + } + if (target instanceof Map) { + return ((Map) target).get(key); + } else if (key instanceof Number) { + int index = ((Number) key).intValue(); + if (target instanceof List) { + List list = (List) target; + return index < list.size() ? list.get(index) : null; + } else if (target.getClass().isArray()) { + int length = Array.getLength(target); + return index < length ? Array.get(target, ((Number) key).intValue()) : null; + } + } else if (target instanceof DynamicAttribute && key instanceof String) { + return ((DynamicAttribute) target).getDynamicAttribute((String) key); + } + return reject(target, key, ".或[]"); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/ArrayKeyValueIterator.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/ArrayKeyValueIterator.java new file mode 100644 index 0000000..e26c547 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/ArrayKeyValueIterator.java @@ -0,0 +1,25 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.lang; + +public class ArrayKeyValueIterator extends ArrayValueIterator implements KeyIterator { + + public ArrayKeyValueIterator(Object target) { + super(target); + } + + @Override + public Object getKey() { + return index - 1; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/ArrayValueIterator.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/ArrayValueIterator.java new file mode 100644 index 0000000..5f111fb --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/ArrayValueIterator.java @@ -0,0 +1,40 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.lang; + +import java.lang.reflect.Array; +import java.util.Iterator; + +public class ArrayValueIterator implements Iterator { + + private final Object target; + + private final int len; + + protected int index = 0; + + public ArrayValueIterator(Object target) { + this.target = target; + this.len = Array.getLength(target); + } + + @Override + public boolean hasNext() { + return index < len; + } + + @Override + public Object next() { + return Array.get(target, index++); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/KeyIterator.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/KeyIterator.java new file mode 100644 index 0000000..b45e648 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/KeyIterator.java @@ -0,0 +1,18 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.lang; + +public interface KeyIterator { + + Object getKey(); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/KeyValueIterator.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/KeyValueIterator.java new file mode 100644 index 0000000..b7521d1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/KeyValueIterator.java @@ -0,0 +1,41 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.lang; + +import java.util.Iterator; + +public class KeyValueIterator implements Iterator, KeyIterator { + + private final Iterator iterator; + + private int index = 0; + + public KeyValueIterator(Iterator iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Object next() { + return iterator.next(); + } + + @Override + public Object getKey() { + return index++; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/MapKeyValueIterator.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/MapKeyValueIterator.java new file mode 100644 index 0000000..5b704e1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/lang/MapKeyValueIterator.java @@ -0,0 +1,43 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.lang; + +import java.util.Iterator; +import java.util.Map; + +public class MapKeyValueIterator implements Iterator, KeyIterator { + + private final Iterator> iterator; + + private Map.Entry current; + + public MapKeyValueIterator(Map target) { + this.iterator = target.entrySet().iterator(); + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Object next() { + current = iterator.next(); + return current.getValue(); + } + + @Override + public Object getKey() { + return current.getKey(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/JoinValue.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/JoinValue.java new file mode 100644 index 0000000..75ce9a0 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/JoinValue.java @@ -0,0 +1,33 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.linq; + +public class JoinValue { + + private final int index; + + private final Object value; + + public JoinValue(int index, Object value) { + this.index = index; + this.value = value; + } + + public int getIndex() { + return index; + } + + public Object getValue() { + return value; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/LinQBuilder.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/LinQBuilder.java new file mode 100644 index 0000000..ca5e5a6 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/LinQBuilder.java @@ -0,0 +1,325 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.linq; + +import cn.universal.core.engine.functions.MapExtension; +import cn.universal.core.engine.functions.ObjectConvertExtension; +import cn.universal.core.engine.functions.StreamExtension; +import cn.universal.core.engine.runtime.Variables; +import cn.universal.core.engine.runtime.function.MagicScriptLambdaFunction; +import cn.universal.core.engine.runtime.handle.OperatorHandle; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class LinQBuilder { + + private final Variables variables; + + private static final Object[] EMPTY_PARAMETER = new Object[0]; + + private List fromObjects; + + private int fromAliasIndex; + + private final List selects = new ArrayList<>(); + + private MagicScriptLambdaFunction where; + + private MagicScriptLambdaFunction having; + + private final List groups = new ArrayList<>(); + + private final List joins = new ArrayList<>(); + + private int joinSize = 0; + + private final List orders = new ArrayList<>(); + + private long limit = Long.MIN_VALUE; + + private long offset = Long.MIN_VALUE; + + private LinQBuilder(Variables variables) { + this.variables = variables; + } + + public static LinQBuilder create(Variables variables) { + return new LinQBuilder(variables); + } + + public LinQBuilder where(MagicScriptLambdaFunction condition) { + where = condition; + return this; + } + + public LinQBuilder from(Object object, int aliasIndex) { + this.fromAliasIndex = aliasIndex; + this.fromObjects = convertToList(object); + return this; + } + + private List convertToList(Object object) { + if (object instanceof Map) { + return (List) + MapExtension.asList( + (Map) object, (entry) -> Collections.singletonMap(entry[0], entry[1])); + } else { + try { + return StreamExtension.arrayLikeToList(object); + } catch (Exception e) { + return Collections.singletonList(object); + } + } + } + + public LinQBuilder group(MagicScriptLambdaFunction function) { + groups.add(function); + return this; + } + + public LinQBuilder join( + MagicScriptLambdaFunction condition, Object object, boolean isLeftJoin, int aliasIndex) { + joins.add(new LinQJoinValue(condition, convertToList(object), isLeftJoin, aliasIndex)); + joinSize++; + return this; + } + + public LinQBuilder having(MagicScriptLambdaFunction condition) { + having = condition; + return this; + } + + public LinQBuilder order(MagicScriptLambdaFunction function, int order) { + this.orders.add(new LinQOrder(function, order)); + return this; + } + + public LinQBuilder select(MagicScriptLambdaFunction function, String aliasName, int aliasIndex) { + selects.add(new SelectField(function, aliasName, aliasIndex)); + return this; + } + + public Object executeAndFetchFirst() { + List list = execute(); + return list.isEmpty() ? null : list.get(0); + } + + public LinQBuilder limit(Object limit) { + this.limit = ObjectConvertExtension.asLong(limit, Long.MIN_VALUE); + return this; + } + + public LinQBuilder offset(Object offset) { + this.offset = ObjectConvertExtension.asLong(offset, Long.MIN_VALUE); + return this; + } + + public List execute() { + List records = new ArrayList<>(); + for (Object object : fromObjects) { + variables.setValue(fromAliasIndex, object); + // 处理 join + where + records.addAll(processWhere(processJoin(object), object)); + } + // 处理 group + having + records = processGroup(records); + // 处理 select + List result = processSelect(records); + Stream> stream = result.stream().sorted().map(SelectValue::getValue); + if (offset > 0) { + stream = stream.skip(offset); + } + if (limit > 0) { + stream = stream.limit(limit); + } + return stream.collect(Collectors.toList()); + } + + private void processRow(Object item, Map row, SelectField field) { + if (item instanceof Map) { + row.putAll((Map) item); + } else if (field.isWhole() && item instanceof List) { + row.putAll((Map) StreamExtension.first(item)); + } else { + row.put(field.getAliasName(), item); + } + } + + private List processSelect(List records) { + List result = new ArrayList<>(records.size()); + int fieldSize = selects.size(); + for (Record record : records) { + Map row = new LinkedHashMap<>(fieldSize); + variables.setValue(fromAliasIndex, record.getValue()); + record.setVariableValue(variables); + for (SelectField field : selects) { + MagicScriptLambdaFunction function = field.getFunction(); + if (function == null) { + processRow(record.getValue(), row, field); + record.getJoinValues().forEach(item -> processRow(item, row, field)); + } else { + processRow(apply(function, variables, EMPTY_PARAMETER), row, field); + } + } + List orderValues = new ArrayList<>(); + if (!orders.isEmpty()) { + for (LinQOrder order : orders) { + orderValues.add( + new OrderValue( + apply(order.getFunction(), variables, EMPTY_PARAMETER), order.getOrder())); + } + } + result.add(new SelectValue(row, orderValues)); + record.removeVariableValue(variables); + } + return result; + } + + private List processGroup(List records) { + if (!groups.isEmpty()) { + Map, List> group = new LinkedHashMap<>(); + for (Record record : records) { + variables.setValue(fromAliasIndex, record.getValue()); + record.setVariableValue(variables); + List keys = + groups.stream() + .map(field -> apply(field, variables, EMPTY_PARAMETER)) + .collect(Collectors.toList()); + group.computeIfAbsent(keys, k -> new ArrayList<>()).add(record); + } + records = new ArrayList<>(); + for (Map.Entry, List> entry : group.entrySet()) { + List value = entry.getValue(); + Map> joinMap = new HashMap<>(); + List joinValues = new ArrayList<>(); + value.stream() + .map(Record::getJoinValues) + .flatMap(Collection::stream) + .forEach( + item -> + joinMap + .computeIfAbsent(item.getIndex(), k -> new ArrayList<>()) + .add(item.getValue())); + joinMap.forEach((key, joinValue) -> joinValues.add(new JoinValue(key, joinValue))); + Record record = + new Record( + value.stream().map(Record::getValue).collect(Collectors.toList()), joinValues); + boolean valid = true; + if (having != null) { + variables.setValue(fromAliasIndex, record.getValue()); + record.setVariableValue(variables); + valid = OperatorHandle.isTrue(apply(having, variables, EMPTY_PARAMETER)); + } + if (valid) { + records.add(record); + } + } + } + return records; + } + + private List processWhere(List records, Object object) { + if (where != null) { + variables.setValue(fromAliasIndex, object); + return records.stream() + .peek(record -> record.setVariableValue(variables)) + .filter(record -> OperatorHandle.isTrue(apply(where, variables, EMPTY_PARAMETER))) + .collect(Collectors.toList()); + } + return records; + } + + private Object apply(MagicScriptLambdaFunction function, Variables variables, Object[] args) { + return function.apply(variables, args); + } + + private boolean processOtherJoin(int index, Map> container) { + if (index < joinSize) { + LinQJoinValue join = joins.get(index); + boolean matched = false; + for (Object joinItem : join.getTarget()) { + // 设置当前关联表所在行的数据 + variables.setValue(join.getAliasIndex(), joinItem); + // 判断关联条件是否成立 + if (OperatorHandle.isTrue(apply(join.getCondition(), variables, EMPTY_PARAMETER))) { + matched = true; + if (processOtherJoin(index + 1, container)) { + // join关联不上时直接跳过当前行 + continue; + } + // 关联上时,把当前结果存放至容器中 + container.get(join.getAliasIndex()).add(joinItem); + } + } + variables.setValue(join.getAliasIndex(), Collections.emptyMap()); + if (!matched) { + if (!join.isLeftJoin()) { + return true; + } + return processOtherJoin(index + 1, container); + } + } + return false; + } + + private List processJoin(Object object) { + // 当没有join语句时,直接返回该行数据 + if (joinSize == 0) { + return Collections.singletonList(new Record(object)); + } + // 定义存放关联结果集的容器并初始化 + Map> map = new HashMap<>(joinSize); + joins.forEach(join -> map.put(join.getAliasIndex(), new ArrayList<>())); + if (processOtherJoin(0, map)) { + return Collections.emptyList(); + } + + // 计算当前行关联到的行数 + int maxSize = map.values().stream().mapToInt(Collection::size).max().getAsInt(); + // 全没关联上,且没有 join + if (maxSize == 0) { + return Collections.singletonList( + new Record( + object, + map.keySet().stream() + .map(objects -> new JoinValue(objects, Collections.emptyMap())) + .collect(Collectors.toList()))); + } + List records = new ArrayList<>(maxSize); + Set>> entries = map.entrySet(); + for (int i = 0; i < maxSize; i++) { + List joinValues = new ArrayList<>(entries.size()); + for (Map.Entry> entry : entries) { + List item = entry.getValue(); + int size = item.size(); + if (size > 0) { + if (size > i) { + joinValues.add(new JoinValue(entry.getKey(), item.get(i))); + } else { + joinValues.add(new JoinValue(entry.getKey(), Collections.emptyMap())); + } + } + } + records.add(new Record(object, joinValues)); + } + return records; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/LinQJoinValue.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/LinQJoinValue.java new file mode 100644 index 0000000..3d54a7f --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/LinQJoinValue.java @@ -0,0 +1,58 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.linq; + +import cn.universal.core.engine.runtime.function.MagicScriptLambdaFunction; +import java.util.List; + +public class LinQJoinValue { + + private final MagicScriptLambdaFunction condition; + + private final List target; + + private final boolean isLeftJoin; + + private int aliasIndex = -1; + + public LinQJoinValue( + MagicScriptLambdaFunction condition, + List objects, + boolean isLeftJoin, + int aliasIndex) { + this.condition = condition; + this.target = objects; + this.isLeftJoin = isLeftJoin; + this.aliasIndex = aliasIndex; + } + + public MagicScriptLambdaFunction getCondition() { + return condition; + } + + public int size() { + return target.size(); + } + + public List getTarget() { + return target; + } + + public boolean isLeftJoin() { + return isLeftJoin; + } + + public int getAliasIndex() { + return aliasIndex; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/LinQOrder.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/LinQOrder.java new file mode 100644 index 0000000..f984f99 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/LinQOrder.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.linq; + +import cn.universal.core.engine.runtime.function.MagicScriptLambdaFunction; + +public class LinQOrder { + + private final MagicScriptLambdaFunction function; + + private final int order; + + public LinQOrder(MagicScriptLambdaFunction function, int order) { + this.function = function; + this.order = order; + } + + public MagicScriptLambdaFunction getFunction() { + return function; + } + + public int getOrder() { + return order; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/OrderValue.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/OrderValue.java new file mode 100644 index 0000000..d98b2cb --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/OrderValue.java @@ -0,0 +1,33 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.linq; + +public class OrderValue { + + private final Object value; + + private final int order; + + public OrderValue(Object value, int order) { + this.value = value; + this.order = order; + } + + public Object getValue() { + return value; + } + + public int getOrder() { + return order; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/Record.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/Record.java new file mode 100644 index 0000000..60a2b36 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/Record.java @@ -0,0 +1,51 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.linq; + +import cn.universal.core.engine.runtime.Variables; +import java.util.Collections; +import java.util.List; + +public class Record { + + private final Object value; + + private final List joinValues; + + public Record(Object value) { + this(value, Collections.emptyList()); + } + + public Record(Object value, List joinValues) { + this.value = value; + this.joinValues = joinValues; + } + + public Object getValue() { + return value; + } + + public List getJoinValues() { + return joinValues; + } + + public void setVariableValue(Variables variables) { + this.joinValues.forEach( + joinValue -> variables.setValue(joinValue.getIndex(), joinValue.getValue())); + } + + public void removeVariableValue(Variables variables) { + this.joinValues.forEach( + joinValue -> variables.setValue(joinValue.getIndex(), Collections.emptyMap())); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/SelectField.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/SelectField.java new file mode 100644 index 0000000..656c399 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/SelectField.java @@ -0,0 +1,42 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.linq; + +import cn.universal.core.engine.runtime.function.MagicScriptLambdaFunction; + +public class SelectField { + + private final MagicScriptLambdaFunction function; + + private final String aliasName; + + private final int aliasIndex; + + public SelectField(MagicScriptLambdaFunction function, String aliasName, int aliasIndex) { + this.function = function; + this.aliasName = aliasName; + this.aliasIndex = aliasIndex; + } + + public MagicScriptLambdaFunction getFunction() { + return function; + } + + public String getAliasName() { + return aliasName; + } + + public boolean isWhole() { + return aliasIndex == -1 && "*".equals(aliasName); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/SelectValue.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/SelectValue.java new file mode 100644 index 0000000..5947829 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/engine/runtime/linq/SelectValue.java @@ -0,0 +1,52 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.engine.runtime.linq; + +import cn.universal.core.engine.parsing.ast.BinaryOperation; +import java.util.List; +import java.util.Map; + +public class SelectValue implements Comparable { + + private final Map value; + + private final List orderValues; + + private final boolean hasOrder; + + public SelectValue(Map value, List orderValues) { + this.value = value; + this.orderValues = orderValues; + this.hasOrder = !orderValues.isEmpty(); + } + + public Map getValue() { + return value; + } + + @Override + public int compareTo(SelectValue o2) { + if (!hasOrder) { + return 0; + } + for (int i = 0, size = orderValues.size(); i < size; i++) { + OrderValue ov1 = orderValues.get(i); + OrderValue ov2 = o2.orderValues.get(i); + int compareValue = BinaryOperation.compare(ov1.getValue(), ov2.getValue()); + if (compareValue != 0) { + return compareValue * ov1.getOrder(); + } + } + return 0; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/message/DownCommonData.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/message/DownCommonData.java new file mode 100644 index 0000000..dc72eb0 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/message/DownCommonData.java @@ -0,0 +1,59 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.message; + +import cn.hutool.json.JSONObject; +import lombok.Data; + +/** + * @version 1.0 @Author Aleo + * @since 2023/10/11 + */ +@Data +public class DownCommonData { + + /** imei运营商NB号码,长度15 */ + private String imei; + + /** 长度不超过15 */ + private String imsi; + + /** 水电表 表号 */ + private String meterNo; + + /** 设备型号 */ + private String deviceModel; + + /** 设备名称 */ + private String deviceName; + + /** 公司名称 */ + private String companyNo; + + private JSONObject configuration; + + /** 经度 */ + private String longitude; + + /** 维度 */ + private String latitude; + + /** 安装位置 */ + private String location; + + /** extDeviceId */ + private String extDeviceId; + + /** */ + private String appKey; +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/message/DownRequest.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/message/DownRequest.java new file mode 100644 index 0000000..8c6eca5 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/message/DownRequest.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.message; + +import cn.universal.common.constant.IoTConstant.DownCmd; +import java.io.Serializable; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * @version 1.0 @Author Aleo + * @since 2025/8/13 9:34 + */ +@EqualsAndHashCode(callSuper = true) +@AllArgsConstructor +@NoArgsConstructor +@Data +@SuperBuilder +public class DownRequest extends Request implements Serializable { + + /* 网关绑定设备 ProductKey */ + private String gwProductKey; + /* 网关绑定设备deviceId */ + private String gwDeviceId; + + /** 设备密钥 */ + private String deviceKey; + + /** 命令名称 */ + private DownCmd cmd; + + /** 消息标识符 */ + private String msgId; + + // 方法调用参数 + /** 备注 */ + private String detail; + + private Map function; + + private Long storeTime; + + /** 设备复用 */ + private boolean deviceReuse; + + /** 是否超管 */ + private boolean isAdmin; + + /** 是否自动注册 */ + private boolean allowInsert; +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/message/Request.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/message/Request.java new file mode 100644 index 0000000..5fa5697 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/message/Request.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ +package cn.universal.core.message; + +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class Request implements Serializable { + + /** 应用唯一标识,同数据库(app_unique_id) */ + private String applicationId; + + /** 用户标识 */ + private String appUnionId; + + /** 产品唯一标识 */ + private String productKey; + + /** 设备唯一标识 */ + private String iotId; + + /** 设备ID */ + private String deviceId; + + /** 消息原文 */ + private String payload; + + /** 设备名称 */ + private String deviceName; + + private String requestId; +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/message/UPRequest.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/message/UPRequest.java new file mode 100644 index 0000000..0824309 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/message/UPRequest.java @@ -0,0 +1,77 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.message; + +import cn.hutool.json.JSONObject; +import cn.universal.common.constant.IoTConstant.DeviceNode; +import cn.universal.common.constant.IoTConstant.MessageType; +import java.io.Serializable; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * 上行请求类 + * + * @version 1.0 @Author Aleo + * @since 2025/8/9 15:51 + */ +@EqualsAndHashCode(callSuper = true) +@AllArgsConstructor +@NoArgsConstructor +@Data +@SuperBuilder +public class UPRequest extends Request implements Serializable { + + // true:无论设备在线离线设备指令总是缓存 + private transient boolean onlyCache; + // true: 设备不存在时直接新增该设备 + private transient boolean allowInsert; + private transient boolean standardTcp; + private transient boolean preStore; + private String userUnionId; + private String extDeviceId; + + /** 设备节点类型 */ + private DeviceNode deviceNode; + + private MessageType messageType; + private String event; + private String eventName; + private Map data; + private Map properties; + private Map tags; + private String function; + private String functionName; + private JSONObject shadow; + + private String childDeviceId; + + /** 独立订阅地址 */ + private transient Object devSubscribe; + + /** 时间 */ + private Long time; + + /** /** 是否是debug上报 */ + private transient boolean debug; + + /** 空的编解码 */ + private transient boolean emptyProtocol; + + /** 下行topic */ + private String downTopic; +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/AbstractDeviceMetadata.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/AbstractDeviceMetadata.java new file mode 100644 index 0000000..770b8d5 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/AbstractDeviceMetadata.java @@ -0,0 +1,69 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import java.util.List; +import java.util.Optional; + +/** + * 基础设备物模型定义 + * + * @version 1.0 @Author Aleo + * @since 2025/8/25 + */ +public interface AbstractDeviceMetadata extends Metadata, Jsonable { + + /** + * @return 所有属性定义 + */ + List getProperties(); + + /** + * @return 所有功能定义 + */ + List getFunctions(); + + /** + * @return 事件定义 + */ + List getEvents(); + + /** + * @return 标签定义 + */ + List getTags(); + + default Optional getEvent(String id) { + return Optional.ofNullable(getEventOrNull(id)); + } + + AbstractEventMetadata getEventOrNull(String id); + + default Optional getProperty(String id) { + return Optional.ofNullable(getPropertyOrNull(id)); + } + + AbstractPropertyMetadata getPropertyOrNull(String id); + + default Optional getFunction(String id) { + return Optional.ofNullable(getFunctionOrNull(id)); + } + + AbstractFunctionMetadata getFunctionOrNull(String id); + + default Optional getTag(String id) { + return Optional.ofNullable(getTagOrNull(id)); + } + + AbstractPropertyMetadata getTagOrNull(String id); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/AbstractEventMetadata.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/AbstractEventMetadata.java new file mode 100644 index 0000000..a4556ad --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/AbstractEventMetadata.java @@ -0,0 +1,24 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +/** + * 事件元数据 + * + * @version 1.0 @Author Aleo + * @since 2025/8/25 + */ +public interface AbstractEventMetadata extends Metadata, Jsonable { + + ValueType getValueType(); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/AbstractFunctionMetadata.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/AbstractFunctionMetadata.java new file mode 100644 index 0000000..fe2a0fe --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/AbstractFunctionMetadata.java @@ -0,0 +1,39 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import java.util.List; + +/** + * 功能元数据 + * + * @version 1.0 @Author Aleo + * @since 2025/8/25 + */ +public interface AbstractFunctionMetadata extends Metadata, Jsonable { + + /** + * @return 输入参数定义 + */ + List getInputs(); + + /** + * @return 输出类型,为null表示无输出 + */ + ValueType getOutput(); + + /** + * @return 是否异步 + */ + boolean isAsync(); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/AbstractPropertyMetadata.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/AbstractPropertyMetadata.java new file mode 100644 index 0000000..5500992 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/AbstractPropertyMetadata.java @@ -0,0 +1,26 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +/** + * 属性元数据 + * + * @version 1.0 @Author Aleo + * @since 2025/8/25 + */ +public interface AbstractPropertyMetadata extends Metadata, Jsonable { + + ValueType getValueType(); + + String getMode(); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/Converter.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/Converter.java new file mode 100644 index 0000000..3999e6e --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/Converter.java @@ -0,0 +1,18 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +public interface Converter { + + T convert(Object value); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/DeviceMetadata.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/DeviceMetadata.java new file mode 100644 index 0000000..0ba0247 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/DeviceMetadata.java @@ -0,0 +1,353 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import cn.hutool.json.JSONObject; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.Setter; + +/** + * @since 1.0.0 + */ +public class DeviceMetadata implements AbstractDeviceMetadata { + + @JsonIgnore private JSONObject jsonObject; + + private volatile Map properties; + + private volatile Map functions; + + private volatile Map events; + + private volatile Map tags; + + @Getter @Setter private String id; + + @Getter @Setter private String name; + + @Getter @Setter private String description; + + @Setter private Map expands; + + /** 默认构造函数,用于Jackson反序列化 */ + public DeviceMetadata() { + // 初始化空对象,用于Jackson反序列化 + } + + public DeviceMetadata(JSONObject jsonObject) { + this.jsonObject = jsonObject; + getProperties(); + getEvents(); + getFunctions(); + getTags(); + } + + public DeviceMetadata(AbstractDeviceMetadata another) { + this.id = another.getId(); + this.name = another.getName(); + this.description = another.getDescription(); + this.expands = another.getExpands(); + this.properties = + another.getProperties().stream() + .map(PropertyMetadata::new) + .filter( + property -> + property.getId() != null && !property.getId().trim().isEmpty()) // 过滤掉id为null的项 + .collect( + Collectors.toMap( + PropertyMetadata::getId, Function.identity(), (a, b) -> a, LinkedHashMap::new)); + + this.functions = + another.getFunctions().stream() + .map(FunctionMetadata::new) + .filter( + function -> + function.getId() != null && !function.getId().trim().isEmpty()) // 过滤掉id为null的项 + .collect( + Collectors.toMap( + FunctionMetadata::getId, Function.identity(), (a, b) -> a, LinkedHashMap::new)); + + this.events = + another.getEvents().stream() + .map(EventMetadata::new) + .filter( + event -> event.getId() != null && !event.getId().trim().isEmpty()) // 过滤掉id为null的项 + .collect( + Collectors.toMap( + EventMetadata::getId, Function.identity(), (a, b) -> a, LinkedHashMap::new)); + } + + // Jackson序列化用的getter方法 + @JsonProperty("properties") + public Map getPropertiesMap() { + if (properties == null) { + getProperties(); // 初始化properties + } + return properties; + } + + // Jackson反序列化用的setter方法 + @JsonProperty("properties") + public void setPropertiesMap(Map properties) { + this.properties = properties; + } + + @Override + public List getProperties() { + if (properties == null && jsonObject != null) { + properties = + Optional.ofNullable(jsonObject.getJSONArray("properties")) + .map(Collection::stream) + .>map( + stream -> + stream + .map(JSONObject.class::cast) + .map(PropertyMetadata::new) + .map(AbstractPropertyMetadata.class::cast) + .filter( + property -> + property.getId() != null + && !property.getId().trim().isEmpty()) // 过滤掉id为null的项 + .collect( + Collectors.toMap( + AbstractPropertyMetadata::getId, + Function.identity(), + (a, b) -> a, + LinkedHashMap::new))) + .orElse(Collections.emptyMap()); + } + if (properties == null) { + this.properties = new HashMap<>(); + } + return new ArrayList<>(properties.values()); + } + + // Jackson序列化用的getter方法 + @JsonProperty("functions") + public Map getFunctionsMap() { + if (functions == null) { + getFunctions(); // 初始化functions + } + return functions; + } + + // Jackson反序列化用的setter方法 + @JsonProperty("functions") + public void setFunctionsMap(Map functions) { + this.functions = functions; + } + + @Override + public List getFunctions() { + if (functions == null && jsonObject != null) { + functions = + Optional.ofNullable(jsonObject.getJSONArray("functions")) + .map(Collection::stream) + .>map( + stream -> + stream + .map(JSONObject.class::cast) + .map(FunctionMetadata::new) + .map(AbstractFunctionMetadata.class::cast) + .filter( + function -> + function.getId() != null + && !function.getId().trim().isEmpty()) // 过滤掉id为null的项 + .collect( + Collectors.toMap( + AbstractFunctionMetadata::getId, + Function.identity(), + (a, b) -> a, + LinkedHashMap::new))) + .orElse(Collections.emptyMap()); + } + if (functions == null) { + this.functions = new HashMap<>(); + } + return new ArrayList<>(functions.values()); + } + + // Jackson序列化用的getter方法 + @JsonProperty("tags") + public Map getTagsMap() { + if (tags == null) { + getTags(); // 初始化tags + } + return tags; + } + + // Jackson反序列化用的setter方法 + @JsonProperty("tags") + public void setTagsMap(Map tags) { + this.tags = tags; + } + + @Override + public List getTags() { + if (tags == null && jsonObject != null) { + tags = + Optional.ofNullable(jsonObject.getJSONArray("tags")) + .map(Collection::stream) + .>map( + stream -> + stream + .map(JSONObject.class::cast) + .map(PropertyMetadata::new) + .map(AbstractPropertyMetadata.class::cast) + .filter( + tag -> + tag.getId() != null + && !tag.getId().trim().isEmpty()) // 过滤掉id为null的项 + .collect( + Collectors.toMap( + AbstractPropertyMetadata::getId, + Function.identity(), + (a, b) -> a, + LinkedHashMap::new))) + .orElse(Collections.emptyMap()); + } + if (tags == null) { + this.tags = new HashMap<>(); + } + return new ArrayList<>(tags.values()); + } + + // Jackson序列化用的getter方法 + @JsonProperty("events") + public Map getEventsMap() { + if (events == null) { + getEvents(); // 初始化events + } + return events; + } + + // Jackson反序列化用的setter方法 + @JsonProperty("events") + public void setEventsMap(Map events) { + this.events = events; + } + + @Override + public List getEvents() { + if (events == null && jsonObject != null) { + events = + Optional.ofNullable(jsonObject.getJSONArray("events")) + .map(Collection::stream) + .>map( + stream -> + stream + .map(JSONObject.class::cast) + .map(EventMetadata::new) + .map(AbstractEventMetadata.class::cast) + .filter( + event -> + event.getId() != null + && !event.getId().trim().isEmpty()) // 过滤掉id为null的项 + .collect( + Collectors.toMap( + AbstractEventMetadata::getId, + Function.identity(), + (a, b) -> a, + LinkedHashMap::new))) + .orElse(Collections.emptyMap()); + } + if (events == null) { + this.events = new HashMap<>(); + } + return new ArrayList<>(events.values()); + } + + @Override + public AbstractEventMetadata getEventOrNull(String id) { + if (events == null) { + getEvents(); + } + return events.get(id); + } + + @Override + public AbstractPropertyMetadata getPropertyOrNull(String id) { + if (properties == null) { + getProperties(); + } + return properties.get(id); + } + + @Override + public AbstractFunctionMetadata getFunctionOrNull(String id) { + if (functions == null) { + getFunctions(); + } + return functions.get(id); + } + + @Override + public AbstractPropertyMetadata getTagOrNull(String id) { + if (tags == null) { + getTags(); + } + return tags.get(id); + } + + public Map getExpands() { + if (this.expands == null) { + if (jsonObject != null) { + this.expands = jsonObject.getJSONObject("expands"); + } + } + return this.expands; + } + + @Override + @JsonIgnore + public JSONObject toJson() { + JSONObject json = new JSONObject(); + json.put("id", id); + json.put("name", name); + json.put("description", description); + json.put( + "properties", getProperties().stream().map(Jsonable::toJson).collect(Collectors.toList())); + json.put( + "functions", getFunctions().stream().map(Jsonable::toJson).collect(Collectors.toList())); + json.put("events", getEvents().stream().map(Jsonable::toJson).collect(Collectors.toList())); + json.put("expands", expands); + return json; + } + + @Override + @JsonIgnore + public void fromJson(JSONObject json) { + this.jsonObject = json; + this.properties = null; + this.events = null; + this.functions = null; + if (json != null) { + this.id = json.getStr("id"); + this.name = json.getStr("name"); + this.description = json.getStr("description"); + this.expands = json.getJSONObject("expands"); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/EventMetadata.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/EventMetadata.java new file mode 100644 index 0000000..57b7327 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/EventMetadata.java @@ -0,0 +1,112 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.types.UnknownType; +import cn.universal.core.metadata.types.ValueTypes; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import lombok.Getter; +import lombok.Setter; + +/** + * @since 1.0.0 + */ +public class EventMetadata implements AbstractEventMetadata { + + @JsonIgnore private JSONObject jsonObject; + + @JsonIgnore private ValueType valueType; + + @Getter @Setter private String id; + + @Getter @Setter private String name; + + @Getter @Setter private String description; + + @Getter @Setter private Map expands; + + /** 默认构造函数,用于Jackson反序列化 */ + public EventMetadata() { + // 初始化空对象,用于Jackson反序列化 + } + + public EventMetadata(JSONObject jsonObject) { + fromJson(jsonObject); + } + + public EventMetadata(AbstractEventMetadata another) { + this.id = another.getId(); + this.name = another.getName(); + this.description = another.getDescription(); + this.expands = another.getExpands(); + this.valueType = another.getValueType(); + this.expands = another.getExpands() == null ? null : new HashMap<>(another.getExpands()); + } + + @Override + public ValueType getValueType() { + if (valueType == null) { + if (jsonObject != null) { + JSONObject typeJson = jsonObject.getJSONObject("valueType"); + + if (typeJson == null) { + // 如果valueType为空,返回UnknownType + valueType = new UnknownType(); + } else { + valueType = + Optional.ofNullable(typeJson.getStr("type")) + .map(ValueTypes::lookup) + .map(Supplier::get) + .orElseGet(UnknownType::new); + + valueType = ValueTypeCodecs.decode(valueType, typeJson); + } + } else { + // 如果jsonObject为空,返回UnknownType + valueType = new UnknownType(); + } + } + // if (valueType == null && another != null) { + // type = another.getType(); + // } + return valueType; + } + + @Override + public JSONObject toJson() { + JSONObject jsonObject = new JSONObject(); + jsonObject.set("id", id); + jsonObject.set("name", name); + jsonObject.set("description", description); + jsonObject.set("valueType", ValueTypeCodecs.encode(getValueType()).orElse(null)); + jsonObject.set("expands", expands); + return jsonObject; + } + + @Override + public void fromJson(JSONObject json) { + if (json != null) { + this.jsonObject = json; + this.valueType = null; + this.id = json.getStr("id"); + this.name = json.getStr("name"); + this.description = json.getStr("description"); + this.expands = json.getJSONObject("expands"); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/FormatSupport.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/FormatSupport.java new file mode 100644 index 0000000..20338ac --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/FormatSupport.java @@ -0,0 +1,18 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +public interface FormatSupport { + + Object format(Object value); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/FunctionMetadata.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/FunctionMetadata.java new file mode 100644 index 0000000..1e106df --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/FunctionMetadata.java @@ -0,0 +1,160 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.types.UnknownType; +import cn.universal.core.metadata.types.ValueTypes; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.Setter; + +/** + * @since 1.0.0 + */ +public class FunctionMetadata implements AbstractFunctionMetadata { + + @JsonIgnore private transient JSONObject jsonObject; + + @JsonIgnore private transient AbstractFunctionMetadata another; + + @Setter private List inputs; + + @Setter private ValueType output; + + @Getter @Setter private String id; + + @Getter @Setter private String name; + + @Getter @Setter private String description; + + @Getter @Setter private boolean async; + + @Getter @Setter private Map expands; + + public FunctionMetadata() {} + + public FunctionMetadata(JSONObject jsonObject) { + fromJson(jsonObject); + } + + public FunctionMetadata(AbstractFunctionMetadata another) { + this.id = another.getId(); + this.name = another.getName(); + this.description = another.getDescription(); + this.expands = another.getExpands() == null ? null : new HashMap<>(another.getExpands()); + this.another = another; + this.async = another.isAsync(); + } + + @Override + public List getInputs() { + if (inputs == null) { + if (jsonObject != null) { + inputs = + Optional.ofNullable(jsonObject.getJSONArray("inputs")) + .map(Collection::stream) + .map( + stream -> + stream + .map(JSONObject.class::cast) + .map(PropertyMetadata::new) + .map(AbstractPropertyMetadata.class::cast) + .collect(Collectors.toList())) + .orElse(Collections.emptyList()); + } else if (another != null) { + inputs = + another.getInputs().stream().map(PropertyMetadata::new).collect(Collectors.toList()); + } else { + inputs = Collections.emptyList(); + } + } + return inputs; + } + + @Override + public ValueType getOutput() { + if (output == null) { + if (jsonObject != null) { + output = + Optional.ofNullable(jsonObject.getJSONObject("output")) + .flatMap( + conf -> + Optional.ofNullable(ValueTypes.lookup(conf.getStr("type"))) + .map(Supplier::get) + .map(type -> ValueTypeCodecs.decode(type, conf))) + .orElseGet(UnknownType::new); + } else if (another != null) { + output = another.getOutput(); + } else { + output = new UnknownType(); + } + } + return output; + } + + @Override + public String toString() { + // /*获取系统信息*/ getSysInfo(Type name,) + + return String.join( + "", + new String[] { + "/* ", + getName(), + " */", + getId(), + "(", + String.join( + ",", + getInputs().stream().map(AbstractPropertyMetadata::toString).toArray(String[]::new)), + ")" + }); + } + + @Override + public JSONObject toJson() { + JSONObject json = new JSONObject(); + json.put("id", id); + json.put("name", name); + json.put("description", description); + json.put("async", async); + json.put("inputs", getInputs().stream().map(Jsonable::toJson).collect(Collectors.toList())); + ValueTypeCodecs.encode(getOutput()).ifPresent(ot -> json.set("output", ot)); + json.put("expands", expands); + + return json; + } + + @Override + public void fromJson(JSONObject json) { + if (json != null) { + this.jsonObject = json; + this.inputs = null; + this.output = null; + this.id = json.getStr("id"); + this.name = json.getStr("name"); + this.description = json.getStr("description", ""); + this.async = json.getBool("async", false); + this.expands = json.getJSONObject("expands"); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/Jsonable.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/Jsonable.java new file mode 100644 index 0000000..554a6f1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/Jsonable.java @@ -0,0 +1,25 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; + +public interface Jsonable { + + default JSONObject toJson() { + return JSONUtil.parseObj(this); + } + + default void fromJson(JSONObject json) {} +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/Metadata.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/Metadata.java new file mode 100644 index 0000000..e9e3e40 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/Metadata.java @@ -0,0 +1,43 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import java.util.Map; +import java.util.Optional; + +/** + * 元数据 + * + * @version 1.0 @Author Aleo + * @since 2025/8/25 + */ +public interface Metadata { + + String getId(); + + String getName(); + + String getDescription(); + + Map getExpands(); + + default Optional getExpand(String name) { + return Optional.ofNullable(getExpands()).map(map -> map.get(name)); + } + + default void setExpands(Map expands) {} + + default void setName(String name) {} + + default void setDescription(String description) {} +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/PropertyMetadata.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/PropertyMetadata.java new file mode 100644 index 0000000..27a9b25 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/PropertyMetadata.java @@ -0,0 +1,133 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.types.UnknownType; +import cn.universal.core.metadata.types.ValueTypes; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import lombok.Getter; +import lombok.Setter; + +/** + * @since 1.0.0 + */ +public class PropertyMetadata implements AbstractPropertyMetadata { + + @JsonIgnore private JSONObject json; + // + @Setter private transient ValueType valueType; + + // private ValueType valueType; + + @Getter @Setter private String id; + + @Getter @Setter private String name; + + @Getter @Setter private String description; + + @Setter private String mode; + + @Getter @Setter private Map expands; + + public PropertyMetadata() {} + + public PropertyMetadata(JSONObject json) { + fromJson(json); + } + + public PropertyMetadata(AbstractPropertyMetadata another) { + this.id = another.getId(); + this.name = another.getName(); + this.description = another.getDescription(); + this.valueType = another.getValueType(); + this.expands = another.getExpands() == null ? null : new HashMap<>(another.getExpands()); + } + + protected Optional> getDataTypeCodec(ValueType dataType) { + + return ValueTypeCodecs.getCodec(dataType.getId()); + } + + protected ValueType parseDataType() { + // System.out.println(111111); + JSONObject dataTypeJson = json.getJSONObject("valueType"); + if (dataTypeJson == null) { + // 如果valueType为空,返回UnknownType而不是抛出异常 + return new UnknownType(); + } + ValueType dataType = + Optional.ofNullable(dataTypeJson.getStr("type")) + .map(ValueTypes::lookup) + .map(Supplier::get) + .orElseGet(UnknownType::new); + // System.out.println("dataType-"+dataType.getId()+dataType.getType()); + getDataTypeCodec(dataType).ifPresent(codec -> codec.decode(dataType, dataTypeJson)); + // System.out.println("dataType--"+dataType); + return dataType; + } + + @Override + public ValueType getValueType() { + if (valueType == null) { + if (json != null) { + valueType = parseDataType(); + } else { + // 如果json为空,返回UnknownType + valueType = new UnknownType(); + } + } + return valueType; + } + + public String getMode() { + if (StrUtil.isBlank(mode)) { + mode = PropertyMode.r.name(); + } + return mode; + } + + @Override + public JSONObject toJson() { + JSONObject json = new JSONObject(); + json.set("id", id); + json.set("name", name); + json.set("description", description); + json.set("valueType", ValueTypeCodecs.encode(getValueType()).orElse(null)); + json.set("expands", expands); + return json; + } + + @Override + public void fromJson(JSONObject jsonObject) { + if (jsonObject != null) { + this.json = jsonObject; + this.id = json.getStr("id"); + this.name = json.getStr("name"); + this.description = json.getStr("description"); + this.valueType = null; + this.expands = json.getJSONObject("expands"); + } + } + + @Override + public String toString() { + // /* 测试 */ int name, + return String.join("", getValueType().getId(), " ", getId(), " /* ", getName(), " */ "); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/PropertyMode.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/PropertyMode.java new file mode 100644 index 0000000..4590c3e --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/PropertyMode.java @@ -0,0 +1,24 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +/** + * @version 1.0 @Author Aleo + * @since 2025/11/12 + */ +public enum PropertyMode { + /** 读写 */ + rw, + /** 只读 */ + r +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/UnitSupported.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/UnitSupported.java new file mode 100644 index 0000000..4b716b1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/UnitSupported.java @@ -0,0 +1,22 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import cn.universal.core.metadata.unit.ValueUnit; + +public interface UnitSupported { + + ValueUnit getUnit(); + + void setUnit(ValueUnit unit); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/ValidateResult.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/ValidateResult.java new file mode 100644 index 0000000..008368e --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/ValidateResult.java @@ -0,0 +1,71 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import java.util.function.Consumer; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 参数合法性校验 + * + * @version 1.0 @Author Aleo + * @since 2025/8/25 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ValidateResult { + + private boolean success; + + private Object value; + + private String errorMsg; + + public static ValidateResult success(Object value) { + ValidateResult result = new ValidateResult(); + result.setSuccess(true); + result.setValue(value); + return result; + } + + public static ValidateResult success() { + ValidateResult result = new ValidateResult(); + result.setSuccess(true); + return result; + } + + public static ValidateResult fail(String message) { + ValidateResult result = new ValidateResult(); + result.setSuccess(false); + result.setErrorMsg(message); + return result; + } + + public Object assertSuccess() { + if (!success) { + throw new IllegalArgumentException(errorMsg); + } + return value; + } + + public void ifFail(Consumer resultConsumer) { + if (!success) { + resultConsumer.accept(this); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/ValueType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/ValueType.java new file mode 100644 index 0000000..6ba15e4 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/ValueType.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import java.util.Map; + +/** + * 物模型值数据 + * + * @version 1.0 @Author Aleo + * @since 2025/8/25 + */ +public interface ValueType extends Metadata, FormatSupport { + + /** + * 验证是否合法 + * + * @param value 值 + * @return ValidateResult + */ + ValidateResult validate(Object value); + + /** + * @return 类型标识 + */ + default String getType() { + return getId(); + } + + /** + * @return 拓展属性 + */ + @Override + default Map getExpands() { + return null; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/ValueTypeCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/ValueTypeCodec.java new file mode 100644 index 0000000..a62db9a --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/ValueTypeCodec.java @@ -0,0 +1,24 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import java.util.Map; + +public interface ValueTypeCodec { + + String getTypeId(); + + T decode(T type, Map config); + + Map encode(T type); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/ValueTypeCodecs.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/ValueTypeCodecs.java new file mode 100644 index 0000000..90bef94 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/ValueTypeCodecs.java @@ -0,0 +1,77 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata; + +import cn.universal.core.metadata.types.codec.ArrayCodec; +import cn.universal.core.metadata.types.codec.BooleanCodec; +import cn.universal.core.metadata.types.codec.DateCodec; +import cn.universal.core.metadata.types.codec.DoubleCodec; +import cn.universal.core.metadata.types.codec.EnumCodec; +import cn.universal.core.metadata.types.codec.FileCodec; +import cn.universal.core.metadata.types.codec.FloatCodec; +import cn.universal.core.metadata.types.codec.GeoPointCodec; +import cn.universal.core.metadata.types.codec.GeoShapeCodec; +import cn.universal.core.metadata.types.codec.IntCodec; +import cn.universal.core.metadata.types.codec.LongCodec; +import cn.universal.core.metadata.types.codec.ObjectCodec; +import cn.universal.core.metadata.types.codec.PasswordCodec; +import cn.universal.core.metadata.types.codec.StringCodec; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class ValueTypeCodecs { + + private static final Map> codecMap = new HashMap<>(); + + static { + register(new ArrayCodec()); + register(new BooleanCodec()); + register(new DateCodec()); + register(new DoubleCodec()); + register(new EnumCodec()); + register(new FloatCodec()); + register(new GeoPointCodec()); + register(new IntCodec()); + register(new LongCodec()); + register(new ObjectCodec()); + register(new StringCodec()); + register(new PasswordCodec()); + register(new FileCodec()); + register(new GeoShapeCodec()); + } + + public static void register(ValueTypeCodec codec) { + codecMap.put(codec.getTypeId(), codec); + } + + @SuppressWarnings("all") + public static Optional> getCodec(String typeId) { + + return Optional.ofNullable((ValueTypeCodec) codecMap.get(typeId)); + } + + public static ValueType decode(ValueType type, Map config) { + if (type == null) { + return null; + } + return getCodec(type.getId()).map(codec -> codec.decode(type, config)).orElse(type); + } + + public static Optional> encode(ValueType type) { + if (type == null) { + return Optional.empty(); + } + return getCodec(type.getId()).map(codec -> codec.encode(type)); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/AbstractType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/AbstractType.java new file mode 100644 index 0000000..9c16f69 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/AbstractType.java @@ -0,0 +1,58 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.universal.core.metadata.ValueType; +import java.util.HashMap; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; +import org.springframework.util.CollectionUtils; + +@Getter +@Setter +@SuppressWarnings("all") +public abstract class AbstractType implements ValueType { + + private Map expands; + + private String description; + + public R expands(Map expands) { + if (CollectionUtils.isEmpty(expands)) { + return (R) this; + } + if (this.expands == null) { + this.expands = new HashMap<>(); + } + this.expands.putAll(expands); + return (R) this; + } + + public R expand(String configKey, Object value) { + + if (value == null) { + return (R) this; + } + if (expands == null) { + expands = new HashMap<>(); + } + expands.put(configKey, value); + return (R) this; + } + + public R description(String description) { + this.description = description; + return (R) this; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/ArrayType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/ArrayType.java new file mode 100644 index 0000000..5123cfb --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/ArrayType.java @@ -0,0 +1,97 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONUtil; +import cn.universal.core.metadata.Converter; +import cn.universal.core.metadata.ValidateResult; +import cn.universal.core.metadata.ValueType; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ArrayType extends AbstractType + implements ValueType, Converter> { + + public static final String ID = "array"; + + private ValueType elementType; + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "数组"; + } + + public ArrayType elementType(ValueType elementType) { + this.elementType = elementType; + return this; + } + + @Override + public ValidateResult validate(Object value) { + + List listValue = convert(value); + if (elementType != null && value instanceof Collection) { + for (Object data : listValue) { + ValidateResult result = elementType.validate(data); + if (!result.isSuccess()) { + return result; + } + } + } + return ValidateResult.success(listValue); + } + + @Override + public Object format(Object value) { + + if (elementType != null && value instanceof Collection) { + Collection collection = ((Collection) value); + return new JSONArray( + collection.stream().map(data -> elementType.format(data)).collect(Collectors.toList())); + } + + return JSONUtil.toJsonStr(value); + } + + @Override + public List convert(Object value) { + if (value instanceof Collection) { + return ((Collection) value) + .stream() + .map( + val -> { + if (elementType instanceof Converter) { + return ((Converter) elementType).convert(val); + } + return val; + }) + .collect(Collectors.toList()); + } + if (value instanceof String) { + return JSONUtil.parseArray(String.valueOf(value)); + } + return Collections.singletonList(value); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/BooleanType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/BooleanType.java new file mode 100644 index 0000000..5de9756 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/BooleanType.java @@ -0,0 +1,113 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.universal.core.metadata.Converter; +import cn.universal.core.metadata.FormatSupport; +import cn.universal.core.metadata.ValidateResult; +import cn.universal.core.metadata.ValueType; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class BooleanType extends AbstractType + implements ValueType, FormatSupport, Converter { + + public static final String ID = "boolean"; + + public static final BooleanType GLOBAL = new BooleanType(); + + private String trueText = "是"; + + private String falseText = "否"; + + private String trueValue = "true"; + + private String falseValue = "false"; + + public BooleanType trueText(String trueText) { + this.trueText = trueText; + return this; + } + + public BooleanType falseText(String falseText) { + this.falseText = falseText; + return this; + } + + public BooleanType trueValue(String trueValue) { + this.trueValue = trueValue; + return this; + } + + public BooleanType falseValue(String falseValue) { + this.falseText = falseValue; + return this; + } + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "布尔值"; + } + + public Boolean convert(Object value) { + if (value instanceof Boolean) { + return ((Boolean) value); + } + + String stringVal = String.valueOf(value).trim(); + if (stringVal.equals(trueValue) || stringVal.equals(trueText)) { + return true; + } + + if (stringVal.equals(falseValue) || stringVal.equals(falseText)) { + return false; + } + return stringVal.equals("1") + || stringVal.equals("true") + || stringVal.equals("y") + || stringVal.equals("yes") + || stringVal.equals("ok") + || stringVal.equals("是") + || stringVal.equals("正常"); + } + + @Override + public ValidateResult validate(Object value) { + + Boolean trueOrFalse = convert(value); + + return trueOrFalse == null + ? ValidateResult.fail("不支持的值:" + value) + : ValidateResult.success(trueOrFalse); + } + + @Override + public String format(Object value) { + Boolean trueOrFalse = convert(value); + + if (Boolean.TRUE.equals(trueOrFalse)) { + return trueText; + } + if (Boolean.FALSE.equals(trueOrFalse)) { + return falseText; + } + return "未知:" + value; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/DateTimeType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/DateTimeType.java new file mode 100644 index 0000000..30af5c7 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/DateTimeType.java @@ -0,0 +1,136 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.universal.core.metadata.Converter; +import cn.universal.core.metadata.ValidateResult; +import cn.universal.core.metadata.ValueType; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Getter +@Setter +@Slf4j +public class DateTimeType extends AbstractType implements ValueType, Converter { + + public static final String ID = "date"; + + public static final String TIMESTAMP_FORMAT = "timestamp"; + + public static final DateTimeType GLOBAL = new DateTimeType(); + + private String format = TIMESTAMP_FORMAT; + + private ZoneId zoneId = ZoneId.systemDefault(); + + private DateTimeFormatter formatter; + + static { + // DateFormatter.supportFormatter.add(new ISODateTimeFormatter()); + } + + public DateTimeType timeZone(ZoneId zoneId) { + this.zoneId = zoneId; + + return this; + } + + public DateTimeType format(String format) { + this.format = format; + this.getFormatter(); + return this; + } + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "时间"; + } + + protected DateTimeFormatter getFormatter() { + if (formatter == null && !TIMESTAMP_FORMAT.equals(format)) { + formatter = DateTimeFormatter.ofPattern(format); + } + return formatter; + } + + @Override + public ValidateResult validate(Object value) { + if ((value = convert(value)) == null) { + return ValidateResult.fail("不是合法的时间格式"); + } + return ValidateResult.success(value); + } + + @Override + public String format(Object value) { + try { + if (TIMESTAMP_FORMAT.equals(format)) { + return String.valueOf(convert(value).getTime()); + } + Date dateValue = convert(value); + if (dateValue == null) { + return ""; + } + return LocalDateTime.ofInstant(dateValue.toInstant(), zoneId).format(getFormatter()); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + return ""; + } + + public Date convert(Object value) { + + if (value instanceof Instant) { + return Date.from(((Instant) value)); + } + if (value instanceof LocalDateTime) { + return Date.from(((LocalDateTime) value).atZone(zoneId).toInstant()); + } + + if (value instanceof Date) { + return ((Date) value); + } + if (value instanceof Number) { + return new Date(((Number) value).longValue()); + } + if (value instanceof String) { + // if (NumberUtil.isLong(value)) { + // return new Date(Long.parseLong((String) value)); + // } + // Date data = LocalDateTime.parse((String) value, DateTimeFormatter.ISO_DATE_TIME) + // .atZone(ZoneId.systemDefault()) + // .toInstant(); + // if (data != null) { + // return data; + // } + // DateTimeFormatter formatter = getFormatter(); + // if (null == formatter) { + // throw new IllegalArgumentException("unsupported date format:" + value); + // } + return Date.from(LocalDateTime.parse(((String) value), formatter).atZone(zoneId).toInstant()); + } + throw new IllegalArgumentException("can not format datetime :" + value); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/DoubleType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/DoubleType.java new file mode 100644 index 0000000..4370097 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/DoubleType.java @@ -0,0 +1,62 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import static java.math.BigDecimal.ROUND_HALF_UP; + +import java.math.BigDecimal; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@SuppressWarnings("all") +public class DoubleType extends NumberType { + + public static final String ID = "double"; + + private Integer scale; + + public static final DoubleType GLOBAL = new DoubleType(); + + @Override + public Object format(Object value) { + Number val = convertNumber(value); + if (val == null) { + return super.format(value); + } + int scale = this.scale == null ? 2 : this.scale; + String scaled = new BigDecimal(val.toString()).setScale(scale, ROUND_HALF_UP).toString(); + return super.format(scaled); + } + + public DoubleType scale(Integer scale) { + this.scale = scale; + return this; + } + + @Override + public Double convert(Object value) { + return super.convertNumber(value, Number::doubleValue); + } + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "双精度浮点数"; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/EnumType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/EnumType.java new file mode 100644 index 0000000..010ae30 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/EnumType.java @@ -0,0 +1,127 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.universal.core.metadata.ValidateResult; +import cn.universal.core.metadata.ValueType; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +public class EnumType extends AbstractType implements ValueType { + + public static final String ID = "enum"; + + private volatile List elements; + + private boolean multi; + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "枚举"; + } + + public EnumType multi(boolean multi) { + this.multi = multi; + return this; + } + + @Override + public ValidateResult validate(Object value) { + if (elements == null) { + return ValidateResult.fail("值[" + value + "]不在枚举中"); + } + return elements.stream() + .filter(ele -> match(value, ele)) + .findFirst() + .map(e -> ValidateResult.success(e.value)) + .orElseGet(() -> ValidateResult.fail("值[" + value + "]不在枚举中")); + } + + private boolean match(Object value, Element ele) { + if (value instanceof Map) { + // 适配map情况下的枚举信息 + @SuppressWarnings("all") + Map mapVal = ((Map) value); + return match(mapVal.getOrDefault("value", mapVal.get("id")), ele); + } + return ele.value.equals(String.valueOf(value)) || ele.text.equals(String.valueOf(value)); + } + + @Override + public String format(Object value) { + String stringVal = String.valueOf(value); + if (elements == null) { + return stringVal; + } + return elements.stream() + .filter(ele -> ele.value.equals(String.valueOf(value))) + .findFirst() + .map(Element::getText) + .orElse(stringVal); + } + + public EnumType addElement(Element element) { + if (elements == null) { + synchronized (this) { + if (elements == null) { + elements = new ArrayList<>(); + } + } + } + elements.add(element); + return this; + } + + @Getter + @Setter + @AllArgsConstructor(staticName = "of") + @NoArgsConstructor + public static class Element { + + private String value; + + private String text; + + private String description; + + public static Element of(String value, String text) { + return of(value, text, null); + } + + public static Element of(Map map) { + return Element.of(map.get("value"), map.get("text"), map.get("description")); + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("value", value); + map.put("text", text); + map.put("description", description); + + return map; + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/FileType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/FileType.java new file mode 100644 index 0000000..0c55282 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/FileType.java @@ -0,0 +1,77 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.universal.core.metadata.Converter; +import cn.universal.core.metadata.ValidateResult; +import cn.universal.core.metadata.ValueType; +import java.util.Optional; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class FileType extends AbstractType implements ValueType, Converter { + + public static final String ID = "file"; + + private BodyType bodyType = BodyType.url; + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "文件"; + } + + public FileType bodyType(BodyType type) { + this.bodyType = type; + return this; + } + + @Override + public ValidateResult validate(Object value) { + return ValidateResult.success(String.valueOf(value)); + } + + @Override + public String format(Object value) { + return String.valueOf(value); + } + + @Override + public String convert(Object value) { + return value == null ? null : String.valueOf(value); + } + + public enum BodyType { + url, + base64, + binary; + + public static Optional of(String name) { + if (name == null) { + return Optional.empty(); + } + for (BodyType value : values()) { + if (value.name().equalsIgnoreCase(name)) { + return Optional.of(value); + } + } + return Optional.empty(); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/FloatType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/FloatType.java new file mode 100644 index 0000000..bbb75a6 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/FloatType.java @@ -0,0 +1,61 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import static java.math.BigDecimal.ROUND_HALF_UP; + +import java.math.BigDecimal; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@SuppressWarnings("all") +public class FloatType extends NumberType { + + public static final String ID = "float"; + + private Integer scale; + public static final FloatType GLOBAL = new FloatType(); + + @Override + public Object format(Object value) { + Number val = convertNumber(value); + if (val == null) { + return super.format(value); + } + int scale = this.scale == null ? 2 : this.scale; + String scaled = new BigDecimal(val.toString()).setScale(scale, ROUND_HALF_UP).toString(); + return super.format(scaled); + } + + public FloatType scale(Integer scale) { + this.scale = scale; + return this; + } + + @Override + public Float convert(Object value) { + return super.convertNumber(value, Number::floatValue); + } + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "单精度浮点数"; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/GeoPoint.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/GeoPoint.java new file mode 100644 index 0000000..3658297 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/GeoPoint.java @@ -0,0 +1,121 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.hutool.json.JSONUtil; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.lang.Nullable; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class GeoPoint implements Serializable { + + private static final long serialVersionUID = -6849794470754667710L; + + // 经度 + private double lon; + + // 纬度 + private double lat; + + public static GeoPoint of(Object val) { + if (val == null) { + return null; + } + Object tmp = val; + if (val instanceof GeoPoint) { + return ((GeoPoint) val); + } + + if (val instanceof String) { + String strVal = String.valueOf(val); + if (strVal.startsWith("{")) { + // {"lon":"lon","lat":lat} + val = JSONUtil.parseObj(strVal); + } else if (strVal.startsWith("[")) { + // [lon,lat] + val = JSONUtil.parseArray(strVal); + } else { + // lon,lat + val = strVal.split("[,]"); + } + } + // {"lat":lat,"lon":lon} or {"x":lon,"y":lat} + if (val instanceof Map) { + @SuppressWarnings("unchecked") + Map mapVal = ((Map) val); + Object lon = mapVal.getOrDefault("lon", mapVal.get("x")); + Object lat = mapVal.getOrDefault("lat", mapVal.get("y")); + val = new Object[] {lon, lat}; + } + // [lon,lat] + if (val instanceof Collection) { + val = ((Collection) val).toArray(); + } + // [lon,lat] + if (val instanceof Object[]) { + Object[] arr = ((Object[]) val); + if (arr.length >= 2) { + return new GeoPoint( + new BigDecimal(String.valueOf(arr[0])).doubleValue(), + new BigDecimal(String.valueOf(arr[1])).doubleValue()); + } + } + throw new IllegalArgumentException("unsupported geo format:" + tmp); + } + + @Override + public int hashCode() { + + int result = 1; + + long temp = Double.doubleToLongBits(lat); + result = 31 * result + (int) (temp ^ temp >>> 32); + + temp = Double.doubleToLongBits(lon); + result = 31 * result + (int) (temp ^ temp >>> 32); + + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof GeoPoint)) { + return false; + } + + GeoPoint other = (GeoPoint) obj; + + return Double.doubleToLongBits(lon) == Double.doubleToLongBits(other.lon) + && Double.doubleToLongBits(lat) == Double.doubleToLongBits(other.lat); + } + + @Override + public String toString() { + return lon + "," + lat; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/GeoShape.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/GeoShape.java new file mode 100644 index 0000000..669116c --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/GeoShape.java @@ -0,0 +1,102 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.hutool.json.JSONUtil; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +/** + * 地理地形 + * + * @since 1.1 + */ +@Getter +@Setter +public class GeoShape implements Serializable { + + // 类型 + private Type type; + + // 坐标,类型不同,坐标维度不同 + private List coordinates; + + public enum Type { + Point, // 点 + MultiPoint, // 多个点 + LineString, // 线 + MultiLineString, // 多条线 + Polygon, // 多边形 + MultiPolygon, // 多个多边形 + GeometryCollection // 数据集合,包含点线 + ; + + public static Type of(Object val) { + for (Type value : values()) { + if (value.name().equalsIgnoreCase(String.valueOf(val))) { + return value; + } + } + throw new IllegalArgumentException("unsupported GeoShape type:" + val); + } + + public List parseCoordinates(Object coordinates) { + if (coordinates instanceof Collection) { + return new ArrayList<>(((Collection) coordinates)); + } + if (coordinates instanceof String) { + if (((String) coordinates).startsWith("[")) { + return JSONUtil.parseArray(String.valueOf(coordinates)); + } + return new ArrayList<>(Arrays.asList(((String) coordinates).split(","))); + } + throw new IllegalArgumentException("unsupported coordinates type :" + coordinates); + } + } + + public static GeoShape of(Object value) { + if (value instanceof GeoShape) { + return ((GeoShape) value); + } + if (value instanceof GeoPoint) { + return GeoShape.fromPoint(((GeoPoint) value)); + } + if (value instanceof String && ((String) value).startsWith("{")) { + value = JSONUtil.parseObj(String.valueOf(value)); + } + if (value instanceof Map) { + return GeoShape.of(((Map) value)); + } + throw new IllegalArgumentException("unsupported GeoShape:" + value); + } + + public static GeoShape of(Map map) { + GeoShape shape = new GeoShape(); + shape.type = Type.of(map.get("type")); + shape.coordinates = shape.type.parseCoordinates(map.get("coordinates")); + return shape; + } + + public static GeoShape fromPoint(GeoPoint point) { + GeoShape shape = new GeoShape(); + shape.type = Type.Point; + shape.coordinates = new ArrayList<>(Arrays.asList(point.getLon(), point.getLat())); + return shape; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/GeoShapeType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/GeoShapeType.java new file mode 100644 index 0000000..5cf10b9 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/GeoShapeType.java @@ -0,0 +1,57 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.universal.core.metadata.Converter; +import cn.universal.core.metadata.ValidateResult; + +public class GeoShapeType extends AbstractType implements Converter { + + public static final String ID = "geoShape"; + public static final GeoShapeType GLOBAL = new GeoShapeType(); + + @Override + public ValidateResult validate(Object value) { + GeoShape shape; + if (null == (shape = convert(value))) { + return ValidateResult.builder().success(false).errorMsg("不支持的GepShape格式:" + value).build(); + } + return ValidateResult.success(shape); + } + + @Override + public GeoShape convert(Object value) { + + return GeoShape.of(value); + } + + @Override + public String getId() { + return "geoShape"; + } + + @Override + public String getName() { + return "地理地形"; + } + + @Override + public String getDescription() { + return null; + } + + @Override + public Object format(Object value) { + return value; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/GeoType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/GeoType.java new file mode 100644 index 0000000..bdd099f --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/GeoType.java @@ -0,0 +1,90 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.universal.core.metadata.Converter; +import cn.universal.core.metadata.FormatSupport; +import cn.universal.core.metadata.ValidateResult; +import cn.universal.core.metadata.ValueType; +import java.util.HashMap; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class GeoType extends AbstractType + implements ValueType, FormatSupport, Converter { + + public static final String ID = "geoPoint"; + + public static final GeoType GLOBAL = new GeoType(); + + // 经度字段 + private String latProperty = "lat"; + + // 纬度字段 + private String lonProperty = "lon"; + + public GeoType latProperty(String property) { + this.latProperty = property; + return this; + } + + public GeoType lonProperty(String property) { + this.lonProperty = property; + return this; + } + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "地理位置"; + } + + public Map convertToMap(Object value) { + Map mapGeoPoint = new HashMap<>(); + + GeoPoint point = convert(value); + if (point != null) { + mapGeoPoint.put("lat", point.getLat()); + mapGeoPoint.put("lon", point.getLon()); + } + return mapGeoPoint; + } + + public GeoPoint convert(Object value) { + return GeoPoint.of(value); + } + + @Override + public ValidateResult validate(Object value) { + + GeoPoint geoPoint = convert(value); + + return geoPoint == null + ? ValidateResult.fail("不支持的Geo格式:" + value) + : ValidateResult.success(geoPoint); + } + + @Override + public String format(Object value) { + GeoPoint geoPoint = convert(value); + + return String.valueOf(geoPoint); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/IntType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/IntType.java new file mode 100644 index 0000000..ea965dd --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/IntType.java @@ -0,0 +1,40 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class IntType extends NumberType { + + public static final String ID = "int"; + + public static final IntType GLOBAL = new IntType(); + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "整型"; + } + + @Override + public Integer convert(Object value) { + return super.convertNumber(value, Number::intValue); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/LongType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/LongType.java new file mode 100644 index 0000000..d7dcd7c --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/LongType.java @@ -0,0 +1,39 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LongType extends NumberType { + + public static final String ID = "long"; + public static final LongType GLOBAL = new LongType(); + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "长整型"; + } + + @Override + public Long convert(Object value) { + return super.convertNumber(value, Number::longValue); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/NumberType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/NumberType.java new file mode 100644 index 0000000..d5a85e1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/NumberType.java @@ -0,0 +1,124 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.universal.core.metadata.Converter; +import cn.universal.core.metadata.UnitSupported; +import cn.universal.core.metadata.ValidateResult; +import cn.universal.core.metadata.ValueType; +import cn.universal.core.metadata.unit.ValueUnit; +import java.math.BigDecimal; +import java.util.Date; +import java.util.Optional; +import java.util.function.Function; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public abstract class NumberType extends AbstractType> + implements UnitSupported, ValueType, Converter { + + // 最大值 + private Number max; + + // 最小值 + private Number min; + + // 单位 + private ValueUnit unit; + + public NumberType unit(ValueUnit unit) { + this.unit = unit; + return this; + } + + public NumberType max(Number max) { + this.max = max; + return this; + } + + public NumberType min(Number min) { + this.min = min; + return this; + } + + public Object format(Object value) { + if (value == null) { + return null; + } + ValueUnit unit = getUnit(); + if (unit == null) { + return value; + } + return unit.format(value); + } + + @Override + public ValidateResult validate(Object value) { + try { + N numberValue = convert(value); + if (numberValue == null) { + return ValidateResult.fail("数字格式错误:" + value); + } + if (max != null && numberValue.doubleValue() > max.doubleValue()) { + return ValidateResult.fail("超过最大值:" + max); + } + if (min != null && numberValue.doubleValue() < min.doubleValue()) { + return ValidateResult.fail("小于最小值:" + min); + } + return ValidateResult.success(numberValue); + } catch (NumberFormatException e) { + return ValidateResult.fail(e.getMessage()); + } + } + + public N convertNumber(Object value, Function mapper) { + return Optional.ofNullable(convertNumber(value)).map(mapper).orElse(null); + } + + public Number convertNumber(Object value) { + if (value instanceof Number) { + return ((Number) value); + } + if (value instanceof String) { + try { + return new BigDecimal(((String) value)); + } catch (NumberFormatException e) { + return null; + } + } + if (value instanceof Date) { + return ((Date) value).getTime(); + } + return null; + } + + public abstract N convert(Object value); + + public long getMax(long defaultVal) { + return Optional.ofNullable(getMax()).map(Number::longValue).orElse(defaultVal); + } + + public long getMin(long defaultVal) { + return Optional.ofNullable(getMin()).map(Number::longValue).orElse(defaultVal); + } + + public double getMax(double defaultVal) { + return Optional.ofNullable(getMax()).map(Number::doubleValue).orElse(defaultVal); + } + + public double getMin(double defaultVal) { + return Optional.ofNullable(getMin()).map(Number::doubleValue).orElse(defaultVal); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/ObjectType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/ObjectType.java new file mode 100644 index 0000000..c8f9f3e --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/ObjectType.java @@ -0,0 +1,155 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.core.metadata.AbstractPropertyMetadata; +import cn.universal.core.metadata.Converter; +import cn.universal.core.metadata.PropertyMetadata; +import cn.universal.core.metadata.ValidateResult; +import cn.universal.core.metadata.ValueType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; + +@Getter +@Setter +@Slf4j +public class ObjectType extends AbstractType + implements ValueType, Converter> { + + public static final String ID = "object"; + + private List properties; + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "对象类型"; + } + + public ObjectType addPropertyMetadata(AbstractPropertyMetadata property) { + + if (this.properties == null) { + this.properties = new ArrayList<>(); + } + + this.properties.add(property); + + return this; + } + + public List getProperties() { + if (properties == null) { + return Collections.emptyList(); + } + return properties; + } + + public ObjectType addProperty(String property, ValueType type) { + return this.addProperty(property, property, type); + } + + public ObjectType addProperty(String property, String name, ValueType type) { + PropertyMetadata metadata = new PropertyMetadata(); + metadata.setId(property); + metadata.setName(name); + metadata.setValueType(type); + return addPropertyMetadata(metadata); + } + + @Override + public ValidateResult validate(Object value) { + + if (properties == null || properties.isEmpty()) { + return ValidateResult.success(value); + } + Map mapValue = convert(value); + + for (AbstractPropertyMetadata property : properties) { + Object data = mapValue.get(property.getId()); + if (data == null) { + continue; + } + ValidateResult result = property.getValueType().validate(data); + if (!result.isSuccess()) { + return result; + } + } + return ValidateResult.success(mapValue); + } + + @Override + public JSONObject format(Object value) { + return new JSONObject(handle(value, ValueType::format)); + } + + @SuppressWarnings("all") + public Map handle(Object value, BiFunction mapping) { + if (value == null) { + return null; + } + if (value instanceof String && ((String) value).startsWith("{")) { + value = JSONUtil.parseObj(String.valueOf(value)); + } + if (!(value instanceof Map)) { + value = (Map) value; + } + if (value instanceof Map) { + Map mapValue = new HashMap<>(((Map) value)); + if (properties != null) { + for (AbstractPropertyMetadata property : properties) { + Object data = mapValue.get(property.getId()); + ValueType type = property.getValueType(); + if (data != null) { + mapValue.put(property.getId(), mapping.apply(type, data)); + } + } + } + return mapValue; + } + return null; + } + + @Override + public Map convert(Object value) { + return handle( + value, + (type, data) -> { + if (type instanceof Converter) { + return ((Converter) type).convert(data); + } + return data; + }); + } + + public Optional getProperty(String key) { + if (CollectionUtils.isEmpty(properties)) { + return Optional.empty(); + } + return properties.stream().filter(prop -> prop.getId().equals(key)).findAny(); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/PasswordType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/PasswordType.java new file mode 100644 index 0000000..25eff83 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/PasswordType.java @@ -0,0 +1,53 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.universal.core.metadata.Converter; +import cn.universal.core.metadata.ValidateResult; +import cn.universal.core.metadata.ValueType; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class PasswordType extends AbstractType + implements ValueType, Converter { + + public static final String ID = "password"; + public static final PasswordType GLOBAL = new PasswordType(); + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "密码"; + } + + @Override + public ValidateResult validate(Object value) { + return ValidateResult.success(String.valueOf(value)); + } + + @Override + public String format(Object value) { + return String.valueOf(value); + } + + @Override + public String convert(Object value) { + return value == null ? null : String.valueOf(value); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/StringType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/StringType.java new file mode 100644 index 0000000..1a72e20 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/StringType.java @@ -0,0 +1,62 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.universal.core.metadata.Converter; +import cn.universal.core.metadata.UnitSupported; +import cn.universal.core.metadata.ValidateResult; +import cn.universal.core.metadata.ValueType; +import cn.universal.core.metadata.unit.ValueUnit; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class StringType extends AbstractType + implements ValueType, Converter, UnitSupported { + + public static final String ID = "string"; + public static final StringType GLOBAL = new StringType(); + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "字符串"; + } + + // 支持单位 + private ValueUnit unit; + + @Override + public ValidateResult validate(Object value) { + return ValidateResult.success(String.valueOf(value)); + } + + @Override + public String format(Object value) { + ValueUnit unit = getUnit(); + if (unit == null) { + return String.valueOf(value); + } + return (String) unit.format(value); + } + + @Override + public String convert(Object value) { + return value == null ? null : String.valueOf(value); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/UnknownType.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/UnknownType.java new file mode 100644 index 0000000..a193565 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/UnknownType.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.universal.core.metadata.ValidateResult; +import cn.universal.core.metadata.ValueType; + +/** + * @since 1.0.0 + */ +public class UnknownType implements ValueType { + + @Override + public ValidateResult validate(Object value) { + return ValidateResult.success(); + } + + @Override + public String getId() { + return "unknown"; + } + + @Override + public String getName() { + return "未知类型"; + } + + @Override + public String getDescription() { + return "未知类型"; + } + + @Override + public String format(Object value) { + return String.valueOf(value); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/ValueTypes.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/ValueTypes.java new file mode 100644 index 0000000..f69e4d0 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/ValueTypes.java @@ -0,0 +1,58 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types; + +import cn.universal.core.metadata.ValueType; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +public class ValueTypes { + + private static final Map> supports = new ConcurrentHashMap<>(); + + static { + supports.put(ArrayType.ID, ArrayType::new); + supports.put(BooleanType.ID, BooleanType::new); + supports.put(DateTimeType.ID, DateTimeType::new); + supports.put(DoubleType.ID, DoubleType::new); + supports.put(EnumType.ID, EnumType::new); + supports.put(FloatType.ID, FloatType::new); + supports.put(IntType.ID, IntType::new); + supports.put(LongType.ID, LongType::new); + supports.put(ObjectType.ID, ObjectType::new); + + supports.put(StringType.ID, StringType::new); + supports.put("text", StringType::new); + + supports.put(GeoType.ID, GeoType::new); + supports.put(FileType.ID, FileType::new); + supports.put(PasswordType.ID, PasswordType::new); + supports.put(GeoShapeType.ID, GeoShapeType::new); + } + + public static void register(String id, Supplier supplier) { + supports.put(id, supplier); + } + + public static Supplier lookup(String id) { + if (id == null) { + return null; + } + Supplier value = supports.get(id); + if (value != null) { + return value; + } + return supports.get(id); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/AbstractValueTypeCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/AbstractValueTypeCodec.java new file mode 100644 index 0000000..13b04ff --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/AbstractValueTypeCodec.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import static java.util.Optional.ofNullable; + +import cn.universal.core.metadata.ValueType; +import cn.universal.core.metadata.ValueTypeCodec; +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractValueTypeCodec implements ValueTypeCodec { + + @Override + public T decode(T type, Map config) { + ofNullable(config.get("description")).map(String::valueOf).ifPresent(type::setDescription); + + ofNullable(config.get("expands")) + .filter(Map.class::isInstance) + .map(Map.class::cast) + .ifPresent(type::setExpands); + return type; + } + + @Override + public Map encode(T type) { + Map encoded = new HashMap<>(); + encoded.put("type", getTypeId()); + encoded.put("description", type.getDescription()); + encoded.put("expands", type.getExpands()); + doEncode(encoded, type); + return encoded; + } + + protected void doEncode(Map encoded, T type) {} +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/ArrayCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/ArrayCodec.java new file mode 100644 index 0000000..26257ed --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/ArrayCodec.java @@ -0,0 +1,69 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import static java.util.Optional.ofNullable; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.ValueType; +import cn.universal.core.metadata.ValueTypeCodecs; +import cn.universal.core.metadata.types.ArrayType; +import cn.universal.core.metadata.types.ValueTypes; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ArrayCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return ArrayType.ID; + } + + @Override + public ArrayType decode(ArrayType type, Map config) { + super.decode(type, config); + JSONObject jsonObject = new JSONObject(config); + ofNullable(jsonObject.get("elementType")) + .map( + v -> { + if (v instanceof JSONObject) { + return ((JSONObject) v); + } + JSONObject eleType = new JSONObject(); + eleType.put("type", v); + return eleType; + }) + .map( + eleType -> { + ValueType ValueType = ValueTypes.lookup(eleType.getStr("type")).get(); + + ValueTypeCodecs.getCodec(ValueType.getId()) + .ifPresent(codec -> codec.decode(ValueType, eleType)); + + return ValueType; + }) + .ifPresent(type::setElementType); + + return type; + } + + @Override + protected void doEncode(Map encoded, ArrayType type) { + super.doEncode(encoded, type); + ValueTypeCodecs.getCodec(type.getElementType().getId()) + .ifPresent(codec -> encoded.put("elementType", codec.encode(type.getElementType()))); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/BooleanCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/BooleanCodec.java new file mode 100644 index 0000000..aaea582 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/BooleanCodec.java @@ -0,0 +1,54 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import static java.util.Optional.ofNullable; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.types.BooleanType; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class BooleanCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return BooleanType.ID; + } + + @Override + public BooleanType decode(BooleanType type, Map config) { + super.decode(type, config); + JSONObject jsonObject = new JSONObject(config); + + ofNullable(jsonObject.getStr("trueText")).ifPresent(type::setTrueText); + ofNullable(jsonObject.getStr("falseText")).ifPresent(type::setFalseText); + ofNullable(jsonObject.getStr("trueValue")).ifPresent(type::setTrueValue); + ofNullable(jsonObject.getStr("falseValue")).ifPresent(type::setFalseValue); + ofNullable(jsonObject.getStr("description")).ifPresent(type::setDescription); + + return type; + } + + @Override + protected void doEncode(Map encoded, BooleanType type) { + super.doEncode(encoded, type); + encoded.put("trueText", type.getTrueText()); + encoded.put("falseText", type.getFalseText()); + encoded.put("trueValue", type.getTrueValue()); + encoded.put("falseValue", type.getFalseValue()); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/DateCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/DateCodec.java new file mode 100644 index 0000000..7913994 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/DateCodec.java @@ -0,0 +1,49 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import static java.util.Optional.ofNullable; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.types.DateTimeType; +import java.time.ZoneId; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DateCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return DateTimeType.ID; + } + + @Override + public DateTimeType decode(DateTimeType type, Map config) { + super.decode(type, config); + JSONObject jsonObject = new JSONObject(config); + ofNullable(jsonObject.getStr("format")).ifPresent(type::setFormat); + ofNullable(jsonObject.getStr("tz")).map(ZoneId::of).ifPresent(type::setZoneId); + + return type; + } + + @Override + protected void doEncode(Map encoded, DateTimeType type) { + super.doEncode(encoded, type); + encoded.put("format", type.getFormat()); + encoded.put("tz", type.getZoneId().toString()); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/DoubleCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/DoubleCodec.java new file mode 100644 index 0000000..233d68f --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/DoubleCodec.java @@ -0,0 +1,54 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import static java.util.Optional.ofNullable; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.types.DoubleType; +import cn.universal.core.metadata.unit.ValueUnits; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DoubleCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return DoubleType.ID; + } + + @Override + public DoubleType decode(DoubleType type, Map config) { + super.decode(type, config); + JSONObject jsonObject = new JSONObject(config); + ofNullable(jsonObject.getDouble("max")).ifPresent(type::setMax); + ofNullable(jsonObject.getDouble("min")).ifPresent(type::setMin); + ofNullable(jsonObject.getInt("scale")).ifPresent(type::setScale); + ofNullable(jsonObject.getStr("unit")).flatMap(ValueUnits::lookup).ifPresent(type::setUnit); + return type; + } + + @Override + protected void doEncode(Map encoded, DoubleType type) { + encoded.put("max", type.getMax()); + encoded.put("min", type.getMin()); + + encoded.put("scale", type.getScale()); + if (type.getUnit() != null) { + encoded.put("unit", type.getUnit().getId()); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/EnumCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/EnumCodec.java new file mode 100644 index 0000000..5b3ba93 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/EnumCodec.java @@ -0,0 +1,63 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import static java.util.Optional.ofNullable; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.types.EnumType; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class EnumCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return EnumType.ID; + } + + @Override + public EnumType decode(EnumType type, Map config) { + super.decode(type, config); + JSONObject jsonObject = new JSONObject(config); + + ofNullable(jsonObject.getJSONArray("elements")) + .map( + list -> + list.stream() + .map(JSONObject.class::cast) + .map( + e -> + EnumType.Element.of( + e.getStr("value"), e.getStr("text"), e.getStr("description"))) + .collect(Collectors.toList())) + .ifPresent(type::setElements); + + return type; + } + + @Override + protected void doEncode(Map encoded, EnumType type) { + super.doEncode(encoded, type); + if (type.getElements() == null) { + return; + } + encoded.put( + "elements", + type.getElements().stream().map(EnumType.Element::toMap).collect(Collectors.toList())); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/FileCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/FileCodec.java new file mode 100644 index 0000000..6aae729 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/FileCodec.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import cn.universal.core.metadata.types.FileType; +import java.util.Map; +import java.util.Optional; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class FileCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return FileType.ID; + } + + @Override + public FileType decode(FileType type, Map config) { + super.decode(type, config); + + Optional.ofNullable(config.get("bodyType")) + .map(String::valueOf) + .flatMap(FileType.BodyType::of) + .ifPresent(type::setBodyType); + + return type; + } + + @Override + protected void doEncode(Map encoded, FileType type) { + super.doEncode(encoded, type); + encoded.put("bodyType", type.getBodyType().name()); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/FloatCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/FloatCodec.java new file mode 100644 index 0000000..7c6c7cc --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/FloatCodec.java @@ -0,0 +1,55 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import static java.util.Optional.ofNullable; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.types.FloatType; +import cn.universal.core.metadata.unit.ValueUnits; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class FloatCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return FloatType.ID; + } + + @Override + public FloatType decode(FloatType type, Map config) { + super.decode(type, config); + JSONObject jsonObject = new JSONObject(config); + ofNullable(jsonObject.getFloat("max")).ifPresent(type::setMax); + ofNullable(jsonObject.getFloat("min")).ifPresent(type::setMin); + ofNullable(jsonObject.getInt("scale")).ifPresent(type::setScale); + ofNullable(jsonObject.getStr("unit")).flatMap(ValueUnits::lookup).ifPresent(type::setUnit); + + return type; + } + + @Override + protected void doEncode(Map encoded, FloatType type) { + encoded.put("max", type.getMax()); + encoded.put("min", type.getMin()); + + encoded.put("scale", type.getScale()); + if (type.getUnit() != null) { + encoded.put("unit", type.getUnit().getId()); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/GeoPointCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/GeoPointCodec.java new file mode 100644 index 0000000..fa98d0c --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/GeoPointCodec.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import static java.util.Optional.ofNullable; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.types.GeoType; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class GeoPointCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return GeoType.ID; + } + + @Override + public GeoType decode(GeoType type, Map config) { + super.decode(type, config); + JSONObject jsonObject = new JSONObject(config); + ofNullable(jsonObject.getStr("latProperty")).ifPresent(type::latProperty); + ofNullable(jsonObject.getStr("lonProperty")).ifPresent(type::lonProperty); + return type; + } + + @Override + protected void doEncode(Map encoded, GeoType type) { + encoded.put("latProperty", type.getLatProperty()); + encoded.put("lonProperty", type.getLonProperty()); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/GeoShapeCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/GeoShapeCodec.java new file mode 100644 index 0000000..24dcd5c --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/GeoShapeCodec.java @@ -0,0 +1,27 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import cn.universal.core.metadata.types.GeoShapeType; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class GeoShapeCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return GeoShapeType.ID; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/IntCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/IntCodec.java new file mode 100644 index 0000000..27de4a5 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/IntCodec.java @@ -0,0 +1,53 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import static java.util.Optional.ofNullable; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.types.IntType; +import cn.universal.core.metadata.unit.ValueUnits; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class IntCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return IntType.ID; + } + + @Override + public IntType decode(IntType type, Map config) { + super.decode(type, config); + JSONObject jsonObject = new JSONObject(config); + ofNullable(jsonObject.getInt("max")).ifPresent(type::setMax); + ofNullable(jsonObject.getInt("min")).ifPresent(type::setMin); + ofNullable(jsonObject.getStr("unit")).flatMap(ValueUnits::lookup).ifPresent(type::setUnit); + + return type; + } + + @Override + protected void doEncode(Map encoded, IntType type) { + encoded.put("max", type.getMax()); + encoded.put("min", type.getMin()); + + if (type.getUnit() != null) { + encoded.put("unit", type.getUnit().getId()); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/LongCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/LongCodec.java new file mode 100644 index 0000000..0a83bc9 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/LongCodec.java @@ -0,0 +1,51 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import static java.util.Optional.ofNullable; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.types.LongType; +import cn.universal.core.metadata.unit.ValueUnits; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LongCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return LongType.ID; + } + + @Override + public LongType decode(LongType type, Map config) { + super.decode(type, config); + JSONObject jsonObject = new JSONObject(config); + ofNullable(jsonObject.getInt("max")).ifPresent(type::setMax); + ofNullable(jsonObject.getInt("min")).ifPresent(type::setMin); + ofNullable(jsonObject.getStr("unit")).flatMap(ValueUnits::lookup).ifPresent(type::setUnit); + return type; + } + + @Override + protected void doEncode(Map encoded, LongType type) { + encoded.put("max", type.getMax()); + encoded.put("min", type.getMin()); + if (type.getUnit() != null) { + encoded.put("unit", type.getUnit().getId()); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/ObjectCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/ObjectCodec.java new file mode 100644 index 0000000..a01dcfb --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/ObjectCodec.java @@ -0,0 +1,64 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import static java.util.Optional.ofNullable; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.AbstractPropertyMetadata; +import cn.universal.core.metadata.PropertyMetadata; +import cn.universal.core.metadata.types.ObjectType; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ObjectCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return ObjectType.ID; + } + + @Override + public ObjectType decode(ObjectType type, Map config) { + super.decode(type, config); + JSONObject jsonObject = new JSONObject(config); + + ofNullable(jsonObject.getJSONArray("properties")) + .map( + list -> + list.stream() + .map(JSONObject.class::cast) + .map(PropertyMetadata::new) + .collect(Collectors.toList())) + .ifPresent(type::setProperties); + + return type; + } + + @Override + protected void doEncode(Map encoded, ObjectType type) { + super.doEncode(encoded, type); + if (type.getProperties() != null) { + encoded.put( + "properties", + type.getProperties().stream() + .map(PropertyMetadata::new) + .map(AbstractPropertyMetadata::toJson) + .collect(Collectors.toList())); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/PasswordCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/PasswordCodec.java new file mode 100644 index 0000000..feebf6e --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/PasswordCodec.java @@ -0,0 +1,27 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import cn.universal.core.metadata.types.PasswordType; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class PasswordCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return PasswordType.ID; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/StringCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/StringCodec.java new file mode 100644 index 0000000..c4fdf34 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/types/codec/StringCodec.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.types.codec; + +import static java.util.Optional.ofNullable; + +import cn.hutool.json.JSONObject; +import cn.universal.core.metadata.types.StringType; +import cn.universal.core.metadata.unit.ValueUnits; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class StringCodec extends AbstractValueTypeCodec { + + @Override + public String getTypeId() { + return StringType.ID; + } + + // 支持单位 + @Override + public StringType decode(StringType type, Map config) { + super.decode(type, config); + JSONObject jsonObject = new JSONObject(config); + ofNullable(jsonObject.getStr("unit")).flatMap(ValueUnits::lookup).ifPresent(type::setUnit); + return type; + } + + @Override + protected void doEncode(Map encoded, StringType type) { + if (type.getUnit() != null) { + encoded.put("unit", type.getUnit().getId()); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/JsonValueUnit.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/JsonValueUnit.java new file mode 100644 index 0000000..b6a7007 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/JsonValueUnit.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.unit; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class JsonValueUnit implements ValueUnit { + + private final String symbol; + + private final String name; + + public static JsonValueUnit of(String jsonStr) { + + JSONObject json = JSONUtil.parseObj(jsonStr); + + String symbol = json.getStr("symbol"); + if (null == symbol) { + return null; + } + + return new JsonValueUnit(symbol, (String) json.getOrDefault("name", symbol)); + } + + @Override + public String getSymbol() { + return symbol; + } + + @Override + public Object format(Object value) { + if (value == null) { + return null; + } + return value + "" + symbol; + } + + @Override + public String getId() { + return "custom_" + symbol; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return symbol; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/SymbolValueUnit.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/SymbolValueUnit.java new file mode 100644 index 0000000..6c91661 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/SymbolValueUnit.java @@ -0,0 +1,49 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.unit; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor(staticName = "of") +public class SymbolValueUnit implements ValueUnit { + + private final String symbol; + + @Override + public String getSymbol() { + return symbol; + } + + @Override + public Object format(Object value) { + if (value == null) { + return null; + } + return value + "" + symbol; + } + + @Override + public String getId() { + return symbol; + } + + @Override + public String getName() { + return symbol; + } + + @Override + public String getDescription() { + return symbol; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/UnifyUnit.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/UnifyUnit.java new file mode 100644 index 0000000..2c75e72 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/UnifyUnit.java @@ -0,0 +1,202 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.unit; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 统一单位 + * + * @since 1.0.0 + */ +@Getter +@AllArgsConstructor +public enum UnifyUnit implements ValueUnit { + + // 常用单位 + percent("百分比", "%", "常用单位", "百分比(%)"), + count("次", "count", "常用单位", "次"), + turnPerSeconds("转每分钟", "r/min", "常用单位", "转每分钟"), + centigrade("摄氏度", "℃", "常用单位", "摄氏度"), + signal("信号强度", "dBm", "常用单位", "信号强度"), + calories("千焦", "kj", "常用单位", "热量单位"), + partsPerMillion("百万分率", "ppm", "常用单位", "百万分率"), + signalToInterference("分贝", "db", "常用单位", "信号与干扰加噪声比"), + + // 计量单位 + // =====https://baike.baidu.com/item/%E4%B8%AD%E5%9B%BD%E6%B3%95%E5%AE%9A%E8%AE%A1%E9%87%8F%E5%8D%95%E4%BD%8D/662681#1_1======= + // ==================长度(length)单位=================== + nanometer("纳米", "nm", "长度单位", "长度单位:纳米(nm)"), + micron("微米", "μm", "长度单位", "长度单位:微米(μm)"), + millimeter("毫米", "mm", "长度单位", "长度单位:毫米(mm)"), + centimeter("厘米", "cm", "长度单位", "长度单位:厘米(cm)"), + meter("米", "m", "长度单位", "长度单位:米(m)"), + kilometer("千米", "km", "长度单位", "长度单位:千米(km)"), + + // ==================面积(area)单位=================== + squareMillimeter("平方毫米", "mm²", "面积单位", "面积单位:平方毫米(mm²)"), + squareCentimeter("平方厘米", "cm²", "面积单位", "面积单位:平方厘米(cm²)"), + squareMeter("平方米", "m²", "面积单位", "面积单位:平方米(m²)"), + squareKilometer("平方千米", "km²", "面积单位", "面积单位:平方千米(km²)"), + hectare("公顷", "hm²", "面积单位", "面积单位:公顷(hm²)"), + + // ==================时间(time)单位=================== + + days("天", "d", "时间单位", "时间单位:天(d)"), + hour("小时", "h", "时间单位", "时间单位:小时(h)"), + minutes("分钟", "min", "时间单位", "时间单位:分钟(m)"), + seconds("秒", "s", "时间单位", "时间单位:秒(s)"), + milliseconds("毫秒", "ms", "时间单位", "时间单位:毫秒(ms)"), + microseconds("微秒", "μs", "时间单位", "时间单位:微秒(μs)"), + nanoseconds("纳秒", "ns", "时间单位", "时间单位:纳秒(ns)"), + + // ==================体积(volume)单位=================== + cubicMillimeter("立方毫米", "mm³", "体积单位", "体积单位:立方毫米(mm³)"), + cubicCentimeter("立方厘米", "cm³", "体积单位", "体积单位:立方厘米(cm³)"), + cubicMeter("立方米", "m³", "体积单位", "体积单位:立方米(m³)"), + cubicKilometer("立方千米", "km³", "体积单位", "体积单位:立方千米(km³)"), + + // ==================流量单位================== + cubicMeterPerSec("立方米每秒", "m³/s", "流量单位", "流量单位:立方米每秒(m³/s)"), + cubicKilometerPerSec("立方千米每秒", "km³/s", "流量单位", "流量单位:立方千米每秒(km³/s)"), + cubicCentimeterPerSec("立方厘米每秒", "cm³/s", "流量单位", "流量单位:立方厘米每秒(cm³/s)"), + litrePerSec("升每秒", "l/s", "流量单位", "流量单位:升每秒(l/s)"), + + cubicMeterPerHour("立方米每小时", "m³/h", "流量单位", "流量单位:立方米每小时(m³/h)"), + cubicKilometerPerHour("立方千米每小时", "km³/h", "流量单位", "流量单位:立方千米每小时(km³/h)"), + cubicCentimeterPerHour("立方厘米每小时", "cm³/h", "流量单位", "流量单位:立方厘米每小时(cm³/h)"), + litrePerHour("升每小时", "l/h", "流量单位", "流量单位:升每小时(l/h)"), + + // ==================容积(capacity)单位=================== + milliliter("毫升", "mL", "容积单位", "容积单位:毫升(mL)"), + litre("升", "L", "容积单位", "容积单位:升(L)"), + + // ==================质量(mass)单位=================== + milligram("毫克", "mg", "质量单位", "重量单位:毫克(mg)"), + gramme("克", "g", "质量单位", "重量单位:克(g)"), + kilogram("千克", "kg", "质量单位", "重量单位:千克(kg)"), + ton("吨", "t", "质量单位", "重量单位:吨(t)"), + + // ==================压力(pressure)单位=================== + pascal("帕斯卡", "Pa", "压力单位", "压力单位:帕斯卡(Pa)"), + kiloPascal("千帕斯卡", "kPa", "压力单位", "压力单位:千帕斯卡(kPa)"), + newton("牛顿", "N", "力单位", "力单位:牛顿(N)"), + newtonMeter("牛·米", "N.m", "力单位", "力单位:牛·米(N.m)"), + + // ==================温度(temperature)单位=================== + kelvin("开尔文", "K", "温度单位", "温度单位:开尔文(K)"), + celsiusDegrees("摄氏度", "℃", "温度单位", "温度单位:摄氏度(℃)"), + fahrenheit("华氏度", "℉", "温度单位", "温度单位:华氏度(℉)"), + + // ==================能(energy)单位 ==================== + joule("焦耳", "J", "能量单位", "能单位:焦耳(J)"), + cal("卡", "cal", "能量单位", "能单位:卡(cal)"), + + // ==================功率(power)单位=================== + watt("瓦特", "W", "功率单位", "功率单位:瓦特(W)"), + kilowatt("千瓦特", "kW", "功率单位", "功率单位:千瓦特(kW)"), + + // ==================角度(angle)单位=================== + radian("弧度", "rad", "角度单位", "角度单位:弧度(rad)"), + degrees("度", "°", "角度单位", "角度单位:度(°)"), + fen("[角]分", "′", "角度单位", "角度单位:分(′)"), + angleSeconds("[角]秒", "″", "角度单位", "角度单位:度(″)"), + + // ==================频率(frequency)单位=================== + + hertz("赫兹", "Hz", "频率单位", "频率单位:赫兹(Hz)"), + megahertz("兆赫兹", "MHz", "频率单位", "频率单位:兆赫兹(MHz)"), + ghertz("G赫兹", "GHz", "频率单位", "频率单位:G赫兹(GHz)"), + + // ==================速度(speed)单位=================== + + mPerSec("米每秒", "m/s", "速度单位", "速度单位:米每秒(m/s)"), + kmPerHr("千米每小时", "km/h", "速度单位", "速度单位:千米每小时(km/h)"), + knots("节", "kn", "速度单位", "速度单位:节(kn)"), + + // ==================电(electricity)单位=================== + + volt("伏特", "V", "电力单位", "电压:伏特(V)"), + kiloVolt("千伏", "kV", "电力单位", "电压:千伏(kV)"), + milliVolt("毫伏", "mV", "电力单位", "电压:毫伏(mV)"), + microVolt("微伏", "μV", "电力单位", "电压:微伏(μV)"), + + ampere("安培", "A", "电力单位", "电流:安培(A)"), + milliAmpere("毫安", "mA", "电力单位", "电流:毫安(mA)"), + microAmpere("微安", "μA", "电力单位", "电流:微安(μA)"), + nanoAmpere("纳安", "nA", "电力单位", "电流:纳安(nA)"), + + ohm("欧姆", "Ω", "电力单位", "电阻:欧姆(Ω)"), + kiloOhm("千欧", "KΩ", "电力单位", "电阻:千欧(KΩ)"), + millionOhm("兆欧", "MΩ", "电力单位", "电阻:兆欧(MΩ)"), + electronVolts("电子伏", "eV", "电力单位", "能单位:电子伏(eV)"), + kWattsHour("千瓦·时", "kW·h", "电力单位", "能单位:千瓦·时(kW·h)"), + // ==================文件流量单位(flow)单位=================== + flowKb("KB", "KB", "流量单位", "大小:KB"), + flowMb("MB", "MB", "流量单位", "大小:MB"), + flowGb("GB", "GB", "流量单位", "大小:GB"); + + private final String name; + + private final String symbol; + + private final String type; + + private final String description; + + @Override + public String getId() { + return name(); + } + + @Override + public String format(Object value) { + return String.format("%s%s", value, getSymbol()); + } + + static Function template(String strTemplate) { + return o -> String.format(strTemplate, o); + } + + public static UnifyUnit of(String value) { + return Stream.of(UnifyUnit.values()) + .filter(unifyUnit -> unifyUnit.getId().equals(value) || unifyUnit.getSymbol().equals(value)) + .findFirst() + .orElse(null); + } + + public String getValue() { + return name(); + } + + public String getText() { + return getName().concat("(").concat(getSymbol() + ")"); + } + + public Object getWriteJSONObject() { + Map jsonObject = new HashMap<>(); + jsonObject.put("id", this.getValue()); + jsonObject.put("value", this.getValue()); + jsonObject.put("text", this.getText()); + jsonObject.put("symbol", this.getSymbol()); + jsonObject.put("name", this.getName()); + jsonObject.put("type", this.getType()); + jsonObject.put("description", this.getDescription()); + return jsonObject; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/ValueUnit.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/ValueUnit.java new file mode 100644 index 0000000..a014179 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/ValueUnit.java @@ -0,0 +1,33 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.unit; + +import cn.universal.core.metadata.FormatSupport; +import cn.universal.core.metadata.Metadata; +import java.io.Serializable; +import java.util.Map; + +/** + * 值单位 + * + * @version 1.0 + */ +public interface ValueUnit extends Metadata, FormatSupport, Serializable { + + String getSymbol(); + + @Override + default Map getExpands() { + return null; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/ValueUnitSupplier.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/ValueUnitSupplier.java new file mode 100644 index 0000000..e99258b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/ValueUnitSupplier.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.unit; + +import java.util.List; +import java.util.Optional; + +public interface ValueUnitSupplier { + + Optional getById(String id); + + List getAll(); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/ValueUnits.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/ValueUnits.java new file mode 100644 index 0000000..5e91a43 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/metadata/unit/ValueUnits.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.metadata.unit; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +public class ValueUnits { + + private static final List suppliers = new CopyOnWriteArrayList<>(); + + static { + ValueUnits.register( + new ValueUnitSupplier() { + @Override + public Optional getById(String id) { + return Optional.ofNullable(UnifyUnit.of(id)); + } + + @Override + public List getAll() { + return Arrays.asList(UnifyUnit.values()); + } + }); + } + + public static void register(ValueUnitSupplier supplier) { + suppliers.add(supplier); + } + + public static Optional lookup(String id) { + for (ValueUnitSupplier supplier : suppliers) { + Optional unit = supplier.getById(id); + if (unit.isPresent()) { + return unit; + } + } + // json ? + if (id.startsWith("{")) { + return Optional.ofNullable(JsonValueUnit.of(id)); + } + return Optional.of(SymbolValueUnit.of(id)); + } + + public static List getAllUnit() { + return suppliers.stream() + .map(ValueUnitSupplier::getAll) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/ProtocolModuleInfo.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/ProtocolModuleInfo.java new file mode 100644 index 0000000..ec839d3 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/ProtocolModuleInfo.java @@ -0,0 +1,103 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol; + +/** + * 协议模块信息接口 + * + *

定义协议模块的基本元数据信息,包括名称、代码、描述等 + * + * @version 1.0 @Author Aleo + * @since 2025/1/2 + */ +public interface ProtocolModuleInfo { + + /** + * 获取协议代码 + * + * @return 协议代码 (如: mqtt, tcp, http, ctwing, onenet, imoulife) + */ + String getCode(); + + /** + * 获取协议名称 + * + * @return 协议名称 (如: MQTT, TCP, HTTP, CT-AIoT (电信), OneNet (移动), ImouLife (乐橙)) + */ + String getName(); + + /** + * 获取协议描述 + * + * @return 协议描述 + */ + String getDescription(); + + /** + * 获取协议版本 + * + * @return 协议版本 + */ + default String getVersion() { + return "1.0"; + } + + /** + * 获取协议厂商 + * + * @return 协议厂商 + */ + default String getVendor() { + return "Universal IoT"; + } + + /** + * 是否为核心协议(系统必须依赖) + * + * @return true-核心协议,false-可选协议 + */ + default boolean isCore() { + return false; + } + + /** + * 获取协议分类 + * + * @return 协议分类 (如: MESSAGING, TRANSPORT, PLATFORM) + */ + default ProtocolCategory getCategory() { + return ProtocolCategory.TRANSPORT; + } + + /** 协议分类枚举 */ + enum ProtocolCategory { + /** 消息传输协议 */ + MESSAGING("消息传输"), + /** 传输层协议 */ + TRANSPORT("传输层"), + /** 第三方平台协议 */ + PLATFORM("第三方平台"), + /** 应用层协议 */ + APPLICATION("应用层"); + + private final String description; + + ProtocolCategory(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/ProtocolModuleRegistry.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/ProtocolModuleRegistry.java new file mode 100644 index 0000000..c0ab710 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/ProtocolModuleRegistry.java @@ -0,0 +1,128 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * 协议模块注册器 + * + *

统一管理所有协议模块的元数据信息和状态 + * + * @version 1.0 @Author Aleo + * @since 2025/1/2 + */ +@Component +@Slf4j +public class ProtocolModuleRegistry implements ApplicationContextAware { + + private static final Map moduleInfoMap = new HashMap<>(); + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + // 扫描所有实现了 ProtocolModuleInfo 接口的Bean + Map moduleInfoBeans = + applicationContext.getBeansOfType(ProtocolModuleInfo.class); + + moduleInfoBeans.forEach( + (beanName, moduleInfo) -> { + String code = moduleInfo.getCode(); + moduleInfoMap.put(code, moduleInfo); + log.info("[协议模块注册] 注册协议模块: {} -> {}", code, moduleInfo.getName()); + }); + + log.info("[协议模块注册] 完成协议模块注册,共注册 {} 个模块: {}", moduleInfoMap.size(), moduleInfoMap.keySet()); + } + + /** + * 获取协议模块信息 + * + * @param code 协议代码 + * @return 协议模块信息,如果不存在返回null + */ + public static ProtocolModuleInfo getModuleInfo(String code) { + return moduleInfoMap.get(code); + } + + /** + * 获取所有已注册的协议模块信息 + * + * @return 协议模块信息映射 + */ + public static Map getAllModuleInfo() { + return new HashMap<>(moduleInfoMap); + } + + /** + * 获取所有已注册的协议代码 + * + * @return 协议代码集合 + */ + public static Set getAllProtocolCodes() { + return moduleInfoMap.keySet(); + } + + /** + * 获取核心协议模块 + * + * @return 核心协议模块列表 + */ + public static List getCoreModules() { + return moduleInfoMap.values().stream() + .filter(ProtocolModuleInfo::isCore) + .collect(Collectors.toList()); + } + + /** + * 获取可选协议模块 + * + * @return 可选协议模块列表 + */ + public static List getOptionalModules() { + return moduleInfoMap.values().stream() + .filter(info -> !info.isCore()) + .collect(Collectors.toList()); + } + + /** + * 按分类获取协议模块 + * + * @param category 协议分类 + * @return 指定分类的协议模块列表 + */ + public static List getModulesByCategory( + ProtocolModuleInfo.ProtocolCategory category) { + return moduleInfoMap.values().stream() + .filter(info -> info.getCategory() == category) + .collect(Collectors.toList()); + } + + /** + * 检查协议模块是否已注册 + * + * @param code 协议代码 + * @return true-已注册,false-未注册 + */ + public static boolean isRegistered(String code) { + return moduleInfoMap.containsKey(code); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/ProtocolModuleRuntimeRegistry.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/ProtocolModuleRuntimeRegistry.java new file mode 100644 index 0000000..c19e132 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/ProtocolModuleRuntimeRegistry.java @@ -0,0 +1,238 @@ +package cn.universal.core.protocol; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 协议模块运行时注册表 + * + *

用于管理实际启动的协议模块,替代基于字符串的查找机制 + * + *

协议模块启动时自动注册,关闭时自动注销 + * + * @author Universal IoT + * @since 2025/1/2 + */ +@Component +@Slf4j +public class ProtocolModuleRuntimeRegistry { + + /** 运行时协议模块注册表 Key: 协议代码, Value: 协议模块信息 */ + private static final Map runtimeRegistry = new ConcurrentHashMap<>(); + + /** 协议模块启动状态 Key: 协议代码, Value: 启动状态 */ + private static final Map startupStatusMap = + new ConcurrentHashMap<>(); + + /** + * 注册协议模块(启动时调用) + * + * @param protocolInfo 协议模块信息 + */ + public static void registerProtocol(ProtocolModuleInfo protocolInfo) { + if (protocolInfo == null || protocolInfo.getCode() == null) { + log.warn("[协议注册表] 尝试注册无效的协议模块信息"); + return; + } + + String code = protocolInfo.getCode(); + runtimeRegistry.put(code, protocolInfo); + startupStatusMap.put(code, new ProtocolStartupStatus(code, true, System.currentTimeMillis())); + + log.info("[协议注册表] ✅ 协议模块已注册: {} - {}", code, protocolInfo.getName()); + } + + /** + * 注销协议模块(关闭时调用) + * + * @param protocolCode 协议代码 + */ + public static void unregisterProtocol(String protocolCode) { + if (protocolCode == null) { + return; + } + + ProtocolModuleInfo removed = runtimeRegistry.remove(protocolCode); + startupStatusMap.remove(protocolCode); + + if (removed != null) { + log.info("[协议注册表] ❌ 协议模块已注销: {} - {}", protocolCode, removed.getName()); + } + } + + /** + * 获取所有运行时协议模块 + * + * @return 协议模块信息集合 + */ + public static Collection getAllRuntimeProtocols() { + return Collections.unmodifiableCollection(runtimeRegistry.values()); + } + + /** + * 获取运行时协议模块代码集合 + * + * @return 协议代码集合 + */ + public static Set getRuntimeProtocolCodes() { + return Collections.unmodifiableSet(runtimeRegistry.keySet()); + } + + /** + * 检查协议模块是否运行中 + * + * @param protocolCode 协议代码 + * @return 是否运行中 + */ + public static boolean isProtocolRunning(String protocolCode) { + return runtimeRegistry.containsKey(protocolCode); + } + + /** + * 获取协议模块信息 + * + * @param protocolCode 协议代码 + * @return 协议模块信息,如果未找到返回null + */ + public static ProtocolModuleInfo getProtocolInfo(String protocolCode) { + return runtimeRegistry.get(protocolCode); + } + + /** + * 获取核心协议模块 + * + * @return 核心协议模块集合 + */ + public static List getCoreProtocols() { + return runtimeRegistry.values().stream() + .filter(ProtocolModuleInfo::isCore) + .sorted(Comparator.comparing(ProtocolModuleInfo::getCode)) + .toList(); + } + + /** + * 获取可选协议模块 + * + * @return 可选协议模块集合 + */ + public static List getOptionalProtocols() { + return runtimeRegistry.values().stream() + .filter(info -> !info.isCore()) + .sorted(Comparator.comparing(ProtocolModuleInfo::getCode)) + .toList(); + } + + /** + * 按分类获取协议模块 + * + * @param category 协议分类 + * @return 指定分类的协议模块集合 + */ + public static List getProtocolsByCategory( + ProtocolModuleInfo.ProtocolCategory category) { + return runtimeRegistry.values().stream() + .filter(info -> info.getCategory() == category) + .sorted(Comparator.comparing(ProtocolModuleInfo::getCode)) + .toList(); + } + + /** + * 获取协议启动状态 + * + * @param protocolCode 协议代码 + * @return 启动状态信息 + */ + public static ProtocolStartupStatus getStartupStatus(String protocolCode) { + return startupStatusMap.get(protocolCode); + } + + /** + * 获取所有协议启动状态 + * + * @return 启动状态映射 + */ + public static Map getAllStartupStatus() { + return Collections.unmodifiableMap(startupStatusMap); + } + + /** + * 获取运行时统计信息 + * + * @return 统计信息 + */ + public static RuntimeStatistics getStatistics() { + int totalRunning = runtimeRegistry.size(); + int coreCount = + (int) runtimeRegistry.values().stream().filter(ProtocolModuleInfo::isCore).count(); + int optionalCount = totalRunning - coreCount; + + Map categoryStats = new HashMap<>(); + runtimeRegistry + .values() + .forEach(info -> categoryStats.merge(info.getCategory(), 1, Integer::sum)); + + return new RuntimeStatistics(totalRunning, coreCount, optionalCount, categoryStats); + } + + /** 协议启动状态 */ + public static class ProtocolStartupStatus { + private final String protocolCode; + private final boolean running; + private final long startupTime; + + public ProtocolStartupStatus(String protocolCode, boolean running, long startupTime) { + this.protocolCode = protocolCode; + this.running = running; + this.startupTime = startupTime; + } + + public String getProtocolCode() { + return protocolCode; + } + + public boolean isRunning() { + return running; + } + + public long getStartupTime() { + return startupTime; + } + } + + /** 运行时统计信息 */ + public static class RuntimeStatistics { + private final int totalRunning; + private final int coreCount; + private final int optionalCount; + private final Map categoryStats; + + public RuntimeStatistics( + int totalRunning, + int coreCount, + int optionalCount, + Map categoryStats) { + this.totalRunning = totalRunning; + this.coreCount = coreCount; + this.optionalCount = optionalCount; + this.categoryStats = categoryStats; + } + + public int getTotalRunning() { + return totalRunning; + } + + public int getCoreCount() { + return coreCount; + } + + public int getOptionalCount() { + return optionalCount; + } + + public Map getCategoryStats() { + return categoryStats; + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/jar/ProtocolCodecJar.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/jar/ProtocolCodecJar.java new file mode 100644 index 0000000..4fcdb5f --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/jar/ProtocolCodecJar.java @@ -0,0 +1,280 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.jar; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; +import cn.universal.common.exception.CodecException; +import cn.universal.core.protocol.request.ProtocolDecodeRequest; +import cn.universal.core.protocol.request.ProtocolEncodeRequest; +import cn.universal.core.protocol.support.ProtocolCodecLoader; +import cn.universal.core.protocol.support.ProtocolCodecSupport; +import cn.universal.core.protocol.support.ProtocolCodecSupportWrapper; +import cn.universal.core.protocol.support.ProtocolCodecWrapper; +import cn.universal.core.protocol.support.ProtocolSupportDefinition; +import java.io.File; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import lombok.extern.slf4j.Slf4j; + +/** + * jar加解密插件支持,动态加载jar包 + * + * @version 1.0 @Author Aleo + * @since 2025/8/9 19:28 + */ +@Slf4j +public class ProtocolCodecJar extends ProtocolCodecSupportWrapper + implements ProtocolCodecLoader, ProtocolCodecSupport, ProtocolCodecWrapper { + + private final Map protocolLoaders = new ConcurrentHashMap(); + private final Map codecJarProvider = new ConcurrentHashMap(); + + private ProtocolCodecJar() { + // 已移除 beforeSecurity(),兼容 JDK 17+ + } + + private static class ProtocolCodecJarProviderHolder { + + private static ProtocolCodecJar INSTANCE = new ProtocolCodecJar(); + } + + public static ProtocolCodecJar getInstance() { + return ProtocolCodecJarProviderHolder.INSTANCE; + } + + @Override + public String getProviderType() { + return "jar"; + } + + ProtocolJarClassLoader createClassLoader(URL localtion) { + return new ProtocolJarClassLoader(new URL[] {localtion}, this.getClass().getClassLoader()); + } + + @Override + public void load(ProtocolSupportDefinition definition) throws CodecException { + synchronized (this) { + try { + Map config = definition.getConfiguration(); + // 获取位置 + String location = + (String) + Optional.ofNullable(config.get("location")) + .map(String::valueOf) + .orElseThrow( + () -> { + return new IllegalArgumentException("location"); + }); + // 获取提供者 + String provider = + (String) + Optional.ofNullable(config.get("provider")) + .map(String::valueOf) + .map(String::trim) + .orElseThrow( + () -> { + return new IllegalArgumentException("provider"); + }); + + if (location.contains("local")) { + // 本地解码库,直接过滤 + return; + } + URL url; + if (!location.contains("://")) { + url = (new File(location)).toURI().toURL(); + } else { + url = new URL("jar:" + location + "!/"); + } + + // 卸载老的加载器 + codecJarProvider.remove(provider); + protocolLoaders.remove(provider); + ProtocolJarClassLoader loader = createClassLoader(url); + protocolLoaders.put(provider, loader); + log.info("load protocol support from : {}", location); + if (provider != null) { + Object newCodecJarProvider = Class.forName(provider, true, loader).newInstance(); + + codecJarProvider.put(provider, newCodecJarProvider); + } + } catch (Exception e) { + String error = ExceptionUtil.getRootCauseMessage(e); + log.error("加载jar编解码出错={}", e); + throw new CodecException(error); + } + } + } + + @Override + public String decode(ProtocolDecodeRequest decodeRequest) throws CodecException { + try { + if (!codecJarProvider.containsKey(decodeRequest.getDefinition().getProvider())) { + load(decodeRequest.getDefinition()); + } + Object clz = codecJarProvider.get(decodeRequest.getDefinition().getProvider()); + Method method = clz.getClass().getDeclaredMethod(CodecMethod.decode.name(), String.class); + // jar 未实现 decode 方法,直接返回 + if (method == null) { + return decodeRequest.getPayload(); + } + Object result = method.invoke(clz, decodeRequest.getPayload()); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getRootCauseMessage(e); + log.error( + "产品型号={} 提供者={} 解码失败={}", + decodeRequest.getDefinition().getId(), + decodeRequest.getDefinition().getProvider(), + e); + throw new CodecException(error); + } + } + + @Override + public String encode(ProtocolEncodeRequest encodeRequest) throws CodecException { + if (!codecJarProvider.containsKey(encodeRequest.getDefinition().getProvider())) { + load(encodeRequest.getDefinition()); + } + Object clz = codecJarProvider.get(encodeRequest.getDefinition().getProvider()); + try { + Method method = clz.getClass().getDeclaredMethod(CodecMethod.encode.name(), String.class); + // jar 未实现 encode 方法,直接返回 + if (method == null) { + return encodeRequest.getPayload(); + } + Object result = method.invoke(clz, encodeRequest.getPayload()); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getRootCauseMessage(e); + log.error( + "产品型号={} 提供者={} 编码失败={}", + encodeRequest.getDefinition().getId(), + encodeRequest.getDefinition().getProvider(), + error); + throw new CodecException(error); + } + } + + @Override + public String preDecode(ProtocolDecodeRequest protocolDecodeRequest) throws CodecException { + if (!codecJarProvider.containsKey(protocolDecodeRequest.getDefinition().getProvider())) { + load(protocolDecodeRequest.getDefinition()); + } + Object clz = codecJarProvider.get(protocolDecodeRequest.getDefinition().getProvider()); + try { + Method method = clz.getClass().getDeclaredMethod(CodecMethod.preDecode.name(), String.class); + // jar 未实现 preDecode 方法,直接返回 + if (method == null) { + return protocolDecodeRequest.getPayload(); + } + Object result = method.invoke(clz, protocolDecodeRequest.getPayload()); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getRootCauseMessage(e); + log.error( + "产品型号={} 提供者={} 预编码失败={}", + protocolDecodeRequest.getDefinition().getId(), + protocolDecodeRequest.getDefinition().getProvider(), + error); + throw new CodecException(error); + } + } + + @Override + public void load(String provider, Object providerImpl) { + if (StrUtil.isNotBlank(provider) && providerImpl != null) { + codecJarProvider.put(provider, providerImpl); + } + } + + @Override + public void remove(String provider) { + if (StrUtil.isNotBlank(provider)) { + codecJarProvider.remove(provider); + protocolLoaders.remove(provider); + } + } + + // 在ProtocolCodecJar类中添加isLoaded方法 + @Override + public boolean isLoaded(String provider, CodecMethod codecMethod) { + // 检查provider对应的是否已加载到缓存中 + if (!codecJarProvider.containsKey(provider)) { + return false; + } + + // 检查JAR包中是否实现了对应的方法 + try { + Object clz = codecJarProvider.get(provider); + Method method = clz.getClass().getDeclaredMethod(codecMethod.name(), String.class); + return method != null; + } catch (Exception e) { + return false; + } + } + + @Override + public String iotToYour(ProtocolEncodeRequest encodeRequest) throws CodecException { + try { + if (!codecJarProvider.containsKey(encodeRequest.getDefinition().getProvider())) { + load(encodeRequest.getDefinition()); + } + Object clz = codecJarProvider.get(encodeRequest.getDefinition().getProvider()); + Method method = clz.getClass().getDeclaredMethod(CodecMethod.iotToYour.name(), String.class); + // jar 未实现 iotToYour 方法,使用encode方法 + if (method == null) { + return encode(encodeRequest); + } + Object result = method.invoke(clz, encodeRequest.getPayload()); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getRootCauseMessage(e); + log.error( + "产品型号={} 提供者={} iotToYour失败={}", + encodeRequest.getDefinition().getId(), + encodeRequest.getDefinition().getProvider(), + e); + throw new CodecException(error); + } + } + + @Override + public String yourToIot(ProtocolDecodeRequest decodeRequest) throws CodecException { + try { + if (!codecJarProvider.containsKey(decodeRequest.getDefinition().getProvider())) { + load(decodeRequest.getDefinition()); + } + Object clz = codecJarProvider.get(decodeRequest.getDefinition().getProvider()); + Method method = clz.getClass().getDeclaredMethod(CodecMethod.yourToIot.name(), String.class); + // jar 未实现 yourToIot 方法,使用decode方法 + if (method == null) { + return decode(decodeRequest); + } + Object result = method.invoke(clz, decodeRequest.getPayload()); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getRootCauseMessage(e); + log.error( + "产品型号={} 提供者={} yourToIot失败={}", + decodeRequest.getDefinition().getId(), + decodeRequest.getDefinition().getProvider(), + e); + throw new CodecException(error); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/jar/ProtocolJarClassLoader.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/jar/ProtocolJarClassLoader.java new file mode 100644 index 0000000..2c6a8f1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/jar/ProtocolJarClassLoader.java @@ -0,0 +1,42 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.jar; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; + +/** + * 协议jar类加载器 + * + * @version 1.0 @Author Aleo + * @since 2025/8/9 19:23 + */ +public class ProtocolJarClassLoader extends URLClassLoader { + + private final URL[] urls; + + public ProtocolJarClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + this.urls = urls; + } + + @Override + public void close() throws IOException { + super.close(); + } + + public URL[] getUrls() { + return this.urls; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/jscrtipt/JsMethodExtractor.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/jscrtipt/JsMethodExtractor.java new file mode 100644 index 0000000..a7c5056 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/jscrtipt/JsMethodExtractor.java @@ -0,0 +1,47 @@ +package cn.universal.core.protocol.jscrtipt; + +import cn.hutool.core.util.StrUtil; +import java.util.HashSet; +import java.util.Set; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; + +/** 提取 JS 脚本中的方法名 */ +public class JsMethodExtractor { + /** + * 从 JS 脚本内容中提取所有方法名 + * + * @param scriptContent JS 脚本字符串 + * @return 方法名集合(仅包含函数类型的标识符) + */ + public static Set extractMethodNames(String scriptContent) { + if (StrUtil.isEmpty(scriptContent)) { + return new HashSet<>(); + } + Set methodNames = new HashSet<>(); + // 使用 GraalVM 上下文执行脚本并获取全局对象 + try (Context context = Context.create("js")) { + // 执行脚本(加载所有定义) + context.eval("js", scriptContent); + // 获取 JS 全局对象(包含所有顶级定义) + Value global = context.getBindings("js"); + // 遍历全局对象的所有成员 + for (String key : global.getMemberKeys()) { + Value member = global.getMember(key); + // 判断是否为可执行函数(排除变量、常量等非函数类型) + if (isFunction(member)) { + methodNames.add(key); + } + } + } + return methodNames; + } + + /** 判断 GraalVM Value 是否为 JS 函数 */ + private static boolean isFunction(Value value) { + // 函数必须满足:可执行 + 不是 null + 不是 undefined + 类型名称为 "function + return value.canExecute() + && !value.isNull() + && "function".equalsIgnoreCase(value.getMetaObject().getMetaSimpleName()); + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/jscrtipt/ProtocolCodecJscript.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/jscrtipt/ProtocolCodecJscript.java new file mode 100644 index 0000000..e5c2d34 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/jscrtipt/ProtocolCodecJscript.java @@ -0,0 +1,322 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.jscrtipt; + +import cn.hutool.core.codec.Base64Decoder; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import cn.universal.common.exception.CodecException; +import cn.universal.common.exception.IoTException; +import cn.universal.core.protocol.request.ProtocolDecodeRequest; +import cn.universal.core.protocol.request.ProtocolEncodeRequest; +import cn.universal.core.protocol.support.ProtocolCodecLoader; +import cn.universal.core.protocol.support.ProtocolCodecSupport; +import cn.universal.core.protocol.support.ProtocolCodecSupportWrapper; +import cn.universal.core.protocol.support.ProtocolCodecWrapper; +import cn.universal.core.protocol.support.ProtocolSupportDefinition; +import java.nio.charset.Charset; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import javax.script.Bindings; +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.SimpleScriptContext; +import lombok.extern.slf4j.Slf4j; + +/** + * jar加解密插件支持,动态加载jar包 + * + * @version 1.0 @Author Aleo + * @since 2025/8/9 19:28 + */ +@Slf4j +public class ProtocolCodecJscript extends ProtocolCodecSupportWrapper + implements ProtocolCodecLoader, ProtocolCodecSupport, ProtocolCodecWrapper { + + private final Map codecJscriptProvider = new ConcurrentHashMap(); + + private final Map> methodCache = new ConcurrentHashMap<>(); + + private final ScriptEngineManager factory = new ScriptEngineManager(); + + private ProtocolCodecJscript() {} + + private static class ProtocolCodecJscriptProviderHoler { + + private static ProtocolCodecJscript INSTANCE = new ProtocolCodecJscript(); + } + + public static ProtocolCodecJscript getInstance() { + return ProtocolCodecJscriptProviderHoler.INSTANCE; + } + + @Override + public String getProviderType() { + return "jscript"; + } + + @Override + public void load(ProtocolSupportDefinition definition) throws CodecException { + try { + Map config = definition.getConfiguration(); + // JS原始代码 + String location = + (String) + Optional.ofNullable(config.get("location")) + .map(String::valueOf) + .orElseThrow( + () -> { + return new IoTException( + "javascript source code not exist, can not do encode or decode "); + }); + // 获取提供者,产品唯一 + String provider = + (String) + Optional.ofNullable(config.get("provider")) + .map(String::valueOf) + .map(String::trim) + .orElseThrow( + () -> { + return new IllegalArgumentException("provider"); + }); + // 初始化 - 使用 GraalJS 替代 Nashorn + ScriptEngine engine = factory.getEngineByName("graal.js"); + if (engine == null) { + // 如果 GraalJS 不可用,尝试使用 JavaScript 引擎 + engine = factory.getEngineByName("JavaScript"); + } + if (engine == null) { + throw new CodecException( + "No JavaScript engine available. Please ensure GraalJS is in classpath."); + } + // 加载javascript es5的语法 + if (location.startsWith("http://") || location.startsWith("https://")) { + engine.eval(HttpUtil.downloadString(location, Charset.defaultCharset())); + } else { + engine.eval(location); + } + // 处理内部实现了哪些方法 + evalMethodCache(definition, engine); + Invocable invocable = (Invocable) engine; + codecJscriptProvider.put(provider, invocable); + } catch (Exception e) { + String error = ExceptionUtil.getMessage(e); + log.error("加载jscript编解码出错={}", error); + throw new CodecException(error); + } + } + + private void evalMethodCache(ProtocolSupportDefinition definition, ScriptEngine engine) { + SimpleScriptContext scriptContext = (SimpleScriptContext) engine.getContext(); + List scopes = scriptContext.getScopes(); + Set methods = new HashSet<>(); + for (Integer ints : scopes) { + Bindings bindings = scriptContext.getBindings(ints); + if (MapUtil.isNotEmpty(bindings)) { + bindings.forEach( + (k, v) -> { + methods.add(k); + }); + } + } + methodCache.put(definition.getId(), methods); + } + + @Override + public String decode(ProtocolDecodeRequest decodeRequest) throws CodecException { + if (!codecJscriptProvider.containsKey(decodeRequest.getDefinition().getProvider())) { + load(decodeRequest.getDefinition()); + } + // 如果编解码内部不包含decode方法,则直接返回原串 + if (methodCache.get(decodeRequest.getDefinition().getId()) == null + || !methodCache + .get(decodeRequest.getDefinition().getId()) + .contains(CodecMethod.decode.name())) { + return decodeRequest.getPayload(); + } + Invocable invocable = codecJscriptProvider.get(decodeRequest.getDefinition().getProvider()); + try { + Boolean needBs4Decode = + (Boolean) + decodeRequest.getDefinition().getConfiguration().getOrDefault("needBs4Decode", false); + String payload = decodeRequest.getPayload(); + // 发现是Bse64,进行解码且做16进制转换 + // 这块的存在是有争议,理论是有编解码实现 + if (needBs4Decode) { + payload = HexUtil.encodeHexStr(Base64Decoder.decode(payload)); + } + Object result = + invocable.invokeFunction(CodecMethod.decode.name(), payload, decodeRequest.getContext()); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getMessage(e); + log.info("设备 js 解码 payload = {}", decodeRequest.getPayload()); + log.error( + "产品型号={} 提供者={} Jscript解码失败={}", + decodeRequest.getDefinition().getId(), + decodeRequest.getDefinition().getProvider(), + e); + throw new CodecException(error); + } + } + + @Override + public String encode(ProtocolEncodeRequest encodeRequest) throws CodecException { + if (!codecJscriptProvider.containsKey(encodeRequest.getDefinition().getProvider())) { + load(encodeRequest.getDefinition()); + } + // 如果编解码内部不包含 encode 方法,则直接返回原串 + if (methodCache.get(encodeRequest.getDefinition().getId()) == null + || !methodCache + .get(encodeRequest.getDefinition().getId()) + .contains(CodecMethod.encode.name())) { + return encodeRequest.getPayload(); + } + Invocable invocable = codecJscriptProvider.get(encodeRequest.getDefinition().getProvider()); + try { + Object result = + invocable.invokeFunction( + CodecMethod.encode.name(), encodeRequest.getPayload(), encodeRequest.getContext()); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getMessage(e); + log.error( + "产品型号={} 提供者={} Jscript编码失败={}", + encodeRequest.getDefinition().getId(), + encodeRequest.getDefinition().getProvider(), + e); + throw new CodecException(error); + } + } + + @Override + public void remove(String provider) { + if (StrUtil.isNotBlank(provider)) { + codecJscriptProvider.remove(provider); + } + } + + @Override + public void load(String provider, Object providerImpl) {} + + @Override + public String preDecode(ProtocolDecodeRequest protocolDecodeRequest) throws CodecException { + if (!codecJscriptProvider.containsKey(protocolDecodeRequest.getDefinition().getProvider())) { + load(protocolDecodeRequest.getDefinition()); + } + // 如果编解码内部不包含 preDecode 方法,则直接返回原串 + if (methodCache.get(protocolDecodeRequest.getDefinition().getId()) == null + || !methodCache + .get(protocolDecodeRequest.getDefinition().getId()) + .contains(CodecMethod.preDecode.name())) { + return protocolDecodeRequest.getPayload(); + } + Invocable invocable = + codecJscriptProvider.get(protocolDecodeRequest.getDefinition().getProvider()); + try { + Object result = + invocable.invokeFunction( + CodecMethod.preDecode.name(), protocolDecodeRequest.getPayload()); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getMessage(e); + log.error( + "产品型号={} 提供者={} Jscript预编码失败={}", + protocolDecodeRequest.getDefinition().getId(), + protocolDecodeRequest.getDefinition().getProvider(), + e); + throw new CodecException(error); + } + } + + // 在ProtocolCodecJscript类中实现isLoaded方法 + @Override + public boolean isLoaded(String provider, CodecMethod codecMethod) { + // 检查provider是否是否已加载到缓存中 + if (!codecJscriptProvider.containsKey(provider)) { + return false; + } + // 检查是否是否包含目标方法(可选,根据实际需求决定 + Set methods = methodCache.get(provider); + if (methods == null) { + return false; + } + return methods.contains(codecMethod.name()); + } + + @Override + public String iotToYour(ProtocolEncodeRequest encodeRequest) throws CodecException { + if (!codecJscriptProvider.containsKey(encodeRequest.getDefinition().getProvider())) { + load(encodeRequest.getDefinition()); + } + // 如果编解码内部不包含 iotToYour 方法,则使用encode方法 + if (methodCache.get(encodeRequest.getDefinition().getId()) == null + || !methodCache + .get(encodeRequest.getDefinition().getId()) + .contains(CodecMethod.iotToYour.name())) { + return encode(encodeRequest); + } + Invocable invocable = codecJscriptProvider.get(encodeRequest.getDefinition().getProvider()); + try { + Object result = + invocable.invokeFunction( + CodecMethod.iotToYour.name(), encodeRequest.getPayload(), encodeRequest.getContext()); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getMessage(e); + log.error( + "产品型号={} 提供者={} Jscript iotToYour失败={}", + encodeRequest.getDefinition().getId(), + encodeRequest.getDefinition().getProvider(), + e); + throw new CodecException(error); + } + } + + @Override + public String yourToIot(ProtocolDecodeRequest decodeRequest) throws CodecException { + if (!codecJscriptProvider.containsKey(decodeRequest.getDefinition().getProvider())) { + load(decodeRequest.getDefinition()); + } + // 如果编解码内部不包含 yourToIot 方法,则使用decode方法 + if (methodCache.get(decodeRequest.getDefinition().getId()) == null + || !methodCache + .get(decodeRequest.getDefinition().getId()) + .contains(CodecMethod.yourToIot.name())) { + return decode(decodeRequest); + } + Invocable invocable = codecJscriptProvider.get(decodeRequest.getDefinition().getProvider()); + try { + Object result = + invocable.invokeFunction( + CodecMethod.yourToIot.name(), decodeRequest.getPayload(), decodeRequest.getContext()); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getMessage(e); + log.error( + "产品型号={} 提供者={} Jscript yourToIot失败={}", + decodeRequest.getDefinition().getId(), + decodeRequest.getDefinition().getProvider(), + e); + throw new CodecException(error); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/loader/AbstractProtocolCodecLoader.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/loader/AbstractProtocolCodecLoader.java new file mode 100644 index 0000000..d07f9c6 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/loader/AbstractProtocolCodecLoader.java @@ -0,0 +1,89 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.loader; + +import cn.universal.common.exception.CodecException; +import cn.universal.core.protocol.support.ProtocolCodecSupport; +import cn.universal.core.protocol.support.ProtocolCodecSupport.CodecMethod; +import lombok.extern.slf4j.Slf4j; + +/** + * 抽象协议编解码加载器基类 + * + *

提供公共的协议加载器实现,子类可以重写特定方法 + * + * @version 1.0 @Author Aleo + * @since 2025/01/20 + */ +@Slf4j +public abstract class AbstractProtocolCodecLoader implements IProtocolCodecLoader { + + /** 加载协议编解码器 */ + @Override + public void load(String productKey, CodecMethod codecMethod) throws CodecException { + log.debug("加载协议编解码器: productKey={}, method={}", productKey, codecMethod); + doLoad(productKey, codecMethod); + } + + /** 移除协议编解码器 */ + @Override + public void remove(String productKey) { + log.debug("移除协议编解码器: productKey={}", productKey); + doRemove(productKey); + } + + /** 执行编解码操作 */ + @Override + public String execute(String productKey, String payload, CodecMethod codecMethod) + throws CodecException { + if (!isLoaded(productKey)) { + load(productKey, codecMethod); + } + return doExecute(productKey, payload, codecMethod); + } + + /** + * 获取协议编解码支持实例 + * + * @return 协议编解码支持实例 + */ + protected abstract ProtocolCodecSupport getProtocolCodecSupport(); + + /** + * 执行具体的加载逻辑 - 需要子类实现 + * + * @param productKey 产品Key + * @param codecMethod 编解码方法 + * @throws CodecException 编解码异常 + */ + protected abstract void doLoad(String productKey, CodecMethod codecMethod) throws CodecException; + + /** + * 执行具体的移除逻辑 - 需要子类实现 + * + * @param productKey 产品Key + */ + protected abstract void doRemove(String productKey); + + /** + * 执行具体的编解码逻辑 - 需要子类实现 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @param codecMethod 编解码方法 + * @return 编解码结果 + * @throws CodecException 编解码异常 + */ + protected abstract String doExecute(String productKey, String payload, CodecMethod codecMethod) + throws CodecException; +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/loader/IProtocolCodecLoader.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/loader/IProtocolCodecLoader.java new file mode 100644 index 0000000..a2cb723 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/loader/IProtocolCodecLoader.java @@ -0,0 +1,62 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.loader; + +import cn.universal.common.exception.CodecException; +import cn.universal.core.protocol.support.ProtocolCodecSupport.CodecMethod; + +/** + * 协议编解码加载器接口 + * + *

定义协议编解码器的加载和管理方法 + * + * @version 1.0 @Author Aleo + * @since 2025/01/20 + */ +public interface IProtocolCodecLoader { + + /** + * 加载协议编解码器 + * + * @param productKey 产品Key + * @param codecMethod 编解码方法 + * @throws CodecException 编解码异常 + */ + void load(String productKey, CodecMethod codecMethod) throws CodecException; + + /** + * 移除协议编解码器 + * + * @param productKey 产品Key + */ + void remove(String productKey); + + /** + * 检查是否已加载 + * + * @param productKey 产品Key + * @return 是否已加载 + */ + boolean isLoaded(String productKey); + + /** + * 执行编解码操作 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @param codecMethod 编解码方法 + * @return 编解码结果 + * @throws CodecException 编解码异常 + */ + String execute(String productKey, String payload, CodecMethod codecMethod) throws CodecException; +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/magic/MagicScriptMethodExtractor.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/magic/MagicScriptMethodExtractor.java new file mode 100644 index 0000000..96494aa --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/magic/MagicScriptMethodExtractor.java @@ -0,0 +1,57 @@ +package cn.universal.core.protocol.magic; + +import cn.universal.core.engine.MagicScript; +import cn.universal.core.engine.runtime.MagicScriptRuntime; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; + +/** 基于 magic-script 提取JS脚本中的方法名工具类 */ +@Slf4j +public class MagicScriptMethodExtractor { + + /** + * 从magic-script脚本中提取所有变量/方法名(兼容原逻辑) + * + * @param scriptContent 脚本内容字符串 + * @return 脚本中定义的所有变量/方法名集合 + * @throws Exception 脚本编译失败时抛出异常 + */ + public static Set extractVarNames(String scriptContent) { + try { + // 创建并编译脚本 + MagicScript script = MagicScript.create(scriptContent, null); + MagicScriptRuntime compiled = script.compile(); + + // 获取所有变量/方法名并转为Set + String[] varNames = compiled.getVarNames(); + return varNames != null ? new HashSet<>(Arrays.asList(varNames)) : new HashSet<>(); + } catch (Exception e) { + log.info("MagicScriptMethodExtractor.extractVarNames error", e); + } + return new HashSet<>(); + } + + /** + * 从脚本中提取与目标方法集合匹配的支持方法 + * + * @param scriptContent 脚本内容 + * @param targetMethods 目标方法集合(如{decode, encode}) + * @return 脚本中存在的目标方法交集 + * @throws Exception 脚本编译失败时抛出异常 + */ + public static Set extractSupportMethods(String scriptContent, Set targetMethods) { + // 提取脚本中的所有方法名 + Set allVarNames = extractVarNames(scriptContent); + + // 计算与目标方法的交集 + Set supportMethods = new HashSet<>(); + for (String varName : allVarNames) { + if (targetMethods.contains(varName)) { + supportMethods.add(varName); + } + } + return supportMethods; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/magic/ProtocolCodecMagic.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/magic/ProtocolCodecMagic.java new file mode 100644 index 0000000..c5df151 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/magic/ProtocolCodecMagic.java @@ -0,0 +1,368 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.magic; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; +import cn.universal.common.exception.CodecException; +import cn.universal.common.exception.IoTException; +import cn.universal.core.engine.MagicScript; +import cn.universal.core.engine.MagicScriptContext; +import cn.universal.core.engine.runtime.MagicScriptRuntime; +import cn.universal.core.protocol.request.ProtocolDecodeRequest; +import cn.universal.core.protocol.request.ProtocolEncodeRequest; +import cn.universal.core.protocol.support.ProtocolCodecLoader; +import cn.universal.core.protocol.support.ProtocolCodecSupport; +import cn.universal.core.protocol.support.ProtocolCodecSupportWrapper; +import cn.universal.core.protocol.support.ProtocolCodecWrapper; +import cn.universal.core.protocol.support.ProtocolSupportDefinition; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; + +/** + * magic 脚本引擎 + * + *

https://gitee.com/ssssssss-team/magic-script + * + * @version 1.0 @Author Aleo + * @since 2023/05/19 19:28 + */ +@Slf4j +public class ProtocolCodecMagic extends ProtocolCodecSupportWrapper + implements ProtocolCodecLoader, ProtocolCodecSupport, ProtocolCodecWrapper { + + /** 编码 */ + private final Map magicEncoderProvider = new ConcurrentHashMap<>(); + + /** 解码 */ + private final Map magicDecoderProvider = new ConcurrentHashMap<>(); + + /** 解码前置解析 */ + private final Map magicPreDecoderProvider = new ConcurrentHashMap<>(); + + /** 协议方法缓存 */ + private final Map> methodCache = new ConcurrentHashMap<>(); + + private ProtocolCodecMagic() {} + + private static class ProtocolCodecJscriptProviderHoler { + + private static ProtocolCodecMagic INSTANCE = new ProtocolCodecMagic(); + } + + public static ProtocolCodecMagic getInstance() { + return ProtocolCodecJscriptProviderHoler.INSTANCE; + } + + @Override + public String getProviderType() { + return "magic"; + } + + @Override + public void load(ProtocolSupportDefinition definition) { + return; + } + + private void load(ProtocolSupportDefinition definition, CodecMethod codecMethod) + throws CodecException { + try { + Map config = definition.getConfiguration(); + // JS原始代码 + String location = + (String) + Optional.ofNullable(config.get("location")) + .map(String::valueOf) + .orElseThrow( + () -> { + return new IoTException( + "magic engine source code not exist, can not do encode or decode "); + }); + // 获取提供者,产品唯一 + String provider = + (String) + Optional.ofNullable(config.get("provider")) + .map(String::valueOf) + .map(String::trim) + .orElseThrow( + () -> { + return new CodecException("provider 不能为空"); + }); + // magic 方法调用必须使用return 方法名(参数); + switch (codecMethod) { + case decode: + location = location + " \n return decode(payload,context);"; + break; + case encode: + location = location + " \n return encode(payload,context);"; + break; + case preDecode: + location = location + " \n return preDecode(payload,context);"; + break; + case codecAdd: + location = location + " \n return codecAdd(payload,context);"; + break; + case codecDelete: + location = location + " \n return codecDelete(payload,context);"; + break; + case codecUpdate: + location = location + " \n return codecUpdate(payload,context);"; + break; + case codecQuery: + location = location + " \n return codecQuery(payload,context);"; + break; + case iotToYour: + location = location + " \n return iotToYour(payload,context);"; + break; + case yourToIot: + location = location + " \n return yourToIot(payload,context);"; + break; + default: + location = location + " \n return encode(payload,context);"; + break; + } + // 初始化 + long t = System.currentTimeMillis(); + MagicScript script = MagicScript.create(location, null); + MagicScriptRuntime compile = script.compile(); + // 处理内部实现了哪些方法 + evalMethodCache(definition, compile); + log.info("编译耗时:{}", (System.currentTimeMillis() - t) + "ms"); + t = System.currentTimeMillis(); + if (codecMethod.equals(CodecMethod.decode)) { + magicDecoderProvider.put(provider, script); + } else if (codecMethod.equals(CodecMethod.preDecode)) { + magicPreDecoderProvider.put(provider, script); + } else { + magicEncoderProvider.put(provider, script); + } + } catch (Exception e) { + String error = ExceptionUtil.getRootCauseMessage(e); + log.error("加载magic编解码出错={}", e); + throw new CodecException(error); + } + } + + private void evalMethodCache(ProtocolSupportDefinition definition, MagicScriptRuntime compile) { + String[] varNames = compile.getVarNames(); + if (varNames != null && varNames.length > 0) { + Set methods = new HashSet<>(); + Set collect = + Stream.of(CodecMethod.values()).map(Enum::name).collect(Collectors.toSet()); + for (String method : varNames) { + if (collect.contains(method)) { + methods.add(method); + } + } + methodCache.put(definition.getId(), methods); + } + } + + @Override + public String decode(ProtocolDecodeRequest decodeRequest) throws CodecException { + try { + if (!magicDecoderProvider.containsKey(decodeRequest.getDefinition().getProvider())) { + // 增加同步锁,防止异常 + synchronized (magicDecoderProvider) { + load(decodeRequest.getDefinition(), CodecMethod.decode); + } + } + // 如果编解码内部不包含decode方法,则直接返回原串 + if (methodCache.get(decodeRequest.getDefinition().getId()) == null + || !methodCache + .get(decodeRequest.getDefinition().getId()) + .contains(ProtocolCodecSupport.CodecMethod.decode.name())) { + return decodeRequest.getPayload(); + } + MagicScript magicScript = + magicDecoderProvider.get(decodeRequest.getDefinition().getProvider()); + MagicScriptContext context = new MagicScriptContext(); + context.set("payload", decodeRequest.getPayload()); + context.set("context", decodeRequest.getContext()); + Object result = magicScript.execute(context); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getRootCauseMessage(e); + log.error( + "产品型号={} 提供者={} magic解码失败", + decodeRequest.getDefinition().getId(), + decodeRequest.getDefinition().getProvider(), + e); + throw new CodecException(error); + } + } + + @Override + public String encode(ProtocolEncodeRequest encodeRequest) throws CodecException { + try { + if (!magicEncoderProvider.containsKey(encodeRequest.getDefinition().getProvider())) { + load(encodeRequest.getDefinition(), CodecMethod.encode); + } + // 如果编解码内部不包含 encode 方法,则直接返回原串 + if (methodCache.get(encodeRequest.getDefinition().getId()) == null + || !methodCache + .get(encodeRequest.getDefinition().getId()) + .contains(ProtocolCodecSupport.CodecMethod.encode.name())) { + return encodeRequest.getPayload(); + } + MagicScript magicScript = + magicEncoderProvider.get(encodeRequest.getDefinition().getProvider()); + MagicScriptContext context = new MagicScriptContext(); + context.set("payload", encodeRequest.getPayload()); + context.set("context", encodeRequest.getContext()); + Object execute = magicScript.execute(context); + log.debug("execute={}", execute.getClass()); + return str(execute); + } catch (Exception e) { + String error = ExceptionUtil.getRootCauseMessage(e); + log.error( + "产品型号={} 提供者={} magic编码失败", + encodeRequest.getDefinition().getId(), + encodeRequest.getDefinition().getProvider(), + e); + throw new CodecException(error); + } + } + + @Override + public void remove(String provider) { + if (StrUtil.isNotBlank(provider)) { + magicEncoderProvider.remove(provider); + magicDecoderProvider.remove(provider); + magicPreDecoderProvider.remove(provider); + } + } + + @Override + public void load(String provider, Object providerImpl) {} + + @Override + public String preDecode(ProtocolDecodeRequest protocolDecodeRequest) throws CodecException { + try { + if (!magicPreDecoderProvider.containsKey( + protocolDecodeRequest.getDefinition().getProvider())) { + load(protocolDecodeRequest.getDefinition(), CodecMethod.preDecode); + } + // 如果编解码内部不包含 preDecode 方法,则直接返回原串 + if (methodCache.get(protocolDecodeRequest.getDefinition().getId()) == null + || !methodCache + .get(protocolDecodeRequest.getDefinition().getId()) + .contains(ProtocolCodecSupport.CodecMethod.preDecode.name())) { + return protocolDecodeRequest.getPayload(); + } + MagicScript magicScript = + magicPreDecoderProvider.get(protocolDecodeRequest.getDefinition().getProvider()); + MagicScriptContext context = new MagicScriptContext(); + context.set("payload", protocolDecodeRequest.getPayload()); + Object result = magicScript.execute(context); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getRootCauseMessage(e); + log.error( + "产品型号={} 提供者={} magic前置解码失败{}", + protocolDecodeRequest.getDefinition().getId(), + protocolDecodeRequest.getDefinition().getProvider(), + error); + throw new CodecException(error); + } + } + + // 在ProtocolCodecMagic类中修改isLoaded方法 + + @Override + public boolean isLoaded(String provider, CodecMethod codecMethod) { + switch (codecMethod) { + case decode: + return magicDecoderProvider.containsKey(provider); + case encode: + return magicEncoderProvider.containsKey(provider); + case preDecode: + return magicPreDecoderProvider.containsKey(provider); + case iotToYour: + return magicEncoderProvider.containsKey(provider); // 使用encode的provider + case yourToIot: + return magicDecoderProvider.containsKey(provider); // 使用decode的provider + default: + return false; + } + } + + @Override + public String iotToYour(ProtocolEncodeRequest encodeRequest) throws CodecException { + try { + if (!magicEncoderProvider.containsKey(encodeRequest.getDefinition().getProvider())) { + load(encodeRequest.getDefinition(), CodecMethod.iotToYour); + } + // 如果编解码内部不包含 iotToYour 方法,则使用encode方法 + if (methodCache.get(encodeRequest.getDefinition().getId()) == null + || !methodCache + .get(encodeRequest.getDefinition().getId()) + .contains(CodecMethod.iotToYour.name())) { + return encode(encodeRequest); + } + MagicScript magicScript = + magicEncoderProvider.get(encodeRequest.getDefinition().getProvider()); + MagicScriptContext context = new MagicScriptContext(); + context.set("payload", encodeRequest.getPayload()); + context.set("context", encodeRequest.getContext()); + Object execute = magicScript.execute(context); + log.debug("execute={}", execute.getClass()); + return str(execute); + } catch (Exception e) { + String error = ExceptionUtil.getRootCauseMessage(e); + log.error( + "产品型号={} 提供者={} magic iotToYour失败", + encodeRequest.getDefinition().getId(), + encodeRequest.getDefinition().getProvider(), + e); + throw new CodecException(error); + } + } + + @Override + public String yourToIot(ProtocolDecodeRequest decodeRequest) throws CodecException { + try { + if (!magicDecoderProvider.containsKey(decodeRequest.getDefinition().getProvider())) { + load(decodeRequest.getDefinition(), CodecMethod.yourToIot); + } + // 如果编解码内部不包含 yourToIot 方法,则使用decode方法 + if (methodCache.get(decodeRequest.getDefinition().getId()) == null + || !methodCache + .get(decodeRequest.getDefinition().getId()) + .contains(CodecMethod.yourToIot.name())) { + return decode(decodeRequest); + } + MagicScript magicScript = + magicDecoderProvider.get(decodeRequest.getDefinition().getProvider()); + MagicScriptContext context = new MagicScriptContext(); + context.set("payload", decodeRequest.getPayload()); + context.set("context", decodeRequest.getContext()); + Object result = magicScript.execute(context); + return str(result); + } catch (Exception e) { + String error = ExceptionUtil.getRootCauseMessage(e); + log.error( + "产品型号={} 提供者={} magic yourToIot失败", + decodeRequest.getDefinition().getId(), + decodeRequest.getDefinition().getProvider(), + e); + throw new CodecException(error); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/package-info.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/package-info.java new file mode 100644 index 0000000..b410989 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/package-info.java @@ -0,0 +1,13 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol; diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/request/ProtocolDecodeRequest.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/request/ProtocolDecodeRequest.java new file mode 100644 index 0000000..eff0ae3 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/request/ProtocolDecodeRequest.java @@ -0,0 +1,41 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.request; + +import cn.universal.core.protocol.support.ProtocolSupportDefinition; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @version 1.0 @Author Aleo + * @since 2025/8/9 21:17 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ProtocolDecodeRequest { + + private ProtocolSupportDefinition definition; + // 原始消息 + private String payload; + // 上下文 + private Object context; + + public ProtocolDecodeRequest(ProtocolSupportDefinition definition, String payload) { + this.definition = definition; + this.payload = payload; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/request/ProtocolEncodeRequest.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/request/ProtocolEncodeRequest.java new file mode 100644 index 0000000..bbffda1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/request/ProtocolEncodeRequest.java @@ -0,0 +1,39 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.request; + +import cn.universal.core.protocol.support.ProtocolSupportDefinition; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @version 1.0 @Author Aleo + * @since 2025/8/9 21:17 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ProtocolEncodeRequest { + + private ProtocolSupportDefinition definition; + private String payload; + private Object context; + + public ProtocolEncodeRequest(ProtocolSupportDefinition definition, String payload) { + this.definition = definition; + this.payload = payload; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolCodecLoader.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolCodecLoader.java new file mode 100644 index 0000000..25dbe36 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolCodecLoader.java @@ -0,0 +1,39 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.support; + +import cn.universal.common.exception.CodecException; + +/** + * 协议编解码支持 + * + * @version 1.0 @Author Aleo + * @since 2025/8/9 19:06 + */ +public interface ProtocolCodecLoader { + + /** + * 协议全路径包名 + * + * @return + */ + String getProviderType(); + + /** + * 根据全路径包名加载jar包插件 + * + * @param definition + * @return + */ + default void load(ProtocolSupportDefinition definition) throws CodecException {} +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolCodecSupport.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolCodecSupport.java new file mode 100644 index 0000000..7bf986f --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolCodecSupport.java @@ -0,0 +1,84 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.support; + +import cn.universal.common.exception.CodecException; +import cn.universal.core.protocol.request.ProtocolDecodeRequest; +import cn.universal.core.protocol.request.ProtocolEncodeRequest; + +/** + * 编解码接口 + * + * @version 1.0 @Author Aleo + * @since 2025/8/9 19:08 + */ +public interface ProtocolCodecSupport { + + default String preDecode(ProtocolDecodeRequest protocolDecodeRequest) throws CodecException { + return null; + } + + /** + * 解码 + * + * @param decodeRequest 消息内容 + * @return JSON字符串 + */ + String decode(ProtocolDecodeRequest decodeRequest) throws CodecException; + + /** + * 编码 + * + * @param encodeRequest 消息内容 + */ + String encode(ProtocolEncodeRequest encodeRequest) throws CodecException; + + /** + * IoT到第三方数据转换 + * + * @param encodeRequest 消息内容 + * @return 转换后的数据 + */ + default String iotToYour(ProtocolEncodeRequest encodeRequest) throws CodecException { + return encode(encodeRequest); + } + + /** + * 第三方到IoT数据转换 + * + * @param decodeRequest 消息内容 + * @return 转换后的数据 + */ + default String yourToIot(ProtocolDecodeRequest decodeRequest) throws CodecException { + return decode(decodeRequest); + } + + boolean isLoaded(String provider, CodecMethod codecMethod); + + static enum CodecMethod { + /** 解码 */ + decode, + /** 编码 */ + encode, + /** 预解码 */ + preDecode, + codecAdd, + codecDelete, + codecUpdate, + codecQuery, + iotToYour, + yourToIot, + codecFunction, + codecOther + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolCodecSupportWrapper.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolCodecSupportWrapper.java new file mode 100644 index 0000000..3f91d31 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolCodecSupportWrapper.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.support; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.json.JSONUtil; + +/** + * 编解码支持类 + * + * @version 1.0 @Author Aleo + * @since 2023/5/20 19:08 + */ +public abstract class ProtocolCodecSupportWrapper { + + protected String str(Object obj) { + if (null == obj || "null".equalsIgnoreCase(obj + "")) { + return CharSequenceUtil.EMPTY; + } else if (obj instanceof String) { + return obj.toString(); + } else { + return JSONUtil.toJsonStr(obj); + } + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolCodecWrapper.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolCodecWrapper.java new file mode 100644 index 0000000..04d97fe --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolCodecWrapper.java @@ -0,0 +1,20 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.support; + +public interface ProtocolCodecWrapper { + + void load(String provider, Object providerImpl); + + void remove(String provider); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolSupportDefinition.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolSupportDefinition.java new file mode 100644 index 0000000..bc371b1 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/protocol/support/ProtocolSupportDefinition.java @@ -0,0 +1,45 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.protocol.support; + +import cn.universal.core.protocol.support.ProtocolCodecSupport.CodecMethod; +import java.util.Map; +import java.util.Set; +import lombok.Data; + +/** + * @version 1.0 @Author Aleo + * @since 2025/8/9 19:11 + */ +@Data +public class ProtocolSupportDefinition { + + private String id; + private String name; + private String description; + private String provider; + private String type; + private byte state; + private Map configuration; + private Set supportMethods; + + public boolean supportMethod(CodecMethod method) { + if (supportMethods == null) { + return false; + } + if (supportMethods.contains(method.name())) { + return true; + } + return false; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/AbstractCodecService.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/AbstractCodecService.java new file mode 100644 index 0000000..8a02bfa --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/AbstractCodecService.java @@ -0,0 +1,429 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.service; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant.MessageType; +import cn.universal.common.exception.CodecException; +import cn.universal.core.message.UPRequest; +import cn.universal.core.protocol.jar.ProtocolCodecJar; +import cn.universal.core.protocol.jscrtipt.ProtocolCodecJscript; +import cn.universal.core.protocol.magic.ProtocolCodecMagic; +import cn.universal.core.protocol.request.ProtocolDecodeRequest; +import cn.universal.core.protocol.request.ProtocolEncodeRequest; +import cn.universal.core.protocol.support.ProtocolCodecSupport; +import cn.universal.core.protocol.support.ProtocolCodecSupport.CodecMethod; +import cn.universal.core.protocol.support.ProtocolSupportDefinition; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; + +/** + * 抽象编解码服务基类 + * + *

提供公共的编解码实现,子类可以重写特定方法 + * + * @version 1.0 @Author Aleo + * @since 2025/01/20 + */ +@Slf4j +public abstract class AbstractCodecService implements ICodecService { + + /** + * 获取协议定义,确保脚本已加载 + * + * @param productKey 产品Key + * @param codecMethod 编解码方法 + * @return 协议支持定义 + */ + protected ProtocolSupportDefinition getProtocolDefinitionWithScriptIfNeeded( + String productKey, CodecMethod codecMethod) { + ProtocolSupportDefinition protocolDef = getProtocolDefinitionNoScript(productKey); + + if (protocolDef != null && protocolDef.supportMethod(codecMethod)) { + ProtocolCodecSupport protocolCodecSupport = getProtocolCodecProvider(protocolDef.getType()); + if (!protocolCodecSupport.isLoaded(protocolDef.getProvider(), codecMethod)) { + synchronized (this) { + protocolDef = getProtocolDefinitionWithScript(productKey); + } + } + } + + return protocolDef; + } + + /** 解码 - 带上下文 */ + @Override + public List decode( + String productKey, String payload, Object context, Class elementType) { + ProtocolSupportDefinition protocolDef = + getProtocolDefinitionWithScriptIfNeeded(productKey, CodecMethod.decode); + List rs = null; + long t1 = System.currentTimeMillis(); + + if (protocolDef != null && protocolDef.supportMethod(CodecMethod.decode)) { + ProtocolCodecSupport protocolCodecSupport = getProtocolCodecProvider(protocolDef.getType()); + try { + String result = + protocolCodecSupport.decode(new ProtocolDecodeRequest(protocolDef, payload, context)); + if (StrUtil.isNotBlank(result)) { + if (JSONUtil.isTypeJSONObject(result)) { + rs = Stream.of(JSONUtil.toBean(result, elementType)).collect(Collectors.toList()); + } else if (JSONUtil.isTypeJSONArray(result)) { + rs = JSONUtil.toList(result, elementType); + } else { + rs = emptyProtocol(result, elementType); + } + } + log.debug("产品ProductKey={} 原始报文={} 解码报文={}", productKey, payload, result); + } catch (Exception e) { + log.error("产品编号={} 原始报文={} 解码报错", productKey, payload, e); + } + } else { + rs = emptyProtocol(payload, elementType); + } + + long t2 = System.currentTimeMillis(); + log.info( + "产品编号={} 原始报文={} 解码={} 耗时={}ms", productKey, payload, JSONUtil.toJsonStr(rs), (t2 - t1)); + return rs; + } + + /** 解码 - 简化版本 */ + @Override + public List decode(String productKey, String payload, Class elementType) { + return decode(productKey, payload, null, elementType); + } + + /** 解码为UPRequest列表 */ + @Override + public List decode(String productKey, String payload) { + return decode(productKey, payload, null, UPRequest.class); + } + + /** 编码 */ + @Override + public String encode(String productKey, String payload) { + ProtocolSupportDefinition protocolDef = + getProtocolDefinitionWithScriptIfNeeded(productKey, CodecMethod.encode); + String result = null; + long t1 = System.currentTimeMillis(); + + if (protocolDef != null && protocolDef.supportMethod(CodecMethod.encode)) { + ProtocolCodecSupport protocolCodecSupport = getProtocolCodecProvider(protocolDef.getType()); + try { + result = protocolCodecSupport.encode(new ProtocolEncodeRequest(protocolDef, payload)); + } catch (CodecException e) { + log.error("产品编号={} 原始报文={} 编码报错", productKey, payload, e); + } + } + + long t2 = System.currentTimeMillis(); + log.info("产品编号={} 原始报文={} 编码={} 耗时={}ms", productKey, payload, result, (t2 - t1)); + return result; + } + + /** 预解码 */ + @Override + public UPRequest preDecode(String productKey, String payload) { + ProtocolSupportDefinition protocolDef = + getProtocolDefinitionWithScriptIfNeeded(productKey, CodecMethod.preDecode); + UPRequest result = null; + long t1 = System.currentTimeMillis(); + + if (protocolDef != null && protocolDef.supportMethod(CodecMethod.preDecode)) { + ProtocolCodecSupport protocolCodecSupport = getProtocolCodecProvider(protocolDef.getType()); + try { + String preDecodeResult = + protocolCodecSupport.preDecode(new ProtocolDecodeRequest(protocolDef, payload)); + if (StrUtil.isNotBlank(preDecodeResult)) { + result = JSONUtil.toBean(preDecodeResult, UPRequest.class); + } + } catch (Exception e) { + log.error("产品编号={} 原始报文={} 预解码报错", productKey, payload, e); + } + } + + long t2 = System.currentTimeMillis(); + log.info("产品编号={} 原始报文={} 预解码={} 耗时={}ms", productKey, payload, result, (t2 - t1)); + return result; + } + + /** 通用编解码方法 - 支持所有CodecMethod类型 */ + @Override + public String codec(String productKey, String payload, CodecMethod codecMethod) { + ProtocolSupportDefinition protocolDef = + getProtocolDefinitionWithScriptIfNeeded(productKey, codecMethod); + String result = null; + long t1 = System.currentTimeMillis(); + + if (protocolDef != null && protocolDef.supportMethod(codecMethod)) { + ProtocolCodecSupport protocolCodecSupport = getProtocolCodecProvider(protocolDef.getType()); + try { + switch (codecMethod) { + case decode: + result = protocolCodecSupport.decode(new ProtocolDecodeRequest(protocolDef, payload)); + break; + case encode: + result = protocolCodecSupport.encode(new ProtocolEncodeRequest(protocolDef, payload)); + break; + case preDecode: + result = + protocolCodecSupport.preDecode(new ProtocolDecodeRequest(protocolDef, payload)); + break; + case codecAdd: + result = protocolCodecSupport.encode(new ProtocolEncodeRequest(protocolDef, payload)); + break; + case codecDelete: + result = protocolCodecSupport.encode(new ProtocolEncodeRequest(protocolDef, payload)); + break; + case codecUpdate: + result = protocolCodecSupport.encode(new ProtocolEncodeRequest(protocolDef, payload)); + break; + case codecQuery: + result = protocolCodecSupport.decode(new ProtocolDecodeRequest(protocolDef, payload)); + break; + case iotToYour: + // 使用基础执行引擎的iotToYour方法 + result = + protocolCodecSupport.iotToYour(new ProtocolEncodeRequest(protocolDef, payload)); + break; + case yourToIot: + // 使用基础执行引擎的yourToIot方法 + result = + protocolCodecSupport.yourToIot(new ProtocolDecodeRequest(protocolDef, payload)); + break; + case codecFunction: + // 映射到iotToYour + result = + protocolCodecSupport.iotToYour(new ProtocolEncodeRequest(protocolDef, payload)); + break; + case codecOther: + // 映射到yourToIot + result = + protocolCodecSupport.yourToIot(new ProtocolDecodeRequest(protocolDef, payload)); + break; + default: + log.warn("不支持的编解码方法: {}", codecMethod); + break; + } + } catch (Exception e) { + log.error("产品编号={} 原始报文={} {}报错", productKey, payload, codecMethod, e); + } + } + + long t2 = System.currentTimeMillis(); + log.info("产品编号={} 原始报文={} {}={} 耗时={}ms", productKey, payload, codecMethod, result, (t2 - t1)); + return result; + } + + /** 检查是否支持指定的编解码方法 */ + @Override + public boolean isSupported(String productKey, CodecMethod codecMethod) { + ProtocolSupportDefinition protocolDef = getProtocolDefinitionNoScript(productKey); + return protocolDef != null && protocolDef.supportMethod(codecMethod); + } + + /** IoT到第三方数据转换编解码 */ + @Override + public String iotToYour(String productKey, String payload) { + return codec(productKey, payload, CodecMethod.iotToYour); + } + + /** 第三方到IoT数据转换编解码 */ + @Override + public String yourToIot(String productKey, String payload) { + return codec(productKey, payload, CodecMethod.yourToIot); + } + + /** + * 获取协议定义 - 需要子类实现 + * + * @param productKey 产品Key + * @return 协议支持定义 + */ + protected abstract ProtocolSupportDefinition getProtocolDefinition(String productKey); + + /** + * 获取协议定义(不包含脚本内容)- 需要子类实现 + * + *

用于性能优化,避免每次加载包含脚本内容的完整协议定义 + * + * @param productKey 产品Key + * @return 协议支持定义(不包含脚本内容) + */ + protected abstract ProtocolSupportDefinition getProtocolDefinitionNoScript(String productKey); + + /** + * 获取协议定义(带脚本内容)- 需要子类实现 + * + * @param productKey 产品Key + * @return 协议支持定义(包含脚本内容) + */ + protected abstract ProtocolSupportDefinition getProtocolDefinitionWithScript(String productKey); + + /** + * 获取编解码提供者 + * + * @param supportType 支持类型 + * @return 协议编解码支持实例 + */ + protected ProtocolCodecSupport getProtocolCodecProvider(String supportType) { + if (supportType == null) { + return null; + } + if (supportType.equalsIgnoreCase("jar")) { + return ProtocolCodecJar.getInstance(); + } else if (supportType.equalsIgnoreCase("jscript")) { + return ProtocolCodecJscript.getInstance(); + } else if (supportType.equalsIgnoreCase("magic")) { + return ProtocolCodecMagic.getInstance(); + } + return null; + } + + /** + * 当编解码为空时的处理 - 泛型版本 + * + * @param payload 原始数据 + * @param elementType 目标类型 + * @param 泛型类型 + * @return 处理后的对象列表 + */ + protected List emptyProtocol(String payload, Class elementType) { + List list = new ArrayList<>(); + try { + // 1.消息是JSON + List list1 = doJson(payload, elementType, list); + if (list1 != null) return list1; + + // 2.消息是JSONArray + List list2 = doJsonArray(payload, elementType, list); + if (list2 != null) return list2; + + // 3.不是JSON也不是JSONArray,创建UPRequest + UPRequest upRequest = new UPRequest(); + upRequest.setEmptyProtocol(true); + upRequest.setMessageType(MessageType.PROPERTIES); + upRequest.setPayload(payload); + // 将UPRequest转换为R类型 + R upMsg = JSONUtil.toBean(JSONUtil.toJsonStr(upRequest), elementType); + list.add(upMsg); + } catch (Exception e) { + log.warn("字符串转换失败: {}", e.getMessage()); + } + return list; + } + + /** + * 处理JSON格式的数据 + * + * @param payload 原始数据 + * @param elementType 目标类型 + * @param list 结果列表 + * @param 泛型类型 + * @return 处理后的对象列表 + */ + private List doJson(String payload, Class elementType, List list) { + if (JSONUtil.isTypeJSON(payload)) { + JSONObject payloadJson = JSONUtil.parseObj(payload); + + // 平台的消息结构示例: + // { + // "messageType": "PROPERTIES", + // "properties": { + // "battery": "93", + // "csq": 26, + // "ecl": "87", + // "sn": "37532", + // "switchStatus": 0 + // } + // } + if (payloadJson != null + && (MessageType.PROPERTIES + .name() + .equalsIgnoreCase(payloadJson.getStr("messageType", ""))) + || MessageType.EVENT.name().equalsIgnoreCase(payloadJson.getStr("messageType", ""))) { + R upMsg = JSONUtil.toBean(payloadJson, elementType); + list.add(upMsg); + return list; + } + // 否则可能是物模型结构示例: + // { + // "battery": "93", + // "csq": 26, + // "ecl": "87", + // "sn": "37532", + // "switchStatus": 0 + // } + else { + UPRequest things = new UPRequest(); + things.setProperties(payloadJson); + things.setMessageType(MessageType.PROPERTIES); + things.setPayload(payload); + // 将UPRequest转换为R类型 + R upMsg = JSONUtil.toBean(JSONUtil.toJsonStr(things), elementType); + list.add(upMsg); + return list; + } + } + return null; + } + + /** + * 处理JSONArray格式的数据 + * + * @param payload 原始数据 + * @param elementType 目标类型 + * @param list 结果列表 + * @param 泛型类型 + * @return 处理后的对象列表 + */ + private List doJsonArray(String payload, Class elementType, List list) { + if (JSONUtil.isTypeJSONArray(payload)) { + JSONArray payloadArray = JSONUtil.parseArray(payload); + + // JSONArray结构示例: + // [ + // { + // "messageType": "PROPERTIES", + // "properties": { + // "battery": "93", + // "csq": 26 + // } + // }, + // { + // "messageType": "EVENT", + // "event": "alarm", + // "data": { + // "level": "high" + // } + // } + // ] + for (int i = 0; i < payloadArray.size(); i++) { + JSONObject item = payloadArray.getJSONObject(i); + if (item != null) { + R upMsg = JSONUtil.toBean(item, elementType); + list.add(upMsg); + } + } + return list; + } + return null; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/Codec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/Codec.java new file mode 100644 index 0000000..ade9205 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/Codec.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2025, cn-universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ +package cn.universal.core.service; + +import cn.universal.core.message.UPRequest; +import java.util.List; + +public interface Codec { + + /** + * 消息解码->物模型 + * + * @param productKey 产品ProductKey + * @param payload 实际入参字符串 + * @param elementType 格式化类型 + * @param 泛型 + * @return 返回解码后的结构化消息 + */ + List decode(String productKey, String payload, Class elementType); + + /** + * 消息解码->物模型 + * + * @param productKey 产品ProductKey + * @param payload 实际入参字符串 + * @return 返回解码后的结构化消息 + */ + default List decode(String productKey, String payload) { + return null; + } + + /** + * 结构化消息->设备识别(二进制、16进制、其他) + * + * @param productKey 产品key + * @param payload 消息的原串 + */ + String encode(String productKey, String payload); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/CodecServiceFactory.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/CodecServiceFactory.java new file mode 100644 index 0000000..51c08ae --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/CodecServiceFactory.java @@ -0,0 +1,61 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 编解码服务工厂 + * + *

用于获取不同协议的编解码服务 + * + * @version 1.0 @Author Aleo + * @since 2025/01/20 + */ +@Slf4j +@Component +public class CodecServiceFactory { + + @Autowired private ICodecService defaultCodecService; + + /** + * 获取默认编解码服务 + * + * @return 默认编解码服务 + */ + public ICodecService getDefaultCodecService() { + return defaultCodecService; + } + + /** + * 根据协议类型获取编解码服务 + * + * @param protocolType 协议类型 + * @return 对应的编解码服务 + */ + public ICodecService getCodecService(String protocolType) { + return defaultCodecService; + } + + /** + * 检查是否支持指定协议 + * + * @param protocolType 协议类型 + * @return 是否支持 + */ + public boolean isSupported(String protocolType) { + return true; // 默认服务支持所有协议 + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/ICodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/ICodec.java new file mode 100644 index 0000000..1c7bd6a --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/ICodec.java @@ -0,0 +1,38 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.service; + +import cn.universal.core.message.UPRequest; + +/** 消息编解码 */ +public interface ICodec { + + default String version() { + return "1.0.0"; + } + + /** 上行消息消息,预编码 */ + default UPRequest preDecode(String productKey, String message) { + return null; + } + + /** 进编码前特殊处理 附加影子 */ + default String beforeEncode(String productKey, String deviceId, String config, String function) { + return function; + } + + /** 下行消息编码 */ + default String spliceDown(String productKey, String payload) { + return null; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/ICodecService.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/ICodecService.java new file mode 100644 index 0000000..3407409 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/ICodecService.java @@ -0,0 +1,115 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.service; + +import cn.universal.core.message.UPRequest; +import cn.universal.core.protocol.support.ProtocolCodecSupport.CodecMethod; +import java.util.List; + +/** + * 编解码服务接口 + * + *

定义统一的编解码方法,支持面向接口编程 + * + * @version 1.0 @Author Aleo + * @since 2025/01/20 + */ +public interface ICodecService { + + /** + * 解码 - 最完善的方法,支持所有协议类型 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @param context 上下文对象 + * @param elementType 目标类型 + * @param 泛型类型 + * @return 解码后的对象列表 + */ + List decode(String productKey, String payload, Object context, Class elementType); + + /** + * 解码 - 简化版本 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @param elementType 目标类型 + * @param 泛型类型 + * @return 解码后的对象列表 + */ + List decode(String productKey, String payload, Class elementType); + + /** + * 解码为UPRequest列表 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @return UPRequest列表 + */ + List decode(String productKey, String payload); + + /** + * 编码 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @return 编码后的字符串 + */ + String encode(String productKey, String payload); + + /** + * 预解码 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @return 预解码后的UPRequest + */ + UPRequest preDecode(String productKey, String payload); + + /** + * 通用编解码方法 - 支持所有CodecMethod类型 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @param codecMethod 编解码方法类型 + * @return 编解码结果 + */ + String codec(String productKey, String payload, CodecMethod codecMethod); + + /** + * IoT到第三方数据转换编解码 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @return 转换后的数据 + */ + String iotToYour(String productKey, String payload); + + /** + * 第三方到IoT数据转换编解码 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @return 转换后的数据 + */ + String yourToIot(String productKey, String payload); + + /** + * 检查是否支持指定的编解码方法 + * + * @param productKey 产品Key + * @param codecMethod 编解码方法类型 + * @return 是否支持 + */ + boolean isSupported(String productKey, CodecMethod codecMethod); +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/IDown.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/IDown.java new file mode 100644 index 0000000..05dca21 --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/IDown.java @@ -0,0 +1,45 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.service; + +import cn.hutool.json.JSONObject; +import cn.universal.common.domain.R; + +/** (下行)平台到设备 */ +public interface IDown { + /** 执行服务名称 */ + String name(); + + /** 执行服务code */ + String code(); + + /** 下行处理 */ + R doAction(String msg); + + /** 产品级下行处理 */ + default R downToThirdPlatform(String msg) { + return null; + } + + default R doAction(JSONObject msg) { + return null; + } + + /** + * 保存设备云端指令 + * + * @param productKey 产品key + * @param deviceId 设备序列号 + */ + default void storeCommand(String productKey, String deviceId, Object data) {} +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/IOta.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/IOta.java new file mode 100644 index 0000000..e78b8fa --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/IOta.java @@ -0,0 +1,13 @@ +package cn.universal.core.service; + +/* + * + * Copyright (c) 2025, platform-universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ public interface IOta {} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/IUP.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/IUP.java new file mode 100644 index 0000000..c25e5bf --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/IUP.java @@ -0,0 +1,53 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.service; + +import cn.universal.core.message.UPRequest; +import java.util.List; + +/** 上行,进行业务处理和转发 */ +public interface IUP { + + /** 返回服务名称 */ + String name(); + + default void asyncUP(String request) {} + + default void asyncUP(UPRequest request) {} + + /** 上行处理 */ + default Object debugUP(String upMsg) { + return null; + } + + /** 上行处理 */ + default Object up(String upMsg) { + return null; + } + + /** */ + default List up(UPRequest request) { + return null; + } + + /** 模拟上行异步 */ + default void debugAsyncUP(String debugMsg) {} + + /** + * 重新发送云端指令 + * + * @param productKey 产品key + * @param deviceId 设备序列号 + */ + default void resendStoreCommand(String productKey, String deviceId) {} +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/IoTDownlFactory.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/IoTDownlFactory.java new file mode 100644 index 0000000..d39f90b --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/IoTDownlFactory.java @@ -0,0 +1,196 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.core.service; + +import cn.universal.common.domain.R; +import cn.universal.common.exception.IoTException; +import cn.universal.core.protocol.ProtocolModuleInfo; +import cn.universal.core.protocol.ProtocolModuleRegistry; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * 多实现路由 + * + * @version 1.0 @Author Aleo + * @since 2025/8/12 19:11 + */ +@Component +@Slf4j +public class IoTDownlFactory implements ApplicationContextAware { + + private static Map iDownMap = new HashMap<>(); + private static Map iupMap = new HashMap<>(); + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + // 处理下行 + Map downMap = applicationContext.getBeansOfType(IDown.class); + downMap.forEach((key, value) -> iDownMap.put(value.code(), value)); + log.info("[CORE][服务工厂] 初始化IDown服务: {}", iDownMap.keySet()); + // 处理上行 + Map upMap = applicationContext.getBeansOfType(IUP.class); + upMap.forEach((key, value) -> iupMap.put(value.name(), value)); + log.info("[CORE][服务工厂] 初始化IUP服务: {}", iupMap.keySet()); + } + + public static T getIDown(String code) { + // T service = (T) iDownMap.get(code); + // if (service == null) { + // log.warn("[CORE][服务工厂] 未找到下行服务: {}, 可用服务: {}", code, iDownMap.keySet()); + // } + // return service; + return getIDownOrThrow(code); + } + + public static T getIUP(String code) { + T service = (T) iupMap.get(code); + if (service == null) { + log.warn("[CORE][服务工厂] 未找到上行服务: {}, 可用服务: {}", code, iupMap.keySet()); + } + return service; + } + + /** 获取下行服务,如果不存在则抛出BizException */ + public static T getIDownOrThrow(String code) { + T service = (T) iDownMap.get(code); + if (service == null) { + log.error("[CORE][服务工厂] 未找到下行服务: {}, 可用服务: {}", code, iDownMap.keySet()); + throw new IoTException(String.format("协议模块 [%s] 未启用或不可用,可用协议: %s", code, iDownMap.keySet())); + } + return service; + } + + /** 获取上行服务,如果不存在则抛出BizException */ + public static T getIUPOrThrow(String code) { + T service = (T) iupMap.get(code); + if (service == null) { + log.error("[CORE][服务工厂] 未找到上行服务: {}, 可用服务: {}", code, iupMap.keySet()); + throw new IoTException(String.format("协议模块 [%s] 未启用或不可用,可用协议: %s", code, iupMap.keySet())); + } + return service; + } + + /** 获取已启用的下行服务列表 */ + public static Set getEnabledDownServices() { + return iDownMap.keySet(); + } + + /** 获取已启用的上行服务列表 */ + public static Set getEnabledUpServices() { + return iupMap.keySet(); + } + + /** 检查服务是否可用 */ + public static boolean isServiceAvailable(String code) { + return iDownMap.containsKey(code) || iupMap.containsKey(code); + } + + /** 安全调用下行服务,捕获异常并返回错误响应 */ + public static R safeInvokeDown(String code, String operation, String msg) { + IDown service = getIDownOrThrow(code); // 使用抛出异常的版本 + try { + switch (operation) { + case "down": + return service.doAction(msg); + case "downPro": + return service.downToThirdPlatform(msg); + default: + return R.error("不支持的操作: " + operation); + } + } catch (IoTException e) { + // 业务异常直接返回错误信息 + return R.error(e.getMessage()); + } catch (Exception e) { + log.error( + "[CORE][服务工厂] 调用下行服务异常: code={}, operation={}, error={}", + code, + operation, + e.getMessage()); + return R.error("服务调用异常: " + e.getMessage()); + } + } + + /** + * 获取协议模块详细信息 + * + * @param code 协议代码 + * @return 协议模块详细信息,包含元数据和运行状态 + */ + public static Map getProtocolModuleDetail(String code) { + Map detail = new HashMap<>(); + + // 获取协议元数据 + ProtocolModuleInfo moduleInfo = ProtocolModuleRegistry.getModuleInfo(code); + if (moduleInfo != null) { + detail.put("code", moduleInfo.getCode()); + detail.put("name", moduleInfo.getName()); + detail.put("description", moduleInfo.getDescription()); + detail.put("version", moduleInfo.getVersion()); + detail.put("vendor", moduleInfo.getVendor()); + detail.put("isCore", moduleInfo.isCore()); + detail.put("category", moduleInfo.getCategory().name()); + detail.put("categoryDescription", moduleInfo.getCategory().getDescription()); + } else { + // 如果没有注册元数据,使用默认信息 + detail.put("code", code); + detail.put("name", code.toUpperCase()); + detail.put("description", "未知协议"); + detail.put("version", "unknown"); + detail.put("vendor", "unknown"); + detail.put("isCore", false); + detail.put("category", "UNKNOWN"); + detail.put("categoryDescription", "未知"); + } + + // 获取运行状态 + boolean downAvailable = iDownMap.containsKey(code); + boolean upAvailable = iupMap.containsKey(code); + boolean available = downAvailable || upAvailable; + + detail.put("available", available); + detail.put("downAvailable", downAvailable); + detail.put("upAvailable", upAvailable); + detail.put("status", available ? "已启用" : "未启用"); + + return detail; + } + + /** + * 获取所有协议模块的详细信息 + * + * @return 所有协议模块的详细信息映射 + */ + public static Map> getAllProtocolModuleDetails() { + Map> allDetails = new HashMap<>(); + + // 获取所有已注册的协议代码 + Set allCodes = ProtocolModuleRegistry.getAllProtocolCodes(); + + // 添加运行时发现的协议(可能没有注册元数据) + allCodes.addAll(iDownMap.keySet()); + allCodes.addAll(iupMap.keySet()); + + for (String code : allCodes) { + allDetails.put(code, getProtocolModuleDetail(code)); + } + + return allDetails; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/UniversalCodec.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/UniversalCodec.java new file mode 100644 index 0000000..ac325ea --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/UniversalCodec.java @@ -0,0 +1,32 @@ +package cn.universal.core.service; + +import cn.universal.core.message.UPRequest; + +/** + * @version 1.0 @Author Aleo + * @since 2025/6/26 19:41 + */ +public interface UniversalCodec extends Codec { + + /** + * 预解码,用于TCP表示出哪个ProductKey + * + * @param productKey 产品Key + * @param message 消息原文 + * @return 解码内容 + */ + default UPRequest preDecode(String productKey, String message) { + return null; + } + + /** + * 上行消息转换->转换为阿里云、腾讯云、三方平台 + * + * @param productKey 产品Key + * @param message 消息原文 + * @return 消息原文 + */ + default String messageFormatUP(String productKey, String message) { + return null; + } +} diff --git a/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/UniversalUP.java b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/UniversalUP.java new file mode 100644 index 0000000..c6f890e --- /dev/null +++ b/cn-universal-framework/cn-universal-core/src/main/java/cn/universal/core/service/UniversalUP.java @@ -0,0 +1,33 @@ +/* + * + * Copyright (c) 2025, cn-universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ +package cn.universal.core.service; + +public interface UniversalUP { + + /** 通用消息组件 */ + String name(); + + /** + * 上行消息入口类 + * + * @param msg 消息内容 + */ + void up(String msg); + + /** + * 重新发送云端指令 + * + * @param productKey 产品key + * @param deviceId 设备序列号 + */ + default void resendStoreCommand(String productKey, String deviceId) {} +} diff --git a/cn-universal-framework/cn-universal-dm/pom.xml b/cn-universal-framework/cn-universal-dm/pom.xml new file mode 100644 index 0000000..34861ad --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/pom.xml @@ -0,0 +1,54 @@ + + + + cn-universal-framework + cn.universal.iot + 1.5-SNAPSHOT + + 4.0.0 + + cn-universal-dm + + + cn-universal-persistence + cn.universal.iot + ${project.version} + + + cn-universal-core + cn.universal.iot + ${project.version} + + + junit + junit + 4.12 + test + + + + + + + + org.aspectj + aspectjweaver + + + cn.universal.iot + cn-universal-cache + 1.5-SNAPSHOT + compile + + + + + org.redisson + redisson-spring-boot-starter + 3.24.3 + + + + \ No newline at end of file diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/config/RedissonConfig.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/config/RedissonConfig.java new file mode 100644 index 0000000..b76de51 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/config/RedissonConfig.java @@ -0,0 +1,124 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ +package cn.universal.dm.device.config; + +import lombok.extern.slf4j.Slf4j; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * Redisson 配置类 用于配置分布式锁和 Redis 连接 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +@Configuration +public class RedissonConfig { + + @Value("${spring.data.redis.host:localhost}") + private String redisHost; + + @Value("${spring.data.redis.port:6379}") + private int redisPort; + + @Value("${spring.data.redis.password:}") + private String redisPassword; + + @Value("${spring.data.redis.database:0}") + private int redisDatabase; + + @Value("${spring.data.redis.timeout:3000}") + private int redisTimeout; + + @Value("${spring.data.redis.lettuce.pool.max-active:8}") + private int maxActive; + + @Value("${spring.data.redis.lettuce.pool.max-idle:8}") + private int maxIdle; + + @Value("${spring.data.redis.lettuce.pool.min-idle:0}") + private int minIdle; + + @Value("${spring.data.redis.lettuce.pool.max-wait:-1}") + private long maxWait; + + // Redisson 特定配置 + @Value("${redisson.lock.watchdog-timeout:30000}") + private int lockWatchdogTimeout; + + @Value("${redisson.lock.wait-time:100}") + private int lockWaitTime; + + @Value("${redisson.lock.lease-time:30}") + private int lockLeaseTime; + + @Value("${redisson.connection-pool.size:8}") + private int redissonPoolSize; + + @Value("${redisson.connection-pool.min-idle:0}") + private int redissonMinIdle; + + @Value("${redisson.connection.timeout:3000}") + private int redissonTimeout; + + @Value("${redisson.connection.retry-attempts:3}") + private int redissonRetryAttempts; + + @Value("${redisson.connection.retry-interval:1500}") + private int redissonRetryInterval; + + /** 配置 Redisson 客户端 */ + @Bean + @Primary + public RedissonClient redissonClient() { + try { + Config config = new Config(); + + // 单机模式配置 + config + .useSingleServer() + .setAddress("redis://" + redisHost + ":" + redisPort) + .setDatabase(redisDatabase) + .setConnectionPoolSize(redissonPoolSize) + .setConnectionMinimumIdleSize(redissonMinIdle) + .setConnectTimeout(redissonTimeout) + .setIdleConnectionTimeout(redissonTimeout) + .setRetryAttempts(redissonRetryAttempts) + .setRetryInterval(redissonRetryInterval) + .setKeepAlive(true) + .setTcpNoDelay(true); + + // 如果设置了密码 + if (redisPassword != null && !redisPassword.trim().isEmpty()) { + config.useSingleServer().setPassword(redisPassword); + } + + // 分布式锁配置 + config.setLockWatchdogTimeout(lockWatchdogTimeout); // 看门狗超时时间 + + RedissonClient redissonClient = Redisson.create(config); + log.info("Redisson 客户端初始化成功: {}:{}", redisHost, redisPort); + + return redissonClient; + + } catch (Exception e) { + log.error("Redisson 客户端初始化失败: {}:{}, error={}", redisHost, redisPort, e.getMessage(), e); + throw new RuntimeException("Redisson 初始化失败", e); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/constant/DeviceManagerConstant.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/constant/DeviceManagerConstant.java new file mode 100644 index 0000000..fdb80bb --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/constant/DeviceManagerConstant.java @@ -0,0 +1,25 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.constant; + +/** + * @version 1.0 @Author Aleo + * @since 2023/9/21 + */ +public interface DeviceManagerConstant { + + /** 物模型经纬度位置字段 */ + String COORDINATE = "coordinate"; + + String CACHE_DEV_INSTANCE_BO = "dev_instance_bo:"; +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/entity/IoTDevicePropertiesBO.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/entity/IoTDevicePropertiesBO.java new file mode 100644 index 0000000..af7134f --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/entity/IoTDevicePropertiesBO.java @@ -0,0 +1,170 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.entity; + +import static java.util.Optional.ofNullable; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.core.metadata.AbstractPropertyMetadata; +import cn.universal.core.metadata.ValueType; +import cn.universal.core.metadata.types.ArrayType; +import cn.universal.core.metadata.types.BooleanType; +import cn.universal.core.metadata.types.DateTimeType; +import cn.universal.core.metadata.types.EnumType; +import cn.universal.core.metadata.types.GeoPoint; +import cn.universal.core.metadata.types.GeoType; +import cn.universal.core.metadata.types.NumberType; +import cn.universal.core.metadata.types.ObjectType; +import cn.universal.core.metadata.types.StringType; +import java.math.BigDecimal; +import java.util.Date; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Slf4j +public class IoTDevicePropertiesBO { + + // private String id; + + private String iotId; + + private String deviceId; + + private String property; + + private String propertyName; + + private String stringValue; + private Object desireValue; + + private String formatValue; + + private BigDecimal numberValue; + + private GeoPoint geoValue; + + /** 最后更新时间 */ + private long timestamp; + + private Object objectValue; + + private String value; + + private Date timeValue; + + private String type; + + private String symbol; + + private String customized; + // 是否设置存储策略 + private boolean storagePolicy; + + public Map toMap() { + return new JSONObject(this); + } + + public IoTDevicePropertiesBO withValue(AbstractPropertyMetadata metadata, Object value) { + if (metadata == null) { + setValue(String.valueOf(value)); + if (value instanceof Number) { + numberValue = new BigDecimal(value.toString()); + } else if (value instanceof Date) { + timeValue = ((Date) value); + } + return this; + } + setProperty(metadata.getId()); + setPropertyName(metadata.getName()); + return withValue(metadata.getValueType(), value); + } + + public IoTDevicePropertiesBO withValue(ValueType type, Object value) { + if (value == null) { + return this; + } + setType(type.getType()); + String convertedValue; + + if (type instanceof NumberType) { + NumberType numberType = (NumberType) type; + Number number = numberType.convertNumber(value); + if (number == null) { + log.error("无法将" + value + "转为" + type.getId()); + // throw new UnsupportedOperationException("无法将" + value + "转为" + type.getId()); + return this; + } + setSymbol(numberType.getUnit() != null ? numberType.getUnit().getSymbol() : ""); + convertedValue = number.toString(); + BigDecimal numberVal; + if (number instanceof BigDecimal) { + numberVal = ((BigDecimal) number); + } else if (number instanceof Integer) { + numberVal = BigDecimal.valueOf(number.intValue()); + } else if (number instanceof Long) { + numberVal = BigDecimal.valueOf(number.longValue()); + } else { + numberVal = BigDecimal.valueOf(number.doubleValue()); + } + setNumberValue(numberVal); + } else if (type instanceof DateTimeType) { + DateTimeType dateTimeType = (DateTimeType) type; + convertedValue = String.valueOf(value); + setTimeValue(dateTimeType.convert(value)); + } else if (type instanceof ObjectType) { + ObjectType objectType = (ObjectType) type; + Object val = objectType.convert(value); + convertedValue = JSONUtil.toJsonStr(val); + setObjectValue(val); + } else if (type instanceof ArrayType) { + ArrayType objectType = (ArrayType) type; + Object val = objectType.convert(value); + convertedValue = JSONUtil.toJsonStr(val); + setObjectValue(val); + } else if (type instanceof GeoType) { + GeoType geoType = (GeoType) type; + GeoPoint val = geoType.convert(value); + convertedValue = String.valueOf(val); + setGeoValue(val); + } else if (type instanceof StringType) { + StringType stringType = ((StringType) type); + setStringValue(convertedValue = String.valueOf(value)); + setSymbol(stringType.getUnit() != null ? stringType.getUnit().getSymbol() : ""); + } else if (type instanceof EnumType) { + EnumType enumType = (EnumType) type; + setFormatValue(enumType.format(value)); + setStringValue(convertedValue = String.valueOf(value)); + } else if (type instanceof BooleanType) { + BooleanType booleanType = (BooleanType) type; + setFormatValue(booleanType.format(value)); + setStringValue(convertedValue = String.valueOf(value)); + } else { + setStringValue(convertedValue = String.valueOf(value)); + } + setValue(convertedValue); + ofNullable(type.format(value)).map(String::valueOf).ifPresent(this::setFormatValue); + + return this; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/entity/IoTPushResult.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/entity/IoTPushResult.java new file mode 100644 index 0000000..deb15b8 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/entity/IoTPushResult.java @@ -0,0 +1,147 @@ +package cn.universal.dm.device.entity; + +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Data; + +/** + * 消息推送结果 + * + * @author Aleo + * @version 1.0 + * @since 2025/7/31 23:06 + */ +@Data +@Builder +public class IoTPushResult { + + /** 推送是否成功 */ + private boolean ok; + + /** 第三方平台类型 */ + private String platform; + + /** 产品Key */ + private String productKey; + + /** 设备ID */ + private String deviceId; + + /** 推送渠道 (HTTP, MQTT, Kafka, RocketMQ等) */ + private String channel; + + /** 推送消息内容 */ + private String messageContent; + + /** 推送时间 */ + private LocalDateTime pushTime; + + /** 响应时间(毫秒) */ + @Builder.Default private Long responseTime = 0L; + + /** 错误信息 */ + private String errorMessage; + + /** 错误代码 */ + private String errorCode; + + /** 重试次数 */ + private Integer retryCount; + + /** 最大重试次数 */ + private Integer maxRetryCount; + + /** 推送状态 (SUCCESS, FAILED, RETRYING, TIMEOUT) */ + private PushStatus status; + + /** 推送配置ID */ + private String configId; + + /** 请求ID,用于追踪 */ + private String requestId; + + /** 推送状态枚举 */ + public enum PushStatus { + SUCCESS("成功"), + FAILED("失败"), + RETRYING("重试中"), + TIMEOUT("超时"), + CANCELLED("已取消"); + + private final String description; + + PushStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } + + /** 创建成功结果 */ + public static IoTPushResult success( + String platform, + String productKey, + String deviceId, + String channel, + String messageContent, + Long responseTime) { + return IoTPushResult.builder() + .ok(true) + .platform(platform) + .productKey(productKey) + .deviceId(deviceId) + .channel(channel) + .messageContent(messageContent) + .pushTime(LocalDateTime.now()) + .responseTime(responseTime) + .status(PushStatus.SUCCESS) + .retryCount(0) + .build(); + } + + /** 创建失败结果 */ + public static IoTPushResult failed( + String platform, + String productKey, + String deviceId, + String channel, + String messageContent, + String errorMessage, + String errorCode) { + return IoTPushResult.builder() + .ok(false) + .platform(platform) + .productKey(productKey) + .deviceId(deviceId) + .channel(channel) + .messageContent(messageContent) + .pushTime(LocalDateTime.now()) + .errorMessage(errorMessage) + .errorCode(errorCode) + .status(PushStatus.FAILED) + .retryCount(0) + .build(); + } + + /** 创建重试结果 */ + public static IoTPushResult retry(IoTPushResult original, String errorMessage) { + return IoTPushResult.builder() + .ok(false) + .platform(original.getPlatform()) + .productKey(original.getProductKey()) + .deviceId(original.getDeviceId()) + .channel(original.getChannel()) + .messageContent(original.getMessageContent()) + .pushTime(LocalDateTime.now()) + .errorMessage(errorMessage) + .errorCode(original.getErrorCode()) + .status(PushStatus.RETRYING) + .retryCount(original.getRetryCount() + 1) + .maxRetryCount(original.getMaxRetryCount()) + .configId(original.getConfigId()) + .requestId(original.getRequestId()) + .build(); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractCodecService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractCodecService.java new file mode 100644 index 0000000..243ac2d --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractCodecService.java @@ -0,0 +1,15 @@ +package cn.universal.dm.device.service; + +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; + +/** + * @version 1.0 @Author Aleo + * @since 2025/6/26 20:10 + */ +@Slf4j +public abstract class AbstractCodecService { + + @Resource private IoTProductDeviceService iotProductDeviceService; +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractDownService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractDownService.java new file mode 100644 index 0000000..35d0554 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractDownService.java @@ -0,0 +1,45 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service; + +import cn.universal.core.service.ICodec; +import cn.universal.core.service.ICodecService; +import cn.universal.core.service.IDown; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * 下发抽象工具类 + * + * @version 1.0 @Author Aleo + * @since 2025/8/9 10:50 + */ +@Slf4j +public abstract class AbstractDownService extends AbstratIoTService implements IDown, ICodec { + + @Autowired protected ICodecService codecService; + + /** + * 消息转换和编解码 + * + * @param request + * @return + */ + protected abstract T convert(String request); + + @Override + public String spliceDown(String productKey, String payload) { + // 使用新的统一编解码服务 + return codecService.encode(productKey, payload); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractUPService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractUPService.java new file mode 100644 index 0000000..72dd308 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractUPService.java @@ -0,0 +1,121 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service; + +import cn.hutool.core.util.IdUtil; +import cn.universal.common.constant.IoTConstant; +import cn.universal.core.message.UPRequest; +import cn.universal.core.service.ICodec; +import cn.universal.core.service.ICodecService; +import cn.universal.core.service.IUP; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; + +/** + * 上行消息工具抽象类,提供统一的消息编解码、协议处理及请求转发能力。 子类需实现消息解析逻辑,并处理特定类型的上行请求(如设备注册、数据上报等)。 + * + * @param 泛型参数,代表具体的上行请求类型,需继承自 {@link UPRequest} + * @version 1.0 @Author Aleo + * @since 2020/10/21 + */ +@Slf4j +public abstract class AbstractUPService extends AbstratIoTService + implements IUP, ICodec { + + @Autowired protected ICodecService codecService; + + /** 消息转换 */ + protected abstract List convert(String request); + + @Override + @Async + public void asyncUP(String request) { + String traceId = IdUtil.fastSimpleUUID(); + MDC.put(IoTConstant.TRACE_ID, traceId); + + Thread currentThread = Thread.currentThread(); + String threadInfo = + String.format( + "%s(id=%d)", + currentThread.getName().isEmpty() + ? "VirtualThread-" + currentThread.getId() + : currentThread.getName(), + currentThread.getId()); + + long startTime = System.currentTimeMillis(); + log.info( + "异步开始 - 组件名={}, 线程={}, TraceId={}, 开始时间={}, 上行消息={}", + currentComponent(), + threadInfo, + traceId, + startTime, + request); + + try { + // 模拟一些处理时间来观察并发效果 + // Thread.sleep(3000); // 3秒处理时间 + realUPAction(request); + + long endTime = System.currentTimeMillis(); + log.info( + "异步完成 - 组件名={}, 线程={}, TraceId={}, 结束时间={}, 耗时={}ms", + currentComponent(), + threadInfo, + traceId, + endTime, + (endTime - startTime)); + } catch (Exception e) { + log.warn("异步处理被中断 - TraceId={}", traceId); + } finally { + MDC.remove(IoTConstant.TRACE_ID); + } + } + + // 测试方法:模拟简单的异步处理 + @Async("virtualThreadExecutor") + public void testAsync(String message) { + Thread currentThread = Thread.currentThread(); + String threadInfo = + String.format( + "%s(id=%d)", + currentThread.getName().isEmpty() + ? "VirtualThread-" + currentThread.getId() + : currentThread.getName(), + currentThread.getId()); + + log.info("测试异步开始 - 线程={}, 消息={}", threadInfo, message); + + try { + // 模拟处理时间,但不阻塞 + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + log.info("测试异步完成 - 线程={}, 消息={}", threadInfo, message); + } + + protected abstract Object realUPAction(String upMsg); + + /** 当前iot组件名称 */ + protected abstract String currentComponent(); + + @Override + public UPRequest preDecode(String productKey, String message) { + // 使用新的统一编解码服务 + return codecService.preDecode(productKey, message); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstratIoTService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstratIoTService.java new file mode 100644 index 0000000..a00c0a9 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstratIoTService.java @@ -0,0 +1,481 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.constant.IoTConstant.DeviceNode; +import cn.universal.common.constant.IoTConstant.MessageType; +import cn.universal.common.utils.AESOperator; +import cn.universal.core.message.UPRequest; +import cn.universal.core.metadata.AbstractEventMetadata; +import cn.universal.core.metadata.AbstractFunctionMetadata; +import cn.universal.core.metadata.DeviceMetadata; +import cn.universal.core.protocol.jar.ProtocolCodecJar; +import cn.universal.core.protocol.jscrtipt.ProtocolCodecJscript; +import cn.universal.core.protocol.magic.ProtocolCodecMagic; +import cn.universal.core.protocol.support.ProtocolCodecSupport; +import cn.universal.core.protocol.support.ProtocolSupportDefinition; +import cn.universal.core.service.ICodecService; +import cn.universal.dm.device.service.action.IoTDeviceActionAfterService; +import cn.universal.dm.device.service.impl.IoTDeviceService; +import cn.universal.dm.device.service.impl.IoTDeviceShadowService; +import cn.universal.dm.device.service.impl.IoTDeviceSubscribeService; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import cn.universal.dm.device.service.log.IIoTDeviceDataService; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.base.IoTDeviceLifeCycle; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.entity.IoTDeviceSubscribe; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.query.IoTDeviceQuery; +import jakarta.annotation.Resource; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * IoT设备服务抽象基类 + * + *

提供IoT平台中设备管理的通用功能,包括: - 产品信息查询和管理 - 设备生命周期管理 - 协议编解码支持 - 设备数据日志记录 - 设备影子服务 - 消息订阅管理 - AES加解密功能 + * + *

子类通过继承此类获得设备管理的核心能力,专注于实现特定的业务逻辑 + * + * @version 1.0 @Author Aleo + * @since 2025/8/9 8:56 + */ +@Slf4j +public abstract class AbstratIoTService { + + @Resource(name = "ioTDeviceActionAfterService") + protected IoTDeviceActionAfterService ioTDeviceActionAfterService; + + @Autowired protected IoTProductDeviceService iotProductDeviceService; + @Autowired private IoTDeviceService iotDeviceService; + + @Autowired protected IoTDeviceShadowService iotDeviceShadowService; + + @Resource protected IIoTDeviceDataService iIoTDeviceDataService; + + @Resource private IoTDeviceSubscribeService iotDeviceSubscribeService; + + @Resource(name = "ioTDeviceActionAfterService") + private IoTDeviceLifeCycle ioTDeviceLifeCycle; + + @Resource private StringRedisTemplate stringRedisTemplate; + + @Autowired private ICodecService codecService; + + /** + * 根据产品唯一编号查询产品信息 + * + *

用于获取产品的配置信息,包括产品元数据、协议配置等 + * + * @param productKey 产品唯一标识 + * @return 产品信息,如果不存在返回null + */ + protected IoTProduct getProduct(String productKey) { + return iotProductDeviceService.getProduct(productKey); + } + protected IoTDeviceDTO getIoTDeviceDTO(IoTDeviceQuery query) { + IoTDeviceDTO instanceBO = iotDeviceService.lifeCycleDevInstance(query); + if (instanceBO == null) { + return null; + } + return instanceBO; + } + /** + * 查询产品是否配置了离线周期阈值 + * + *

用于判断产品是否需要监控设备离线状态 + * + * @param productKey 产品唯一标识 + * @return true表示配置了离线周期,false表示未配置 + */ + protected boolean offlineThreshold(String productKey) { + return iotProductDeviceService.offlineThreshold(productKey); + } + + /** + * 查询产品的消息订阅URL列表 + * + *

根据产品标识、设备标识和消息类型获取对应的订阅地址 用于消息推送和事件通知 + * + * @param productKey 产品唯一标识 + * @param iotId 设备唯一标识 + * @param messageType 消息类型 + * @return 订阅URL列表 + */ + protected List querySubscribeUrl( + String productKey, String iotId, MessageType messageType) { + return iotDeviceSubscribeService.selectByProductKeyAndMsgType(productKey, iotId, messageType); + } + + /** + * 设备生命周期管理 - 设备实例查询 + * + *

注意:此方法仅限第三方平台上行消息的入口使用,用作设备上线判断 其他地方禁止使用,避免重复触发生命周期事件 + * + *

功能包括: - 查询设备实例信息 - 检测设备离线变上线状态 - 自动触发设备上线生命周期事件 + * + * @param query 设备查询条件 + * @return 设备实例信息 + */ + protected IoTDeviceDTO lifeCycleDevInstance(IoTDeviceQuery query) { + IoTDeviceDTO instanceBO = iotDeviceService.lifeCycleDevInstance(query); + if (instanceBO == null) { + return null; + } + + // 检查Redis中的离线标记 + String offlineKey = "offline:" + instanceBO.getProductKey() + instanceBO.getDeviceId(); + String onlineKey = "online:" + instanceBO.getProductKey() + instanceBO.getDeviceId(); + boolean hasOfflineFlag = stringRedisTemplate.hasKey(offlineKey); + boolean recentlyOnline = stringRedisTemplate.hasKey(onlineKey); + + // 判断是否需要上线 + boolean needOnline = false; + String reason = ""; + + if (!Boolean.TRUE.equals(instanceBO.getState())) { + // 数据库状态为离线 + needOnline = true; + reason = "数据库状态为离线"; + } else if (hasOfflineFlag) { + // 数据库状态为在线,但Redis有离线标记(状态不一致) + needOnline = true; + reason = "Redis有离线标记"; + } + + // 防重复上线:如果最近已经上线过,则不重复执行 + if (needOnline && recentlyOnline) { + log.debug("设备最近已上线,跳过重复上线: deviceId={}, reason={}", instanceBO.getDeviceId(), reason); + needOnline = false; + } + + if (needOnline) { + log.info("设备需要上线,deviceId={},原因={}", instanceBO.getDeviceId(), reason); + ioTDeviceLifeCycle.online(instanceBO.getProductKey(), instanceBO.getDeviceId()); + } else { + log.debug( + "设备状态检查: deviceId={}, dbState={}, hasOfflineFlag={}, recentlyOnline={}, needOnline={}", + instanceBO.getDeviceId(), + instanceBO.getState(), + hasOfflineFlag, + recentlyOnline, + needOnline); + } + return instanceBO; + } + + /** + * 查询产品的协议解码定义 + * + *

获取产品配置的协议解码规则,用于消息的编解码处理 + * + * @param productKey 产品唯一标识 + * @return 协议支持定义,包含解码规则和配置 + */ + protected ProtocolSupportDefinition selectProtocolDef(String productKey) { + return iotProductDeviceService.selectProtocolDef(productKey); + } + + /** + * 查询产品的协议解码定义(不带超长脚本) + * + *

获取产品配置的协议解码规则,用于消息的编解码处理 + * + * @param productKey 产品唯一标识 + * @return 协议支持定义,包含解码规则和配置 + */ + protected ProtocolSupportDefinition selectProtocolDefNoScript(String productKey) { + return iotProductDeviceService.selectProtocolDefNoScript(productKey); + } + + /** + * 保存设备上行日志和影子数据 + * + *

批量处理设备上行请求,包括: - 保存设备数据日志到数据库 - 更新设备影子状态 - 过滤调试模式的消息 + * + * @param upRequest 上行请求列表 + * @param ioTDeviceDTO 设备信息 + * @param ioTProduct 产品信息 + */ + protected void doLogMetadataAndShadow( + List upRequest, IoTDeviceDTO ioTDeviceDTO, IoTProduct ioTProduct) { + if (CollectionUtil.isNotEmpty(upRequest)) { + upRequest.stream() + .filter(s -> !s.isDebug()) + .forEach( + s -> { + iIoTDeviceDataService.saveDeviceLog(s, ioTDeviceDTO, ioTProduct); + iotDeviceShadowService.doShadow(s, ioTDeviceDTO); + }); + } + } + + /** + * 获取编解码插件提供者 + * + *

根据支持类型获取对应的协议编解码实现,支持: - jar: Java插件模式 - jscript: JavaScript脚本模式 - magic: 魔法字节模式 + * + * @param supportType 支持类型 + * @return 协议编解码支持实例,如果类型不支持返回null + */ + protected ProtocolCodecSupport getProtocolCodecProvider(String supportType) { + if (supportType == null) { + return null; + } + if (supportType.equalsIgnoreCase("jar")) { + return ProtocolCodecJar.getInstance(); + } else if (supportType.equalsIgnoreCase("jscript")) { + return ProtocolCodecJscript.getInstance(); + } else if (supportType.equalsIgnoreCase("magic")) { + return ProtocolCodecMagic.getInstance(); + } + return null; + } + + /** + * 透传上层应用原始报文AES加密 + * + *

使用设备唯一标识作为密钥对原始报文进行AES加密 密钥生成规则:使用iotId的MD5值作为密钥 + * + * @param payload 原始报文内容 + * @param iotId 设备唯一标识 + * @return 加密后的报文 + */ + protected String playloadEncode(String payload, String iotId) { + return AESOperator.getInstance() + .encrypt(payload, DigestUtil.md5Hex16(iotId), DigestUtil.md5Hex(iotId)); + } + + /** + * AES解密 + * + *

使用设备唯一标识作为密钥对加密报文进行AES解密 密钥生成规则:使用iotId的MD5值作为密钥 + * + * @param payload 加密的报文内容 + * @param iotId 设备唯一标识 + * @return 解密后的原始报文 + */ + protected String playloadDecode(String payload, String iotId) { + return AESOperator.getInstance() + .decrypt(payload, DigestUtil.md5Hex16(iotId), DigestUtil.md5Hex(iotId)); + } + + /** + * 根据设备序列号查询设备信息 + * + *

通过设备序列号获取设备的完整信息,包括设备状态、配置等 + * + * @param deviceId 设备序列号 + * @return 设备实例信息 + */ + public IoTDeviceDTO getIotDeviceByDeviceIdLimitOne(String deviceId) { + IoTDeviceDTO devInstance = + iotDeviceService.lifeCycleDevInstance(IoTDeviceQuery.builder().deviceId(deviceId).build()); + return devInstance; + } + + protected List decode( + String productKey, String payload, Object context, Class elementType) { + // 使用新的统一编解码服务 + return codecService.decode(productKey, payload, context, elementType); + } + + protected List decode(String productKey, String payload, Class elementType) { + return decode(productKey, payload, null, elementType); + } + + protected List decode(String productKey, String payload) { + return decode(productKey, payload, null, UPRequest.class); + } + + protected void buildCodecNotNullBean( + BaseUPRequest hasData, BaseUPRequest.BaseUPRequestBuilder builder) { + if (CollUtil.isNotEmpty(hasData.getProperties())) { + builder.properties(hasData.getProperties()); + } + // 设置属性值 + if (CollUtil.isNotEmpty(hasData.getData())) { + builder.data(hasData.getData()); + } + // 设置原始串 + if (hasData + .getIoTDeviceDTO() + .getProductConfig() + .getBool(IoTConstant.REQUIRE_PAYLOAD, Boolean.FALSE)) { + builder.payload(hasData.getPayload()); + } + builder.tags(hasData.getTags()); + // 设置回复值 + builder.function(hasData.getFunction()); + } + + protected void buildCodecNotNullBean( + JSONObject jsonObject, + IoTDeviceDTO ioTDeviceDTO, + UPRequest codec, + BaseUPRequest.BaseUPRequestBuilder builder) { + if (codec == null) { + codec = new UPRequest(); + } + // 编解码覆盖原始消息 + messageProAndData(jsonObject, codec); + if (MessageType.EVENT.equals(codec.getMessageType())) { + String event = codec.getEvent(); + if (StrUtil.isBlank(event)) { + event = jsonObject.getStr("event"); + } + builder.event(event); + builder.messageType(codec.getMessageType()); + if ("offline".equals(event)) { + ioTDeviceActionAfterService.offline( + ioTDeviceDTO.getProductKey(), ioTDeviceDTO.getDeviceId()); + } + } + // 设置data + builder.data(codec.getData()); + builder.properties(codec.getProperties()); + // 设置原始串 + if (ioTDeviceDTO.getProductConfig().getBool(IoTConstant.REQUIRE_PAYLOAD, Boolean.FALSE)) { + builder.payload(ioTDeviceDTO.getPayload()); + } + builder.tags(codec.getTags()); + // 设置回复值 + if (StrUtil.isNotBlank(codec.getFunction())) { + builder.function(codec.getFunction()); + DeviceMetadata deviceMetadata = + iotProductDeviceService.getDeviceMetadata(ioTDeviceDTO.getProductKey()); + AbstractFunctionMetadata functionOrNull = + deviceMetadata.getFunctionOrNull(codec.getFunction()); + builder.functionName(functionOrNull == null ? "" : functionOrNull.getName()); + } + } + + private static void messageProAndData(JSONObject jsonObject, UPRequest codec) { + if (codec.getMessageType() == null) { + String messageType = jsonObject.getStr("messageType"); + if (StrUtil.isNotBlank(messageType)) { + codec.setMessageType(MessageType.find(messageType)); + } else { + codec.setMessageType(MessageType.PROPERTIES); + } + } + if (codec.getProperties() == null) { + JSONObject properties = jsonObject.getJSONObject("properties"); + if (properties != null) { + codec.setProperties(properties); + } + } else { + JSONObject properties = jsonObject.getJSONObject("properties"); + if (properties != null) { + codec.getProperties().putAll(jsonObject.getJSONObject("properties")); + } + } + if (codec.getData() == null) { + JSONObject data = jsonObject.getJSONObject("data"); + if (data != null) { + codec.setData(data); + } + } else { + JSONObject data = jsonObject.getJSONObject("data"); + if (data != null) { + codec.getData().putAll(jsonObject.getJSONObject("data")); + } + } + } + + /** 处理事件名称和订阅消息 */ + protected void doEventNameAndSubscribe( + List upRequests, IoTDeviceDTO ioTDeviceDTO) { + if (CollectionUtil.isNotEmpty(upRequests)) { + for (UPRequest ur : upRequests) { + // 如果是事件,则完善事件名称 + if (MessageType.EVENT.equals(ur.getMessageType())) { + DeviceMetadata deviceMetadata = + iotProductDeviceService.getDeviceMetadata(ur.getProductKey()); + AbstractEventMetadata metadata = deviceMetadata.getEventOrNull(ur.getEvent()); + if (metadata != null) { + ur.setEventName(metadata.getName()); + } + } + // 如果是订阅消息,则填充subscribeUrl + List ioTDeviceSubscribe = + querySubscribeUrl(ur.getProductKey(), ur.getIotId(), ur.getMessageType()); + if (ioTDeviceSubscribe != null) { + ur.setDevSubscribe(ioTDeviceSubscribe); + } + } + } + } + + protected BaseUPRequest.BaseUPRequestBuilder getBaseUPRequest(IoTDeviceDTO ioTDeviceDTO) { + return BaseUPRequest.builder() + .deviceNode(DeviceNode.DEVICE) + .ioTDeviceDTO(ioTDeviceDTO) + .iotId(ioTDeviceDTO.getIotId()) + .deviceName(ioTDeviceDTO.getDeviceName()) + .messageType(MessageType.PROPERTIES) + .deviceId(ioTDeviceDTO.getDeviceId()) + .time(System.currentTimeMillis()) + .productKey(ioTDeviceDTO.getProductKey()) + .userUnionId(ioTDeviceDTO.getUserUnionId()); + } + + protected BaseUPRequest buildCodecNullBean(JSONObject jsonObject, BaseUPRequest mqttUPRequest) { + final BaseUPRequest.BaseUPRequestBuilder builder = + getBaseUPRequest(mqttUPRequest.getIoTDeviceDTO()); + if (MessageType.PROPERTIES.name().equalsIgnoreCase(jsonObject.getStr("messageType"))) { + builder.messageType(MessageType.PROPERTIES); + } + if (MessageType.EVENT.name().equalsIgnoreCase(jsonObject.getStr("messageType"))) { + String event = jsonObject.getStr("event"); + builder.event(event); + builder.messageType(MessageType.EVENT); + if ("offline".equals(event)) { + ioTDeviceActionAfterService.offline( + mqttUPRequest.getIoTDeviceDTO().getProductKey(), + mqttUPRequest.getIoTDeviceDTO().getDeviceId()); + } + } + if (ObjectUtil.isNotNull(jsonObject.getJSONObject("properties"))) { + builder.properties(jsonObject.getJSONObject("properties")); + } + if (ObjectUtil.isNotNull(jsonObject.getJSONObject("data"))) { + builder.data(jsonObject.getJSONObject("data")); + } + if (ObjectUtil.isNotNull(jsonObject.getJSONObject("tags"))) { + builder.tags(jsonObject.getJSONObject("tags")); + } + // 设置原始串 + builder.payload(mqttUPRequest.getPayload()); + // 设置回复值 + return builder.build(); + } + + protected JSONObject parseJsonPayload(String payload) { + if (JSONUtil.isTypeJSON(payload)) { + return JSONUtil.parseObj(payload); + } + return new JSONObject().set("payload", payload); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/CodecServiceExample.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/CodecServiceExample.java new file mode 100644 index 0000000..4aac265 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/CodecServiceExample.java @@ -0,0 +1,171 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service; + +import cn.universal.core.message.UPRequest; +import cn.universal.core.protocol.support.ProtocolCodecSupport.CodecMethod; +import cn.universal.core.service.ICodecService; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 编解码服务使用示例 + * + *

展示如何使用统一的编解码服务进行各种编解码操作 + * + * @version 1.0 @Author Aleo + * @since 2025/01/20 + */ +@Slf4j +@Service +public class CodecServiceExample { + + @Autowired private ICodecService codecService; + + /** + * 示例:解码设备上报数据 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @return 解码后的UPRequest列表 + */ + public List decodeDeviceData(String productKey, String payload) { + log.info("开始解码设备数据: productKey={}, payload={}", productKey, payload); + + // 使用统一的解码服务 + List result = codecService.decode(productKey, payload); + + log.info("解码完成: 结果数量={}", result.size()); + return result; + } + + /** + * 示例:编码下行指令 + * + * @param productKey 产品Key + * @param payload 原始指令数据 + * @return 编码后的指令 + */ + public String encodeDownCommand(String productKey, String payload) { + log.info("开始编码下行指令: productKey={}, payload={}", productKey, payload); + + // 使用统一的编码服务 + String result = codecService.encode(productKey, payload); + + log.info("编码完成: result={}", result); + return result; + } + + /** + * 示例:预解码TCP数据 + * + * @param productKey 产品Key + * @param payload 原始TCP数据 + * @return 预解码后的UPRequest + */ + public UPRequest preDecodeTcpData(String productKey, String payload) { + log.info("开始预解码TCP数据: productKey={}, payload={}", productKey, payload); + + // 使用统一的预解码服务 + UPRequest result = codecService.preDecode(productKey, payload); + + log.info("预解码完成: result={}", result); + return result; + } + + /** + * 示例:通用编解码方法 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @param codecMethod 编解码方法类型 + * @return 编解码结果 + */ + public String generalCodec(String productKey, String payload, CodecMethod codecMethod) { + log.info("开始通用编解码: productKey={}, payload={}, method={}", productKey, payload, codecMethod); + + // 使用统一的通用编解码服务 + String result = codecService.codec(productKey, payload, codecMethod); + + log.info("通用编解码完成: result={}", result); + return result; + } + + /** + * 示例:检查是否支持特定编解码方法 + * + * @param productKey 产品Key + * @param codecMethod 编解码方法类型 + * @return 是否支持 + */ + public boolean checkSupport(String productKey, CodecMethod codecMethod) { + boolean supported = codecService.isSupported(productKey, codecMethod); + log.info("检查编解码支持: productKey={}, method={}, supported={}", productKey, codecMethod, supported); + return supported; + } + + /** + * 示例:泛型解码 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @param elementType 目标类型 + * @param 泛型类型 + * @return 解码后的对象列表 + */ + public List decodeWithType(String productKey, String payload, Class elementType) { + log.info("开始泛型解码: productKey={}, payload={}, elementType={}", productKey, payload, elementType); + + // 使用统一的泛型解码服务 + List result = codecService.decode(productKey, payload, elementType); + + log.info("泛型解码完成: 结果数量={}", result.size()); + return result; + } + + /** + * 示例:IoT到第三方数据转换 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @return 转换后的数据 + */ + public String iotToYour(String productKey, String payload) { + log.info("开始IoT到第三方数据转换: productKey={}, payload={}", productKey, payload); + + // 使用统一的IoT到第三方转换服务 + String result = codecService.iotToYour(productKey, payload); + + log.info("IoT到第三方数据转换完成: result={}", result); + return result; + } + + /** + * 示例:第三方到IoT数据转换 + * + * @param productKey 产品Key + * @param payload 原始数据 + * @return 转换后的数据 + */ + public String yourToIot(String productKey, String payload) { + log.info("开始第三方到IoT数据转换: productKey={}, payload={}", productKey, payload); + + // 使用统一的第三方到IoT转换服务 + String result = codecService.yourToIot(productKey, payload); + + log.info("第三方到IoT数据转换完成: result={}", result); + return result; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/DashboardService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/DashboardService.java new file mode 100644 index 0000000..6f4195e --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/DashboardService.java @@ -0,0 +1,587 @@ +package cn.universal.dm.device.service; + +import cn.universal.dm.device.service.push.processor.PushStatisticsProcessor; +import cn.universal.persistence.entity.IoTDashboardStatistics; +import cn.universal.persistence.mapper.IoTDashboardStatisticsMapper; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +/** + * 仪表盘服务 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +@Slf4j +@Service +public class DashboardService { + + @Autowired private IoTDashboardStatisticsMapper dashboardStatisticsMapper; + + @Autowired private StringRedisTemplate redisTemplate; + + @Autowired private SystemMonitorService systemMonitorService; + + @Autowired private PushStatisticsProcessor pushStatisticsProcessor; + + @Autowired private IoTDeviceMapper ioTDeviceMapper; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * 记录统计数据 + * + * @param statistics 统计数据 + */ + public void recordStatistics(IoTDashboardStatistics statistics) { + try { + // 先查找是否存在记录 + IoTDashboardStatistics existing = + dashboardStatisticsMapper.selectByUniqueKey( + statistics.getStatDate(), + statistics.getProductKey(), + statistics.getChannel(), + statistics.getMetricType()); + + if (existing == null) { + // 不存在则插入新记录 + statistics.setCreateTime(LocalDateTime.now()); + statistics.setUpdateTime(LocalDateTime.now()); + dashboardStatisticsMapper.insert(statistics); + log.debug("[仪表盘统计] 插入新记录: {}", statistics); + } else { + // 存在则更新记录 + dashboardStatisticsMapper.updateByUniqueKey( + statistics.getStatDate(), + statistics.getProductKey(), + statistics.getChannel(), + statistics.getMetricType(), + statistics.getMetricValue(), + LocalDateTime.now()); + log.debug("[仪表盘统计] 更新现有记录: {}", statistics); + } + } catch (Exception e) { + log.error("[仪表盘统计] 记录统计数据失败: {}", statistics, e); + } + } + + /** + * 批量记录统计数据 + * + * @param statisticsList 统计数据列表 + */ + public void batchRecordStatistics(List statisticsList) { + if (statisticsList == null || statisticsList.isEmpty()) { + return; + } + + for (IoTDashboardStatistics statistics : statisticsList) { + recordStatistics(statistics); + } + } + + /** 调试方法:查看数据库中的统计记录 */ + public List debugStatistics(LocalDate date) { + log.info("[仪表盘调试] 查询日期: {}", date); + + // 查询所有记录 + List allRecords = + dashboardStatisticsMapper.selectByDateAndMetric(date, null, null, null); + + log.info("[仪表盘调试] 找到 {} 条记录", allRecords.size()); + + for (IoTDashboardStatistics record : allRecords) { + log.info( + "[仪表盘调试] 记录: date={}, product={}, channel={}, type={}, value={}", + record.getStatDate(), + record.getProductKey(), + record.getChannel(), + record.getMetricType(), + record.getMetricValue()); + } + + return allRecords; + } + + /** 获取实时数据 */ + public Map getRealtimeData() { + return getTodayOverview(); // 实时数据就是今日概览 + } + + /** 获取图表数据 */ + public Map getChartData(LocalDate date, String type, String channel) { + Map chartData = new HashMap<>(); + + try { + String dateKey = date.format(DATE_FORMATTER); + + switch (type) { + case "device_trend": + chartData = getDeviceTrendData(dateKey); + break; + case "message_trend": + chartData = getMessageTrendData(dateKey, channel); + break; + case "channel_distribution": + chartData = getChannelDistributionData(dateKey); + break; + case "performance_metrics": + chartData = getPerformanceMetricsData(dateKey); + break; + default: + chartData.put("error", "不支持的图表类型: " + type); + } + } catch (Exception e) { + log.error("[仪表盘] 获取图表数据失败,类型: {}", type, e); + chartData.put("error", "获取图表数据失败"); + } + + return chartData; + } + + /** 获取系统监控数据 */ + public Map getSystemMonitorData() { + Map monitorData = new HashMap<>(); + + try { + // 获取系统监控数据 + Map systemMetrics = systemMonitorService.getSystemMetrics(); + + // 获取系统信息 + Map systemInfo = systemMonitorService.getSystemInfo(); + + monitorData.put("systemMetrics", systemMetrics); + monitorData.put("systemInfo", systemInfo); + monitorData.put("isLinuxSystem", systemMonitorService.isLinuxSystem()); + monitorData.put("timestamp", System.currentTimeMillis()); + + } catch (Exception e) { + log.error("[仪表盘] 获取系统监控数据失败", e); + monitorData.put("error", "获取系统监控数据失败"); + } + + return monitorData; + } + + /** 获取统计管理器状态 */ + public Map getStatisticsManagerStatus() { + return pushStatisticsProcessor.getManagerStatus(); + } + + /** 强制刷新统计到Redis */ + public void forceFlushStatistics() { + pushStatisticsProcessor.forceFlushToRedis(); + } + + /** 获取系统监控服务 */ + public SystemMonitorService getSystemMonitorService() { + return systemMonitorService; + } + + /** 获取今日概览数据 */ + public Map getTodayOverview() { + Map overview = new HashMap<>(); + + try { + LocalDate today = LocalDate.now(); + String dateKey = today.format(DATE_FORMATTER); + + // 设备统计 + Map deviceStats = getDeviceStats(dateKey); + overview.put("deviceStats", deviceStats); + + // 消息统计 + Map messageStats = getMessageStats(dateKey); + overview.put("messageStats", messageStats); + + // 性能统计 + Map performanceStats = getPerformanceStats(dateKey); + overview.put("performanceStats", performanceStats); + + // 渠道统计 + Map channelStats = getChannelStats(dateKey); + overview.put("channelStats", channelStats); + + overview.put("timestamp", System.currentTimeMillis()); + + } catch (Exception e) { + log.error("[仪表盘] 获取今日概览数据失败", e); + overview.put("error", "获取今日概览数据失败"); + } + + return overview; + } + + /** 获取今日概览数据(带用户权限过滤) */ + public Map getTodayOverviewWithUserFilter(String unionId, boolean isAdmin) { + Map overview = new HashMap<>(); + + try { + LocalDate today = LocalDate.now(); + String dateKey = today.format(DATE_FORMATTER); + + // 设备统计(根据用户权限过滤) + Map deviceStats = getDeviceStatsWithUserFilter(dateKey, unionId, isAdmin); + overview.put("deviceStats", deviceStats); + + // 消息统计(根据用户权限过滤) + Map messageStats = getMessageStatsWithUserFilter(dateKey, unionId, isAdmin); + overview.put("messageStats", messageStats); + + // 性能统计 + Map performanceStats = getPerformanceStats(dateKey); + overview.put("performanceStats", performanceStats); + + // 渠道统计(根据用户权限过滤) + Map channelStats = getChannelStatsWithUserFilter(dateKey, unionId, isAdmin); + overview.put("channelStats", channelStats); + + overview.put("timestamp", System.currentTimeMillis()); + + } catch (Exception e) { + log.error("[仪表盘] 获取今日概览数据失败", e); + overview.put("error", "获取今日概览数据失败"); + } + + return overview; + } + + /** 从数据库获取设备统计数据(带用户权限过滤) */ + public Map getDeviceStatsFromDB(String unionId, boolean isAdmin) { + Map deviceStats = new HashMap<>(); + + try { + long totalDevices = 0; + long onlineDevices = 0; + + if (isAdmin) { + // 管理员:查询所有设备 + totalDevices = ioTDeviceMapper.countAllDevices(); + onlineDevices = ioTDeviceMapper.countOnlineDevices(); + } else { + // 普通用户:只查询自己的设备 + totalDevices = ioTDeviceMapper.countDevicesByCreator(unionId); + onlineDevices = ioTDeviceMapper.countOnlineDevicesByCreator(unionId); + } + + long offlineDevices = totalDevices - onlineDevices; + double onlineRate = totalDevices > 0 ? (double) onlineDevices / totalDevices * 100 : 0.0; + + deviceStats.put("totalDevices", totalDevices); + deviceStats.put("onlineDevices", onlineDevices); + deviceStats.put("offlineDevices", offlineDevices); + deviceStats.put("onlineRate", onlineRate); + + log.debug( + "[仪表盘] 设备统计: 总数={}, 在线={}, 离线={}, 在线率={}%", + totalDevices, onlineDevices, offlineDevices, onlineRate); + + } catch (Exception e) { + log.error("[仪表盘] 获取设备统计数据失败", e); + deviceStats.put("error", "获取设备统计数据失败"); + } + + return deviceStats; + } + + // ==================== 私有方法 ==================== + + /** 获取设备统计数据(带用户权限过滤) */ + private Map getDeviceStatsWithUserFilter( + String dateKey, String unionId, boolean isAdmin) { + Map deviceStats = new HashMap<>(); + + try { + long totalDevices = 0; + long onlineDevices = 0; + + if (isAdmin) { + // 管理员:查询所有设备 + totalDevices = ioTDeviceMapper.countAllDevices(); + onlineDevices = ioTDeviceMapper.countOnlineDevices(); + } else { + // 普通用户:只查询自己的设备 + totalDevices = ioTDeviceMapper.countDevicesByCreator(unionId); + onlineDevices = ioTDeviceMapper.countOnlineDevicesByCreator(unionId); + } + + long offlineDevices = totalDevices - onlineDevices; + double onlineRate = totalDevices > 0 ? (double) onlineDevices / totalDevices * 100 : 0.0; + + deviceStats.put("totalDevices", totalDevices); + deviceStats.put("onlineDevices", onlineDevices); + deviceStats.put("offlineDevices", offlineDevices); + deviceStats.put("onlineRate", onlineRate); + + log.debug( + "[仪表盘] 设备统计(用户过滤): 总数={}, 在线={}, 离线={}, 在线率={}%", + totalDevices, onlineDevices, offlineDevices, onlineRate); + + } catch (Exception e) { + log.error("[仪表盘] 获取设备统计数据失败", e); + deviceStats.put("totalDevices", 0L); + deviceStats.put("onlineDevices", 0L); + deviceStats.put("offlineDevices", 0L); + deviceStats.put("onlineRate", 0.0); + } + + return deviceStats; + } + + /** 获取消息统计数据(带用户权限过滤) */ + private Map getMessageStatsWithUserFilter( + String dateKey, String unionId, boolean isAdmin) { + Map messageStats = new HashMap<>(); + + try { + if (isAdmin) { + // 管理员:获取所有消息统计 + return getMessageStats(dateKey); + } else { + // 普通用户:获取自己的消息统计 + // 这里需要根据用户权限过滤,暂时返回空数据 + messageStats.put("totalMessages", 0L); + messageStats.put("successMessages", 0L); + messageStats.put("failedMessages", 0L); + messageStats.put("successRate", 0.0); + messageStats.put("avgPerHour", 0.0); + } + } catch (Exception e) { + log.error("[仪表盘] 获取消息统计数据失败", e); + } + + return messageStats; + } + + /** 获取渠道统计数据(带用户权限过滤) */ + private Map getChannelStatsWithUserFilter( + String dateKey, String unionId, boolean isAdmin) { + Map channelStats = new HashMap<>(); + + try { + if (isAdmin) { + // 管理员:获取所有渠道统计 + return getChannelStats(dateKey); + } else { + // 普通用户:获取自己的渠道统计 + // 这里需要根据用户权限过滤,暂时返回空数据 + channelStats.put("channels", new HashMap<>()); + } + } catch (Exception e) { + log.error("[仪表盘] 获取渠道统计数据失败", e); + } + + return channelStats; + } + + /** 获取设备统计数据 */ + private Map getDeviceStats(String dateKey) { + Map deviceStats = new HashMap<>(); + + try { + // 直接从数据库查询设备总数 + long totalDevices = ioTDeviceMapper.countAllDevices(); + + // 直接从数据库查询在线设备数 + long onlineDevices = ioTDeviceMapper.countOnlineDevices(); + + long offlineDevices = totalDevices - onlineDevices; + double onlineRate = totalDevices > 0 ? (double) onlineDevices / totalDevices * 100 : 0.0; + + deviceStats.put("totalDevices", totalDevices); + deviceStats.put("onlineDevices", onlineDevices); + deviceStats.put("offlineDevices", offlineDevices); + deviceStats.put("onlineRate", onlineRate); + + log.debug( + "[仪表盘] 设备统计: 总数={}, 在线={}, 离线={}, 在线率={}%", + totalDevices, onlineDevices, offlineDevices, onlineRate); + + } catch (Exception e) { + log.error("[仪表盘] 获取设备统计数据失败", e); + deviceStats.put("totalDevices", 0L); + deviceStats.put("onlineDevices", 0L); + deviceStats.put("offlineDevices", 0L); + deviceStats.put("onlineRate", 0.0); + } + + return deviceStats; + } + + /** 获取消息统计数据 */ + private Map getMessageStats(String dateKey) { + Map messageStats = new HashMap<>(); + + try { + // 消息总数 + String totalKey = String.format("dashboard:metric:%s:all:all:all:message_total", dateKey); + String totalValue = redisTemplate.opsForValue().get(totalKey); + long totalMessages = totalValue != null ? Long.valueOf(totalValue) : 0L; + + // 成功消息数 + String successKey = String.format("dashboard:metric:%s:all:all:all:message_success", dateKey); + String successValue = redisTemplate.opsForValue().get(successKey); + long successMessages = successValue != null ? Long.valueOf(successValue) : 0L; + + // 失败消息数 + String failedKey = String.format("dashboard:metric:%s:all:all:all:message_failed", dateKey); + String failedValue = redisTemplate.opsForValue().get(failedKey); + long failedMessages = failedValue != null ? Long.valueOf(failedValue) : 0L; + + messageStats.put("totalMessages", totalMessages); + messageStats.put("successMessages", successMessages); + messageStats.put("failedMessages", failedMessages); + messageStats.put( + "successRate", totalMessages > 0 ? (double) successMessages / totalMessages * 100 : 0.0); + messageStats.put("avgPerHour", totalMessages / 24.0); // 假设24小时 + + } catch (Exception e) { + log.error("[仪表盘] 获取消息统计数据失败", e); + } + + return messageStats; + } + + /** 获取性能统计数据 */ + private Map getPerformanceStats(String dateKey) { + Map performanceStats = new HashMap<>(); + + try { + // 平均响应时间 + String responseTimeKey = + String.format("dashboard:metric:%s:all:all:all:response_time_total", dateKey); + String responseCountKey = + String.format("dashboard:metric:%s:all:all:all:response_count", dateKey); + + String totalTime = redisTemplate.opsForValue().get(responseTimeKey); + String count = redisTemplate.opsForValue().get(responseCountKey); + + double avgResponseTime = 0.0; + if (totalTime != null && count != null) { + long total = Long.valueOf(totalTime); + long responseCount = Long.valueOf(count); + avgResponseTime = responseCount > 0 ? (double) total / responseCount : 0.0; + } + + performanceStats.put("avgResponseTime", avgResponseTime); + performanceStats.put("messageProcessingSpeed", getMessageProcessingSpeed(dateKey)); + performanceStats.put("systemThroughput", getSystemThroughput()); + performanceStats.put("connectionStability", getConnectionStability(dateKey)); + + } catch (Exception e) { + log.error("[仪表盘] 获取性能统计数据失败", e); + } + + return performanceStats; + } + + /** 获取渠道统计数据 */ + private Map getChannelStats(String dateKey) { + Map channelStats = new HashMap<>(); + + try { + String[] channels = {"HTTP", "MQTT", "TCP", "Kafka"}; + Map channelData = new HashMap<>(); + + for (String channel : channels) { + String channelKey = + String.format("dashboard:metric:%s:all:%s:all:message_total", dateKey, channel); + String value = redisTemplate.opsForValue().get(channelKey); + long count = value != null ? Long.valueOf(value) : 0L; + channelData.put(channel, count); + } + + channelStats.put("channels", channelData); + + } catch (Exception e) { + log.error("[仪表盘] 获取渠道统计数据失败", e); + } + + return channelStats; + } + + /** 获取消息处理速度(消息/秒) */ + private double getMessageProcessingSpeed(String dateKey) { + try { + String totalKey = String.format("dashboard:metric:%s:all:all:all:message_total", dateKey); + String value = redisTemplate.opsForValue().get(totalKey); + long totalMessages = value != null ? Long.valueOf(value) : 0L; + return totalMessages > 0 ? (double) totalMessages / 86400 : 0.0; // 86400秒/天 + } catch (Exception e) { + log.error("[仪表盘] 获取消息处理速度失败", e); + } + return 0.0; + } + + /** 获取系统吞吐量(MB/s) */ + private double getSystemThroughput() { + // 这里可以集成系统监控数据 + // 暂时返回模拟数据 + return Math.random() * 10.0 + 1.0; // 1.0-11.0 MB/s + } + + /** 获取连接稳定性(%) */ + private double getConnectionStability(String dateKey) { + try { + String totalKey = String.format("dashboard:metric:%s:all:all:all:device_total", dateKey); + String onlineKey = String.format("dashboard:metric:%s:all:all:all:device_online", dateKey); + + String totalValue = redisTemplate.opsForValue().get(totalKey); + String onlineValue = redisTemplate.opsForValue().get(onlineKey); + + if (totalValue != null && onlineValue != null) { + long total = Long.valueOf(totalValue); + long online = Long.valueOf(onlineValue); + return total > 0 ? (double) online / total * 100 : 0.0; + } + } catch (Exception e) { + log.error("[仪表盘] 获取连接稳定性失败", e); + } + return 0.0; + } + + /** 获取设备趋势数据 */ + private Map getDeviceTrendData(String dateKey) { + Map data = new HashMap<>(); + // 这里可以实现24小时设备在线趋势数据 + // 暂时返回模拟数据 + data.put("title", "设备在线趋势"); + data.put("series", new Object[0]); // 实际应该返回24小时数据 + return data; + } + + /** 获取消息趋势数据 */ + private Map getMessageTrendData(String dateKey, String channel) { + Map data = new HashMap<>(); + // 这里可以实现24小时消息趋势数据 + data.put("title", "消息量趋势"); + data.put("series", new Object[0]); // 实际应该返回24小时数据 + return data; + } + + /** 获取渠道分布数据 */ + private Map getChannelDistributionData(String dateKey) { + Map data = new HashMap<>(); + data.put("title", "渠道消息分布"); + data.put("series", new Object[0]); // 实际应该返回渠道分布数据 + return data; + } + + /** 获取性能指标数据 */ + private Map getPerformanceMetricsData(String dateKey) { + Map data = new HashMap<>(); + data.put("title", "系统性能指标"); + data.put("series", new Object[0]); // 实际应该返回性能指标数据 + return data; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/DashboardStatisticsTask.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/DashboardStatisticsTask.java new file mode 100644 index 0000000..4c2fdca --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/DashboardStatisticsTask.java @@ -0,0 +1,438 @@ +package cn.universal.dm.device.service; + +import cn.universal.dm.device.service.push.processor.PushStatisticsProcessor; +import cn.universal.persistence.entity.IoTDashboardStatistics; +import cn.universal.persistence.mapper.IoTDashboardStatisticsMapper; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 仪表盘统计定时任务 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +@Slf4j +@Component +public class DashboardStatisticsTask implements ApplicationRunner { + + @Autowired private IoTDashboardStatisticsMapper dashboardStatisticsMapper; + + @Autowired private StringRedisTemplate redisTemplate; + + @Autowired private IoTDeviceMapper ioTDeviceMapper; + + @Autowired private DashboardService dashboardService; + + @Autowired private PushStatisticsProcessor pushStatisticsProcessor; + + // 分布式锁key前缀 + private static final String LOCK_KEY_PREFIX = "dashboard:statistics:lock:"; + + // 执行状态key前缀 + private static final String EXECUTION_STATUS_PREFIX = "dashboard:statistics:status:"; + + // 锁超时时间(秒) + private static final long LOCK_TIMEOUT = 300; // 5分钟 + + // 最小执行间隔(秒) + private static final long MIN_EXECUTION_INTERVAL = 300; // 5分钟 + + /** 获取所有平台列表 */ + private List getAllPlatforms() { + try { + // 从PushStatisticsProcessor的本地缓存中获取活跃的平台 + List platforms = pushStatisticsProcessor.getActivePlatforms(); + + // 如果本地缓存中没有平台数据,使用默认的平台列表 + if (platforms.isEmpty()) { + platforms = Arrays.asList("ctaiot", "lvzhou", "ezviz", "onenet", "lechen", "tcp", "snitcp"); + log.warn("[仪表盘统计] 本地缓存中无活跃平台,使用默认平台列表"); + } + + log.info("[仪表盘统计] 获取到 {} 个活跃平台: {}", platforms.size(), platforms); + return platforms; + + } catch (Exception e) { + log.error("[仪表盘统计] 获取活跃平台失败", e); + // 异常时返回默认平台列表 + return Arrays.asList("ctaiot", "lvzhou", "ezviz", "onenet", "lechen", "tcp", "snitcp"); + } + } + + /** 应用启动时初始化统计数据 */ + @Override + public void run(ApplicationArguments args) throws Exception { + log.info("[仪表盘统计] 应用启动,开始初始化统计数据"); + + try { + // 延迟5秒执行,确保所有组件都已初始化 + Thread.sleep(5000); + + // 执行一次初始刷新 + refreshDatabaseStatisticsWithLock(); + + log.info("[仪表盘统计] 统计数据初始化完成"); + } catch (Exception e) { + log.error("[仪表盘统计] 统计数据初始化失败", e); + } + } + + /** 每10分钟刷新数据库统计数据(带分布式锁) */ + @Scheduled(fixedRate = 600000) // 10分钟 = 600000毫秒 + public void refreshDatabaseStatistics() { + refreshDatabaseStatisticsWithLock(); + } + + /** 带分布式锁的数据库统计刷新 */ + private void refreshDatabaseStatisticsWithLock() { + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + String lockKey = LOCK_KEY_PREFIX + today; + String statusKey = EXECUTION_STATUS_PREFIX + today; + + // 检查是否已经执行过 + String lastExecutionTime = redisTemplate.opsForValue().get(statusKey); + if (lastExecutionTime != null) { + try { + long lastTime = Long.parseLong(lastExecutionTime); + long currentTime = System.currentTimeMillis(); + if (currentTime - lastTime < MIN_EXECUTION_INTERVAL * 1000) { + log.info("[仪表盘统计] 距离上次执行时间不足{}秒,跳过本次执行", MIN_EXECUTION_INTERVAL); + return; + } + } catch (NumberFormatException e) { + log.warn("[仪表盘统计] 解析上次执行时间失败", e); + } + } + + // 尝试获取分布式锁 + Boolean lockAcquired = + redisTemplate + .opsForValue() + .setIfAbsent( + lockKey, + String.valueOf(System.currentTimeMillis()), + LOCK_TIMEOUT, + TimeUnit.SECONDS); + + if (Boolean.TRUE.equals(lockAcquired)) { + try { + log.info("[仪表盘统计] 获取到分布式锁,开始执行10分钟数据库刷新"); + + LocalDate todayDate = LocalDate.now(); + + // 刷新平台消息统计数据 + refreshPlatformMessageStatistics(todayDate); + + // 刷新产品消息统计数据 + refreshProductMessageStatistics(todayDate); + + // 记录执行时间 + redisTemplate + .opsForValue() + .set( + statusKey, + String.valueOf(System.currentTimeMillis()), + LOCK_TIMEOUT, + TimeUnit.SECONDS); + + log.info("[仪表盘统计] 10分钟数据库刷新完成"); + } catch (Exception e) { + log.error("[仪表盘统计] 10分钟数据库刷新失败", e); + } finally { + // 释放锁 + redisTemplate.delete(lockKey); + log.debug("[仪表盘统计] 释放分布式锁"); + } + } else { + log.info("[仪表盘统计] 未获取到分布式锁,跳过本次执行"); + } + } + + /** 刷新平台消息统计数据 */ + private void refreshPlatformMessageStatistics(LocalDate date) { + try { + List platforms = getAllPlatforms(); + String dateKey = date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + + for (String platform : platforms) { + // 查询平台消息统计 - 每个平台有独立的key + String platformKey = String.format("push:platform:%s:%s", dateKey, platform); + + // 从Redis Hash中获取平台统计数据 + Object totalObj = redisTemplate.opsForHash().get(platformKey, "total_count"); + Object successObj = redisTemplate.opsForHash().get(platformKey, "success_count"); + Object failedObj = redisTemplate.opsForHash().get(platformKey, "failed_count"); + Object retryObj = redisTemplate.opsForHash().get(platformKey, "retry_count"); + Object pushObj = redisTemplate.opsForHash().get(platformKey, "push_count"); + + long totalValue = totalObj != null ? Long.parseLong(totalObj.toString()) : 0L; + long successValue = successObj != null ? Long.parseLong(successObj.toString()) : 0L; + long failedValue = failedObj != null ? Long.parseLong(failedObj.toString()) : 0L; + long retryValue = retryObj != null ? Long.parseLong(retryObj.toString()) : 0L; + long pushValue = pushObj != null ? Long.parseLong(pushObj.toString()) : 0L; + + // 更新平台消息总数 + IoTDashboardStatistics platformTotal = + IoTDashboardStatistics.builder() + .statDate(date) + .productKey(null) + .channel(platform.toUpperCase()) + .metricType(IoTDashboardStatistics.MetricType.MESSAGE_TOTAL.getCode()) + .metricValue(totalValue) + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build(); + + log.info( + "[仪表盘统计] 平台统计 - 日期:{}, 产品:{}, 渠道:{}, 类型:{}, 值:{}", + platformTotal.getStatDate(), + platformTotal.getProductKey(), + platformTotal.getChannel(), + platformTotal.getMetricType(), + platformTotal.getMetricValue()); + + // 执行前记录 + log.info("[仪表盘统计] 准备执行recordStatistics,平台: {}", platform); + dashboardService.recordStatistics(platformTotal); + log.info("[仪表盘统计] recordStatistics执行完成,平台: {}", platform); + + // 更新平台推送消息数 + IoTDashboardStatistics platformPush = + IoTDashboardStatistics.builder() + .statDate(date) + .productKey(null) + .channel(platform.toUpperCase()) + .metricType(IoTDashboardStatistics.MetricType.MESSAGE_PUSH.getCode()) + .metricValue(pushValue) + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build(); + dashboardService.recordStatistics(platformPush); + + // 更新平台成功消息数 + IoTDashboardStatistics platformSuccess = + IoTDashboardStatistics.builder() + .statDate(date) + .productKey(null) + .channel(platform.toUpperCase()) + .metricType(IoTDashboardStatistics.MetricType.MESSAGE_SUCCESS.getCode()) + .metricValue(successValue) + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build(); + dashboardService.recordStatistics(platformSuccess); + + // 更新平台失败消息数 + IoTDashboardStatistics platformFailed = + IoTDashboardStatistics.builder() + .statDate(date) + .productKey(null) + .channel(platform.toUpperCase()) + .metricType(IoTDashboardStatistics.MetricType.MESSAGE_FAILED.getCode()) + .metricValue(failedValue) + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build(); + dashboardService.recordStatistics(platformFailed); + + // 更新平台重试消息数 + IoTDashboardStatistics platformRetry = + IoTDashboardStatistics.builder() + .statDate(date) + .productKey(null) + .channel(platform.toUpperCase()) + .metricType(IoTDashboardStatistics.MetricType.MESSAGE_RETRY.getCode()) + .metricValue(retryValue) + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build(); + dashboardService.recordStatistics(platformRetry); + + log.debug( + "[仪表盘统计] 平台{}统计刷新完成: 总数={}, 推送={}, 成功={}, 失败={}, 重试={}", + platform, + totalValue, + pushValue, + successValue, + failedValue, + retryValue); + } + } catch (Exception e) { + log.error("[仪表盘统计] 平台消息统计刷新失败", e); + } + } + + /** 刷新产品消息统计数据 */ + private void refreshProductMessageStatistics(LocalDate date) { + try { + // 获取所有活跃的产品key + List productKeys = getActiveProductKeys(); + String dateKey = date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + + for (String productKey : productKeys) { + // 查询产品消息统计 + String productStatsKey = String.format("push:product:%s:%s", dateKey, productKey); + + // 从Redis Hash中获取产品统计数据 + Object totalObj = redisTemplate.opsForHash().get(productStatsKey, "total"); + Object successObj = redisTemplate.opsForHash().get(productStatsKey, "success"); + Object failedObj = redisTemplate.opsForHash().get(productStatsKey, "failed"); + Object retryObj = redisTemplate.opsForHash().get(productStatsKey, "retry"); + Object pushObj = redisTemplate.opsForHash().get(productStatsKey, "push"); + + long totalValue = totalObj != null ? Long.parseLong(totalObj.toString()) : 0L; + long successValue = successObj != null ? Long.parseLong(successObj.toString()) : 0L; + long failedValue = failedObj != null ? Long.parseLong(failedObj.toString()) : 0L; + long retryValue = retryObj != null ? Long.parseLong(retryObj.toString()) : 0L; + long pushValue = pushObj != null ? Long.parseLong(pushObj.toString()) : 0L; + + // 更新产品消息总数 + IoTDashboardStatistics productTotal = + IoTDashboardStatistics.builder() + .statDate(date) + .productKey(productKey) + .channel(null) + .metricType(IoTDashboardStatistics.MetricType.MESSAGE_TOTAL.getCode()) + .metricValue(totalValue) + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build(); + dashboardService.recordStatistics(productTotal); + + // 更新产品推送消息数 + IoTDashboardStatistics productPush = + IoTDashboardStatistics.builder() + .statDate(date) + .productKey(productKey) + .channel(null) + .metricType(IoTDashboardStatistics.MetricType.MESSAGE_PUSH.getCode()) + .metricValue(pushValue) + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build(); + dashboardService.recordStatistics(productPush); + + // 更新产品成功消息数 + IoTDashboardStatistics productSuccess = + IoTDashboardStatistics.builder() + .statDate(date) + .productKey(productKey) + .channel(null) + .metricType(IoTDashboardStatistics.MetricType.MESSAGE_SUCCESS.getCode()) + .metricValue(successValue) + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build(); + dashboardService.recordStatistics(productSuccess); + + // 更新产品失败消息数 + IoTDashboardStatistics productFailed = + IoTDashboardStatistics.builder() + .statDate(date) + .productKey(productKey) + .channel(null) + .metricType(IoTDashboardStatistics.MetricType.MESSAGE_FAILED.getCode()) + .metricValue(failedValue) + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build(); + dashboardService.recordStatistics(productFailed); + + // 更新产品重试消息数 + IoTDashboardStatistics productRetry = + IoTDashboardStatistics.builder() + .statDate(date) + .productKey(productKey) + .channel(null) + .metricType(IoTDashboardStatistics.MetricType.MESSAGE_RETRY.getCode()) + .metricValue(retryValue) + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build(); + dashboardService.recordStatistics(productRetry); + + log.debug( + "[仪表盘统计] 产品{}统计刷新完成: 总数={}, 推送={}, 成功={}, 失败={}, 重试={}", + productKey, + totalValue, + pushValue, + successValue, + failedValue, + retryValue); + } + } catch (Exception e) { + log.error("[仪表盘统计] 产品消息统计刷新失败", e); + } + } + + /** 获取活跃的产品key列表 */ + private List getActiveProductKeys() { + try { + // 从PushStatisticsProcessor的本地缓存中获取活跃的产品key + List productKeys = pushStatisticsProcessor.getActiveProductKeys(); + log.info( + "[仪表盘统计] 从PushStatisticsProcessor获取到 {} 个活跃产品key: {}", productKeys.size(), productKeys); + return productKeys; + + } catch (Exception e) { + log.error("[仪表盘统计] 获取活跃产品key失败", e); + // 异常时返回空列表,不进行任何统计 + return new ArrayList<>(); + } + } + + /** 手动刷新统计数据 */ + public void manualRefreshStatistics() { + log.info("[仪表盘统计] 手动触发统计数据刷新"); + refreshDatabaseStatisticsWithLock(); + } + + /** 检查指定日期的统计数据是否存在 */ + public boolean checkStatisticsExists(LocalDate date) { + try { + // 检查是否存在当天的MESSAGE_TOTAL记录 + IoTDashboardStatistics query = + IoTDashboardStatistics.builder() + .statDate(date) + .productKey(null) + .channel(null) + .metricType(IoTDashboardStatistics.MetricType.MESSAGE_TOTAL.getCode()) + .build(); + + List existingStats = dashboardStatisticsMapper.select(query); + return !existingStats.isEmpty(); + } catch (Exception e) { + log.error("[仪表盘统计] 检查统计数据存在性失败", e); + return false; + } + } + + /** 清理重复的统计数据 */ + public void cleanupDuplicateStatistics(LocalDate date) { + try { + log.info("[仪表盘统计] 开始清理{}的重复统计数据", date); + + // 删除指定日期的所有统计数据 + int deletedCount = dashboardStatisticsMapper.deleteByDate(date); + + log.info("[仪表盘统计] 清理完成,删除了{}条重复记录", deletedCount); + } catch (Exception e) { + log.error("[仪表盘统计] 清理重复统计数据失败", e); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/IoTUPPushAdapter.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/IoTUPPushAdapter.java new file mode 100644 index 0000000..7c55d94 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/IoTUPPushAdapter.java @@ -0,0 +1,216 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.dm.device.service.push.UPProcessorManager; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.base.IoTDeviceExtendService; +import cn.universal.persistence.base.IoTUPWrapper; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.entity.bo.UPPushBO; +import jakarta.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; + +/** + * @version 1.0 @Author Aleo + * @since 2025/8/6 11:56 + */ +@Slf4j +public abstract class IoTUPPushAdapter { + + @Resource private IoTDeviceExtendService deviceExtTemplate; + + @Resource private UPProcessorManager upProcessorManager; + + @Resource private UPPushConfigService upPushConfigService; + + @Resource private UPPushExecutor upPushExecutor; + + protected void sendMqtt(String topic, String msg) { + // mqttUPWrapper.mqttPush(topic, msg); + } + + /** + * 推送前钩子方法,子类可重写进行消息转换、规则引擎等处理 + * + * @param upRequests 上行请求列表 + */ + protected void beforePush(List upRequests) { + // 调用现有的beforePush逻辑 + callExistingBeforePush(upRequests); + + // 执行插件化处理器 + if (upProcessorManager != null) { + List ts = upProcessorManager.executeBeforePush(upRequests); + if (CollectionUtil.isNotEmpty(ts)) { + upRequests.clear(); + upRequests.addAll(ts); + } + } + + // 子类可重写此方法进行扩展 + onBeforePush(upRequests); + } + + /** + * 推送后钩子方法,子类可重写进行结果处理、日志记录等 + * + * @param upRequests 上行请求列表 + * @param pushResults 推送结果列表 + */ + protected void afterPush(List upRequests, List pushResults) { + // 执行插件化处理器(带推送结果) + if (upProcessorManager != null) { + upProcessorManager.executeAfterPush(upRequests, pushResults); + } + + // 子类可重写此方法进行扩展 + onAfterPush(upRequests, pushResults); + } + + /** + * 推送前扩展点,子类可重写 + * + * @param upRequests 上行请求列表 + */ + protected void onBeforePush(List upRequests) { + // 默认空实现,子类可重写 + } + + /** + * 推送后扩展点,子类可重写 + * + * @param upRequests 上行请求列表 + * @param pushResults 推送结果列表 + */ + protected void onAfterPush(List upRequests, List pushResults) { + // 默认空实现,子类可重写 + } + + /** + * 实际的推送逻辑 + * + * @param upRequests 上行请求列表 + * @return 推送结果列表 + */ + protected List executePush(List upRequests) { + upRequests.forEach(req -> deviceExtTemplate.upExt(req)); + + // 按应用分组处理推送 + Map> appGroups = groupByApplication(upRequests); + List allResults = new java.util.ArrayList<>(); + + for (Map.Entry> entry : appGroups.entrySet()) { + String appId = entry.getKey(); + List requests = entry.getValue(); + + try { + // 获取推送配置 + UPPushBO pushConfig = upPushConfigService.getPushConfig(appId); + List results = + upPushExecutor.executeBatchPush((List) requests, pushConfig); + log.debug( + "[推送执行] 应用 {} 推送完成,消息数量: {}, 结果数量: {}", + appId, + requests.size(), + results != null ? results.size() : 0); + + // 收集推送结果 + if (results != null) { + allResults.addAll(results); + } + } catch (Exception e) { + log.error("[推送执行] 应用 {} 推送异常", appId, e); + } + } + + return allResults; + } + + /** + * 按应用ID分组,过滤掉无效的请求 + * + * @param upRequests 上行请求列表 + * @return 分组后的请求映射 + */ + private Map> groupByApplication(List upRequests) { + return upRequests.stream() + .filter( + request -> { + IoTDeviceDTO deviceDTO = request.getIoTDeviceDTO(); + if (deviceDTO == null) { + log.warn("[分组] 设备DTO为空,丢弃消息: {}", request); + return false; + } + + String applicationId = deviceDTO.getApplicationId(); + if (applicationId == null || applicationId.trim().isEmpty()) { + log.warn("[分组] 应用ID为空,设备ID: {}, 丢弃消息", deviceDTO.getIotId()); + return false; + } + return true; + }) + .collect( + Collectors.groupingBy( + request -> { + IoTDeviceDTO deviceDTO = request.getIoTDeviceDTO(); + return deviceDTO.getApplicationId(); + })); + } + + /** + * 模板方法:统一的上行消息处理流程 + * + * @param upRequests 上行请求列表 + * @return 处理结果 + */ + protected String doUp(List upRequests) { + if (CollectionUtil.isEmpty(upRequests)) { + return null; + } + // 推送前处理 + beforePush(upRequests); + + // 执行推送 + List pushResults = executePush(upRequests); + + // 推送后处理 + afterPush(upRequests, pushResults); + + return null; + } + + /** + * 调用现有的beforePush逻辑 + * + * @param upRequests 上行请求列表 + */ + private void callExistingBeforePush(List upRequests) { + Map downWrapperMap = SpringUtil.getBeansOfType(IoTUPWrapper.class); + if (MapUtil.isNotEmpty(downWrapperMap)) { + for (Map.Entry entry : downWrapperMap.entrySet()) { + String serviceName = entry.getKey(); + IoTUPWrapper iotDownWrapper = entry.getValue(); + iotDownWrapper.beforePush(upRequests); + log.debug("调用推送前置处理,实现名称={}.返回结果={}", serviceName); + } + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/SystemMonitorService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/SystemMonitorService.java new file mode 100644 index 0000000..3ca4a3a --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/SystemMonitorService.java @@ -0,0 +1,428 @@ +package cn.universal.dm.device.service; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * Linux系统监控服务 获取CPU、内存、磁盘、网络流量数据 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +@Slf4j +@Service +public class SystemMonitorService { + + // 系统监控数据缓存 + private final AtomicReference> systemMetrics = + new AtomicReference<>(new HashMap<>()); + + // 上次CPU时间 + private long lastCpuTime = 0; + private long lastCpuIdleTime = 0; + + /** 定时更新系统监控数据(每5秒执行一次) */ + // @Scheduled(fixedRate = 5000) + public void updateSystemMetrics() { + try { + Map metrics = new HashMap<>(); + + // 获取CPU使用率 + double cpuUsage = getCpuUsage(); + metrics.put("cpuUsage", cpuUsage); + + // 获取内存使用率 + double memoryUsage = getMemoryUsage(); + metrics.put("memoryUsage", memoryUsage); + + // 获取磁盘使用率 + double diskUsage = getDiskUsage(); + metrics.put("diskUsage", diskUsage); + + // 获取网络流量 + Map networkTraffic = getNetworkTraffic(); + metrics.put("networkTraffic", networkTraffic); + + // 获取系统负载 + Map systemLoad = getSystemLoad(); + metrics.put("systemLoad", systemLoad); + + // 更新时间戳 + metrics.put("timestamp", System.currentTimeMillis()); + + // 更新缓存 + systemMetrics.set(metrics); + + log.debug("[系统监控] 系统指标已更新: CPU={}%, Memory={}%, Disk={}%", cpuUsage, memoryUsage, diskUsage); + + } catch (Exception e) { + log.error("[系统监控] 更新系统指标失败", e); + } + } + + /** 获取CPU使用率 */ + private double getCpuUsage() { + try { + // 读取/proc/stat文件获取CPU信息 + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + new ProcessBuilder("cat", "/proc/stat").start().getInputStream())); + + String line = reader.readLine(); + if (line != null && line.startsWith("cpu ")) { + String[] parts = line.split("\\s+"); + + if (parts.length >= 8) { + try { + long user = Long.parseLong(parts[1]); + long nice = Long.parseLong(parts[2]); + long system = Long.parseLong(parts[3]); + long idle = Long.parseLong(parts[4]); + long iowait = Long.parseLong(parts[5]); + long irq = Long.parseLong(parts[6]); + long softirq = Long.parseLong(parts[7]); + + long totalCpuTime = user + nice + system + idle + iowait + irq + softirq; + long totalCpuIdleTime = idle + iowait; + + if (lastCpuTime > 0) { + long totalDiff = totalCpuTime - lastCpuTime; + long idleDiff = totalCpuIdleTime - lastCpuIdleTime; + + if (totalDiff > 0) { + double cpuUsage = 100.0 * (1.0 - (double) idleDiff / totalDiff); + lastCpuTime = totalCpuTime; + lastCpuIdleTime = totalCpuIdleTime; + return Math.min(100.0, Math.max(0.0, cpuUsage)); + } + } else { + lastCpuTime = totalCpuTime; + lastCpuIdleTime = totalCpuIdleTime; + } + } catch (NumberFormatException e) { + log.error("[系统监控] 解析CPU数据失败: {}", line, e); + } + } + } + reader.close(); + } catch (Exception e) { + log.error("[系统监控] 获取CPU使用率失败", e); + } + return 0.0; + } + + /** 获取内存使用率 */ + private double getMemoryUsage() { + try { + // 读取/proc/meminfo文件获取内存信息 + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + new ProcessBuilder("cat", "/proc/meminfo").start().getInputStream())); + + long totalMemory = 0; + long availableMemory = 0; + + String line; + while ((line = reader.readLine()) != null) { + try { + if (line.startsWith("MemTotal:")) { + // 解析格式: "MemTotal: 8131912 kB" + String[] parts = line.split("\\s+"); + if (parts.length >= 3) { + totalMemory = Long.parseLong(parts[1]); + log.debug("[系统监控] 总内存: {} kB", totalMemory); + } + } else if (line.startsWith("MemAvailable:")) { + // 解析格式: "MemAvailable: 4029336 kB" + String[] parts = line.split("\\s+"); + if (parts.length >= 3) { + availableMemory = Long.parseLong(parts[1]); + log.debug("[系统监控] 可用内存: {} kB", availableMemory); + break; + } + } + } catch (NumberFormatException e) { + log.debug("[系统监控] 跳过无法解析的内存信息行: {}", line); + } + } + reader.close(); + + if (totalMemory > 0) { + double usage = 100.0 * (1.0 - (double) availableMemory / totalMemory); + log.debug( + "[系统监控] 内存使用率计算: 总内存={} kB, 可用内存={} kB, 使用率={}%", totalMemory, availableMemory, usage); + return Math.min(100.0, Math.max(0.0, usage)); + } + } catch (Exception e) { + log.error("[系统监控] 获取内存使用率失败", e); + } + return 0.0; + } + + /** 获取磁盘使用率 */ + private double getDiskUsage() { + try { + // 使用df命令获取磁盘使用情况 + Process process = new ProcessBuilder("df", "/").start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + + // 跳过标题行 + reader.readLine(); + + String line = reader.readLine(); + if (line != null) { + String[] parts = line.split("\\s+"); + if (parts.length >= 5) { + try { + String usageStr = parts[4].replace("%", ""); + return Double.parseDouble(usageStr); + } catch (NumberFormatException e) { + log.error("[系统监控] 解析磁盘使用率失败: {}", parts[4], e); + } + } + } + reader.close(); + } catch (Exception e) { + log.error("[系统监控] 获取磁盘使用率失败", e); + } + return 0.0; + } + + /** 获取网络流量 */ + private Map getNetworkTraffic() { + Map networkData = new HashMap<>(); + + try { + // 读取/proc/net/dev文件获取网络统计 + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + new ProcessBuilder("cat", "/proc/net/dev").start().getInputStream())); + + String line; + long totalRxBytes = 0; + long totalTxBytes = 0; + + while ((line = reader.readLine()) != null) { + // 跳过标题行 + if (line.contains("Inter-|") || line.trim().isEmpty()) { + continue; + } + + // 解析网络接口行 + if (line.contains(":")) { + String[] parts = line.split("\\s+"); + if (parts.length >= 10) { + try { + // 获取接口名称(去掉冒号) + String interfaceName = parts[0].replace(":", ""); + + // 跳过lo接口(本地回环)和虚拟接口 + if (!interfaceName.equals("lo") + && !interfaceName.startsWith("docker") + && !interfaceName.startsWith("veth") + && !interfaceName.startsWith("br-")) { + + // 解析接收字节数(第2个数字) + long rxBytes = Long.parseLong(parts[1]); + // 解析发送字节数(第10个数字) + long txBytes = Long.parseLong(parts[9]); + + totalRxBytes += rxBytes; + totalTxBytes += txBytes; + + log.debug( + "[系统监控] 网络接口: {}, 接收: {} bytes, 发送: {} bytes", interfaceName, rxBytes, txBytes); + } + } catch (NumberFormatException e) { + // 跳过无法解析的行 + log.debug("[系统监控] 跳过无法解析的网络接口行: {}", line); + } + } + } + } + reader.close(); + + // 转换为MB/s(这里简化处理,实际应该计算速率) + double rxMBps = totalRxBytes / (1024.0 * 1024.0); + double txMBps = totalTxBytes / (1024.0 * 1024.0); + + networkData.put("rxMBps", rxMBps); + networkData.put("txMBps", txMBps); + networkData.put("totalRxBytes", totalRxBytes); + networkData.put("totalTxBytes", totalTxBytes); + + log.debug("[系统监控] 网络流量统计: 接收 {} MB, 发送 {} MB", rxMBps, txMBps); + + // 如果没有检测到网络流量,记录调试信息 + if (totalRxBytes == 0 && totalTxBytes == 0) { + log.debug("[系统监控] 未检测到网络流量,可能需要检查网络接口配置"); + } + + } catch (Exception e) { + log.error("[系统监控] 获取网络流量失败", e); + // 返回默认值 + networkData.put("rxMBps", 0.0); + networkData.put("txMBps", 0.0); + networkData.put("totalRxBytes", 0L); + networkData.put("totalTxBytes", 0L); + } + + return networkData; + } + + /** 获取系统负载 */ + private Map getSystemLoad() { + Map loadData = new HashMap<>(); + + try { + // 读取/proc/loadavg文件获取系统负载 + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + new ProcessBuilder("cat", "/proc/loadavg").start().getInputStream())); + + String line = reader.readLine(); + if (line != null) { + String[] parts = line.split("\\s+"); + if (parts.length >= 3) { + try { + loadData.put("load1min", Double.parseDouble(parts[0])); + loadData.put("load5min", Double.parseDouble(parts[1])); + loadData.put("load15min", Double.parseDouble(parts[2])); + } catch (NumberFormatException e) { + log.error("[系统监控] 解析系统负载失败: {}", line, e); + } + } + } + reader.close(); + + } catch (Exception e) { + log.error("[系统监控] 获取系统负载失败", e); + } + + return loadData; + } + + /** 获取系统监控数据 */ + public Map getSystemMetrics() { + return systemMetrics.get(); + } + + /** 获取CPU使用率 */ + public double getCpuUsageRate() { + Map metrics = systemMetrics.get(); + return (Double) metrics.getOrDefault("cpuUsage", 0.0); + } + + /** 获取内存使用率 */ + public double getMemoryUsageRate() { + Map metrics = systemMetrics.get(); + return (Double) metrics.getOrDefault("memoryUsage", 0.0); + } + + /** 获取磁盘使用率 */ + public double getDiskUsageRate() { + Map metrics = systemMetrics.get(); + return (Double) metrics.getOrDefault("diskUsage", 0.0); + } + + /** 获取网络流量(MB/s) */ + public double getNetworkTrafficMBps() { + Map metrics = systemMetrics.get(); + Map networkData = + (Map) metrics.getOrDefault("networkTraffic", new HashMap<>()); + return (Double) networkData.getOrDefault("rxMBps", 0.0); + } + + /** 检查是否为Linux系统 */ + public boolean isLinuxSystem() { + String os = System.getProperty("os.name").toLowerCase(); + return os.contains("linux"); + } + + /** 获取系统信息 */ + public Map getSystemInfo() { + Map systemInfo = new HashMap<>(); + + try { + systemInfo.put("osName", System.getProperty("os.name")); + systemInfo.put("osVersion", System.getProperty("os.version")); + systemInfo.put("osArch", System.getProperty("os.arch")); + systemInfo.put("javaVersion", System.getProperty("java.version")); + systemInfo.put("javaVendor", System.getProperty("java.vendor")); + systemInfo.put("userHome", System.getProperty("user.home")); + systemInfo.put("userDir", System.getProperty("user.dir")); + + // 获取处理器信息 + int processors = Runtime.getRuntime().availableProcessors(); + systemInfo.put("processors", processors); + + // 获取JVM内存信息 + Runtime runtime = Runtime.getRuntime(); + long totalMemory = runtime.totalMemory(); + long freeMemory = runtime.freeMemory(); + long usedMemory = totalMemory - freeMemory; + + systemInfo.put("jvmTotalMemory", totalMemory); + systemInfo.put("jvmFreeMemory", freeMemory); + systemInfo.put("jvmUsedMemory", usedMemory); + systemInfo.put( + "jvmMemoryUsage", totalMemory > 0 ? (double) usedMemory / totalMemory * 100 : 0.0); + + // 添加调试信息 + systemInfo.put("isLinuxSystem", isLinuxSystem()); + + } catch (Exception e) { + log.error("[系统监控] 获取系统信息失败", e); + } + + return systemInfo; + } + + /** 获取调试信息 */ + public Map getDebugInfo() { + Map debugInfo = new HashMap<>(); + + try { + // 检查关键文件是否存在 + debugInfo.put("procStatExists", new File("/proc/stat").exists()); + debugInfo.put("procMeminfoExists", new File("/proc/meminfo").exists()); + debugInfo.put("procNetDevExists", new File("/proc/net/dev").exists()); + debugInfo.put("procLoadavgExists", new File("/proc/loadavg").exists()); + + // 获取网络接口信息 + try { + Process process = new ProcessBuilder("ip", "link", "show").start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + List interfaces = new ArrayList<>(); + String line; + while ((line = reader.readLine()) != null) { + if (line.contains(":") && !line.contains("lo:")) { + interfaces.add(line.trim()); + } + } + reader.close(); + debugInfo.put("networkInterfaces", interfaces); + } catch (Exception e) { + debugInfo.put("networkInterfacesError", e.getMessage()); + } + + } catch (Exception e) { + log.error("[系统监控] 获取调试信息失败", e); + } + + return debugInfo; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/UPPushConfigService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/UPPushConfigService.java new file mode 100644 index 0000000..1eb496a --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/UPPushConfigService.java @@ -0,0 +1,231 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.persistence.entity.IoTUserApplication; +import cn.universal.persistence.entity.bo.UPPushBO; +import cn.universal.persistence.mapper.IoTUserApplicationMapper; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import jakarta.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +/** + * 上行推送配置服务 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +@Slf4j +@Service +public class UPPushConfigService { + + @Resource private IoTUserApplicationMapper ioTUserApplicationMapper; + + // 本地缓存:应用ID -> 推送配置 + private final Cache pushConfigCache = + Caffeine.newBuilder() + .maximumSize(5000) // 最大缓存1000个应用 + .expireAfterWrite(10, TimeUnit.MINUTES) // 10分钟过期 + .build(); + + /** + * 获取推送配置(带缓存) + * + * @param applicationId 应用ID + * @return 推送配置 + */ + @Cacheable(cacheNames = "getPushConfig", key = "#applicationId", unless = "#result == null") + public UPPushBO getPushConfig(String applicationId) { + if (StrUtil.isBlank(applicationId)) { + log.warn("[推送配置] 应用ID为空,返回默认配置"); + return getDefaultPushConfig(); + } + + // 先从缓存获取 + UPPushBO cachedConfig = pushConfigCache.getIfPresent(applicationId); + if (cachedConfig != null) { + log.debug("[推送配置] 缓存命中,应用ID: {}", applicationId); + return cachedConfig; + } + + // 缓存未命中,从数据库查询 + log.debug("[推送配置] 缓存未命中,从数据库查询,应用ID: {}", applicationId); + UPPushBO config = loadPushConfigFromDB(applicationId); + + // 缓存配置 + if (config != null) { + pushConfigCache.put(applicationId, config); + log.debug("[推送配置] 配置已缓存,应用ID: {}", applicationId); + } + + return config != null ? config : getDefaultPushConfig(); + } + + /** + * 批量获取推送配置 + * + * @param applicationIds 应用ID列表 + * @return 推送配置映射 + */ + public Map getPushConfigs(List applicationIds) { + return applicationIds.stream() + .collect(java.util.stream.Collectors.toMap(appId -> appId, this::getPushConfig)); + } + + /** + * 刷新缓存 + * + * @param applicationId 应用ID + */ + public void refreshCache(String applicationId) { + if (StrUtil.isNotBlank(applicationId)) { + pushConfigCache.invalidate(applicationId); + log.info("[推送配置] 缓存已刷新,应用ID: {}", applicationId); + } + } + + /** 清空所有缓存 */ + public void clearCache() { + pushConfigCache.invalidateAll(); + log.info("[推送配置] 所有缓存已清空"); + } + + /** + * 从数据库加载推送配置 + * + * @param applicationId 应用ID + * @return 推送配置 + */ + private UPPushBO loadPushConfigFromDB(String applicationId) { + try { + IoTUserApplication application = + ioTUserApplicationMapper.selectIotUserApplicationById(applicationId); + if (application == null) { + log.warn("[推送配置] 应用不存在,应用ID: {}", applicationId); + return null; + } + + // 解析推送配置JSON + String pushConfigJson = application.getCfg(); + if (StrUtil.isBlank(pushConfigJson)) { + log.warn("[推送配置] 推送配置为空,应用ID: {}", applicationId); + return getDefaultPushConfig(); + } + + return parsePushConfig(pushConfigJson); + } catch (Exception e) { + log.error("[推送配置] 加载配置异常,应用ID: {}", applicationId, e); + return getDefaultPushConfig(); + } + } + + /** + * 解析推送配置JSON + * + * @param pushConfigJson 推送配置JSON字符串 + * @return 推送配置对象 + */ + private UPPushBO parsePushConfig(String pushConfigJson) { + try { + JSONObject config = JSONUtil.parseObj(pushConfigJson); + + return UPPushBO.builder() + .http(parseHttpConfig(config.getJSONObject("http"))) + .mqtt(parseMqttConfig(config.getJSONObject("mqtt"))) + .kafka(parseKafkaConfig(config.getJSONObject("kafka"))) + .rocketMQ(parseRocketMQConfig(config.getJSONObject("rocketMQ"))) + .build(); + } catch (Exception e) { + log.error("[推送配置] 解析配置JSON异常: {}", pushConfigJson, e); + return getDefaultPushConfig(); + } + } + + /** 解析HTTP配置 */ + private UPPushBO.HttpPushConfig parseHttpConfig(JSONObject httpConfig) { + if (httpConfig == null) { + return UPPushBO.HttpPushConfig.builder().enable(false).support(false).build(); + } + + return UPPushBO.HttpPushConfig.builder() + .url(httpConfig.getStr("url")) + .enable(httpConfig.getBool("enable", false)) + .header(httpConfig.getStr("header")) + .secret(httpConfig.getStr("secret")) + .support(httpConfig.getBool("support", false)) + .build(); + } + + /** 解析MQTT配置 */ + private UPPushBO.MqttPushConfig parseMqttConfig(JSONObject mqttConfig) { + if (mqttConfig == null) { + return UPPushBO.MqttPushConfig.builder().enable(false).support(false).build(); + } + + return UPPushBO.MqttPushConfig.builder() + .url(mqttConfig.getStr("url")) + .topic(mqttConfig.getStr("topic")) + .enable(mqttConfig.getBool("enable", false)) + .support(mqttConfig.getBool("support", false)) + .password(mqttConfig.getStr("password")) + .username(mqttConfig.getStr("username")) + .build(); + } + + /** 解析Kafka配置 */ + private UPPushBO.KafkaPushConfig parseKafkaConfig(JSONObject kafkaConfig) { + if (kafkaConfig == null) { + return UPPushBO.KafkaPushConfig.builder().enable(false).support(false).build(); + } + + return UPPushBO.KafkaPushConfig.builder() + .enable(kafkaConfig.getBool("enable", false)) + .support(kafkaConfig.getBool("support", false)) + .build(); + } + + /** 解析RocketMQ配置 */ + private UPPushBO.RocketMQPushConfig parseRocketMQConfig(JSONObject rocketMQConfig) { + if (rocketMQConfig == null) { + return UPPushBO.RocketMQPushConfig.builder().enable(false).support(false).build(); + } + + return UPPushBO.RocketMQPushConfig.builder() + .enable(rocketMQConfig.getBool("enable", false)) + .support(rocketMQConfig.getBool("support", false)) + .build(); + } + + /** + * 获取默认推送配置 + * + * @return 默认配置 + */ + private UPPushBO getDefaultPushConfig() { + return UPPushBO.builder() + .http(UPPushBO.HttpPushConfig.builder().enable(false).support(false).build()) + .mqtt(UPPushBO.MqttPushConfig.builder().enable(false).support(false).build()) + .kafka(UPPushBO.KafkaPushConfig.builder().enable(false).support(false).build()) + .rocketMQ(UPPushBO.RocketMQPushConfig.builder().enable(false).support(false).build()) + .build(); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/UPPushExecutor.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/UPPushExecutor.java new file mode 100644 index 0000000..2c8f1e5 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/UPPushExecutor.java @@ -0,0 +1,86 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service; + +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.dm.device.service.push.PushStrategyManager; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.entity.bo.UPPushBO; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 上行推送执行器 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +@Slf4j +@Component +public class UPPushExecutor { + + @Autowired private PushStrategyManager pushStrategyManager; + + /** + * 根据配置执行推送 + * + * @param request 上行请求 + * @param config 推送配置 + * @return 推送结果列表 + */ + public List executePush(BaseUPRequest request, UPPushBO config) { + if (request == null || config == null) { + log.warn("[推送执行器] 请求或配置为空,跳过推送"); + return null; + } + + IoTDeviceDTO deviceDTO = request.getIoTDeviceDTO(); + if (deviceDTO == null) { + log.warn("[推送执行器] 设备信息为空,跳过推送"); + return null; + } + + log.debug("[推送执行器] 开始推送消息: {}", request.getIotId()); + + // 使用策略管理器执行推送 + List results = pushStrategyManager.executePush(request, config); + + log.debug("[推送执行器] 推送完成: {}", request.getIotId()); + return results; + } + + /** + * 批量推送 + * + * @param requests 请求列表 + * @param config 推送配置 + * @return 推送结果列表 + */ + public List executeBatchPush(List requests, UPPushBO config) { + if (requests == null || requests.isEmpty()) { + log.warn("[推送执行器] 请求列表为空,跳过批量推送"); + return null; + } + + log.debug("[推送执行器] 开始批量推送,消息数量: {}", requests.size()); + + // 使用策略管理器执行批量推送 + List results = pushStrategyManager.executeBatchPush(requests, config); + + log.debug("[推送执行器] 批量推送完成,消息数量: {}", requests.size()); + return results; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTDeviceActionAfterService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTDeviceActionAfterService.java new file mode 100644 index 0000000..a629e51 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTDeviceActionAfterService.java @@ -0,0 +1,492 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.action; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant.DevLifeCycle; +import cn.universal.common.constant.IoTConstant.MessageType; +import cn.universal.core.message.DownRequest; +import cn.universal.core.metadata.AbstractEventMetadata; +import cn.universal.core.metadata.AbstractFunctionMetadata; +import cn.universal.core.metadata.DeviceMetadata; +import cn.universal.dm.device.service.IoTUPPushAdapter; +import cn.universal.dm.device.service.impl.IoTDeviceService; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import cn.universal.dm.device.service.log.IIoTDeviceDataService; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.base.IoTDeviceLifeCycle; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTDeviceLog; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.mapper.IoTDeviceLogMapper; +import cn.universal.persistence.mapper.IoTDeviceLogShardMapper; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.IoTUserMapper; +import jakarta.annotation.Resource; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import tk.mybatis.mapper.entity.Example; + +/** + * 生命周期后置处理 + * + * @version 1.0 @Author Aleo + * @since 2023/1/20 + */ +@Service("ioTDeviceActionAfterService") +@Slf4j +public class IoTDeviceActionAfterService extends IoTUPPushAdapter + implements IoTDeviceLifeCycle { + + @Resource private IoTDeviceMapper ioTDeviceMapper; + + @Resource private IIoTDeviceDataService iIoTDeviceDataService; + + @Resource private IoTProductDeviceService iotProductDeviceService; + + @Resource private StringRedisTemplate stringRedisTemplate; + + @Resource private IoTDeviceService iotDeviceService; + + @Resource private IoTDeviceLogMapper ioTDeviceLogMapper; + @Resource private IoTDeviceLogShardMapper ioTDeviceLogShardMapper; + @Resource private IoTUserMapper iotUserMapper; + + /** 日志分表是否开启 */ + @Value("${shard.log.enable}") + private Boolean enable; + + /** 加载第三方平台的生命周期支持情况 */ + private Map> lifecycleSupportedConfig = new HashMap<>(); + + @Autowired + void setLifecycleConfig( + @Value("${universal.iot.third.lifecycle.supported}") String lifecyclesupported) { + if (StrUtil.isBlank(lifecyclesupported)) { + return; + } + JSONObject obj = JSONUtil.parseObj(lifecyclesupported); + obj.keySet().stream() + .forEach( + s -> { + lifecycleSupportedConfig.put( + s, + obj.getJSONArray(s).stream().map(c -> c.toString()).collect(Collectors.toSet())); + }); + } + + /** 校验三分平台是否支持此生命周期 */ + @Override + public boolean thirdSupport(IoTDeviceDTO ioTDeviceDTO, DevLifeCycle devLifeCycle) { + if (lifecycleSupportedConfig.containsKey(ioTDeviceDTO.getThirdPlatform())) { + return lifecycleSupportedConfig + .get(ioTDeviceDTO.getThirdPlatform()) + .contains(devLifeCycle.name()); + } + return false; + } + + @Override + public void create(String productKey, String deviceId, DownRequest downRequest) { + if (downRequest.getAppUnionId() != null) { + iotUserMapper.licenseBuckle(downRequest.getAppUnionId()); + } + log.info("设备创建productKey={},deviceId={}", productKey, deviceId); + IoTDeviceDTO ioTDeviceDTO = queryDevInstance(null, deviceId, productKey); + if (ioTDeviceDTO == null) { + return; + } + BaseUPRequest upRequest = build(ioTDeviceDTO); + upRequest.setEvent(DevLifeCycle.create.name()); + upRequest.setEventName(DevLifeCycle.create.getValue()); + // 暂不推送消息,会错误认为设备已经产生通信 + doUp(Stream.of(upRequest).collect(Collectors.toList())); + } + + @Override + public void online(String productKey, String deviceId) { + boolean flag = + stringRedisTemplate + .opsForValue() + .setIfAbsent("online:" + productKey + deviceId, "1", 8, TimeUnit.MINUTES); + IoTDeviceDTO dev = queryDevInstance(null, productKey, deviceId); + if (dev == null || !flag) { + log.debug("设备不存在={},或者在不久刚离线={}", deviceId, flag); + return; + } + stringRedisTemplate.delete("offline:" + productKey + deviceId); + finalOnline(dev); + } + + private void finalOnline(IoTDeviceDTO dev) { + BaseUPRequest upRequest = build(dev); + upRequest.setEvent(DevLifeCycle.online.name()); + upRequest.setEventName(DevLifeCycle.online.getValue()); + saveLog(upRequest, dev); + doOnOffline(dev); + // 避免短期时间内无法下线 + log.info( + "设备上线,deviceId={},设备名称={},所属用户={},上次通信时间={},", + dev.getDeviceId(), + dev.getDeviceName(), + dev.getUserUnionId(), + dev.getOnlineTime() == null + ? 0 + : DateUtil.format(DateUtil.date(dev.getOnlineTime() * 1000L), "yyyy-MM-dd HH:mm:ss")); + doUp(Stream.of(upRequest).collect(Collectors.toList())); + } + + /** 上下线事件 */ + private void doOnOffline(IoTDeviceDTO ioTDeviceDTO) { + DeviceMetadata deviceMetadata = ioTDeviceDTO.getDeviceMetadata(); + AbstractEventMetadata onOffline = deviceMetadata.getEventOrNull("onOffline"); + if (onOffline != null) { + BaseUPRequest upRequest = build(ioTDeviceDTO); + upRequest.setEvent(DevLifeCycle.onOffline.name()); + upRequest.setEventName(DevLifeCycle.onOffline.getValue()); + doUp(Stream.of(upRequest).collect(Collectors.toList())); + } + } + + private void finalOffline(IoTDeviceDTO dev) { + BaseUPRequest upRequest = build(dev); + upRequest.setEvent(DevLifeCycle.offline.name()); + upRequest.setEventName(DevLifeCycle.offline.getValue()); + saveLog(upRequest, dev); + doOnOffline(dev); + log.info( + "设备离线,deviceId={},设备名称={},所属用户={},上次通信时间={},", + dev.getDeviceId(), + dev.getDeviceName(), + dev.getUserUnionId(), + dev.getOnlineTime() == null + ? 0 + : DateUtil.format(DateUtil.date(dev.getOnlineTime() * 1000L), "yyyy-MM-dd HH:mm:ss")); + doUp(Stream.of(upRequest).collect(Collectors.toList())); + } + + @Override + public void offline(String productKey, String deviceId) { + IoTDeviceDTO dev = queryDevInstance(null, productKey, deviceId); + if (dev == null) { + return; + } + stringRedisTemplate.delete("online:" + productKey + deviceId); + int count = doDbOffline(productKey, deviceId); + if (count > 0) { + finalOffline(dev); + } + } + + @Override + public void update(String iotId) { + IoTDeviceDTO dev = queryDevInstance(iotId, null, null); + if (dev == null) { + return; + } + BaseUPRequest upRequest = build(dev); + upRequest.setEvent(DevLifeCycle.update.name()); + upRequest.setEventName(DevLifeCycle.update.getValue()); + doUp(Stream.of(upRequest).collect(Collectors.toList())); + log.info( + "设备修改,deviceId={},设备名称={},所属用户={},上次通信时间={},", + dev.getDeviceId(), + dev.getDeviceName(), + dev.getUserUnionId(), + dev.getOnlineTime() == null + ? 0 + : DateUtil.format(DateUtil.date(dev.getOnlineTime() * 1000L), "yyyy-MM-dd HH:mm:ss")); + } + + /** 执行离线状态变更 */ + private int doDbOffline(String iotId) { + if (StrUtil.isBlank(iotId)) { + return 0; + } + Example example = new Example(IoTDevice.class); + example.createCriteria().andEqualTo("iotId", iotId); + IoTDevice ioTDevice = new IoTDevice(); + // true=1 false=0 + ioTDevice.setState(Boolean.FALSE); + // 添加条件:只有当当前状态为true时才更新为false + example.and().andEqualTo("state", Boolean.TRUE); + int count = ioTDeviceMapper.updateByConditionSelective(ioTDevice, example); + log.info("执行设备离线状态变更,iotId={},影响条数={}", iotId, count); + return count; + } + + /** 执行离线状态变更 */ + private int doDbOffline(String productKey, String deviceId) { + if (StrUtil.isBlank(productKey) || StrUtil.isBlank(deviceId)) { + return 0; + } + Example example = new Example(IoTDevice.class); + example.createCriteria().andEqualTo("productKey", productKey).andEqualTo("deviceId", deviceId); + IoTDevice ioTDevice = new IoTDevice(); + // true=1 false=0 + ioTDevice.setState(Boolean.FALSE); + // 添加条件:只有当当前状态为true时才更新为false + example.and().andEqualTo("state", Boolean.TRUE); + int count = ioTDeviceMapper.updateByConditionSelective(ioTDevice, example); + log.info("执行设备离线状态变更,deviceId={},影响条数={}", deviceId, count); + return count; + } + + @Override + public void enable(String iotId) { + IoTDeviceDTO dev = queryDevInstance(iotId, null, null); + if (dev == null) { + return; + } + BaseUPRequest upRequest = build(dev); + upRequest.setEvent(DevLifeCycle.enable.name()); + upRequest.setEventName(DevLifeCycle.enable.getValue()); + doUp(Stream.of(upRequest).collect(Collectors.toList())); + log.info( + "设备启用,deviceId={},设备名称={},所属用户={},上次通信时间={},", + dev.getDeviceId(), + dev.getDeviceName(), + dev.getUserUnionId(), + dev.getOnlineTime() == null + ? 0 + : DateUtil.format(DateUtil.date(dev.getOnlineTime() * 1000L), "yyyy-MM-dd HH:mm:ss")); + } + + @Override + public void disable(String iotId) { + IoTDeviceDTO dev = queryDevInstance(iotId, null, null); + if (dev == null) { + return; + } + BaseUPRequest upRequest = build(dev); + upRequest.setEvent(DevLifeCycle.disable.name()); + upRequest.setEventName(DevLifeCycle.disable.getValue()); + doUp(Stream.of(upRequest).collect(Collectors.toList())); + log.info( + "设备禁用,deviceId={},设备名称={},所属用户={},上次通信时间={},", + dev.getDeviceId(), + dev.getDeviceName(), + dev.getUserUnionId(), + dev.getOnlineTime() == null + ? 0 + : DateUtil.format(DateUtil.date(dev.getOnlineTime() * 1000L), "yyyy-MM-dd HH:mm:ss")); + } + + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount" + }, + allEntries = true) + public void delete(IoTDeviceDTO ioTDeviceDTO, DownRequest downRequest) { + if (ioTDeviceDTO == null) { + return; + } + if (downRequest.getAppUnionId() != null) { + iotUserMapper.licenseAdd(downRequest.getAppUnionId()); + } + log.info( + "设备删除,deviceId={},productKey={},unionId={}", + ioTDeviceDTO.getDeviceId(), + ioTDeviceDTO.getProductKey(), + ioTDeviceDTO.getUserUnionId()); + BaseUPRequest upRequest = build(ioTDeviceDTO); + upRequest.setEvent(DevLifeCycle.delete.name()); + upRequest.setEventName(DevLifeCycle.delete.getValue()); + doUp(Stream.of(upRequest).collect(Collectors.toList())); + } + + @Override + public void commandSuccess(IoTDeviceDTO ioTDeviceDTO, String commandId, Object functions) { + command(ioTDeviceDTO, commandId, functions, true); + } + + private void command( + IoTDeviceDTO ioTDeviceDTO, String commandId, Object functions, boolean success) { + command(ioTDeviceDTO, commandId, functions); + } + + @Override + public void command(IoTDeviceDTO ioTDeviceDTO, String commandId, Object functions) { + // 默认function + String REPLY_DEFAULT_FUNCTION = "codecReply"; + String REPLY_DEFAULT_FUNCTION_NAME = "编解码自动回复"; + + if (ioTDeviceDTO == null) { + return; + } + JSONObject jsonObject = null; + String functionObj = StrUtil.str(functions, Charset.defaultCharset()); + if (JSONUtil.isTypeJSON(functionObj)) { + jsonObject = JSONUtil.parseObj(functions); + } else { + jsonObject = new JSONObject(); + jsonObject.set("payload", functionObj); + } + + // 坚决 + if (!jsonObject.containsKey("function")) { + jsonObject.set("function", REPLY_DEFAULT_FUNCTION); + jsonObject.set("functionName", REPLY_DEFAULT_FUNCTION_NAME); + } + log.info( + "设备功能下发,deviceId={},productKey={},unionId={},commandId={},functions={}", + ioTDeviceDTO.getDeviceId(), + ioTDeviceDTO.getProductKey(), + ioTDeviceDTO.getUserUnionId(), + commandId, + functionObj); + IoTDeviceLog ioTDeviceLog = buildCommandLog(ioTDeviceDTO, commandId); + ioTDeviceLog.setContent(JSONUtil.toJsonStr(jsonObject)); + DeviceMetadata deviceMetadata = + iotProductDeviceService.getDeviceMetadata(ioTDeviceDTO.getProductKey()); + AbstractFunctionMetadata functionMetadata = + deviceMetadata.getFunctionOrNull(jsonObject.getStr("function")); + if (functionMetadata != null && StrUtil.isNotBlank(functionMetadata.getName())) { + ioTDeviceLog.setEvent(functionMetadata.getId() + "(" + functionMetadata.getName() + ")"); + } else { + ioTDeviceLog.setEvent(functionMetadata.getId()); + } + saveLog(ioTDeviceLog, ioTDeviceDTO); + // 不推送日志 + } + + @Override + public void commandResp(IoTDeviceDTO ioTDeviceDTO, String commandId, Object resp) { + if (ioTDeviceDTO == null) { + return; + } + log.info( + "设备功能下发回复,deviceId={},productKey={},unionId={},commandId={},functions={}", + ioTDeviceDTO.getDeviceId(), + ioTDeviceDTO.getProductKey(), + ioTDeviceDTO.getUserUnionId(), + commandId, + JSONUtil.toJsonStr(resp)); + // 只查询2天前内的指令 + long twoDaysAgo = DateUtil.currentSeconds() - (2 * 24 * 60 * 60); + // Example example = new Example(IoTDeviceLog.class); + // example.createCriteria(). + // andEqualTo("iotId", ioTDeviceDTO.getIotId()). + // andEqualTo("commandId", commandId). + // andGreaterThanOrEqualTo("createTime", twoDaysAgo). + // andEqualTo("commandStatus", 0); + // IoTDeviceLog devLog = ioTDeviceLogMapper.selectOneByExample(example); + IoTDeviceLog devShardLog = + ioTDeviceLogShardMapper.selectOneForCtwing( + ioTDeviceDTO.getIotId(), commandId, twoDaysAgo, 0); + // + // if (enable) { + // IoTDeviceLog newlog = IoTDeviceLog.builder().commandStatus(1).build(); + // Example updateExample = new Example(IoTDeviceLog.class); + // updateExample.createCriteria().andEqualTo("id", devShardLog.getId()); + // //日志分表 暂时双写单读 + // if (devShardLog != null) { + // ioTDeviceLogShardMapper.updateByExampleSelective(newlog, updateExample); + // } + // } + if (devShardLog != null) { + IoTDeviceLog newlog = + IoTDeviceLog.builder() + .commandStatus(1) + .iotId(devShardLog.getIotId()) + .id(devShardLog.getId()) + .build(); + // Example updateExample = new Example(IoTDeviceLog.class); + // updateExample.createCriteria().andEqualTo("id", devShardLog.getId()); + // ioTDeviceLogShardMapper.updateByExampleSelective(newlog, updateExample); + ioTDeviceLogShardMapper.updateLogByIdForCtwing(newlog, devShardLog.getIotId()); + /** 推送消息 */ + BaseUPRequest upRequest = build(ioTDeviceDTO); + upRequest.setMessageType(MessageType.REPLY); + upRequest.setFunction(devShardLog.getEvent()); + // upRequest.setEvent(devLog.getEvent()); + upRequest.setCommandId(commandId); + upRequest.setCommandStatus(1); + // 保存回复日志 + saveLog(upRequest, ioTDeviceDTO); + doUp(Stream.of(upRequest).collect(Collectors.toList())); + } + } + + private IoTDeviceLog buildCommandLog(IoTDeviceDTO instanceBO, String commandId) { + IoTDeviceLog ioTDeviceLog = + IoTDeviceLog.builder() + .deviceId(instanceBO.getDeviceId()) + .deviceName(instanceBO.getDeviceName()) + .productKey(instanceBO.getProductKey()) + // .extDeviceId(instanceBO.getExtDeviceId()) + .messageType(MessageType.FUNCTIONS.name()) + .iotId(instanceBO.getIotId()) + .createId(instanceBO.getUserUnionId()) + .createTime(DateUtil.currentSeconds()) + .commandStatus(0) + // 使用此字段作为校验标识 + .commandId(StrUtil.length(commandId) > 10 ? StrUtil.sub(commandId, 0, 10) : commandId) + .build(); + return ioTDeviceLog; + } + + private BaseUPRequest build(IoTDeviceDTO instanceBO) { + BaseUPRequest request = new BaseUPRequest(); + // 基础信息 + request.setIotId(instanceBO.getIotId()); + request.setDeviceName(instanceBO.getDeviceName()); + request.setDeviceId(instanceBO.getDeviceId()); + request.setTime(System.currentTimeMillis()); + request.setProductKey(instanceBO.getProductKey()); + request.setUserUnionId(instanceBO.getUserUnionId()); + request.setMessageType(MessageType.EVENT); + request.setDeviceNode(instanceBO.getDeviceNode()); + request.setIoTDeviceDTO(instanceBO); + return request; + } + + private IoTDeviceDTO queryDevInstance(String iotId, String productKey, String deviceId) { + Map param = new HashMap<>(); + param.put("iotId", iotId); + param.put("deviceId", deviceId); + param.put("productKey", productKey); + IoTDeviceDTO ioTDeviceDTO = iotDeviceService.selectDevInstanceBONoCache(param); + return ioTDeviceDTO; + } + + public void saveLog(BaseUPRequest upRequest, IoTDeviceDTO ioTDeviceDTO) { + IoTProduct product = iotProductDeviceService.getProduct(upRequest.getProductKey()); + iIoTDeviceDataService.saveDeviceLog(upRequest, ioTDeviceDTO, product); + } + + public void saveLog(IoTDeviceLog ioTDeviceLog, IoTDeviceDTO ioTDeviceDTO) { + IoTProduct product = iotProductDeviceService.getProduct(ioTDeviceDTO.getProductKey()); + iIoTDeviceDataService.saveDeviceLog(ioTDeviceLog, ioTDeviceDTO, product); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTDeviceActionBeforeService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTDeviceActionBeforeService.java new file mode 100644 index 0000000..0002170 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTDeviceActionBeforeService.java @@ -0,0 +1,62 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.action; + +import cn.universal.core.message.DownRequest; +import cn.universal.persistence.base.IoTDeviceLifeCycle; +import cn.universal.persistence.dto.IoTDeviceDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 生命周期前置处理 + * + * @version 1.0 @Author Aleo + * @since 2023/5/30 + */ +@Service("ioTDeviceActionBeforeService") +@Slf4j +public class IoTDeviceActionBeforeService implements IoTDeviceLifeCycle { + + @Override + public void create(String productKey, String deviceId, DownRequest downRequest) { + // if (downRequest.getAppUnionId() != null) { + // IoTUser iotUser = IoTUser.builder().unionId(downRequest.getAppUnionId()).build(); + // IoTUser iotUser1 = iotUserMapper.selectOne(iotUser); + // if (iotUser1.getLicenseTotalAmount() == null) { + // throw new IoTException("设备接入数已用完"); + // } + // if (iotUser1.getLicense() <= 0) { + // throw new IoTException("设备接入数已用完"); + // } + // } + } + + @Override + public void online(String productKey, String deviceId) {} + + @Override + public void offline(String productKey, String deviceId) {} + + @Override + public void update(String iotId) {} + + @Override + public void enable(String iotId) {} + + @Override + public void disable(String iotId) {} + + @Override + public void delete(IoTDeviceDTO ioTDeviceDTO, DownRequest downRequest) {} +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTProductActionService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTProductActionService.java new file mode 100644 index 0000000..02295f8 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTProductActionService.java @@ -0,0 +1,123 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.action; + +import cn.universal.dm.device.service.impl.IoTCacheRemoveService; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import cn.universal.persistence.base.IoTProductAction; +import cn.universal.persistence.entity.IoTProduct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.stereotype.Service; + +/** + * 产品生命周期,用于统一处理 + * + * @version 1.0 @Author Aleo + * @since 2023/6/8 + */ +@Service("ioTProductActionService") +@Slf4j +public class IoTProductActionService implements IoTProductAction { + + @Resource private IoTProductDeviceService iotProductDeviceService; + + @Resource private IoTCacheRemoveService iotCacheRemoveService; + + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "iot_dev_product_list", + "iot_product_device" + }, + allEntries = true) + public void create(String productKey, String unionId) { + // TODO 产品创建,清除列表缓存 + log.info("产品创建,productKey={},创建用户={}", productKey, unionId); + iotCacheRemoveService.removeProductCache(); + } + + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "iot_dev_product_list", + "iot_product_device" + }, + allEntries = true) + public void update(IoTProduct product) {} + + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "iot_dev_product_list", + "iot_product_device" + }, + allEntries = true) + public void delete(String productKey) {} + + @Override + public void enable(String productKey) {} + + @Override + public void disable(String productKey) {} + + @Override + public void publish(String productKey) {} + + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "iot_dev_product_list", + "iot_product_device" + }, + allEntries = true) + public void metadataCreate(String productKey) {} + + @Override + public void metadataUpdate(String productKey) {} + + @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "iot_dev_product_list", + "iot_product_device" + }, + allEntries = true) + public void metadataDelete(String productKey) {} +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/codec/CodecImpl.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/codec/CodecImpl.java new file mode 100644 index 0000000..2d00ac4 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/codec/CodecImpl.java @@ -0,0 +1,38 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.codec; + +import cn.universal.core.message.UPRequest; +import cn.universal.core.service.ICodec; +import cn.universal.core.service.ICodecService; +import cn.universal.dm.device.service.AbstratIoTService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * @version 1.0 @Author Aleo + * @since 2023/6/20 + */ +@Service("codecImpl") +@Slf4j +public class CodecImpl extends AbstratIoTService implements ICodec { + + @Autowired private ICodecService codecService; + + @Override + public UPRequest preDecode(String productKey, String message) { + // 使用新的统一编解码服务 + return codecService.preDecode(productKey, message); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/AbstractDeviceService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/AbstractDeviceService.java new file mode 100644 index 0000000..116c129 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/AbstractDeviceService.java @@ -0,0 +1,15 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.impl; + +public abstract class AbstractDeviceService {} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTCacheRemoveService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTCacheRemoveService.java new file mode 100644 index 0000000..7b924c4 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTCacheRemoveService.java @@ -0,0 +1,60 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.stereotype.Component; + +/** + * 缓存操作类 + * + * @version 1.0 @Author Aleo + * @since 2025/8/12 16:10 + */ +@Component +@Slf4j +public class IoTCacheRemoveService { + + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount" + }, + allEntries = true) + public void removeDevInstanceBOCache() {} + + @CacheEvict( + cacheNames = { + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "iot_dev_product_list", + "selectDevCount" + }, + allEntries = true) + public void removeProductCache() {} + + @CacheEvict( + cacheNames = {"iot_protocol_def", "selectProtocolDefNoScript"}, + allEntries = true) + public void removeDevProtocolCache() {} + + @CacheEvict( + cacheNames = {"iot_dev_subscribe"}, + allEntries = true) + public void removeIotDeviceSubscribeCache() {} +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceService.java new file mode 100644 index 0000000..d34effe --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceService.java @@ -0,0 +1,314 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import cn.universal.cache.annotation.MultiLevelCacheable; +import cn.universal.common.exception.IoTErrorCode; +import cn.universal.common.exception.IoTException; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.dto.IoTDeviceMetadataBO; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTDeviceShadow; +import cn.universal.persistence.entity.IoTDeviceSubscribe; +import cn.universal.persistence.entity.IoTDeviceTags; +import cn.universal.persistence.entity.IoTUserApplication; +import cn.universal.persistence.entity.SupportMapAreas; +import cn.universal.persistence.entity.bo.IoTDeviceHistoryBO; +import cn.universal.persistence.entity.vo.IoTDeviceVO; +import cn.universal.persistence.mapper.IoTDeviceFenceRelMapper; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.IoTDeviceShadowMapper; +import cn.universal.persistence.mapper.IoTDeviceSubscribeMapper; +import cn.universal.persistence.mapper.IoTDeviceTagsMapper; +import cn.universal.persistence.mapper.IoTUserApplicationMapper; +import cn.universal.persistence.mapper.SupportMapAreasMapper; +import cn.universal.persistence.query.IoTAPIQuery; +import cn.universal.persistence.query.IoTDeviceQuery; +import com.github.pagehelper.Page; +import com.github.pagehelper.PageHelper; +import jakarta.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + + +@Component +@Slf4j +public class IoTDeviceService { + + @Resource + private SupportMapAreasMapper supportMapAreasMapper; + @Resource + private IoTDeviceMapper ioTDeviceMapper; + + @Resource + private IoTDeviceTagsMapper ioTDeviceTagsMapper; + + @Resource + private IoTDeviceShadowMapper ioTDeviceShadowMapper; + + @Resource + private IoTDeviceSubscribeMapper ioTDeviceSubscribeMapper; + @Resource + private IoTUserApplicationMapper iotUserApplicationMapper; + @Resource + private IoTDeviceFenceRelMapper ioTDeviceFenceRelMapper; + + @Resource + private IoTCacheRemoveService iotCacheRemoveService; + + public Page apiDeviceList(IoTAPIQuery iotAPIQuery) { + Page page = PageHelper.startPage(iotAPIQuery.getPage(), iotAPIQuery.getSize()); + List ioTDeviceVOList = ioTDeviceMapper.apiDeviceList(iotAPIQuery); + return page; + } + + /** + * 设备绑定应用 对外API + */ + public int apiAppBind(String appid, String iotId) { + IoTUserApplication iotUserApplication = iotUserApplicationMapper.selectIotUserApplicationByAppId( + appid); + if (iotUserApplication == null) { + throw new IoTException("应用不存在"); + } + int i = ioTDeviceMapper.apiBindApp(iotUserApplication.getAppUniqueId(), iotId); + return i; + } + + /** + * 设备解绑应用 对外API + */ + public int apiAppUnBind(String iotId) { + if (StrUtil.isBlank(iotId)) { + throw new IoTException("设备编号不能为空"); + } + return ioTDeviceMapper.apiUnBindApp(iotId); + } + + /** + * 更新设备信息 + */ + public Map apiUpdateDevInfo(IoTAPIQuery iotAPIQuery) { + IoTDevice instance = IoTDevice.builder().creatorId(iotAPIQuery.getIotUnionId()) + .iotId(iotAPIQuery.getIotId()).build(); + instance = ioTDeviceMapper.selectOne(instance); + if (instance == null) { + throw new IoTException(IoTErrorCode.DEV_NOT_FIND.getName(), + IoTErrorCode.DEV_NOT_FIND.getCode()); + } + if (StrUtil.isNotBlank(iotAPIQuery.getDeviceName())) { + instance.setDeviceName(iotAPIQuery.getDeviceName()); + } + if (StrUtil.isNotBlank(iotAPIQuery.getDetail())) { + instance.setDetail(iotAPIQuery.getDetail()); + } + if (StrUtil.isNotBlank(iotAPIQuery.getLatitude()) && StrUtil.isNotBlank( + iotAPIQuery.getLongitude())) { + + instance.setCoordinate( + StrUtil.join(",", iotAPIQuery.getLongitude(), iotAPIQuery.getLatitude())); + + // 更新设备所属经纬度 + SupportMapAreas supportMapAreas = supportMapAreasMapper.selectMapAreas( + iotAPIQuery.getLongitude(), iotAPIQuery.getLatitude()); + if (supportMapAreas == null) { + log.info("查询区域id为空,lot={},lat={}", iotAPIQuery.getLongitude(), + iotAPIQuery.getLatitude()); + } else { + instance.setAreasId(supportMapAreas.getId()); + } + } + + ioTDeviceMapper.updateByPrimaryKeySelective(instance); + // 组件返回字段 + Map result = new HashMap<>(); + result.put("deviceId", instance.getDeviceId()); + result.put("areasId", instance.getAreasId() == null ? "" : instance.getAreasId()); + return result; + } + + @Cacheable(cacheNames = "selectDevCount", unless = "#result == null", keyGenerator = "redisKeyGenerate") + public boolean selectDevCount(IoTAPIQuery iotAPIQuery) { + if (iotAPIQuery == null) { + throw new IoTException("参数不能为空"); + } + IoTDevice instance = IoTDevice.builder().creatorId(iotAPIQuery.getIotUnionId()) + .deviceId(iotAPIQuery.getDeviceId()).application(iotAPIQuery.getApplicationId()) + .productKey(iotAPIQuery.getProductKey()).iotId(iotAPIQuery.getIotId()).build(); + + int count = ioTDeviceMapper.selectCount(instance); + return count > 0; + } + + @Cacheable(cacheNames = "iot_dev_action", unless = "#result == null", keyGenerator = "redisKeyGenerate") + public IoTDeviceDTO lifeCycleDevInstance(IoTDeviceQuery query) { + if (query == null || query.emptyParams()) { + log.warn("no query condition"); + return null; + } + IoTDeviceDTO ioTDeviceDTO = ioTDeviceMapper.selectIoTDeviceBO(BeanUtil.beanToMap(query)); + + return ioTDeviceDTO; + } + + /** + * deviceId 设备编号 + * + *

extDeviceId 扩展设备编号 + * + *

iotId 设备统一编号 + * + *

productKey 产品唯一编号 + */ + @Cacheable(cacheNames = "iot_dev_instance_bo", unless = "#result == null", keyGenerator = "redisKeyGenerate") + public IoTDeviceDTO selectDevInstanceBO(Map map) { + if (map == null || map.isEmpty()) { + throw new IoTException("deviceId can not be null"); + } + IoTDeviceDTO ioTDeviceDTO = ioTDeviceMapper.selectIoTDeviceBO(map); + if (ioTDeviceDTO != null) { + } + + return ioTDeviceDTO; + } + + @MultiLevelCacheable(cacheNames = "apiIoTDeviceVOInfo", unless = "#result == null", keyGenerator = "redisKeyGenerate") + public IoTDeviceVO apiIoTDeviceVOInfo(IoTAPIQuery query) { + IoTDeviceVO ioTDeviceVO = ioTDeviceMapper.apiDeviceInfo(query); + return ioTDeviceVO; + } + + /** + * 不缓存 供设备生命周期调用 deviceId 设备编号 + * + *

extDeviceId 扩展设备编号 + * + *

iotId 设备统一编号 + * + *

productKey 产品唯一编号 + */ + public IoTDeviceDTO selectDevInstanceBONoCache(Map map) { + if (map == null || map.isEmpty()) { + throw new IoTException("deviceId can not be null"); + } + IoTDeviceDTO ioTDeviceDTO = ioTDeviceMapper.selectIoTDeviceBO(map); + return ioTDeviceDTO; + } + + /** + * deviceId 设备编号 + * + *

extDeviceId 扩展设备编号 + * + *

iotId 设备统一编号 + * + *

productKey 产品唯一编号 + */ + @Cacheable(cacheNames = "iot_dev_instance_bo", unless = "#result == null", key = + "'selectDevInstanceBO" + ":'+#productKey+#deviceId") + public IoTDeviceDTO selectDevInstanceBO(String productKey, String deviceId) { + if (StrUtil.isEmpty(productKey) || StrUtil.isEmpty(deviceId)) { + throw new IoTException("productKey or deviceId can not be null"); + } + Map map = new HashMap<>(); + map.put("productKey", productKey); + map.put("deviceId", deviceId); + IoTDeviceDTO ioTDeviceDTO = ioTDeviceMapper.selectIoTDeviceBO(map); + if (ioTDeviceDTO != null) { + return ioTDeviceDTO; + } + return null; + } + + @Cacheable(cacheNames = "iot_dev_instance_bo", unless = "#result == null", key = "''+#productKey+#deviceId") + public IoTDevice selectDevInstance(String productKey, String deviceId) { + if (StrUtil.isBlank(productKey) || StrUtil.isBlank(deviceId)) { + return null; + } + IoTDevice ioTDevice = IoTDevice.builder().productKey(productKey).deviceId(deviceId).build(); + ioTDevice = ioTDeviceMapper.selectOne(ioTDevice); + return ioTDevice; + } + + @Cacheable(cacheNames = "iot_dev_instance_bo", unless = "#result == null", key = "''+#iotId") + public IoTDevice selectDevInstance(String iotId) { + if (StrUtil.isBlank(iotId)) { + return null; + } + IoTDevice ioTDevice = IoTDevice.builder().iotId(iotId).build(); + ioTDevice = ioTDeviceMapper.selectOne(ioTDevice); + return ioTDevice; + } + + /** + * iotId 设备统一编号 + * + * @return 设备BO + */ + @Cacheable(cacheNames = "iot_dev_instance_iotId", key = "''+#iotId", unless = "#result==null") + public IoTDeviceDTO selectDevInstanceBO(String iotId) { + if (StrUtil.isBlank(iotId)) { + throw new IoTException("iotId can not be null"); + } + IoTDeviceDTO ioTDeviceDTO = ioTDeviceMapper.selectIoTDeviceBO( + BeanUtil.beanToMap(IoTAPIQuery.builder().iotId(iotId).build())); + return ioTDeviceDTO; + } + + @Cacheable(cacheNames = "iot_dev_metadata_bo", key = "''+#iotId", unless = "#result==null") + public IoTDeviceMetadataBO selectDevMetadataBo(String iotId) { + IoTDeviceMetadataBO metadataBO = ioTDeviceMapper.selectDevMetadataBo(iotId); + return metadataBO; + } + + /** + * 根据 extDeviceId 删除设备信息 + */ + @Transactional + public int delDevInstance(String iotId) { + if (StrUtil.isBlank(iotId)) { + return 0; + } + addDevHistory(iotId); + int dev = ioTDeviceMapper.delete(IoTDevice.builder().iotId(iotId).build()); + int tags = ioTDeviceTagsMapper.delete(IoTDeviceTags.builder().iotId(iotId).build()); + int sha = ioTDeviceShadowMapper.delete(IoTDeviceShadow.builder().iotId(iotId).build()); + int subscribe = ioTDeviceSubscribeMapper.delete( + IoTDeviceSubscribe.builder().iotId(iotId).build()); + int fence = ioTDeviceFenceRelMapper.deleteFenceInstance(iotId); + log.info("删除设备 tag={},platform={},shadow={},subscribe={},fence={}", tags, dev, sha, + subscribe, fence); + iotCacheRemoveService.removeDevProtocolCache(); + return dev; + } + + public void addDevHistory(String iotId) { + IoTDevice ioTDevice = ioTDeviceMapper.selectOne(IoTDevice.builder().iotId(iotId).build()); + if (ioTDevice != null && ioTDevice.getRegistryTime() != null) { + IoTDeviceHistoryBO ioTDeviceHistoryBO = IoTDeviceHistoryBO.builder() + .deviceId(ioTDevice.getDeviceId()).deviceName(ioTDevice.getDeviceName()) + .productKey(ioTDevice.getProductKey()) + .firstOnlineTime(ioTDevice.getRegistryTime().longValue()) + .creater(ioTDevice.getCreatorId()).createTime(ioTDevice.getCreateTime()) + .deleteTime(System.currentTimeMillis()).coordinate(ioTDevice.getCoordinate()).build(); +// ioTDeviceMapper.insertDevInstanceHistory(ioTDeviceHistoryBO); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceShadowService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceShadowService.java new file mode 100644 index 0000000..8cb95c4 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceShadowService.java @@ -0,0 +1,460 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.constant.IoTConstant.MessageType; +import cn.universal.core.message.UPRequest; +import cn.universal.core.metadata.AbstractPropertyMetadata; +import cn.universal.dm.device.entity.IoTDevicePropertiesBO; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.dto.LogStorePolicyDTO; +import cn.universal.persistence.entity.IoTDeviceShadow; +import cn.universal.persistence.entity.admin.SysDictData; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.IoTDeviceShadowMapper; +import cn.universal.persistence.mapper.admin.SysDictDataMapper; +import cn.universal.persistence.shadow.Shadow; +import cn.universal.persistence.shadow.State; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +/** + * @version 1.0 @Author Aleo + * @since 2025/9/17 + */ +@Component +@Slf4j +public class IoTDeviceShadowService { + + @Resource private IoTDeviceShadowMapper ioTDeviceShadowMapper; + + @Resource private IoTCacheRemoveService iotCacheRemoveService; + + @Resource private IoTDeviceMapper ioTDeviceMapper; + + @Resource private StringRedisTemplate stringRedisTemplate; + + @Resource private SysDictDataMapper dictDataMapper; + + @Resource private IoTProductDeviceService iotProductDeviceService; + + // 注入优化版服务 + @Resource private IoTDeviceShadowServiceOptimized ioTDeviceShadowServiceOptimized; + + public IoTDeviceShadow getDeviceShadow(String iotId) { + return ioTDeviceShadowMapper.getDeviceShadow(iotId); + } + + public JSONObject getDeviceShadowObj(String productKey, String deviceId) { + if (StrUtil.isBlank(productKey) || StrUtil.isBlank(deviceId)) { + return null; + } + String shadowMetadata = ioTDeviceShadowMapper.getShadowMetadata(productKey, deviceId); + if (StrUtil.isBlank(shadowMetadata)) { + return null; + } + return JSONUtil.parseObj(shadowMetadata); + } + + // @Cacheable(cacheNames = "iot_dev_shadow_bo", key = "''+#iotId", unless = "#result==null") + public List getDevState(String iotId) { + List result = new ArrayList<>(); + IoTDeviceShadow ioTDeviceShadow = + ioTDeviceShadowMapper.selectOne(IoTDeviceShadow.builder().iotId(iotId).build()); + if (ioTDeviceShadow == null) { + // throw new IoTException("设备=[" + iotId + "]影子不存在,清检查"); + log.warn("设备=[" + iotId + "]影子不存在,清检查"); + return result; + } + Shadow shadow = JSONUtil.toBean(ioTDeviceShadow.getMetadata(), Shadow.class); + + JSONObject properties = shadow.getState().getReported(); + JSONObject desireProperties = shadow.getState().getDesired(); + Map map = new HashMap<>(); + map.put("iotId", iotId); + IoTDeviceDTO ioTDeviceDTO = ioTDeviceMapper.selectIoTDeviceBO(map); + List propertyMetadataList = + ioTDeviceDTO.getDeviceMetadata().getProperties(); + if (CollectionUtil.isEmpty(propertyMetadataList)) { + return result; + } + LogStorePolicyDTO storePolicyDTO = + iotProductDeviceService.getProductLogStorePolicy(ioTDeviceDTO.getProductKey()); + // 过滤没有上报过的属性 + propertyMetadataList.stream() + .filter(s -> properties.get(s.getId()) != null) + .forEach( + s -> { + Object obj = properties.get(s.getId()); + IoTDevicePropertiesBO entity = new IoTDevicePropertiesBO(); + entity.setDesireValue(desireProperties.get(s.getId())); + entity.withValue(s.getValueType(), obj); + entity.setPropertyName(s.getName()); + entity.setIotId(iotId); + entity.setDeviceId(ioTDeviceDTO.getDeviceId()); + entity.setTimestamp( + shadow.getMetadata().getReported().getJSONObject(s.getId()).getLong("timestamp")); + entity.setProperty(s.getId()); + entity.setStoragePolicy(storePolicyDTO.getProperties().containsKey(s.getId())); + // 期望值 + if (desireProperties.get(s.getId()) != null) { + entity.setCustomized(IoTConstant.DEVICE_SHADOW_DESIRED_PROPERTY); + } + result.add(entity); + }); + result.addAll(devCommonProperties(ioTDeviceDTO)); + return result; + } + + /** 数据为空也返回结果 */ + public List getDevStateWithNullResult(String iotId) { + List result = new ArrayList<>(); + IoTDeviceShadow ioTDeviceShadow = + ioTDeviceShadowMapper.selectOne(IoTDeviceShadow.builder().iotId(iotId).build()); + Shadow shadow = null; + JSONObject properties = null; + JSONObject desireProperties = null; + + if (ioTDeviceShadow == null) { + // throw new IoTException("设备=[" + iotId + "]影子不存在,清检查"); + log.warn("设备=[" + iotId + "]影子不存在,清检查"); + shadow = Shadow.builder().build(); + properties = new JSONObject(); + desireProperties = new JSONObject(); + } else { + shadow = JSONUtil.toBean(ioTDeviceShadow.getMetadata(), Shadow.class); + properties = shadow.getState().getReported(); + desireProperties = shadow.getState().getDesired(); + } + Map map = new HashMap<>(); + map.put("iotId", iotId); + IoTDeviceDTO ioTDeviceDTO = ioTDeviceMapper.selectIoTDeviceBO(map); + if (ioTDeviceDTO == null) { + return result; + } + List propertyMetadataList = + ioTDeviceDTO.getDeviceMetadata().getProperties(); + if (CollectionUtil.isEmpty(propertyMetadataList)) { + return result; + } + // 过滤没有上报过的属性 + Shadow finalShadow = shadow; + JSONObject finalProperties = properties; + JSONObject finalDesireProperties = desireProperties; + propertyMetadataList.forEach( + s -> { + Object obj = finalProperties.get(s.getId()); + IoTDevicePropertiesBO entity = new IoTDevicePropertiesBO(); + long times = 0; + if (obj != null) { + entity.withValue(s.getValueType(), obj); + entity.setTimestamp( + finalShadow + .getMetadata() + .getReported() + .getJSONObject(s.getId()) + .getLong("timestamp")); + } else { + entity.withValue(s.getValueType(), null); + entity.setTimestamp(System.currentTimeMillis()); + } + entity.withValue(s.getValueType(), obj); + entity.setPropertyName(s.getName()); + entity.setIotId(iotId); + entity.setDeviceId(ioTDeviceDTO.getDeviceId()); + entity.setProperty(s.getId()); + // 期望值 + if (finalDesireProperties.get(s.getId()) != null) { + entity.setDesireValue(finalDesireProperties.get(s.getId())); + entity.setCustomized(IoTConstant.DEVICE_SHADOW_DESIRED_PROPERTY); + } + result.add(entity); + }); + result.addAll(devCommonProperties(ioTDeviceDTO)); + return result; + } + + private List queryCommonPropertyDict() { + String key = "dict:" + IoTConstant.DEVICE_SHADOW_CUSTOMIZED_PROPERTY; + String jsonStr = stringRedisTemplate.opsForValue().get(key); + List sysDictData = null; + if (StrUtil.isNotBlank(jsonStr)) { + try { + sysDictData = JSONUtil.toList(jsonStr, SysDictData.class); + } catch (Exception e) { + log.warn("Failed to deserialize Redis data for key: {}, error: {}", key, e.getMessage()); + } + } + if (CollectionUtil.isEmpty(sysDictData)) { + sysDictData = + dictDataMapper.selectDictDataByType(IoTConstant.DEVICE_SHADOW_CUSTOMIZED_PROPERTY); + if (CollectionUtil.isNotEmpty(sysDictData)) { + stringRedisTemplate + .opsForValue() + .set(key, JSONUtil.toJsonStr(sysDictData), 2, TimeUnit.HOURS); + } + } + return sysDictData; + } + + /** + * 处理部分产品设备配置的表号,imei,deviceId字段 + * + *

查看dict字段表`device_common_property` + */ + private List devCommonProperties(IoTDeviceDTO ioTDeviceDTO) { + List result = new ArrayList<>(); + if (ioTDeviceDTO == null) { + return result; + } + if (StrUtil.isNotBlank(ioTDeviceDTO.getDevConfiguration())) { + JSONObject cfg = JSONUtil.parseObj(ioTDeviceDTO.getDevConfiguration()); + List property = queryCommonPropertyDict(); + if (CollectionUtil.isNotEmpty(property)) { + property.stream() + .forEach( + s -> { + if (cfg.containsKey(s.getDictValue())) { + IoTDevicePropertiesBO imeiValue = + IoTDevicePropertiesBO.builder() + .iotId(ioTDeviceDTO.getIotId()) + .deviceId(ioTDeviceDTO.getDeviceId()) + .formatValue(cfg.getStr(s.getDictValue())) + .property(s.getDictValue()) + .propertyName(s.getDictLabel()) + .value(cfg.getStr(s.getDictValue())) + .timestamp(0L) + .stringValue(cfg.getStr(s.getDictValue())) + .customized(IoTConstant.DEVICE_SHADOW_CUSTOMIZED_PROPERTY) + .build(); + result.add(imeiValue); + } + }); + } + } + return result; + } + + public void doShadowOriginal(UPRequest upRequest, IoTDeviceDTO ioTDeviceDTO) { + // 设备影子创建或更新 + boolean flag = + stringRedisTemplate + .opsForValue() + .setIfAbsent("doCreateShadow:" + ioTDeviceDTO.getIotId(), "1", 10, TimeUnit.MINUTES); + if (!ioTDeviceDTO.isShadow() && flag) { + IoTDeviceShadow ioTDeviceShadow = + IoTDeviceShadow.builder() + .iotId(ioTDeviceDTO.getIotId()) + .extDeviceId(ioTDeviceDTO.getExtDeviceId()) + .productKey(ioTDeviceDTO.getProductKey()) + .deviceId(ioTDeviceDTO.getDeviceId()) + .activeTime(new Date()) + .onlineTime(new Date()) + .updateDate(new Date()) + .lastTime(new Date()) + .build(); + State state = State.builder().desired(new JSONObject()).reported(new JSONObject()).build(); + State metadata = State.builder().desired(new JSONObject()).reported(new JSONObject()).build(); + Shadow shadow = + Shadow.builder() + .state(state) + .metadata(metadata) + .timestamp(DateUtil.currentSeconds()) + .version(1L) + .build(); + doDesired(shadow, ioTDeviceDTO, upRequest); + ioTDeviceShadow.setMetadata(JSONUtil.toJsonStr(shadow)); + ioTDeviceShadowMapper.insertSelective(ioTDeviceShadow); + // 代理调用内部方法,清除缓存 + iotCacheRemoveService.removeDevInstanceBOCache(); + } else { + // 更新最后通信时间 + IoTDeviceShadow ioTDeviceShadow = + IoTDeviceShadow.builder().iotId(upRequest.getIotId()).build(); + ioTDeviceShadow = ioTDeviceShadowMapper.selectOne(ioTDeviceShadow); + Shadow shadow = null; + if (ioTDeviceShadow == null || StrUtil.isBlank(ioTDeviceShadow.getMetadata())) { + shadow = Shadow.builder().timestamp(DateUtil.currentSeconds()).version(1L).build(); + } else { + shadow = JSONUtil.toBean(ioTDeviceShadow.getMetadata(), Shadow.class); + } + doDesired(shadow, ioTDeviceDTO, upRequest); + ioTDeviceShadow.setMetadata(JSONUtil.toJsonStr(shadow)); + ioTDeviceShadow.setLastTime(new Date()); + ioTDeviceShadow.setUpdateDate(new Date()); + ioTDeviceShadowMapper.updateByPrimaryKeySelective(ioTDeviceShadow); + } + } + + /** + * 影子期望值 + * + * @param shadow 影子 + * @param ioTDeviceDTO 设备信息 + * @param request 消息原文 + */ + private void doDesired(Shadow shadow, IoTDeviceDTO ioTDeviceDTO, UPRequest request) { + if (request == null) { + return; + } + Map properties = request.getProperties(); + Map data = request.getData(); + // 属性上报 + if (CollectionUtil.isNotEmpty(properties)) { + // 如果属性中存在iccid,进行存储 + Object iccid = properties.get("iccid"); + if (iccid != null) { + String iccidLast = null; + JSONObject configuration = JSONUtil.parseObj(ioTDeviceDTO.getDevConfiguration()); + String icc = configuration.getStr("iccid"); + if (iccid != null && !iccid.equals(icc)) { + iccidLast = iccid.toString(); + } + configuration.set("iccid", iccidLast); + Map map = new HashMap<>(); + map.put("deviceId", ioTDeviceDTO.getDeviceId()); + map.put("configuration", configuration.toString()); + ioTDeviceMapper.updateDevConfiguration(map); + } + + /** 开始处理state */ + State state = shadow.getState() != null ? shadow.getState() : new State(); + // 处理state-reported + JSONObject stateReported = + state.getReported() != null ? state.getReported() : new JSONObject(); + properties.forEach( + (property, value) -> { + stateReported.set(property, value); + }); + state.setReported(stateReported); + // 处理state-reported + // TODO 设备期望值功能暂未实现, + JSONObject stateDesired = state.getDesired() != null ? state.getDesired() : new JSONObject(); + state.setDesired(stateDesired); + /** 开始处理metadata */ + State metadata = shadow.getMetadata() != null ? shadow.getMetadata() : new State(); + // 处理state-reported + JSONObject metadataReported = + metadata.getReported() != null ? metadata.getReported() : new JSONObject(); + + properties.forEach( + (property, value) -> { + metadataReported.set(property, new Timestamp(DateUtil.currentSeconds())); + }); + // 处理state-reported + // TODO 设备期望值功能暂未实现, + JSONObject metadataDesired = + metadata.getDesired() != null ? metadata.getDesired() : new JSONObject(); + metadata.setDesired(metadataDesired); + metadata.setReported(metadataReported); + + shadow.setMetadata(metadata); + shadow.setState(state); + // 处理时间和版本 + shadow.setTimestamp(DateUtil.currentSeconds()); + shadow.setVersion( + (shadow.getVersion() == null || shadow.getVersion() <= 0) + ? 1L + : (shadow.getVersion() + 1)); + } + + // 处理消息类型为reply且data不为空 + if (MessageType.REPLY.equals(request.getMessageType()) && CollectionUtil.isNotEmpty(data)) { + /** 开始处理state */ + State state = shadow.getState() != null ? shadow.getState() : new State(); + JSONObject stateReported = + state.getReported() != null ? state.getReported() : new JSONObject(); + JSONObject stateDesired = state.getDesired() != null ? state.getDesired() : new JSONObject(); + + // 处理metadata + State metadata = shadow.getMetadata() != null ? shadow.getMetadata() : new State(); + JSONObject metadataReported = + metadata.getReported() != null ? metadata.getReported() : new JSONObject(); + JSONObject metadataDesired = + metadata.getReported() != null ? metadata.getDesired() : new JSONObject(); + Timestamp timestamp = new Timestamp(DateUtil.currentSeconds()); + + data.forEach( + (key, value) -> { + if (stateDesired.containsKey(key)) { + stateReported.set(key, stateDesired.get(key)); + // 删除当前reply回复的期望 + stateDesired.remove(key); + metadataDesired.remove(key); + metadataReported.set(key, timestamp); + } + // 回复内有数据时以回复为准 + if (!"".equals(value)) { + stateReported.set(key, value); + metadataReported.set(key, timestamp); + } + }); + state.setReported(stateReported); + state.setDesired(stateDesired); + metadata.setDesired(metadataDesired); + metadata.setReported(metadataReported); + shadow.setMetadata(metadata); + shadow.setState(state); + // 处理时间和版本 + shadow.setTimestamp(DateUtil.currentSeconds()); + shadow.setVersion( + (shadow.getVersion() == null || shadow.getVersion() <= 0) + ? 1L + : (shadow.getVersion() + 1)); + } + } + + @AllArgsConstructor + @Getter + private class Timestamp { + + private Long timestamp; + } + + // ==================== 新优化方法 ==================== + + /** 优化的设备影子处理方法 - 直接调用优化类 */ + public void doShadow(UPRequest upRequest, IoTDeviceDTO ioTDeviceDTO) { + doShadowOriginal(upRequest, ioTDeviceDTO); + // try { + // // 直接调用优化类的doShadow方法 + // if (ioTDeviceShadowServiceOptimized != null) { + // ioTDeviceShadowServiceOptimized.doShadow(upRequest, ioTDeviceDTO); + // } else { + // log.warn("优化版服务未注入,使用原方法"); + // doShadowOriginal(upRequest, ioTDeviceDTO); + // } + // } catch (Exception e) { + // log.error("优化方法处理失败,降级到原方法: iotId={}, error={}", upRequest.getIotId(), e.getMessage(), + // e); + // // 降级处理:使用原来的同步方式 + // doShadowOriginal(upRequest, ioTDeviceDTO); + // } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceShadowServiceOptimized.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceShadowServiceOptimized.java new file mode 100644 index 0000000..6bcd022 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceShadowServiceOptimized.java @@ -0,0 +1,628 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ +package cn.universal.dm.device.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.core.message.UPRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.entity.IoTDeviceShadow; +import cn.universal.persistence.mapper.IoTDeviceShadowMapper; +import cn.universal.persistence.shadow.Shadow; +import cn.universal.persistence.shadow.State; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +/** + * 设备影子服务优化版本 使用异步批量处理提高高并发性能 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +@Service("iotDeviceShadowServiceOptimized") +public class IoTDeviceShadowServiceOptimized { + + @Autowired private IoTDeviceShadowMapper ioTDeviceShadowMapper; + + @Autowired private IoTCacheRemoveService iotCacheRemoveService; + + @Autowired private StringRedisTemplate stringRedisTemplate; + + @Autowired private RedissonClient redissonClient; + + // 批量处理配置 + private static final int BATCH_SIZE = 200; + private static final long BATCH_TIMEOUT_MS = 5000; // 1秒超时 + private static final int MAX_QUEUE_SIZE = 10000; + + // 异步处理线程池 + private final ExecutorService asyncExecutor = + new ThreadPoolExecutor( + 4, + 8, + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(MAX_QUEUE_SIZE), + new ThreadPoolExecutor.CallerRunsPolicy()); + + // 批量处理队列 + private final BlockingQueue updateQueue = + new LinkedBlockingQueue<>(MAX_QUEUE_SIZE); + + // 批量插入队列 + private final BlockingQueue insertQueue = + new LinkedBlockingQueue<>(MAX_QUEUE_SIZE); + + // 统计计数器 + private final AtomicLong totalProcessed = new AtomicLong(0); + private final AtomicLong totalBatches = new AtomicLong(0); + + public IoTDeviceShadowServiceOptimized() { + // 启动批量处理线程 + startBatchProcessor(); + startBatchInserter(); + } + + /** 优化的设备影子处理方法 使用异步批量处理,减少数据库操作频率 */ + public void doShadow(UPRequest upRequest, IoTDeviceDTO ioTDeviceDTO) { + try { + // 快速检查是否需要创建影子 + boolean needCreateShadow = checkNeedCreateShadow(ioTDeviceDTO); + + if (needCreateShadow) { + // 异步创建影子 + asyncCreateShadow(ioTDeviceDTO, upRequest); + } else { + // 异步更新影子 + asyncUpdateShadow(upRequest, ioTDeviceDTO); + } + + // 更新统计 + totalProcessed.incrementAndGet(); + + } catch (Exception e) { + log.error("处理设备影子失败: iotId={}, error={}", upRequest.getIotId(), e.getMessage(), e); + // 降级处理:使用同步方式 + fallbackDoShadow(upRequest, ioTDeviceDTO); + } + } + + /** 检查是否需要创建影子 - 高并发优化版本(带数据缓存) */ + private boolean checkNeedCreateShadow(IoTDeviceDTO ioTDeviceDTO) { + if (ioTDeviceDTO.isShadow()) { + return false; + } + + String iotId = ioTDeviceDTO.getIotId(); + + // 1. 先检查Redis缓存,避免数据库查询 + String cacheKey = "shadow_exists:" + iotId; + String dataCacheKey = "shadow_data:" + iotId; + + // 检查状态缓存 + String exists = stringRedisTemplate.opsForValue().get(cacheKey); + if ("1".equals(exists)) { + // 缓存显示影子已存在,直接返回false + return false; + } + + if ("creating".equals(exists)) { + // 其他请求正在创建中,直接返回false + return false; + } + + // 2. 使用Redisson分布式锁,防止重复创建 + String lockKey = "doCreateShadow:" + iotId; + RLock lock = redissonClient.getLock(lockKey); + + try { + // 尝试获取锁,等待100ms,锁超时时间30秒 + boolean lockAcquired = lock.tryLock(100, 30, TimeUnit.MILLISECONDS); + + if (lockAcquired) { + try { + // 3. 获取锁后,再次检查缓存(双重检查) + exists = stringRedisTemplate.opsForValue().get(cacheKey); + if ("1".equals(exists) || "creating".equals(exists)) { + return false; + } + + // 4. 标记为正在创建,防止其他请求重复创建 + stringRedisTemplate.opsForValue().set(cacheKey, "creating", 5, TimeUnit.MINUTES); + + // 5. 缓存未命中,查询数据库(只有获取锁的请求才查询) + IoTDeviceShadow existingShadow = + ioTDeviceShadowMapper.selectOne(IoTDeviceShadow.builder().iotId(iotId).build()); + + if (existingShadow != null) { + // 6. 数据库存在,缓存完整数据,避免后续查询 + cacheShadowData(iotId, existingShadow); + return false; + } + + return true; + + } finally { + // 7. Redisson自动释放锁(支持看门狗机制) + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } else { + // 8. 未获取到锁,等待一段时间后检查缓存状态 + try { + Thread.sleep(50); // 等待50ms + exists = stringRedisTemplate.opsForValue().get(cacheKey); + if ("1".equals(exists) || "creating".equals(exists)) { + return false; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("获取分布式锁被中断: iotId={}", iotId); + } + + return false; + } + + /** 异步创建影子 */ + @Async + public void asyncCreateShadow(IoTDeviceDTO ioTDeviceDTO, UPRequest upRequest) { + try { + // 构建影子对象 + IoTDeviceShadow shadow = buildDeviceShadow(ioTDeviceDTO); + Shadow shadowData = buildShadowData(ioTDeviceDTO, upRequest); + shadow.setMetadata(JSONUtil.toJsonStr(shadowData)); + + // 加入批量插入队列 + insertQueue.offer(shadow, 100, TimeUnit.MILLISECONDS); + + } catch (Exception e) { + log.error("异步创建影子失败: iotId={}", ioTDeviceDTO.getIotId(), e); + } + } + + /** 异步更新影子 */ + @Async + public void asyncUpdateShadow(UPRequest upRequest, IoTDeviceDTO ioTDeviceDTO) { + try { + // 构建更新任务 + ShadowUpdateTask task = + ShadowUpdateTask.builder() + .iotId(upRequest.getIotId()) + .ioTDeviceDTO(ioTDeviceDTO) + .upRequest(upRequest) + .timestamp(System.currentTimeMillis()) + .build(); + + // 加入批量更新队列 + updateQueue.offer(task, 100, TimeUnit.MILLISECONDS); + + } catch (Exception e) { + log.error("异步更新影子失败: iotId={}", upRequest.getIotId(), e); + } + } + + /** 启动批量处理器 */ + private void startBatchProcessor() { + CompletableFuture.runAsync( + () -> { + while (!Thread.currentThread().isInterrupted()) { + try { + processBatchUpdates(); + } catch (Exception e) { + log.error("批量处理异常", e); + } + } + }, + asyncExecutor); + } + + /** 启动批量插入器 */ + private void startBatchInserter() { + CompletableFuture.runAsync( + () -> { + while (!Thread.currentThread().isInterrupted()) { + try { + processBatchInserts(); + } catch (Exception e) { + log.error("批量插入异常", e); + } + } + }, + asyncExecutor); + } + + /** 批量处理更新操作 */ + private void processBatchUpdates() throws InterruptedException { + List batch = new ArrayList<>(); + + // 收集批量任务 + ShadowUpdateTask firstTask = updateQueue.poll(BATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS); + if (firstTask != null) { + batch.add(firstTask); + updateQueue.drainTo(batch, BATCH_SIZE - 1); + } + + if (batch.isEmpty()) { + return; + } + + try { + // 批量更新 + batchUpdateShadows(batch); + totalBatches.incrementAndGet(); + + } catch (Exception e) { + log.error("批量更新影子失败,批次大小: {}", batch.size(), e); + // 失败的任务重新入队 + for (ShadowUpdateTask task : batch) { + if (System.currentTimeMillis() - task.getTimestamp() < 60000) { // 1分钟内重试 + updateQueue.offer(task); + } + } + } + } + + /** 批量处理插入操作 */ + private void processBatchInserts() throws InterruptedException { + List batch = new ArrayList<>(); + + // 收集批量任务 + IoTDeviceShadow firstShadow = insertQueue.poll(BATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS); + if (firstShadow != null) { + batch.add(firstShadow); + insertQueue.drainTo(batch, BATCH_SIZE - 1); + } + + if (batch.isEmpty()) { + return; + } + + try { + // 批量插入 + batchInsertShadows(batch); + totalBatches.incrementAndGet(); + + // 延迟清理缓存,避免频繁清理 + if (totalBatches.get() % 10 == 0) { // 每10个批次清理一次缓存 + iotCacheRemoveService.removeDevInstanceBOCache(); + } + + } catch (Exception e) { + log.error("批量插入影子失败,批次大小: {}", batch.size(), e); + } + } + + /** 批量更新影子 - 智能合并版本 */ + private void batchUpdateShadows(List tasks) { + if (CollUtil.isEmpty(tasks)) { + return; + } + + // 按iotId分组处理,相同设备的多次更新会被合并 + Map> groupedTasks = new HashMap<>(); + for (ShadowUpdateTask task : tasks) { + groupedTasks.computeIfAbsent(task.getIotId(), k -> new ArrayList<>()).add(task); + } + + // 批量查询现有影子 + List iotIds = new ArrayList<>(groupedTasks.keySet()); + List existingShadows = batchSelectByIotIds(iotIds); + Map shadowMap = new HashMap<>(); + for (IoTDeviceShadow shadow : existingShadows) { + shadowMap.put(shadow.getIotId(), shadow); + } + + // 智能合并更新:每个设备只更新一次,合并所有状态 + List updateList = new ArrayList<>(); + for (Map.Entry> entry : groupedTasks.entrySet()) { + String iotId = entry.getKey(); + List deviceTasks = entry.getValue(); + + IoTDeviceShadow existingShadow = shadowMap.get(iotId); + if (existingShadow != null && !deviceTasks.isEmpty()) { + // 智能合并:将多个任务的状态合并到一个影子中 + mergeMultipleUpdates(existingShadow, deviceTasks); + updateList.add(existingShadow); + + log.debug("设备 {} 合并处理了 {} 条消息", iotId, deviceTasks.size()); + } + } + + if (!updateList.isEmpty()) { + batchUpdate(updateList); + } + } + + /** 批量插入影子 */ + private void batchInsertShadows(List shadows) { + if (CollUtil.isEmpty(shadows)) { + return; + } + + // 批量插入 + for (IoTDeviceShadow shadow : shadows) { + ioTDeviceShadowMapper.insertSelective(shadow); + // 插入成功后更新缓存状态 + updateShadowCacheStatus(shadow.getIotId(), true); + } + } + + /** 批量查询影子 */ + private List batchSelectByIotIds(List iotIds) { + List result = new ArrayList<>(); + for (String iotId : iotIds) { + IoTDeviceShadow shadow = + ioTDeviceShadowMapper.selectOne(IoTDeviceShadow.builder().iotId(iotId).build()); + if (shadow != null) { + result.add(shadow); + } + } + return result; + } + + /** 批量更新影子 */ + private void batchUpdate(List shadows) { + for (IoTDeviceShadow shadow : shadows) { + ioTDeviceShadowMapper.updateByPrimaryKeySelective(shadow); + } + } + + /** 更新现有影子 - 单个任务版本 */ + private void updateExistingShadow(IoTDeviceShadow existingShadow, ShadowUpdateTask task) { + Shadow shadow = null; + if (StrUtil.isBlank(existingShadow.getMetadata())) { + shadow = Shadow.builder().timestamp(DateUtil.currentSeconds()).version(1L).build(); + } else { + shadow = JSONUtil.toBean(existingShadow.getMetadata(), Shadow.class); + } + + // 处理期望状态 + doDesired(shadow, task.getIoTDeviceDTO(), task.getUpRequest()); + + // 更新元数据 + existingShadow.setMetadata(JSONUtil.toJsonStr(shadow)); + existingShadow.setLastTime(new Date()); + existingShadow.setUpdateDate(new Date()); + } + + /** 智能合并多个更新任务 - 核心优化方法 */ + private void mergeMultipleUpdates(IoTDeviceShadow existingShadow, List tasks) { + if (CollUtil.isEmpty(tasks)) { + return; + } + + // 按时间戳排序,确保处理顺序 + tasks.sort((t1, t2) -> Long.compare(t1.getTimestamp(), t2.getTimestamp())); + + Shadow shadow = null; + if (StrUtil.isBlank(existingShadow.getMetadata())) { + shadow = Shadow.builder().timestamp(DateUtil.currentSeconds()).version(1L).build(); + } else { + shadow = JSONUtil.toBean(existingShadow.getMetadata(), Shadow.class); + } + + // 智能合并:按时间顺序处理所有任务,保留最终状态 + for (ShadowUpdateTask task : tasks) { + try { + // 处理期望状态(可以在这里实现增量合并逻辑) + doDesired(shadow, task.getIoTDeviceDTO(), task.getUpRequest()); + + // 更新版本号和时间戳 + shadow.setVersion(shadow.getVersion() + 1); + shadow.setTimestamp(DateUtil.currentSeconds()); + + } catch (Exception e) { + log.warn("合并处理任务失败: iotId={}, task={}", task.getIotId(), task, e); + } + } + + // 更新元数据(只更新一次) + existingShadow.setMetadata(JSONUtil.toJsonStr(shadow)); + existingShadow.setLastTime(new Date()); + existingShadow.setUpdateDate(new Date()); + + log.debug( + "设备 {} 成功合并 {} 条消息,最终版本: {}", existingShadow.getIotId(), tasks.size(), shadow.getVersion()); + } + + /** 构建设备影子 */ + private IoTDeviceShadow buildDeviceShadow(IoTDeviceDTO ioTDeviceDTO) { + return IoTDeviceShadow.builder() + .iotId(ioTDeviceDTO.getIotId()) + .extDeviceId(ioTDeviceDTO.getExtDeviceId()) + .productKey(ioTDeviceDTO.getProductKey()) + .deviceId(ioTDeviceDTO.getDeviceId()) + .activeTime(new Date()) + .onlineTime(new Date()) + .updateDate(new Date()) + .lastTime(new Date()) + .build(); + } + + /** 构建影子数据 */ + private Shadow buildShadowData(IoTDeviceDTO ioTDeviceDTO, UPRequest upRequest) { + State state = State.builder().desired(new JSONObject()).reported(new JSONObject()).build(); + + State metadata = State.builder().desired(new JSONObject()).reported(new JSONObject()).build(); + + Shadow shadow = + Shadow.builder() + .state(state) + .metadata(metadata) + .timestamp(DateUtil.currentSeconds()) + .version(1L) + .build(); + + doDesired(shadow, ioTDeviceDTO, upRequest); + return shadow; + } + + /** 降级处理:同步方式 */ + private void fallbackDoShadow(UPRequest upRequest, IoTDeviceDTO ioTDeviceDTO) { + // 这里可以调用原来的同步方法 + log.warn("使用降级处理方式: iotId={}", upRequest.getIotId()); + } + + /** 更新影子缓存状态 */ + private void updateShadowCacheStatus(String iotId, boolean exists) { + try { + String cacheKey = "shadow_exists:" + iotId; + if (exists) { + // 影子存在,缓存30分钟 + stringRedisTemplate.opsForValue().set(cacheKey, "1", 30, TimeUnit.MINUTES); + + // 如果存在,尝试从数据库获取最新数据并缓存 + IoTDeviceShadow shadow = + ioTDeviceShadowMapper.selectOne(IoTDeviceShadow.builder().iotId(iotId).build()); + if (shadow != null) { + cacheShadowData(iotId, shadow); + } + } else { + // 影子不存在,缓存5分钟 + stringRedisTemplate.opsForValue().set(cacheKey, "0", 5, TimeUnit.MINUTES); + + // 清除数据缓存 + String dataCacheKey = "shadow_data:" + iotId; + stringRedisTemplate.delete(dataCacheKey); + } + } catch (Exception e) { + log.warn("更新影子缓存状态失败: iotId={}", iotId, e); + } + } + + /** 处理期望状态(需要根据实际业务逻辑实现) */ + private void doDesired(Shadow shadow, IoTDeviceDTO ioTDeviceDTO, UPRequest upRequest) { + // 实现期望状态处理逻辑 + // 这里保持原有的业务逻辑不变 + log.debug("处理期望状态: iotId={}", ioTDeviceDTO.getIotId()); + } + + // 注意:getPerformanceStats 和 shutdown 方法已被删除,因为未被外部调用 + + // 内部类:影子更新任务 + public static class ShadowUpdateTask { + private String iotId; + private IoTDeviceDTO ioTDeviceDTO; + private UPRequest upRequest; + private long timestamp; + + // 构建器模式 + public static ShadowUpdateTaskBuilder builder() { + return new ShadowUpdateTaskBuilder(); + } + + // getter和setter方法 + public String getIotId() { + return iotId; + } + + public void setIotId(String iotId) { + this.iotId = iotId; + } + + public IoTDeviceDTO getIoTDeviceDTO() { + return ioTDeviceDTO; + } + + public void setIoTDeviceDTO(IoTDeviceDTO ioTDeviceDTO) { + this.ioTDeviceDTO = ioTDeviceDTO; + } + + public UPRequest getUpRequest() { + return upRequest; + } + + public void setUpRequest(UPRequest upRequest) { + this.upRequest = upRequest; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + // 构建器 + public static class ShadowUpdateTaskBuilder { + private ShadowUpdateTask task = new ShadowUpdateTask(); + + public ShadowUpdateTaskBuilder iotId(String iotId) { + task.iotId = iotId; + return this; + } + + public ShadowUpdateTaskBuilder ioTDeviceDTO(IoTDeviceDTO ioTDeviceDTO) { + task.ioTDeviceDTO = ioTDeviceDTO; + return this; + } + + public ShadowUpdateTaskBuilder upRequest(UPRequest upRequest) { + task.upRequest = upRequest; + return this; + } + + public ShadowUpdateTaskBuilder timestamp(long timestamp) { + task.timestamp = timestamp; + return this; + } + + public ShadowUpdateTask build() { + return task; + } + } + } + + /** + * 缓存影子数据到Redis + * + * @param iotId 设备ID + * @param shadow 影子数据 + */ + private void cacheShadowData(String iotId, IoTDeviceShadow shadow) { + try { + // 1. 缓存状态标记 + String cacheKey = "shadow_exists:" + iotId; + stringRedisTemplate.opsForValue().set(cacheKey, "1", 30, TimeUnit.MINUTES); + + // 2. 缓存完整数据(JSON格式) + String dataCacheKey = "shadow_data:" + iotId; + String shadowJson = JSONUtil.toJsonStr(shadow); + stringRedisTemplate.opsForValue().set(dataCacheKey, shadowJson, 30, TimeUnit.MINUTES); + + log.debug("影子数据缓存成功: iotId={}, cacheKey={}", iotId, dataCacheKey); + + } catch (Exception e) { + log.warn("缓存影子数据失败: iotId={}", iotId, e); + } + } + + // 注意:其他缓存方法已被删除,因为未被外部调用 +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceSubscribeService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceSubscribeService.java new file mode 100644 index 0000000..3b2d157 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceSubscribeService.java @@ -0,0 +1,154 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.constant.IoTConstant.DeviceSubscribe; +import cn.universal.common.constant.IoTConstant.MessageType; +import cn.universal.common.domain.R; +import cn.universal.common.exception.IoTErrorCode; +import cn.universal.persistence.dto.IoTDeviceSubscribeBO; +import cn.universal.persistence.entity.IoTDeviceSubscribe; +import cn.universal.persistence.mapper.IoTDeviceSubscribeMapper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +/** + * 设备消息订阅 + * + * @version 1.0 @Author Aleo + * @since 2025/8/12 16:10 + */ +@Component +@Slf4j +public class IoTDeviceSubscribeService { + + /** 订阅所有 */ + private final String MSG_TYPE_ALL = "ALL"; + + @Resource private IoTDeviceSubscribeMapper ioTDeviceSubscribeMapper; + + @Resource private IoTCacheRemoveService iotCacheRemoveService; + + public R doSubscribe( + String iotId, + String productKey, + String creater, + String instance, + IoTDeviceSubscribeBO ioTDeviceSubscribeBO) { + IoTDeviceSubscribe build = + IoTDeviceSubscribe.builder() + .subType(DeviceSubscribe.DEVICE.name()) + .iotId(iotId) + .productKey(productKey) + .build(); + int i = ioTDeviceSubscribeMapper.selectCount(build); + if (i > IoTConstant.MAX_DEV_MSG_SUBSCRIBE_NUM) { + return R.error( + IoTErrorCode.DEV_SUBSCRIBE_REPEAT_ERROR.getCode(), + IoTErrorCode.DEV_SUBSCRIBE_REPEAT_ERROR.getName()); + } + if (ioTDeviceSubscribeBO == null + || (StrUtil.isBlank(ioTDeviceSubscribeBO.getUrl()) + && StrUtil.isBlank(ioTDeviceSubscribeBO.getTopic()))) { + return R.error(IoTErrorCode.DATA_CAN_NOT_NULL.getCode(), "订阅url和topic要求至少有一个"); + } + build.setUrl(ioTDeviceSubscribeBO.getUrl()); + build.setTopic(ioTDeviceSubscribeBO.getTopic()); + build.setCreater(creater); + build.setInstance(StrUtil.isBlank(instance) ? "0" : instance); + build.setCreateDate(new Date()); + build.setSubType(DeviceSubscribe.DEVICE.name()); + build.setEnabled(true); + MessageType messageType = MessageType.find(ioTDeviceSubscribeBO.getMsgType()); + if (messageType == null) { + build.setMsgType(MessageType.ALL.getValue()); + } else { + build.setMsgType(messageType.getValue().toUpperCase()); + } + ioTDeviceSubscribeMapper.insertSelective(build); + iotCacheRemoveService.removeIotDeviceSubscribeCache(); + return R.ok("订阅成功"); + } + + public R deleteSubscribe(String iotId, String creater, String instance) { + IoTDeviceSubscribe build = + IoTDeviceSubscribe.builder() + .iotId(iotId) + .creater(creater) + .instance(StrUtil.isBlank(instance) ? "0" : instance) + .build(); + ioTDeviceSubscribeMapper.delete(build); + iotCacheRemoveService.removeIotDeviceSubscribeCache(); + return R.ok("删除成功"); + } + + @Cacheable( + cacheNames = "iot_dev_subscribe", + unless = "#result == null", + keyGenerator = "redisKeyGenerate") + public List selectByProductKeyAndMsgType( + String productkey, String iotId, MessageType messageType) { + if (StrUtil.isBlank(productkey)) { + log.error("productId or iotId can not be null"); + return null; + } + IoTDeviceSubscribe query = + IoTDeviceSubscribe.builder().enabled(Boolean.TRUE).productKey(productkey).build(); + List ioTDeviceSubscribeList = ioTDeviceSubscribeMapper.select(query); + List subscribes = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(ioTDeviceSubscribeList)) { + for (IoTDeviceSubscribe sub : ioTDeviceSubscribeList) { + // 产品级别 + if (DeviceSubscribe.PRODUCT.name().equalsIgnoreCase(sub.getSubType())) { + if (checkMsg(messageType, sub)) { + subscribes.add(sub); + } + } + // 设备级别 + else if (DeviceSubscribe.DEVICE.name().equalsIgnoreCase(sub.getSubType())) { + // 任一设备iotId为空,直接忽略 + if (StrUtil.isBlank(iotId) || StrUtil.isBlank(sub.getIotId())) { + log.warn("设备订阅为空,跳过. iotId={} subIotId={}", iotId, sub.getIotId()); + continue; + } + // 设备不匹配直接忽略 + if (!sub.getIotId().equalsIgnoreCase(iotId)) { + continue; + } + if (checkMsg(messageType, sub)) { + subscribes.add(sub); + } + } + } + } + return subscribes; + } + + private boolean checkMsg(MessageType messageType, IoTDeviceSubscribe sub) { + if (MSG_TYPE_ALL.equalsIgnoreCase(sub.getMsgType())) { + return true; + } + if (messageType.name().equalsIgnoreCase(sub.getMsgType())) { + return true; + } + return false; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTProductDeviceService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTProductDeviceService.java new file mode 100644 index 0000000..156191f --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTProductDeviceService.java @@ -0,0 +1,359 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.exception.IoTException; +import cn.universal.core.metadata.AbstractEventMetadata; +import cn.universal.core.metadata.AbstractFunctionMetadata; +import cn.universal.core.metadata.DeviceMetadata; +import cn.universal.core.protocol.support.ProtocolSupportDefinition; +import cn.universal.persistence.dto.IoTDeviceLogMaxStorageTime; +import cn.universal.persistence.dto.IoTDeviceOfflineThesholdBO; +import cn.universal.persistence.dto.LogStorePolicyDTO; +import cn.universal.persistence.dto.LogStorePolicyDTO.StorePolicy; +import cn.universal.persistence.entity.IoTDeviceProtocol; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.vo.IoTProductVO; +import cn.universal.persistence.mapper.IoTDeviceProtocolMapper; +import cn.universal.persistence.mapper.IoTProductMapper; +import cn.universal.persistence.query.IoTAPIQuery; +import com.github.pagehelper.Page; +import com.github.pagehelper.PageHelper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.framework.AopContext; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +/** + * 设备和产品鉴权 + * + * @version 1.0 @Author Aleo + * @since 2025/8/12 16:10 + */ +@Component +@Slf4j +public class IoTProductDeviceService { + + @Resource private IoTProductMapper ioTProductMapper; + @Resource private IoTDeviceProtocolMapper ioTDeviceProtocolMapper; + + public Page apiProductList(IoTAPIQuery iotAPIQuery) { + Page page = PageHelper.startPage(iotAPIQuery.getPage(), iotAPIQuery.getSize()); + List devProducts = ioTProductMapper.openAPIProductList(iotAPIQuery); + return page; + } + + public Page apiProductListV2(IoTAPIQuery iotAPIQuery) { + Page page = PageHelper.startPage(iotAPIQuery.getPage(), iotAPIQuery.getSize()); + List devProducts = ioTProductMapper.openAPIProductList(iotAPIQuery); + List results = + ioTProductMapper.countDevNumberByProductKey(iotAPIQuery.getIotUnionId()); + if (CollUtil.isNotEmpty(results)) { + final Map collect = + results.stream() + .collect(Collectors.toMap(IoTProductVO::getProductKey, IoTProductVO::getDevNum)); + for (IoTProductVO devProduct : devProducts) { + devProduct.setDevNum(collect.getOrDefault(devProduct.getProductKey(), 0)); + } + } + + return page; + } + + public IoTProductVO apiProductDetail(String productKey) { + IoTProductVO productVO = ioTProductMapper.apiProductDetail(productKey); + return productVO; + } + + @Cacheable(cacheNames = "iot_product_device", key = "''+#productKey", unless = "#result == null") + public IoTProduct getProduct(String productKey) { + if (StrUtil.isBlank(productKey)) { + throw new IoTException("product [" + productKey + "] can not be null"); + } + // status-0 正常 + IoTProduct ioTProduct = + IoTProduct.builder().productKey(productKey).state(IoTConstant.NORMAL.byteValue()).build(); + ioTProduct = ioTProductMapper.selectOne(ioTProduct); + if (ioTProduct == null) { + throw new IoTException("product [" + productKey + "] not exist"); + } + return ioTProduct; + } + + @Cacheable(cacheNames = "getDeviceMetadata", key = "''+#productKey", unless = "#result == null") + public DeviceMetadata getDeviceMetadata(String productKey) { + // status-0 正常 + IoTProduct product = + IoTProduct.builder().productKey(productKey).state(IoTConstant.NORMAL.byteValue()).build(); + product = ioTProductMapper.selectOne(product); + if (product == null || StrUtil.isBlank(product.getMetadata())) { + return new DeviceMetadata(new JSONObject()); + } + return new DeviceMetadata(new JSONObject(product.getMetadata())); + } + + @Cacheable( + cacheNames = "getEventOrFunctionName", + key = "''+#productKey+#event", + unless = "#result == null") + public String getEventOrFunctionName(String productKey, String event) { + if (StrUtil.isBlank(productKey)) { + return null; + } + // status-0 正常 + IoTProduct product = + IoTProduct.builder().productKey(productKey).state(IoTConstant.NORMAL.byteValue()).build(); + product = ioTProductMapper.selectOne(product); + if (product == null || StrUtil.isBlank(product.getMetadata())) { + return null; + } + DeviceMetadata deviceMetadata = new DeviceMetadata(new JSONObject(product.getMetadata())); + if (deviceMetadata != null && StrUtil.isNotBlank(event)) { + AbstractEventMetadata eventOrNull = deviceMetadata.getEventOrNull(event); + if (eventOrNull != null) { + return eventOrNull == null ? null : eventOrNull.getName(); + } + AbstractFunctionMetadata functionOrNull = deviceMetadata.getFunctionOrNull(event); + if (functionOrNull != null) { + return functionOrNull == null ? null : functionOrNull.getName(); + } + } + return null; + } + + @Cacheable( + cacheNames = "iot_product_offline_threshold", + key = "''+#productKey", + unless = "#result == null") + public boolean offlineThreshold(String productKey) { + // status-0 正常 + IoTProduct ioTProduct = + IoTProduct.builder().productKey(productKey).state(IoTConstant.NORMAL.byteValue()).build(); + ioTProduct = ioTProductMapper.selectOne(ioTProduct); + if (ioTProduct == null || StrUtil.isBlank(ioTProduct.getConfiguration())) { + return false; + } + JSONObject cfg = JSONUtil.parseObj(ioTProduct.getConfiguration()); + if (!cfg.containsKey(IoTConstant.DEFAULT_OFFLINE_THRESHOLD)) { + return false; + } + return true; + } + + @Cacheable( + cacheNames = "iot_product_log_store_policy", + key = "''+#productKey", + unless = "#result == null") + public LogStorePolicyDTO getProductLogStorePolicy(String productKey) { + IoTProduct product = + ((IoTProductDeviceService) AopContext.currentProxy()).getProduct(productKey); + String storePolicy = product.getStorePolicyConfiguration(); + Map propertiesMap = new HashMap<>(); + // 处理属性 + Map eventMap = new HashMap<>(); + if (StrUtil.isBlank(storePolicy)) { + return LogStorePolicyDTO.builder().properties(propertiesMap).event(eventMap).build(); + } + JSONObject storePolicyObj = JSONUtil.parseObj(storePolicy); + storePolicyObj.getJSONArray("event").stream() + .forEach( + s -> { + JSONObject ev = (JSONObject) s; + eventMap.put( + ev.getStr("id"), + new StorePolicy( + ev.getStr("id"), ev.getInt("maxStorage", IoTConstant.maxStorage))); + }); + storePolicyObj.getJSONArray("properties").stream() + .forEach( + s -> { + JSONObject ev = (JSONObject) s; + propertiesMap.put( + ev.getStr("id"), + new StorePolicy( + ev.getStr("id"), ev.getInt("maxStorage", IoTConstant.maxStorage))); + }); + return LogStorePolicyDTO.builder().properties(propertiesMap).event(eventMap).build(); + } + + /** 加载产品解析协议 */ + @Cacheable(cacheNames = "iot_protocol_def", key = "''+#productKey", unless = "#result == null") + public ProtocolSupportDefinition selectProtocolDef(String productKey) { + if (StrUtil.isBlank(productKey)) { + throw new IoTException("product [" + productKey + "] can not be null"); + } + IoTDeviceProtocol ioTDeviceProtocol = + ioTDeviceProtocolMapper.selectDevProtocolByProductKey(productKey); + if (ioTDeviceProtocol != null) { + return ioTDeviceProtocol.toDefinition(); + } + return null; + } + + /** 加载产品解析协议 */ + @Cacheable( + cacheNames = "selectProtocolDefNoScript", + key = "''+#productKey", + unless = "#result == null") + public ProtocolSupportDefinition selectProtocolDefNoScript(String productKey) { + if (StrUtil.isBlank(productKey)) { + throw new IoTException("product [" + productKey + "] can not be null"); + } + IoTDeviceProtocol ioTDeviceProtocol = + ioTDeviceProtocolMapper.selectDevProtocolByProductKey(productKey); + if (ioTDeviceProtocol != null) { + return ioTDeviceProtocol.toDefinitionNoScript(); + } + return null; + } + + /** + * 根据产品编号和归属人查询总数 + * + * @param productKey 产品key + * @param unionId 用户唯一标识 + * @return int 总数 + */ + @Cacheable( + cacheNames = "iot_product_count", + key = "''+#unionId+':'+#productKey", + unless = "#result == null") + public int selectProductCount(String productKey, String unionId) { + if (StrUtil.isBlank(productKey) || StrUtil.isBlank(unionId)) { + return 0; + } + return ioTProductMapper.selectCount( + IoTProduct.builder().productKey(productKey).creatorId(unionId).build()); + } + + /** 获取产品配置的离线阈值,缺省值24小时 */ + @Cacheable( + cacheNames = "iot_product_offline_threshold", + key = "''+#productKey", + unless = "#result == null") + public int getProductOfflineThreshold(String productKey) { + IoTProduct product = getProduct(productKey); + if (StrUtil.isBlank(productKey) || product == null) { + return IoTConstant.DEFAULT_OFFLINE_THRESHOLD_VALUE; + } + JSONObject config = JSONUtil.parseObj(product.getConfiguration()); + return config.getInt( + IoTConstant.DEFAULT_OFFLINE_THRESHOLD, IoTConstant.DEFAULT_OFFLINE_THRESHOLD_VALUE); + } + + /** 获取产品配置列表的离线阈值,缺省值24小时 */ + @Cacheable( + cacheNames = "iot_product_offline_threshold", + key = "''+#productKey", + unless = "#result == null") + public List getProductOfflineThresholds() { + // status-0 正常 + IoTProduct ioTProduct = IoTProduct.builder().state(IoTConstant.NORMAL.byteValue()).build(); + List select = ioTProductMapper.select(ioTProduct); + List productsMap = new ArrayList<>(); + if (!CollectionUtil.isEmpty(select)) { + for (IoTProduct s : select) { + int offlineThreshold = IoTConstant.DEFAULT_OFFLINE_THRESHOLD_VALUE; + IoTDeviceOfflineThesholdBO ioTDeviceOfflineThesholdBO = + IoTDeviceOfflineThesholdBO.builder() + .productKey(s.getProductKey()) + .platform(s.getThirdPlatform()) + .build(); + // 如果没有配置使用默认的 + if (StrUtil.isBlank(s.getConfiguration())) { + ioTDeviceOfflineThesholdBO.setOfflineThreshold(offlineThreshold); + } else { + try { + JSONObject config = JSONUtil.parseObj(s.getConfiguration()); + offlineThreshold = + config.getInt( + IoTConstant.DEFAULT_OFFLINE_THRESHOLD, + IoTConstant.DEFAULT_OFFLINE_THRESHOLD_VALUE); + ioTDeviceOfflineThesholdBO.setOfflineThreshold(offlineThreshold); + } catch (Exception e) { + log.error("执行任务报错,productKey={} msg={}", s.getProductKey(), e.getMessage()); + e.printStackTrace(); + } + } + productsMap.add(ioTDeviceOfflineThesholdBO); + } + } + return productsMap; + } + + /** 获取产品配置列表的日志存储时长 */ + @Cacheable( + cacheNames = "iot_product_logMaxStorageTime", + key = "''+#productKey", + unless = "#result == null") + public List getProductLogMaxStorage() { + // status-0 正常 + IoTProduct ioTProduct = IoTProduct.builder().state(IoTConstant.NORMAL.byteValue()).build(); + List select = ioTProductMapper.select(ioTProduct); + List productsMap = new ArrayList<>(); + if (!CollectionUtil.isEmpty(select)) { + for (IoTProduct s : select) { + int maxStorageTime = IoTConstant.DEFAULT_LOG_MAX_STORAGE_TIME; + IoTDeviceLogMaxStorageTime logMaxStorage = + IoTDeviceLogMaxStorageTime.builder() + .productKey(s.getProductKey()) + .platform(s.getThirdPlatform()) + .build(); + // 如果没有配置使用默认的 + if (StrUtil.isBlank(s.getConfiguration())) { + logMaxStorage.setDays(maxStorageTime); + } else { + try { + JSONObject config = JSONUtil.parseObj(s.getConfiguration()); + maxStorageTime = + config.getInt( + IoTConstant.DEFAULT_LOG_MAX_STORAGE, IoTConstant.DEFAULT_LOG_MAX_STORAGE_TIME); + logMaxStorage.setDays(maxStorageTime); + } catch (Exception e) { + log.error( + "获取产品配置列表的日志存储时长任务报错,productKey={} msg={}", s.getProductKey(), e.getMessage()); + e.printStackTrace(); + } + } + productsMap.add(logMaxStorage); + } + } + return productsMap; + } + + /** + * 查询网络类型 + * + * @param productKey 产品key + * @return int 总数 + */ + @Cacheable(cacheNames = "selectNetworkUnionId", key = "#productKey", unless = "#result == null") + public String selectNetworkUnionId(String productKey) { + if (StrUtil.isBlank(productKey)) { + return null; + } + return ioTProductMapper.selectNetworkUnionId(productKey); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/UniversalCodecServiceImpl.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/UniversalCodecServiceImpl.java new file mode 100644 index 0000000..ae79902 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/UniversalCodecServiceImpl.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.impl; + +import cn.universal.core.protocol.support.ProtocolSupportDefinition; +import cn.universal.core.service.AbstractCodecService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 统一编解码服务实现类 + * + * @version 1.0 @Author Aleo + * @since 2025/01/20 + */ +@Slf4j +@Service +public class UniversalCodecServiceImpl extends AbstractCodecService { + + @Autowired private IoTProductDeviceService iotProductDeviceService; + + @Override + protected ProtocolSupportDefinition getProtocolDefinition(String productKey) { + return iotProductDeviceService.selectProtocolDef(productKey); + } + + @Override + protected ProtocolSupportDefinition getProtocolDefinitionNoScript(String productKey) { + return iotProductDeviceService.selectProtocolDefNoScript(productKey); + } + + @Override + protected ProtocolSupportDefinition getProtocolDefinitionWithScript(String productKey) { + return iotProductDeviceService.selectProtocolDef(productKey); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/AbstractIoTDeviceLogService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/AbstractIoTDeviceLogService.java new file mode 100644 index 0000000..88155f8 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/AbstractIoTDeviceLogService.java @@ -0,0 +1,142 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.log; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant.MessageType; +import cn.universal.core.message.UPRequest; +import cn.universal.core.metadata.DeviceMetadata; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.dto.LogStorePolicyDTO; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.IoTDeviceLog; +import cn.universal.persistence.entity.IoTDeviceLogMetadata; +import cn.universal.persistence.entity.IoTDeviceLogMetadata.IoTDeviceLogMetadataBuilder; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.mapper.IoTProductMapper; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @version 1.0 @Author Aleo + * @since 2025/9/23 + */ +public abstract class AbstractIoTDeviceLogService implements IIoTDeviceLogService { + + @Resource private IoTProductMapper ioTProductMapper; + @Resource IoTProductDeviceService iotProductDeviceService; + + DeviceMetadata getDeviceMetadata(String metadata) { + if (StrUtil.isBlank(metadata)) { + return new DeviceMetadata(new JSONObject()); + } + return new DeviceMetadata(new JSONObject(metadata)); + } + + IoTDeviceLogMetadataBuilder builder(UPRequest up) { + final IoTDeviceLogMetadataBuilder ioTDeviceLogMetadataBuilder = + IoTDeviceLogMetadata.builder() + .createTime(Integer.parseInt(DateUtil.currentSeconds() + "")) + .deviceId(up.getDeviceId()) + .deviceName(StrUtil.sub(up.getDeviceName(), 0, 30)) + .ext1(JSONUtil.toJsonStr(up.getData())) + .iotId(up.getIotId()) + .productKey(up.getProductKey()) + .messageType(up.getMessageType().name()); + return ioTDeviceLogMetadataBuilder; + } + + IoTDeviceLog build(BaseUPRequest upRequest, IoTDeviceDTO ioTDeviceDTO) { + IoTDeviceLog log = + IoTDeviceLog.builder() + .content(peopertiesOrEventData(upRequest)) + .deviceId(ioTDeviceDTO.getDeviceId()) + // .extDeviceId(ioTDeviceDTO.getExtDeviceId()) + .iotId(ioTDeviceDTO.getIotId()) + .productKey(ioTDeviceDTO.getProductKey()) + .messageType(upRequest.getMessageType().name()) + .event(functionOrEvent(upRequest)) + .commandId(upRequest.getCommandId()) + .commandStatus(upRequest.getCommandStatus()) + .createTime( + upRequest.getTime() == null + ? System.currentTimeMillis() / 1000 + : upRequest.getTime() / 1000) + .deviceName(StrUtil.sub(ioTDeviceDTO.getDeviceName(), 0, 30)) + .point(ioTDeviceDTO.getCoordinate()) + .build(); + return log; + } + + private String peopertiesOrEventData(BaseUPRequest upRequest) { + if (MessageType.PROPERTIES.equals(upRequest.getMessageType())) { + return JSONUtil.toJsonStr(upRequest.getProperties()); + } else if (MessageType.EVENT.equals(upRequest.getMessageType())) { + Map content = new HashMap<>(); + Map data = upRequest.getData(); + Map properties = upRequest.getProperties(); + if (MapUtil.isNotEmpty(properties)) { + content.putAll(properties); + } + if (MapUtil.isNotEmpty(data)) { + content.putAll(data); + } + return JSONUtil.toJsonStr(content); + } + return StrUtil.EMPTY; + } + + private String functionOrEvent(BaseUPRequest upRequest) { + if (MessageType.FUNCTIONS.equals(upRequest.getMessageType()) + || MessageType.REPLY.equals(upRequest.getMessageType())) { + // online-上线 + return upRequest.getFunction(); + } else if (MessageType.EVENT.equals(upRequest.getMessageType())) { + return upRequest.getEvent(); + } + return StrUtil.EMPTY; + } + + List selectDevEvents(String productKey) { + IoTProduct ioTProduct = ioTProductMapper.getProductByProductKey(productKey); + JSONObject metadata = JSONUtil.parseObj(ioTProduct.getMetadata()); + List ioTDeviceEventsList = new ArrayList<>(); + JSONArray properties = metadata.getJSONArray("events"); + if (properties != null) { + LogStorePolicyDTO storePolicy = iotProductDeviceService.getProductLogStorePolicy(productKey); + for (Object object : properties) { + JSONObject jsonObject = JSONUtil.parseObj(object); + JSONObject expands = JSONUtil.parseObj(jsonObject.getStr("expands")); + IoTDeviceEvents ioTDeviceEvents = + IoTDeviceEvents.builder() + .id(jsonObject.getStr("id")) + .name(jsonObject.getStr("name")) + .description(jsonObject.getStr("description")) + .level(expands.getStr("level")) + .build(); + ioTDeviceEventsList.add(ioTDeviceEvents); + } + } + return ioTDeviceEventsList; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/ClickHouseIoTDeviceLogService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/ClickHouseIoTDeviceLogService.java new file mode 100644 index 0000000..12504e7 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/ClickHouseIoTDeviceLogService.java @@ -0,0 +1,354 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.log; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.Db; +import cn.hutool.db.Entity; +import cn.hutool.db.GlobalDbConfig; +import cn.hutool.db.Page; +import cn.hutool.db.PageResult; +import cn.hutool.db.ds.DSFactory; +import cn.hutool.db.ds.GlobalDSFactory; +import cn.hutool.db.sql.SqlBuilder; +import cn.hutool.json.JSONObject; +import cn.hutool.setting.Setting; +import cn.universal.common.constant.IoTConstant.MessageType; +import cn.universal.core.message.UPRequest; +import cn.universal.core.metadata.AbstractPropertyMetadata; +import cn.universal.dm.device.entity.IoTDevicePropertiesBO; +import cn.universal.dm.device.service.impl.IoTDeviceService; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.dto.IoTDeviceMetadataBO; +import cn.universal.persistence.dto.LogStorePolicyDTO; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.IoTDeviceLog; +import cn.universal.persistence.entity.IoTDeviceLogMetadata.IoTDeviceLogMetadataBuilder; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.vo.IoTDeviceLogMetadataVO; +import cn.universal.persistence.entity.vo.IoTDeviceLogVO; +import cn.universal.persistence.query.LogQuery; +import cn.universal.persistence.query.PageBean; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import java.sql.SQLException; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * clickhouse 存储日志 + * + *

通过配置文件动态加载 + * + * @version 1.0 @Author Aleo + * @since 2025/9/22 16:10 + */ +@Component +@Slf4j +@ConditionalOnProperty(prefix = "clickhouse", name = "enable", havingValue = "true") +public class ClickHouseIoTDeviceLogService extends AbstractIoTDeviceLogService { + + private String storePolicy = "clickhouse"; + + @Value("${clickhouse.table.iot_device_log}") + private String devLogTableName; + + @Value("${clickhouse.table.iot_device_log_metadata}") + private String devLogMetaTableName; + + @Resource private IoTProductDeviceService iotProductDeviceService; + + @Value(value = "${clickhouse.address}") + private String address; + + @Value(value = "${clickhouse.username}") + private String username; + + @Value(value = "${clickhouse.password}") + private String password; + + @Resource private IoTDeviceService iotDeviceService; + + static { + // clickhouse不支持主键自增 + GlobalDbConfig.setReturnGeneratedKey(false); + } + + @PostConstruct + public void initDb() { + + Setting setting = new Setting(); + setting.set("url", address); + setting.set("user", username); + setting.set("pass", password); + setting.set("showSql", "true"); + setting.set("formatSql", "true"); + setting.set("sqlLevel", "debug"); + GlobalDSFactory.set(DSFactory.create(setting)); + log.info("初始化clickhouse成功"); + } + + @Override + @Async("taskExecutor") + public void saveDeviceLog(BaseUPRequest upRequest, IoTDeviceDTO noUse, IoTProduct ioTProduct) { + /** 产品数据存储策略,不为空则保存日志 */ + if (StrUtil.isNotBlank(ioTProduct.getStorePolicy())) { + IoTDeviceDTO ioTDeviceDTO = iotDeviceService.selectDevInstanceBO(upRequest.getIotId()); + try { + IoTDeviceLog ioTDeviceLog = build(upRequest, ioTDeviceDTO); + /** isToUnderlineCase true 驼峰转下划线 */ + Entity entity = Entity.create(devLogTableName).parseBean(ioTDeviceLog, true, false); + int count = Db.use().insert(entity); + log.info("clickhouse插入成功={}", count); + } catch (Exception e) { + log.error("保存设备日志报错={}", e); + } + String storePolicyConfiguration = ioTProduct.getStorePolicyConfiguration(); + try { + if (StrUtil.isNotBlank(storePolicyConfiguration)) { + LogStorePolicyDTO productLogStorePolicy = + iotProductDeviceService.getProductLogStorePolicy(ioTProduct.getProductKey()); + saveLogStorePolicy(productLogStorePolicy, upRequest, ioTProduct); + } + } catch (Exception e) { + log.error("保存设备属性扩展日志报错={}", e); + } + } + } + + @Override + public void saveDeviceLog( + IoTDeviceLog ioTDeviceLog, IoTDeviceDTO ioTDeviceDTO, IoTProduct ioTProduct) { + /** 产品数据存储策略,不为空则保存日志 */ + if (StrUtil.isNotBlank(ioTProduct.getStorePolicy())) { + IoTDeviceMetadataBO metadataBO = + iotDeviceService.selectDevMetadataBo(ioTDeviceDTO.getIotId()); + try { + /** isToUnderlineCase true 驼峰转下划线 */ + Entity entity = Entity.create(devLogTableName).parseBean(ioTDeviceLog, true, false); + int count = Db.use().insert(entity); + log.info("clickhouse插入成功={}", count); + } catch (Exception e) { + log.error("保存设备日志报错={}", e); + } + } + } + + private void saveLogStorePolicy( + LogStorePolicyDTO logStorePolicyDTO, UPRequest up, IoTProduct ioTProduct) { + if (MessageType.PROPERTIES.equals(up.getMessageType()) && up.getProperties() != null) { + up.getProperties() + .forEach( + (key, value) -> { + if (logStorePolicyDTO.getProperties().containsKey(key)) { + AbstractPropertyMetadata propertyOrNull = + getDeviceMetadata(ioTProduct.getMetadata()).getPropertyOrNull(key); + IoTDevicePropertiesBO ioTDevicePropertiesBO = new IoTDevicePropertiesBO(); + ioTDevicePropertiesBO.withValue(propertyOrNull, value); + // TODO event + IoTDeviceLogMetadataBuilder IoTDeviceLogMetadataBuilder = builder(up); + IoTDeviceLogMetadataBuilder.id(IdUtil.getSnowflake().nextId()); + IoTDeviceLogMetadataBuilder.property(key); + IoTDeviceLogMetadataBuilder.content( + StrUtil.str(value, CharsetUtil.charset("UTF-8"))); + IoTDeviceLogMetadataBuilder.ext1(ioTDevicePropertiesBO.getPropertyName()); + IoTDeviceLogMetadataBuilder.ext2(ioTDevicePropertiesBO.getFormatValue()); + IoTDeviceLogMetadataBuilder.ext3(ioTDevicePropertiesBO.getSymbol()); + /** isToUnderlineCase true 驼峰转下划线 */ + Entity entity = + Entity.create(devLogMetaTableName) + .parseBean(IoTDeviceLogMetadataBuilder.build(), true, false); + try { + Db.use().insert(entity); + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + } + }); + } + if (MessageType.EVENT.equals(up.getMessageType()) + && logStorePolicyDTO.getEvent().containsKey(up.getEvent())) { + IoTDeviceLogMetadataBuilder IoTDeviceLogMetadataBuilder = builder(up); + IoTDeviceLogMetadataBuilder.id(IdUtil.getSnowflake().nextId()); + IoTDeviceLogMetadataBuilder.event(up.getEvent()); + IoTDeviceLogMetadataBuilder.content(up.getEventName()); + Entity entity = + Entity.create(devLogMetaTableName) + .parseBean(IoTDeviceLogMetadataBuilder.build(), true, false); + try { + Db.use().insert(entity); + } catch (SQLException e) { + log.error("插入clickhouse异常,tablename={},e={}", devLogMetaTableName, e); + } + } + } + + @Override + public PageBean pageList(LogQuery logQuery) { + SqlBuilder builder = SqlBuilder.create().select().from(devLogTableName); + StringBuilder builder1 = new StringBuilder(builder.build()); + builder1.append(" WHERE 1=1"); + if (StrUtil.isNotBlank(logQuery.getIotId())) { + builder1.append(" AND iot_id ='" + logQuery.getIotId() + "'"); + } + if (StrUtil.isNotBlank(logQuery.getDeviceId())) { + builder1.append(" AND device_id='" + logQuery.getDeviceId() + "'"); + } + if (StrUtil.isNotBlank(logQuery.getMessageType())) { + builder1.append(" AND message_type='" + logQuery.getMessageType() + "'"); + } + if (StrUtil.isNotBlank(logQuery.getDeviceName())) { + builder1.append(" AND device_name='" + logQuery.getDeviceName() + "'"); + } + if (StrUtil.isNotBlank(logQuery.getEvent())) { + builder1.append(" AND event='" + logQuery.getEvent() + "'"); + } + if (MapUtil.isNotEmpty(logQuery.getParams()) + && ObjectUtil.isNotNull(logQuery.getParams().get("properties"))) { + builder1.append( + " AND JSONHas(content,'properties','" + logQuery.getParams().get("properties") + "')=1"); + } + if (MapUtil.isNotEmpty(logQuery.getParams()) + && ObjectUtil.isNotNull(logQuery.getParams().get("event"))) { + builder1.append(" AND event='" + logQuery.getParams().get("event") + "'"); + } + if (MapUtil.isNotEmpty(logQuery.getParams()) + && ObjectUtil.isNotNull(logQuery.getParams().get("beginCreateTime"))) { + builder1.append(" AND create_time >=" + logQuery.getParams().get("beginCreateTime")); + } + if (MapUtil.isNotEmpty(logQuery.getParams()) + && ObjectUtil.isNotNull(logQuery.getParams().get("endCreateTime"))) { + builder1.append(" AND create_time <= " + logQuery.getParams().get("beginCreateTime")); + } + builder1.append(" ORDER BY create_time DESC "); + try { + // logQuery.getPageNum() - 1 偏移量计算有点问题 + PageResult page = + Db.use() + .page( + builder1.toString(), Page.of(logQuery.getPageNum() - 1, logQuery.getPageSize())); + List ioTDeviceLogVOS = BeanUtil.copyToList(page, IoTDeviceLogVO.class); + return new PageBean( + ioTDeviceLogVOS, + Long.parseLong(page.getTotal() + ""), + logQuery.getPageSize(), + logQuery.getPageNum()); + + } catch (SQLException e) { + log.warn("clickhouse query log error = {}", e); + return null; + } + } + + @Override + public IoTDeviceLogVO queryById(LogQuery logQuery) { + try { + Entity vo = Db.use().get(Entity.create(devLogTableName).set("id", logQuery.getId())); + IoTDeviceLogVO vos = new IoTDeviceLogVO(); + BeanUtil.copyProperties(vo, vos); + return vos; + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + return null; + } + + @Override + public PageBean queryEventTotal(String productKey, String iotId) { + List list = selectDevEvents(productKey); + for (IoTDeviceEvents devEvent : list) { + SqlBuilder builder = + SqlBuilder.create() + .select("COUNT(1) as qty,max(create_time) as create_time") + .from(devLogTableName); + StringBuilder builder1 = new StringBuilder(builder.build()); + builder1.append(" WHERE event='" + devEvent.getId() + "'"); + builder1.append(" AND message_type = 'EVENT'"); + builder1.append(" AND iot_id ='" + iotId + "'"); + builder1.append(" ORDER BY `create_time` DESC"); + try { + Entity event = Db.use().queryOne(builder1.toString()); + devEvent.setQty(event.getInt("qty") >= 100 ? "99+" : event.getInt("qty") + ""); + devEvent.setTime(event.getStr("create_time")); + // PageResult events = Db.use().page(builder1.toString(), Page.of(0, 100)); + // int size = events.size(); + // if (size > 0) { + // devEvent.setTime(events.get(0).getStr("create_time")); + // devEvent.setQty(size >= 100 ? "99+" : String.valueOf(size)); + // } + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + } + return new PageBean(list, 100L, 1, 100); + } + + @Override + public PageBean queryLogMeta(LogQuery logQuery) { + SqlBuilder builder = SqlBuilder.create().select().from(devLogMetaTableName); + StringBuilder builder1 = new StringBuilder(builder.build()); + builder1.append(" WHERE 1=1"); + if (StrUtil.isNotBlank(logQuery.getIotId())) { + builder1.append(" AND iot_id ='" + logQuery.getIotId() + "'"); + } + if (StrUtil.isNotBlank(logQuery.getDeviceId())) { + builder1.append(" AND device_id='" + logQuery.getDeviceId() + "'"); + } + if (StrUtil.isNotBlank(logQuery.getProperty())) { + builder1.append(" AND property='" + logQuery.getProperty() + "'"); + } + if (StrUtil.isNotBlank(logQuery.getEvent())) { + builder1.append(" AND event='" + logQuery.getEvent() + "'"); + } + builder1.append(" ORDER BY create_time DESC "); + try { + // logQuery.getPageNum() - 1 偏移量计算有点问题 + PageResult page = + Db.use() + .page( + builder1.toString(), Page.of(logQuery.getPageNum() - 1, logQuery.getPageSize())); + List devLogVos = + BeanUtil.copyToList(page, IoTDeviceLogMetadataVO.class); + return new PageBean( + devLogVos, + Long.parseLong(page.getTotal() + ""), + logQuery.getPageSize(), + logQuery.getPageNum()); + + } catch (SQLException e) { + log.warn("clichouse query log error = {}", e); + return null; + } + } + + @Override + public String getPolicy() { + return storePolicy; + } + + @Override + public JSONObject configMetadata() { + return null; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/DefaultIoTDeviceLogService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/DefaultIoTDeviceLogService.java new file mode 100644 index 0000000..295c5b9 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/DefaultIoTDeviceLogService.java @@ -0,0 +1,205 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * + * @Author: Aleo + * + * @Email: wo8335224@gmail.com + * + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.log; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.ConcurrentHashSet; +import cn.hutool.core.util.StrUtil; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.constant.IoTConstant.MessageType; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.IoTDeviceLog; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.vo.IoTDeviceLogMetadataVO; +import cn.universal.persistence.entity.vo.IoTDeviceLogVO; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.query.LogQuery; +import cn.universal.persistence.query.PageBean; +import jakarta.annotation.Resource; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * @version 1.0 @Author Aleo + * @since 2025/9/23 + */ +@Component +@Slf4j +public class DefaultIoTDeviceLogService implements IIoTDeviceDataService, ApplicationContextAware { + + @Resource private IoTDeviceMapper ioTDeviceMapper; + + @Resource private IoTProductDeviceService iotProductDeviceService; + private Map policies = new ConcurrentHashMap<>(); + + @Autowired + @Qualifier("virtualScheduledExecutor") + private ScheduledExecutorService scheduledExecutorService; + + private AtomicBoolean scheduled = new AtomicBoolean(); + + private static final int LOG_MAX_SIZE = 1000; + + private static final long LOG_OUTPUT_INTERVAL = 12; + // + // private static ScheduledExecutorService executor = Executors.newScheduledThreadPool(4, + // new NamedThreadFactory("univ-platform-log", true)); + + private Set devBoSet = new ConcurrentHashSet<>(); + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + Map beansOfType = + applicationContext.getBeansOfType(IIoTDeviceLogService.class); + beansOfType.forEach( + (k, v) -> { + policies.put(v.getPolicy().toLowerCase(), v); + }); + if (scheduled.compareAndSet(false, true)) { + scheduledExecutorService.scheduleWithFixedDelay( + this::doOnline, LOG_OUTPUT_INTERVAL, LOG_OUTPUT_INTERVAL, TimeUnit.SECONDS); + } + } + + @Override + public void saveDeviceLog( + BaseUPRequest upRequest, IoTDeviceDTO ioTDeviceDTO, IoTProduct ioTProduct) { + IIoTDeviceLogService logService = + policies.getOrDefault(ioTProduct.getStorePolicy(), policies.get("none")); + logService.saveDeviceLog(upRequest, ioTDeviceDTO, ioTProduct); + // 如果是设备真实上报数据或事件,处理在线状态 + if (!IoTConstant.DevNotReallyReportEvent.contains(upRequest.getEvent()) + && MessageType.devReallyReport(upRequest.getMessageType())) { + updateOnline(ioTDeviceDTO.getIotId()); + } + } + + @Override + public void saveDeviceLog( + IoTDeviceLog ioTDeviceLog, IoTDeviceDTO ioTDeviceDTO, IoTProduct ioTProduct) { + IIoTDeviceLogService logService = + policies.getOrDefault(ioTProduct.getStorePolicy(), policies.get("none")); + logService.saveDeviceLog(ioTDeviceLog, ioTDeviceDTO, ioTProduct); + // 如果是设备真实上报数据或事件,处理在线状态 + if (!IoTConstant.DevNotReallyReportEvent.contains(ioTDeviceLog.getEvent()) + && MessageType.devReallyReport(MessageType.find(ioTDeviceLog.getMessageType()))) { + updateOnline(ioTDeviceDTO.getIotId()); + } + } + + @Override + public PageBean pageList(LogQuery logQuery) { + IoTProduct product = iotProductDeviceService.getProduct(logQuery.getProductKey()); + IIoTDeviceLogService logService = + policies.getOrDefault(product.getStorePolicy(), policies.get("none")); + PageBean ioTDeviceLogVOPageBean = logService.pageList(logQuery); + if (CollectionUtil.isNotEmpty(ioTDeviceLogVOPageBean.getList())) { + ioTDeviceLogVOPageBean.getList().stream() + .forEach( + ioTDeviceLogVO -> { + if (ioTDeviceLogVO.getEvent() != null) { + String eventName = + iotProductDeviceService.getEventOrFunctionName( + ioTDeviceLogVO.getProductKey(), ioTDeviceLogVO.getEvent()); + if (StrUtil.isNotBlank(eventName)) { + ioTDeviceLogVO.setEvent(ioTDeviceLogVO.getEvent() + " " + eventName); + } + } + }); + } + return ioTDeviceLogVOPageBean; + } + + @Override + public IoTDeviceLogVO queryById(LogQuery logQuery) { + IoTProduct product = iotProductDeviceService.getProduct(logQuery.getProductKey()); + IIoTDeviceLogService logService = + policies.getOrDefault(product.getStorePolicy(), policies.get("none")); + return logService.queryById(logQuery); + } + + @Override + public PageBean queryEventTotal(String productKey, String iotId) { + IoTProduct product = iotProductDeviceService.getProduct(productKey); + IIoTDeviceLogService logService = + policies.getOrDefault(product.getStorePolicy(), policies.get("none")); + return logService.queryEventTotal(productKey, iotId); + } + + @Override + public PageBean queryLogMeta(LogQuery logQuery) { + IoTProduct product = iotProductDeviceService.getProduct(logQuery.getProductKey()); + IIoTDeviceLogService logService = + policies.getOrDefault(product.getStorePolicy(), policies.get("none")); + return logService.queryLogMeta(logQuery); + } + + private void updateOnline(String iotId) { + if (devBoSet.size() < LOG_MAX_SIZE) { + devBoSet.add(iotId); + } else { + log.warn("设备最后更新时间达到5000阈值"); + // just write current devBoSet to file. + doOnline(); + // after force writing, add accessLogData to current devBoSet + devBoSet.add(iotId); + } + } + + public void flushLogBatch(Set iotIds) { + if (iotIds == null || iotIds.isEmpty()) { + return; + } + try { + ioTDeviceMapper.batchFlushLog(iotIds); + log.info("批量更新最后通信时间iotIds={}", iotIds); + } catch (Exception e) { + log.error("批量更新设备最后一次通信时间异常={}", e); + } + } + + private void doOnline() { + // log.debug("开始进行设备最后在线时间处理"); + try { + Set snapshot; + synchronized (devBoSet) { + if (devBoSet.isEmpty()) { + return; + } + snapshot = new HashSet<>(devBoSet); + devBoSet.clear(); + } + flushLogBatch(snapshot); + } catch (Exception e) { + log.error("更新设备最后一次通信时间进程异常={}", e); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/IIoTDeviceDataService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/IIoTDeviceDataService.java new file mode 100644 index 0000000..13d70f8 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/IIoTDeviceDataService.java @@ -0,0 +1,70 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.log; + +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.IoTDeviceLog; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.vo.IoTDeviceLogMetadataVO; +import cn.universal.persistence.entity.vo.IoTDeviceLogVO; +import cn.universal.persistence.query.LogQuery; +import cn.universal.persistence.query.PageBean; + +/** + * @version 1.0 @Author Aleo + * @since 2025/9/23 + */ +public interface IIoTDeviceDataService { + + /** + * 保存日志消息 + * + * @param upRequest 上行消息 + * @param ioTDeviceDTO 设备信息 + * @param ioTProduct 产品信息 + */ + void saveDeviceLog(BaseUPRequest upRequest, IoTDeviceDTO ioTDeviceDTO, IoTProduct ioTProduct); + + default void saveDeviceLog( + IoTDeviceLog ioTDeviceLog, IoTDeviceDTO ioTDeviceDTO, IoTProduct ioTProduct) {} + + /** + * 查询设备日志 + * + * @param logQuery 查询条件 + * @return list + */ + default PageBean pageList(LogQuery logQuery) { + return null; + } + + /** 日志详情 */ + default IoTDeviceLogVO queryById(LogQuery logQuery) { + return null; + } + + /** + * 事件详情 + * + * @param productKey 产品标志 + */ + default PageBean queryEventTotal(String productKey, String iotId) { + return null; + } + + default PageBean queryLogMeta(LogQuery logQuery) { + return null; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/IIoTDeviceLogService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/IIoTDeviceLogService.java new file mode 100644 index 0000000..d40b631 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/IIoTDeviceLogService.java @@ -0,0 +1,81 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.log; + +import cn.hutool.json.JSONObject; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.IoTDeviceLog; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.vo.IoTDeviceLogMetadataVO; +import cn.universal.persistence.entity.vo.IoTDeviceLogVO; +import cn.universal.persistence.query.LogQuery; +import cn.universal.persistence.query.PageBean; + +/** + * @version 1.0 @Author Aleo + * @since 2025/9/23 + */ +public interface IIoTDeviceLogService { + + /** + * @return 存储策略 + */ + String getPolicy(); + + /** + * 保存日志消息 + * + * @param upRequest 上行消息 + * @param ioTDeviceDTO 设备信息 + * @param ioTProduct 产品信息 + */ + void saveDeviceLog(BaseUPRequest upRequest, IoTDeviceDTO ioTDeviceDTO, IoTProduct ioTProduct); + + /** + * 保存日志消息 + * + * @param ioTDeviceLog 日志消息 + * @param ioTDeviceDTO 设备信息 + * @param ioTProduct 产品信息 + */ + default void saveDeviceLog( + IoTDeviceLog ioTDeviceLog, IoTDeviceDTO ioTDeviceDTO, IoTProduct ioTProduct) {} + + /** + * 查询设备日志 + * + * @param logQuery 查询条件 + * @return list + */ + PageBean pageList(LogQuery logQuery); + + /** 日志详情 */ + IoTDeviceLogVO queryById(LogQuery logQuery); + + /** + * 事件详情 + * + * @param productKey 产品标志 + */ + PageBean queryEventTotal(String productKey, String iotId); + + /** 查询单属性或事件日志 */ + PageBean queryLogMeta(LogQuery logQuery); + + /** + * @return 存储配置信息 + */ + JSONObject configMetadata(); +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/MysqlIoTDeviceLogService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/MysqlIoTDeviceLogService.java new file mode 100644 index 0000000..2fe93d5 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/MysqlIoTDeviceLogService.java @@ -0,0 +1,330 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.log; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.constant.IoTConstant.MessageType; +import cn.universal.core.message.UPRequest; +import cn.universal.core.metadata.AbstractPropertyMetadata; +import cn.universal.dm.device.entity.IoTDevicePropertiesBO; +import cn.universal.dm.device.service.impl.IoTDeviceService; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.dto.LogStorePolicyDTO; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.IoTDeviceLog; +import cn.universal.persistence.entity.IoTDeviceLogMetadata.IoTDeviceLogMetadataBuilder; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.vo.IoTDeviceLogMetadataVO; +import cn.universal.persistence.entity.vo.IoTDeviceLogVO; +import cn.universal.persistence.mapper.IoTDeviceLogMapper; +import cn.universal.persistence.mapper.IoTDeviceLogMetadataMapper; +import cn.universal.persistence.mapper.IoTDeviceLogMetadataShardMapper; +import cn.universal.persistence.mapper.IoTDeviceLogShardMapper; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.query.LogQuery; +import cn.universal.persistence.query.PageBean; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jakarta.annotation.Resource; +import java.util.List; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 设备日志 + * + * @version 1.0 @Author Aleo + * @since 2025/9/22 16:10 + */ +@Component +@Slf4j +public class MysqlIoTDeviceLogService extends AbstractIoTDeviceLogService { + + private String storePolicy = "mysql"; + + @Resource private IoTDeviceLogMapper ioTDeviceLogMapper; + @Resource private IoTDeviceLogShardMapper ioTDeviceLogShardMapper; + + @Resource private IoTDeviceLogMetadataMapper ioTDeviceLogMetadataMapper; + @Resource private IoTDeviceLogMetadataShardMapper ioTDeviceLogMetadataShardMapper; + + @Resource private IoTDeviceMapper ioTDeviceMapper; + + @Resource private IoTDeviceService iotDeviceService; + + @Resource private IoTProductDeviceService iotProductDeviceService; + @Resource private StringRedisTemplate stringRedisTemplate; + + /** 新旧设备分割时间线 */ + @Value("${shard.device.split.timestamp}") + private Long timestamp; + + /** 日志分表是否开启 */ + @Value("${shard.log.enable}") + private Boolean enable; + + /** 日志meta分表是否开启 */ + @Value("${shard.logMeta.enable}") + private Boolean metaEnable; + + @Override + public String getPolicy() { + return storePolicy; + } + + @Override + @Async + public void saveDeviceLog( + BaseUPRequest upRequest, IoTDeviceDTO ioTDeviceDTO, IoTProduct ioTProduct) { + /** 产品数据存储策略,不为空则保存日志 */ + if (StrUtil.isNotBlank(ioTProduct.getStorePolicy())) { + try { + IoTDeviceLog log = build(upRequest, ioTDeviceDTO); + // ioTDeviceLogMapper.insertSelective(log); + // 日志分表 暂时双写单读 + if (enable) { + ioTDeviceLogShardMapper.insertSelective(log); + } + } catch (Exception e) { + log.error("保存设备日志报错={}", e); + } + String storePolicyConfiguration = ioTProduct.getStorePolicyConfiguration(); + try { + if (StrUtil.isNotBlank(storePolicyConfiguration) + || MessageType.EVENT.equals(upRequest.getMessageType())) { + LogStorePolicyDTO productLogStorePolicy = + iotProductDeviceService.getProductLogStorePolicy(ioTProduct.getProductKey()); + saveLogStorePolicy(productLogStorePolicy, upRequest, ioTProduct); + } + } catch (Exception e) { + log.error("保存设备属性扩展日志报错={}", e); + } + } + } + + @Override + public void saveDeviceLog( + IoTDeviceLog ioTDeviceLog, IoTDeviceDTO ioTDeviceDTO, IoTProduct ioTProduct) { + /** 产品数据存储策略,不为空则保存日志 */ + if (StrUtil.isNotBlank(ioTProduct.getStorePolicy())) { + try { + ioTDeviceLog.setPoint(ioTDeviceDTO.getCoordinate()); + // ioTDeviceLogMapper.insertSelective(ioTDeviceLog); + // 日志分表 暂时双写单读 + if (enable) { + ioTDeviceLogShardMapper.insertSelective(ioTDeviceLog); + } + } catch (Exception e) { + log.error("保存设备日志报错={}", e); + } + } + } + + private void saveLogStorePolicy( + LogStorePolicyDTO logStorePolicyDTO, UPRequest up, IoTProduct ioTProduct) { + if (MessageType.PROPERTIES.equals(up.getMessageType()) + && up.getProperties() != null + && CollectionUtil.isNotEmpty(logStorePolicyDTO.getProperties())) { + up.getProperties() + .forEach( + (key, value) -> { + if (logStorePolicyDTO.getProperties().containsKey(key)) { + AbstractPropertyMetadata propertyOrNull = + getDeviceMetadata(ioTProduct.getMetadata()).getPropertyOrNull(key); + IoTDevicePropertiesBO ioTDevicePropertiesBO = new IoTDevicePropertiesBO(); + ioTDevicePropertiesBO.withValue(propertyOrNull, value); + // TODO event + IoTDeviceLogMetadataBuilder IoTDeviceLogMetadataBuilder = builder(up); + IoTDeviceLogMetadataBuilder.property(key); + IoTDeviceLogMetadataBuilder.content( + StrUtil.str(value, CharsetUtil.charset("UTF-8"))); + IoTDeviceLogMetadataBuilder.ext1(ioTDevicePropertiesBO.getPropertyName()); + IoTDeviceLogMetadataBuilder.ext2(ioTDevicePropertiesBO.getFormatValue()); + IoTDeviceLogMetadataBuilder.ext3(ioTDevicePropertiesBO.getSymbol()); + // + // ioTDeviceLogMetadataMapper.insertUseGeneratedKeys(IoTDeviceLogMetadataBuilder.build()); + // //删除超过数量的记录 + // + // //设置meta删除标记 + // Boolean re = stringRedisTemplate.opsForValue().setIfAbsent( + // IoTConstant.LOG_META_PROPERTY_DELETE_SIGN + ":" + + // up.getProductKey() + ":" + // + up.getDeviceId(), + // "1", 1, TimeUnit.HOURS); + // if (Boolean.TRUE.equals(re)) { + // ioTDeviceLogMetadataMapper + // .deleteTopPropertiesRecord(up.getIotId(), + // logStorePolicyDTO.getProperties().get(key).getMaxStorage(), + // key); + // } + + // 新旧表都改 + if (metaEnable) { + ioTDeviceLogMetadataShardMapper.insertUseGeneratedKeys( + IoTDeviceLogMetadataBuilder.build()); + Boolean re2 = + stringRedisTemplate + .opsForValue() + .setIfAbsent( + IoTConstant.LOG_META_SHARD_PROPERTY_DELETE_SIGN + + ":" + + up.getProductKey() + + ":" + + up.getDeviceId(), + "1", + 20, + TimeUnit.HOURS); + if (Boolean.TRUE.equals(re2)) { + Integer topId = + ioTDeviceLogMetadataShardMapper.getTopPropertiesRecord( + up.getIotId(), + logStorePolicyDTO.getProperties().get(key).getMaxStorage(), + key); + if (topId != null) { + ioTDeviceLogMetadataShardMapper.deleteTopPropertiesRecord( + up.getIotId(), topId, key); + } + } + } + } + }); + } + if (MessageType.EVENT.equals(up.getMessageType())) { + int maxStorage = 10; + if (CollectionUtil.isNotEmpty(logStorePolicyDTO.getEvent()) + && logStorePolicyDTO.getEvent().containsKey(up.getEvent())) { + maxStorage = logStorePolicyDTO.getEvent().get(up.getEvent()).getMaxStorage(); + } + IoTDeviceLogMetadataBuilder IoTDeviceLogMetadataBuilder = builder(up); + IoTDeviceLogMetadataBuilder.event(up.getEvent()); + IoTDeviceLogMetadataBuilder.content(up.getEventName()); + // + // ioTDeviceLogMetadataMapper.insertUseGeneratedKeys(IoTDeviceLogMetadataBuilder.build()); + // //设置meta删除标记 + // Boolean re = stringRedisTemplate.opsForValue().setIfAbsent( + // IoTConstant.LOG_META_EVENT_DELETE_SIGN + ":" + up.getProductKey() + ":" + // + up.getDeviceId(), + // "1", 1, TimeUnit.HOURS); + // if (Boolean.TRUE.equals(re)) { + // //删除超过数量的记录 + // ioTDeviceLogMetadataMapper + // .deleteTopEventRecord(up.getIotId(), maxStorage, + // up.getEvent()); + // } + // 新旧表都改 + if (metaEnable) { + ioTDeviceLogMetadataShardMapper.insertUseGeneratedKeys(IoTDeviceLogMetadataBuilder.build()); + Boolean re2 = + stringRedisTemplate + .opsForValue() + .setIfAbsent( + IoTConstant.LOG_META_SHARD_EVENT_DELETE_SIGN + + ":" + + up.getProductKey() + + ":" + + up.getDeviceId(), + "1", + 20, + TimeUnit.HOURS); + if (Boolean.TRUE.equals(re2)) { + Integer topId = + ioTDeviceLogMetadataShardMapper.getTopEventRecord( + up.getIotId(), maxStorage, up.getEvent()); + if (topId != null) { + ioTDeviceLogMetadataShardMapper.deleteTopEventRecord( + up.getIotId(), topId, up.getEvent()); + } + } + } + } + } + + @Override + public PageBean pageList(LogQuery bo) { + bo.setProductKey(null); + PageHelper.startPage(bo.getPageNum(), bo.getPageSize()); + if (ObjectUtil.isNull(bo.getId())) { + List ioTDeviceLogVOS = ioTDeviceLogShardMapper.queryLogPageV2List(bo); + return new PageBean( + ioTDeviceLogVOS, + new PageInfo(ioTDeviceLogVOS).getTotal(), + bo.getPageSize(), + bo.getPageNum()); + } else { + List ioTDeviceLogVOS = ioTDeviceLogShardMapper.queryLogPageV2ByIdList(bo); + return new PageBean( + ioTDeviceLogVOS, + new PageInfo(ioTDeviceLogVOS).getTotal(), + bo.getPageSize(), + bo.getPageNum()); + } + } + + @Override + public IoTDeviceLogVO queryById(LogQuery logQuery) { + IoTDeviceLogVO ioTDeviceLogVO = ioTDeviceLogMapper.queryLogById(logQuery); + // IoTDeviceLogVO ioTDeviceLogVO = BeanUtil.toBean(devLog, IoTDeviceLogVO.class); + return ioTDeviceLogVO; + } + + @Override + public PageBean queryEventTotal(String productKey, String iotId) { + List list = selectDevEvents(productKey); + for (IoTDeviceEvents devEvent : list) { + List events; + if (metaEnable) { + events = + ioTDeviceLogMetadataShardMapper.queryEventTotalByEventAndId(devEvent.getId(), iotId); + } else { + events = ioTDeviceLogMetadataMapper.queryEventTotalByEventAndId(devEvent.getId(), iotId); + } + + int size = events.size(); + if (size > 0) { + devEvent.setTime(events.get(0)); + devEvent.setQty(size >= 100 ? "99+" : String.valueOf(size)); + } + } + return new PageBean(list, new PageInfo(list).getTotal(), 1, 100); + } + + @Override + public PageBean queryLogMeta(LogQuery logQuery) { + PageHelper.startPage(logQuery.getPageNum(), logQuery.getPageSize()); + List list; + if (metaEnable) { + list = ioTDeviceLogMetadataShardMapper.selectLogMetaList(logQuery); + } else { + list = ioTDeviceLogMetadataMapper.selectLogMetaList(logQuery); + } + + return new PageBean( + list, new PageInfo(list).getTotal(), logQuery.getPageSize(), logQuery.getPageNum()); + } + + @Override + public JSONObject configMetadata() { + return null; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/NoneIoTDeviceLogService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/NoneIoTDeviceLogService.java new file mode 100644 index 0000000..b5d5f7a --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/NoneIoTDeviceLogService.java @@ -0,0 +1,75 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.log; + +import cn.hutool.json.JSONObject; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.entity.IoTDeviceEvents; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.vo.IoTDeviceLogMetadataVO; +import cn.universal.persistence.entity.vo.IoTDeviceLogVO; +import cn.universal.persistence.query.LogQuery; +import cn.universal.persistence.query.PageBean; +import java.util.ArrayList; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 不存储任何设备日志 + * + * @version 1.0 @Author Aleo + * @since 2025/9/22 16:10 + */ +@Component +@Slf4j +public class NoneIoTDeviceLogService extends AbstractIoTDeviceLogService { + + private String storePolicy = "none"; + + @Override + public String getPolicy() { + return storePolicy; + } + + @Override + public void saveDeviceLog( + BaseUPRequest upRequest, IoTDeviceDTO ioTDeviceDTO, IoTProduct ioTProduct) {} + + @Override + public PageBean pageList(LogQuery logQuery) { + return new PageBean( + new ArrayList(), Long.parseLong(0 + ""), logQuery.getPageSize(), logQuery.getPageNum()); + } + + @Override + public IoTDeviceLogVO queryById(LogQuery logQuery) { + return new IoTDeviceLogVO(); + } + + @Override + public PageBean queryEventTotal(String productKey, String iotId) { + return new PageBean(new ArrayList(), Long.parseLong(0 + ""), 10, 1); + } + + @Override + public PageBean queryLogMeta(LogQuery logQuery) { + return new PageBean( + new ArrayList(), Long.parseLong(0 + ""), logQuery.getPageSize(), logQuery.getPageNum()); + } + + @Override + public JSONObject configMetadata() { + return null; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/ota/api/OtaService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/ota/api/OtaService.java new file mode 100644 index 0000000..6d25fb4 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/ota/api/OtaService.java @@ -0,0 +1,285 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.ota.api; + +import cn.universal.core.service.IOta; +import java.util.List; +import java.util.Map; + +/** + * OTA服务通用接口 + * + *

定义设备固件升级的通用服务接口,支持多种协议实现: - MQTT协议实现 - HTTP协议实现 - TCP协议实现 - 其他自定义协议实现 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +public interface OtaService extends IOta { + + /** + * 获取OTA服务类型 + * + * @return OTA服务类型(如:MQTT、HTTP、TCP等) + */ + String getServiceType(); + + /** + * 获取OTA服务描述 + * + * @return 服务描述信息 + */ + String getServiceDescription(); + + /** + * 检查服务是否支持指定产品 + * + * @param productKey 产品Key + * @return true-支持,false-不支持 + */ + boolean supportsProduct(String productKey); + + /** + * 检查服务是否可用 + * + * @return true-可用,false-不可用 + */ + boolean isServiceAvailable(); + + // ==================== 设备端上报接口 ==================== + + /** + * 处理设备上报的固件信息 + * + * @param reportData 固件上报数据 + * @return 处理结果 + */ + OtaServiceResult handleFirmwareReport(Map reportData); + + /** + * 处理设备上报的升级进度 + * + * @param progressData 进度上报数据 + * @return 处理结果 + */ + OtaServiceResult handleUpgradeProgress(Map progressData); + + /** + * 处理设备上报的升级结果 + * + * @param resultData 结果上报数据 + * @return 处理结果 + */ + OtaServiceResult handleUpgradeResult(Map resultData); + + // ==================== 平台端下发接口 ==================== + + /** + * 下发固件升级指令 + * + * @param updateData 升级数据 + * @return 下发结果 + */ + OtaServiceResult sendUpgradeCommand(Map updateData); + + /** + * 取消固件升级 + * + * @param productKey 产品Key + * @param deviceId 设备ID + * @param taskId 升级任务ID + * @return 取消结果 + */ + OtaServiceResult cancelUpgrade(String productKey, String deviceId, String taskId); + + /** + * 查询升级状态 + * + * @param productKey 产品Key + * @param deviceId 设备ID + * @param taskId 升级任务ID + * @return 升级状态数据 + */ + Map queryUpgradeStatus(String productKey, String deviceId, String taskId); + + // ==================== 固件包管理接口 ==================== + + /** + * 获取可用的固件包列表 + * + * @param productKey 产品Key + * @return 固件包列表 + */ + List> getAvailablePackages(String productKey); + + /** + * 获取指定版本的固件包信息 + * + * @param productKey 产品Key + * @param packageVersion 固件包版本 + * @return 固件包信息 + */ + Map getPackageInfo(String productKey, String packageVersion); + + /** + * 验证固件包 + * + * @param packageInfo 固件包信息 + * @return 验证结果 + */ + boolean validatePackage(Map packageInfo); + + // ==================== 统计和监控接口 ==================== + + /** + * 获取OTA统计信息 + * + * @param productKey 产品Key(可选,为null时返回全局统计) + * @return 统计信息 + */ + OtaStatistics getStatistics(String productKey); + + /** + * 获取活跃升级任务数量 + * + * @param productKey 产品Key(可选) + * @return 活跃任务数量 + */ + int getActiveUpgradeCount(String productKey); + + /** + * 获取升级成功率 + * + * @param productKey 产品Key(可选) + * @return 成功率(0.0-1.0) + */ + double getUpgradeSuccessRate(String productKey); + + // ==================== 内部枚举和类 ==================== + + /** OTA服务结果 */ + enum OtaServiceResult { + SUCCESS("成功"), + FAILED("失败"), + DEVICE_OFFLINE("设备离线"), + PACKAGE_NOT_FOUND("固件包不存在"), + VERSION_CONFLICT("版本冲突"), + INSUFFICIENT_SPACE("存储空间不足"), + NETWORK_ERROR("网络错误"), + PROTOCOL_ERROR("协议错误"), + UNSUPPORTED_OPERATION("不支持的操作"); + + private final String description; + + OtaServiceResult(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } + + /** OTA统计信息 */ + class OtaStatistics { + + private long totalUpgrades; // 总升级次数 + private long successfulUpgrades; // 成功升级次数 + private long failedUpgrades; // 失败升级次数 + private long activeUpgrades; // 活跃升级数 + private double averageUpgradeTime; // 平均升级时间(分钟) + private String lastUpgradeTime; // 最后升级时间 + + // Constructors + public OtaStatistics() {} + + public OtaStatistics( + long totalUpgrades, + long successfulUpgrades, + long failedUpgrades, + long activeUpgrades, + double averageUpgradeTime, + String lastUpgradeTime) { + this.totalUpgrades = totalUpgrades; + this.successfulUpgrades = successfulUpgrades; + this.failedUpgrades = failedUpgrades; + this.activeUpgrades = activeUpgrades; + this.averageUpgradeTime = averageUpgradeTime; + this.lastUpgradeTime = lastUpgradeTime; + } + + // Getters and Setters + public long getTotalUpgrades() { + return totalUpgrades; + } + + public void setTotalUpgrades(long totalUpgrades) { + this.totalUpgrades = totalUpgrades; + } + + public long getSuccessfulUpgrades() { + return successfulUpgrades; + } + + public void setSuccessfulUpgrades(long successfulUpgrades) { + this.successfulUpgrades = successfulUpgrades; + } + + public long getFailedUpgrades() { + return failedUpgrades; + } + + public void setFailedUpgrades(long failedUpgrades) { + this.failedUpgrades = failedUpgrades; + } + + public long getActiveUpgrades() { + return activeUpgrades; + } + + public void setActiveUpgrades(long activeUpgrades) { + this.activeUpgrades = activeUpgrades; + } + + public double getAverageUpgradeTime() { + return averageUpgradeTime; + } + + public void setAverageUpgradeTime(double averageUpgradeTime) { + this.averageUpgradeTime = averageUpgradeTime; + } + + public String getLastUpgradeTime() { + return lastUpgradeTime; + } + + public void setLastUpgradeTime(String lastUpgradeTime) { + this.lastUpgradeTime = lastUpgradeTime; + } + + public double getSuccessRate() { + return totalUpgrades > 0 ? (double) successfulUpgrades / totalUpgrades : 0.0; + } + + @Override + public String toString() { + return String.format( + "OtaStatistics{total=%d, success=%d, failed=%d, active=%d, avgTime=%.2fmin, successRate=%.2f%%}", + totalUpgrades, + successfulUpgrades, + failedUpgrades, + activeUpgrades, + averageUpgradeTime, + getSuccessRate() * 100); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/ota/entity/OtaReportRequest.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/ota/entity/OtaReportRequest.java new file mode 100644 index 0000000..67811f0 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/ota/entity/OtaReportRequest.java @@ -0,0 +1,260 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.ota.entity; + +import java.time.LocalDateTime; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * OTA上报请求实体 + * + *

用于封装设备向平台上报的固件信息、升级进度、升级结果等数据 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OtaReportRequest { + + /** 产品Key */ + private String productKey; + + /** 设备ID */ + private String deviceId; + + /** 报告类型 */ + private ReportType reportType; + + /** 当前固件版本 */ + private String currentVersion; + + /** 硬件版本 */ + private String hardwareVersion; + + /** 固件类型 */ + private String firmwareType; + + /** 固件校验和 */ + private String checksum; + + /** 固件大小 */ + private Long fileSize; + + /** 升级任务ID */ + private String taskId; + + /** 目标版本(升级时) */ + private String targetVersion; + + /** 升级进度(0-100) */ + private Integer progress; + + /** 升级状态 */ + private UpgradeStatus upgradeStatus; + + /** 错误代码(失败时) */ + private String errorCode; + + /** 错误消息(失败时) */ + private String errorMessage; + + /** 设备状态信息 */ + private DeviceStatus deviceStatus; + + /** 扩展属性 */ + private Map properties; + + /** 上报时间 */ + @Builder.Default private LocalDateTime reportTime = LocalDateTime.now(); + + /** 协议类型 */ + private String protocol; + + /** 报告类型枚举 */ + public enum ReportType { + /** 固件信息上报 */ + FIRMWARE_INFO("固件信息上报"), + + /** 升级进度上报 */ + UPGRADE_PROGRESS("升级进度上报"), + + /** 升级结果上报 */ + UPGRADE_RESULT("升级结果上报"); + + private final String description; + + ReportType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } + + /** 升级状态枚举 */ + public enum UpgradeStatus { + /** 空闲状态 */ + IDLE("空闲"), + + /** 下载中 */ + DOWNLOADING("下载中"), + + /** 校验中 */ + VERIFYING("校验中"), + + /** 安装中 */ + INSTALLING("安装中"), + + /** 重启中 */ + REBOOTING("重启中"), + + /** 升级成功 */ + SUCCESS("升级成功"), + + /** 升级失败 */ + FAILED("升级失败"); + + private final String description; + + UpgradeStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } + + /** 设备状态信息 */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class DeviceStatus { + + /** 电池电量(百分比) */ + private Integer batteryLevel; + + /** 信号强度 */ + private Integer signalStrength; + + /** 可用存储空间(字节) */ + private Long freeSpace; + + /** CPU使用率(百分比) */ + private Integer cpuUsage; + + /** 内存使用率(百分比) */ + private Integer memoryUsage; + + /** 设备温度(摄氏度) */ + private Double temperature; + + /** 网络类型 */ + private String networkType; + + /** 是否在充电 */ + private Boolean isCharging; + } + + /** 获取设备唯一标识 */ + public String getDeviceUniqueId() { + return productKey + ":" + deviceId; + } + + /** 检查是否为升级相关的上报 */ + public boolean isUpgradeRelated() { + return reportType == ReportType.UPGRADE_PROGRESS || reportType == ReportType.UPGRADE_RESULT; + } + + /** 检查升级是否成功 */ + public boolean isUpgradeSuccessful() { + return upgradeStatus == UpgradeStatus.SUCCESS; + } + + /** 检查升级是否失败 */ + public boolean isUpgradeFailed() { + return upgradeStatus == UpgradeStatus.FAILED; + } + + /** 检查升级是否进行中 */ + public boolean isUpgradeInProgress() { + return upgradeStatus == UpgradeStatus.DOWNLOADING + || upgradeStatus == UpgradeStatus.VERIFYING + || upgradeStatus == UpgradeStatus.INSTALLING + || upgradeStatus == UpgradeStatus.REBOOTING; + } + + /** 获取设备健康状态评分(0-100) */ + public int getDeviceHealthScore() { + if (deviceStatus == null) { + return 50; // 默认中等健康状态 + } + + int score = 100; + + // 电池电量影响 + if (deviceStatus.getBatteryLevel() != null) { + if (deviceStatus.getBatteryLevel() < 20) { + score -= 30; + } else if (deviceStatus.getBatteryLevel() < 50) { + score -= 10; + } + } + + // 存储空间影响 + if (deviceStatus.getFreeSpace() != null) { + if (deviceStatus.getFreeSpace() < 100 * 1024 * 1024) { // 少于100MB + score -= 20; + } + } + + // CPU和内存使用率影响 + if (deviceStatus.getCpuUsage() != null && deviceStatus.getCpuUsage() > 80) { + score -= 10; + } + if (deviceStatus.getMemoryUsage() != null && deviceStatus.getMemoryUsage() > 80) { + score -= 10; + } + + // 温度影响 + if (deviceStatus.getTemperature() != null) { + if (deviceStatus.getTemperature() > 70) { + score -= 15; + } else if (deviceStatus.getTemperature() > 60) { + score -= 5; + } + } + + return Math.max(0, Math.min(100, score)); + } + + @Override + public String toString() { + return String.format( + "OtaReportRequest{device=%s, type=%s, version=%s, status=%s, progress=%d%%}", + getDeviceUniqueId(), + reportType, + currentVersion, + upgradeStatus, + progress != null ? progress : 0); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/ota/entity/OtaUpdateCommand.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/ota/entity/OtaUpdateCommand.java new file mode 100644 index 0000000..8d3c94c --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/ota/entity/OtaUpdateCommand.java @@ -0,0 +1,353 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.ota.entity; + +import java.time.LocalDateTime; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * OTA升级命令实体 + * + *

用于封装平台向设备下发的固件升级命令 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OtaUpdateCommand { + + /** 产品Key */ + private String productKey; + + /** 设备ID */ + private String deviceId; + + /** 升级任务ID */ + private String taskId; + + /** 命令类型 */ + private CommandType commandType; + + /** 固件信息 */ + private FirmwareInfo firmwareInfo; + + /** 升级配置 */ + private UpgradeConfig upgradeConfig; + + /** 重试策略 */ + private RetryStrategy retryStrategy; + + /** 验证配置 */ + private VerificationConfig verificationConfig; + + /** 扩展参数 */ + private Map extraParams; + + /** 命令发送时间 */ + @Builder.Default private LocalDateTime commandTime = LocalDateTime.now(); + + /** 命令优先级(1-10,数字越大优先级越高) */ + @Builder.Default private Integer priority = 5; + + /** 协议类型 */ + private String protocol; + + /** 命令类型枚举 */ + public enum CommandType { + /** 开始升级 */ + START_UPGRADE("开始升级"), + + /** 暂停升级 */ + PAUSE_UPGRADE("暂停升级"), + + /** 恢复升级 */ + RESUME_UPGRADE("恢复升级"), + + /** 取消升级 */ + CANCEL_UPGRADE("取消升级"), + + /** 重新开始升级 */ + RESTART_UPGRADE("重新开始升级"), + + /** 查询升级状态 */ + QUERY_STATUS("查询升级状态"); + + private final String description; + + CommandType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } + + /** 固件信息 */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class FirmwareInfo { + + /** 固件名称 */ + private String firmwareName; + + /** 固件版本 */ + private String firmwareVersion; + + /** 固件类型 */ + private String firmwareType; + + /** 固件下载URL */ + private String downloadUrl; + + /** 固件大小(字节) */ + private Long fileSize; + + /** 文件校验和(MD5/SHA256等) */ + private String checksum; + + /** 校验和算法 */ + private String checksumAlgorithm; + + /** 固件描述 */ + private String description; + + /** 最低硬件版本要求 */ + private String minHardwareVersion; + + /** 最高硬件版本要求 */ + private String maxHardwareVersion; + + /** 发布时间 */ + private LocalDateTime releaseTime; + + /** 更新内容 */ + private String changeLog; + + /** 是否强制升级 */ + @Builder.Default private Boolean forcedUpgrade = false; + + /** 固件标签 */ + private Map tags; + } + + /** 升级配置 */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class UpgradeConfig { + + /** 升级模式 */ + @Builder.Default private UpgradeMode upgradeMode = UpgradeMode.NORMAL; + + /** 超时时间(秒) */ + @Builder.Default private Integer timeoutSeconds = 3600; + + /** 升级窗口开始时间(24小时制,如:20:00) */ + private String upgradeWindowStart; + + /** 升级窗口结束时间(24小时制,如:06:00) */ + private String upgradeWindowEnd; + + /** 是否在低电量时升级 */ + @Builder.Default private Boolean allowLowBattery = false; + + /** 最低电量要求(百分比) */ + @Builder.Default private Integer minBatteryLevel = 20; + + /** 是否需要WiFi */ + @Builder.Default private Boolean requireWifi = false; + + /** 是否在充电时升级 */ + @Builder.Default private Boolean requireCharging = false; + + /** 下载分块大小(字节) */ + @Builder.Default private Integer chunkSize = 4096; + + /** 下载并发数 */ + @Builder.Default private Integer maxConcurrent = 1; + + /** 是否自动重启 */ + @Builder.Default private Boolean autoReboot = true; + + /** 重启延迟(秒) */ + @Builder.Default private Integer rebootDelaySeconds = 10; + + /** 备份当前固件 */ + @Builder.Default private Boolean backupCurrent = true; + } + + /** 升级模式枚举 */ + public enum UpgradeMode { + /** 普通模式(下载完成后升级) */ + NORMAL("普通模式"), + + /** 增量模式(差分升级) */ + INCREMENTAL("增量模式"), + + /** 流式模式(边下载边升级) */ + STREAMING("流式模式"); + + private final String description; + + UpgradeMode(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } + + /** 重试策略 */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RetryStrategy { + + /** 最大重试次数 */ + @Builder.Default private Integer maxRetries = 3; + + /** 重试间隔(秒) */ + @Builder.Default private Integer retryIntervalSeconds = 60; + + /** 重试间隔增长因子 */ + @Builder.Default private Double backoffMultiplier = 2.0; + + /** 最大重试间隔(秒) */ + @Builder.Default private Integer maxRetryIntervalSeconds = 3600; + + /** 失败类型重试策略 */ + private Map failureTypeRetries; + + /** 是否在网络错误时重试 */ + @Builder.Default private Boolean retryOnNetworkError = true; + + /** 是否在校验失败时重试 */ + @Builder.Default private Boolean retryOnVerificationError = false; + + /** 是否在存储空间不足时重试 */ + @Builder.Default private Boolean retryOnStorageError = false; + } + + /** 验证配置 */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class VerificationConfig { + + /** 是否验证数字签名 */ + @Builder.Default private Boolean verifySignature = true; + + /** 公钥信息 */ + private String publicKey; + + /** 签名算法 */ + @Builder.Default private String signatureAlgorithm = "SHA256withRSA"; + + /** 是否验证证书链 */ + @Builder.Default private Boolean verifyCertificateChain = false; + + /** 证书颁发机构 */ + private String certificateAuthority; + + /** 验证超时时间(秒) */ + @Builder.Default private Integer verificationTimeoutSeconds = 300; + + /** 严格模式(验证失败时停止升级) */ + @Builder.Default private Boolean strictMode = true; + } + + /** 获取设备唯一标识 */ + public String getDeviceUniqueId() { + return productKey + ":" + deviceId; + } + + /** 是否为开始升级命令 */ + public boolean isStartUpgradeCommand() { + return commandType == CommandType.START_UPGRADE || commandType == CommandType.RESTART_UPGRADE; + } + + /** 是否为控制类命令(暂停、恢复、取消) */ + public boolean isControlCommand() { + return commandType == CommandType.PAUSE_UPGRADE + || commandType == CommandType.RESUME_UPGRADE + || commandType == CommandType.CANCEL_UPGRADE; + } + + /** 是否为查询命令 */ + public boolean isQueryCommand() { + return commandType == CommandType.QUERY_STATUS; + } + + /** 获取升级超时时间戳 */ + public LocalDateTime getUpgradeTimeoutTime() { + if (upgradeConfig == null || upgradeConfig.getTimeoutSeconds() == null) { + return commandTime.plusHours(1); // 默认1小时超时 + } + return commandTime.plusSeconds(upgradeConfig.getTimeoutSeconds()); + } + + /** 检查是否在升级窗口内 */ + public boolean isInUpgradeWindow() { + if (upgradeConfig == null + || upgradeConfig.getUpgradeWindowStart() == null + || upgradeConfig.getUpgradeWindowEnd() == null) { + return true; // 无时间窗口限制 + } + + // 简化实现,实际应该根据时区和具体时间判断 + return true; + } + + /** 计算下一次重试时间 */ + public LocalDateTime getNextRetryTime(int currentRetryCount) { + if (retryStrategy == null) { + return commandTime.plusMinutes(1); + } + + int intervalSeconds = retryStrategy.getRetryIntervalSeconds(); + for (int i = 0; i < currentRetryCount; i++) { + intervalSeconds = (int) (intervalSeconds * retryStrategy.getBackoffMultiplier()); + if (intervalSeconds > retryStrategy.getMaxRetryIntervalSeconds()) { + intervalSeconds = retryStrategy.getMaxRetryIntervalSeconds(); + break; + } + } + + return LocalDateTime.now().plusSeconds(intervalSeconds); + } + + @Override + public String toString() { + return String.format( + "OtaUpdateCommand{device=%s, task=%s, type=%s, version=%s, priority=%d}", + getDeviceUniqueId(), + taskId, + commandType, + firmwareInfo != null ? firmwareInfo.getFirmwareVersion() : "unknown", + priority); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/plugin/BaseMessageProcessor.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/plugin/BaseMessageProcessor.java new file mode 100644 index 0000000..275bf9f --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/plugin/BaseMessageProcessor.java @@ -0,0 +1,147 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.plugin; + +/** + * 通用消息处理器顶层接口 + * + *

定义所有消息处理器的基础契约,不同模块根据自己的需求实现具体的处理逻辑 避免在顶层接口中定义过多具体方法,保持接口的简洁性和通用性 + * + * @version 2.0 @Author Aleo + * @since 2025/01/20 + */ +public interface BaseMessageProcessor extends MessageProcessor { + + /** + * 处理器名称 + * + * @return 处理器的可读名称 + */ + String getName(); + + /** + * 执行顺序 数字越小越先执行 + * + * @return 执行顺序 + */ + int getOrder(); + + /** + * 处理器描述(可选) + * + * @return 处理器的详细描述 + */ + default String getDescription() { + return getName(); + } + + /** + * 处理器优先级(可选) 当order相同时,按priority排序 + * + * @return 优先级 + */ + default int getPriority() { + return 0; + } + + /** + * 是否启用此处理器 + * + * @return true表示启用,false表示禁用 + */ + default boolean isEnabled() { + return true; + } + + /** + * 获取处理器版本信息(可选) + * + * @return 版本信息,默认返回"1.0" + */ + default String getVersion() { + return "1.0"; + } + + /** + * 获取处理器类型标识(可选) 用于分组和管理处理器 + * + * @return 处理器类型,默认返回"GENERIC" + */ + default String getType() { + return "GENERIC"; + } + + /** + * 获取处理器作者信息(可选) + * + * @return 作者信息,默认返回空字符串 + */ + default String getAuthor() { + return ""; + } + + /** + * 检查处理器是否支持异步执行(可选) + * + * @return true表示支持异步,false表示同步,默认为false + */ + default boolean isAsyncSupported() { + return false; + } + + /** + * 获取处理器超时时间(毫秒)(可选) + * + * @return 超时时间,默认为30秒 + */ + default long getTimeoutMillis() { + return 30000L; + } + + /** + * 获取处理器的最大重试次数(可选) + * + * @return 最大重试次数,默认为0(不重试) + */ + default int getMaxRetryCount() { + return 0; + } + + /** 处理器初始化方法(可选) 在处理器被加载时调用 */ + default void initialize() { + // 默认不做任何操作 + } + + /** 处理器销毁方法(可选) 在处理器被卸载时调用 */ + default void destroy() { + // 默认不做任何操作 + } + + /** + * 健康检查方法(可选) + * + * @return true表示健康,false表示不健康,默认为true + */ + default boolean isHealthy() { + return true; + } + + /** + * 获取处理器统计信息(可选) + * + * @return 统计信息的字符串表示,默认返回空字符串 + */ + default String getStatistics() { + return ""; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/plugin/MessageProcessor.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/plugin/MessageProcessor.java new file mode 100644 index 0000000..8917a8b --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/plugin/MessageProcessor.java @@ -0,0 +1,10 @@ +package cn.universal.dm.device.service.plugin; + +public interface MessageProcessor { + + /** 处理器名称 */ + String getName(); + + /** 处理器优先级,数字越小优先级越高 */ + int getOrder(); +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/plugin/ProcessorExecutor.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/plugin/ProcessorExecutor.java new file mode 100644 index 0000000..ddaba73 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/plugin/ProcessorExecutor.java @@ -0,0 +1,218 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.plugin; + +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 通用处理器执行器 + * + *

独立出处理器链的通用逻辑:获取处理器、排序、循环执行 提供统一的处理器执行模式,各模块可以复用这些通用逻辑 + * + * @version 2.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +@Component +public class ProcessorExecutor { + + /** + * 执行处理器链 + * + * @param processors 处理器列表 + * @param chainName 处理器链名称(用于日志) + * @param executor 单个处理器执行函数 + * @param supportChecker 支持性检查函数 + * @param 处理器类型 + * @param 处理结果类型 + * @return 是否全部执行成功 + */ + public boolean executeChain( + List processors, String chainName, Function executor, Predicate successChecker) { + + return executeChain(processors, chainName, executor, successChecker, null); + } + + /** + * 执行处理器链(带支持性检查) + * + * @param processors 处理器列表 + * @param chainName 处理器链名称(用于日志) + * @param executor 单个处理器执行函数 + * @param successChecker 成功检查函数 + * @param supportChecker 支持性检查函数(可选) + * @param 处理器类型 + * @param 处理结果类型 + * @return 是否全部执行成功 + */ + public boolean executeChain( + List processors, + String chainName, + Function executor, + Predicate successChecker, + Function supportChecker) { + + if (processors == null || processors.isEmpty()) { + log.warn("[{}] 没有找到任何处理器", chainName); + return true; // 没有处理器也算成功 + } + + // 1. 过滤启用的处理器并排序 + List sortedProcessors = getSortedProcessors(processors); + + if (sortedProcessors.isEmpty()) { + log.warn("[{}] 没有启用的处理器", chainName); + return true; + } + + log.debug( + "[{}] 找到 {} 个处理器: {}", + chainName, + sortedProcessors.size(), + sortedProcessors.stream().map(p -> p.getName() + "(" + p.getOrder() + ")").toList()); + + // 2. 循环执行处理器 + for (T processor : sortedProcessors) { + if (!executeProcessor(processor, chainName, executor, successChecker, supportChecker)) { + return false; + } + } + + log.debug("[{}] 处理器链执行完成", chainName); + return true; + } + + /** 执行单个处理器 */ + private boolean executeProcessor( + T processor, + String chainName, + Function executor, + Predicate successChecker, + Function supportChecker) { + + try { + // 1. 支持性检查 + if (supportChecker != null && !supportChecker.apply(processor)) { + log.debug("[{}] 处理器 {} 不支持当前请求,跳过执行", chainName, processor.getName()); + return true; + } + + log.debug("[{}] 执行处理器: {} (order={})", chainName, processor.getName(), processor.getOrder()); + + // 2. 执行处理器 + R result = executor.apply(processor); + + // 3. 检查执行结果 + boolean success = successChecker.test(result); + + if (success) { + log.debug("[{}] 处理器 {} 执行成功", chainName, processor.getName()); + } else { + log.error("[{}] 处理器 {} 执行失败", chainName, processor.getName()); + } + + return success; + + } catch (Exception e) { + log.error("[{}] 处理器 {} 执行异常: {}", chainName, processor.getName(), e.getMessage(), e); + return false; + } + } + + /** 获取排序后的处理器列表 */ + public List getSortedProcessors(List processors) { + return processors.stream() + .filter(BaseMessageProcessor::isEnabled) + .sorted(getProcessorComparator()) + .toList(); + } + + /** 获取处理器比较器 */ + public Comparator getProcessorComparator() { + return Comparator.comparingInt(BaseMessageProcessor::getOrder) + .thenComparingInt(BaseMessageProcessor::getPriority) + .thenComparing(BaseMessageProcessor::getName); + } + + /** 获取处理器名称列表(用于调试) */ + public List getProcessorNames(List processors) { + return getSortedProcessors((List) processors).stream() + .map(BaseMessageProcessor::getName) + .toList(); + } + + /** + * 获取启用的处理器数量 + * + * @param processors 处理器列表 + * @param 处理器类型 + * @return 启用的处理器数量 + */ + public long getEnabledProcessorCount(List processors) { + if (processors == null) { + return 0; + } + + return processors.stream().filter(BaseMessageProcessor::isEnabled).count(); + } + + /** + * 检查指定名称的处理器是否存在且启用 + * + * @param processors 处理器列表 + * @param name 处理器名称 + * @param 处理器类型 + * @return 是否存在且启用 + */ + public boolean isProcessorEnabled( + List processors, String name) { + if (processors == null || name == null) { + return false; + } + + return processors.stream().anyMatch(p -> name.equals(p.getName()) && p.isEnabled()); + } + + /** 支持返回处理器链的第一个非null结果(比如R),不影响原有boolean链式执行 */ + public R executeChainWithResult( + List processors, + String chainName, + Function executor, + Predicate successChecker, + Function supportChecker) { + if (processors == null || processors.isEmpty()) { + log.warn("[{}] 没有找到任何处理器", chainName); + return null; + } + List sortedProcessors = getSortedProcessors(processors); + if (sortedProcessors.isEmpty()) { + log.warn("[{}] 没有启用的处理器", chainName); + return null; + } + for (T processor : sortedProcessors) { + if (supportChecker != null && !supportChecker.apply(processor)) { + continue; + } + R result = executor.apply(processor); + if (successChecker.test(result)) { + return result; // 返回第一个成功的结果 + } + } + return null; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/HttpPushStrategy.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/HttpPushStrategy.java new file mode 100644 index 0000000..9fbc012 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/HttpPushStrategy.java @@ -0,0 +1,218 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.push; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.MD5; +import cn.hutool.http.Header; +import cn.hutool.http.HttpException; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.entity.bo.UPPushBO; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * HTTP推送策略实现 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +@Slf4j +@Component +public class HttpPushStrategy implements PushStrategy { + + /** HTTP请求超时时间(毫秒) */ + private static final Integer HTTP_TIME_OUT = 1200; + + /** URL黑名单缓存 */ + private static final Cache blockUrl = + Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).maximumSize(256).build(); + + /** 超过失败次数则入黑名单 */ + private static final Integer BLOCK_COUNT = 30; + + private static final String CACHE_NOTICE = "DNotice:"; + + private UPPushBO.HttpPushConfig httpConfig; + + public void setConfig(UPPushBO.HttpPushConfig httpConfig) { + this.httpConfig = httpConfig; + } + + @Override + public IoTPushResult execute(BaseUPRequest request, String messageJson) { + if (httpConfig == null) { + log.warn("[HTTP推送] 配置为空,跳过推送"); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "HTTP", + messageJson, + "配置为空", + "CONFIG_NULL"); + } + + try { + if (StrUtil.isBlank(httpConfig.getUrl())) { + log.warn("[HTTP推送] URL为空,跳过推送"); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "HTTP", + messageJson, + "URL为空", + "URL_EMPTY"); + } + + // 检查URL是否被冻结 + if (isUrlBlocked(httpConfig.getUrl())) { + log.warn("[HTTP推送] URL {} 因多次推送失败已被冻结", httpConfig.getUrl()); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "HTTP", + messageJson, + "URL已被冻结", + "URL_BLOCKED"); + } + + // 创建HTTP请求 + HttpRequest httpRequest = HttpUtil.createPost(httpConfig.getUrl()); + httpRequest.timeout(HTTP_TIME_OUT); + + // 添加时间戳和签名 + String timestamp = String.valueOf(System.currentTimeMillis()); + httpRequest.header("X-Timestamp", timestamp); + httpRequest.header(Header.CONTENT_TYPE, "application/json"); + + // 添加自定义请求头(如果有配置) + if (StrUtil.isNotBlank(httpConfig.getHeader())) { + httpRequest.header( + httpConfig.getHeader(), + StrUtil.isBlank(httpConfig.getSecret()) + ? MD5.create().digestHex(httpConfig.getHeader()) + : httpConfig.getSecret()); + } + + // 设置请求体 + httpRequest.body(messageJson); + + // 执行请求 + HttpResponse response = httpRequest.execute(); + String result = response.body(); + log.warn( + "[HTTP推送] 推送, url={}, status={}, messageJson={}", + httpConfig.getUrl(), + response != null ? response.getStatus() : "null", + result); + // 检查响应状态 + if (response == null || response.getStatus() != 200) { + log.warn( + "[HTTP推送] 推送失败, url={}, status={}, response={}", + httpConfig.getUrl(), + response != null ? response.getStatus() : "null", + result); + countFail(httpConfig.getUrl()); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "HTTP", + messageJson, + "HTTP状态码错误: " + (response != null ? response.getStatus() : "null"), + "HTTP_ERROR"); + } + + log.info("[HTTP推送] 推送成功, url={}, deviceId={}", httpConfig.getUrl(), request.getIotId()); + removeSuccess(httpConfig.getUrl()); + + return IoTPushResult.success( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "HTTP", + messageJson, + System.currentTimeMillis()); + + } catch (HttpException e) { + log.warn( + "[HTTP推送] 推送异常, url={}, deviceId={}, error={}", + httpConfig.getUrl(), + request.getIotId(), + ExceptionUtil.getRootCause(e)); + countFail(httpConfig.getUrl()); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "HTTP", + messageJson, + "HTTP异常: " + e.getMessage(), + "HTTP_EXCEPTION"); + } catch (Exception e) { + log.error("[HTTP推送] 推送失败: {}, url={}", request.getIotId(), httpConfig.getUrl(), e); + countFail(httpConfig.getUrl()); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "HTTP", + messageJson, + "推送异常: " + e.getMessage(), + "PUSH_EXCEPTION"); + } + } + + @Override + public boolean isSupported() { + return httpConfig != null && httpConfig.isSupport() && httpConfig.isEnable(); + } + + /** 检查URL是否被冻结 */ + private boolean isUrlBlocked(String url) { + return blockUrl.getIfPresent(url) != null && blockUrl.getIfPresent(url) >= BLOCK_COUNT; + } + + /** 记录失败次数 */ + private void countFail(String url) { + int failure = blockUrl.getIfPresent(url) == null ? 0 : (blockUrl.getIfPresent(url) + 1); + if (failure > BLOCK_COUNT) { + // 5分钟只推送一次 + if (blockUrl.getIfPresent(CACHE_NOTICE + url) != null) { + log.warn("地址:{} 推送失败超过阈值", url); + blockUrl.put(CACHE_NOTICE + url, 0); + } + } else { + blockUrl.put(url, failure); + } + } + + /** 移除成功记录 */ + private void removeSuccess(String url) { + if (blockUrl.getIfPresent(url) != null) { + blockUrl.invalidate(url); + blockUrl.invalidate(CACHE_NOTICE + url); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/KafkaPushStrategy.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/KafkaPushStrategy.java new file mode 100644 index 0000000..b41b6bc --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/KafkaPushStrategy.java @@ -0,0 +1,87 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.push; + +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.entity.bo.UPPushBO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Kafka推送策略实现 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +@Slf4j +@Component +public class KafkaPushStrategy implements PushStrategy { + + private UPPushBO.KafkaPushConfig kafkaConfig; + + public void setConfig(UPPushBO.KafkaPushConfig kafkaConfig) { + this.kafkaConfig = kafkaConfig; + } + + @Override + public IoTPushResult execute(BaseUPRequest request, String messageJson) { + if (kafkaConfig == null) { + log.warn("[Kafka推送] 配置为空,跳过推送"); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "Kafka", + messageJson, + "配置为空", + "CONFIG_NULL"); + } + + try { + log.info("[Kafka推送] 推送消息: {}", request.getIotId()); + + // TODO: 实现具体的Kafka推送逻辑 + // 这里可以集成Spring Kafka或其他Kafka客户端,比如: + // - Spring Kafka Template + // - Apache Kafka Client + // - Confluent Kafka Client + // kafkaTemplate.send("platform-topic", request.getIotId(), messageJson); + + // 暂时返回成功(实际实现时需要根据Kafka推送结果返回) + return IoTPushResult.success( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "Kafka", + messageJson, + System.currentTimeMillis()); + + } catch (Exception e) { + log.error("[Kafka推送] 推送失败: {}", request.getIotId(), e); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "Kafka", + messageJson, + "推送异常: " + e.getMessage(), + "PUSH_EXCEPTION"); + } + } + + @Override + public boolean isSupported() { + return kafkaConfig != null && kafkaConfig.isSupport() && kafkaConfig.isEnable(); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MessageFormatProcessor.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MessageFormatProcessor.java new file mode 100644 index 0000000..b8cd505 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MessageFormatProcessor.java @@ -0,0 +1,81 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.push; + +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 消息格式处理器 - 统一消息格式转换 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +@Slf4j +@Component +public class MessageFormatProcessor implements UPProcessor { + + @Override + public String getName() { + return "MessageFormatProcessor"; + } + + @Override + public String getDescription() { + return "统一消息格式转换处理器"; + } + + @Override + public int getOrder() { + return 100; // 高优先级,优先执行 + } + + @Override + public List beforePush(List upRequests) { + log.debug("[消息格式处理器] 开始处理 {} 条消息", upRequests.size()); + + upRequests.forEach( + request -> { + // 统一消息格式转换 + formatMessage(request); + }); + + log.debug("[消息格式处理器] 处理完成"); + return upRequests; + } + + /** + * 格式化消息 + * + * @param request 请求对象 + */ + private void formatMessage(BaseUPRequest request) { + IoTDeviceDTO deviceDTO = request.getIoTDeviceDTO(); + if (deviceDTO != null) { + // 添加时间戳 + if (request.getTime() == null) { + request.setTime(System.currentTimeMillis()); + } + + // 添加消息来源标识 + if (deviceDTO.getThirdPlatform() == null) { + deviceDTO.setThirdPlatform("universal"); + } + + log.debug("[消息格式处理器] 格式化消息: {}", request.getIotId()); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MqttPushStrategy.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MqttPushStrategy.java new file mode 100644 index 0000000..a8e2840 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MqttPushStrategy.java @@ -0,0 +1,103 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.push; + +import cn.hutool.core.util.StrUtil; +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.entity.bo.UPPushBO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * MQTT推送策略实现 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +@Slf4j +@Component +public class MqttPushStrategy implements PushStrategy { + + private UPPushBO.MqttPushConfig mqttConfig; + + public void setConfig(UPPushBO.MqttPushConfig mqttConfig) { + this.mqttConfig = mqttConfig; + } + + @Override + public IoTPushResult execute(BaseUPRequest request, String messageJson) { + if (mqttConfig == null) { + log.warn("[MQTT推送] 配置为空,跳过推送"); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "MQTT", + messageJson, + "配置为空", + "CONFIG_NULL"); + } + + try { + if (StrUtil.isBlank(mqttConfig.getTopic())) { + log.warn("[MQTT推送] Topic为空,跳过推送"); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "MQTT", + messageJson, + "Topic为空", + "TOPIC_EMPTY"); + } + + // 构建完整的topic + String fullTopic = mqttConfig.getTopic().replace("#", request.getIotId()); + + log.info("[MQTT推送] 推送到Topic: {}, 消息: {}", fullTopic, request.getIotId()); + + // TODO: 实现具体的MQTT推送逻辑 + // 这里可以集成具体的MQTT客户端,比如: + // - Eclipse Paho MQTT Client + // - HiveMQ MQTT Client + // - Spring Integration MQTT + // mqttClient.publish(fullTopic, messageJson.getBytes(), 1, false); + + // 暂时返回成功(实际实现时需要根据MQTT推送结果返回) + return IoTPushResult.success( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "MQTT", + messageJson, + System.currentTimeMillis()); + + } catch (Exception e) { + log.error("[MQTT推送] 推送失败: {}", request.getIotId(), e); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "MQTT", + messageJson, + "推送异常: " + e.getMessage(), + "PUSH_EXCEPTION"); + } + } + + @Override + public boolean isSupported() { + return mqttConfig != null && mqttConfig.isSupport() && mqttConfig.isEnable(); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/PerformanceMonitorProcessor.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/PerformanceMonitorProcessor.java new file mode 100644 index 0000000..490a8ec --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/PerformanceMonitorProcessor.java @@ -0,0 +1,80 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.push; + +import cn.universal.persistence.base.BaseUPRequest; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 性能监控处理器 - 推送性能统计 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +@Slf4j +@Component +public class PerformanceMonitorProcessor implements UPProcessor { + + private final ConcurrentHashMap pushCountMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap pushSuccessMap = new ConcurrentHashMap<>(); + + @Override + public String getName() { + return "PerformanceMonitorProcessor"; + } + + @Override + public String getDescription() { + return "性能监控处理器"; + } + + @Override + public int getOrder() { + return 300; // 最后执行,用于统计 + } + + @Override + public List beforePush(List upRequests) { + log.debug("[性能监控处理器] 开始监控 {} 条消息", upRequests.size()); + + // 记录推送开始时间 + upRequests.forEach( + request -> { + request.setTime(System.currentTimeMillis()); + }); + + return upRequests; + } + + /** + * 获取推送统计信息 + * + * @return 统计信息 + */ + public ConcurrentHashMap getPushCountMap() { + return pushCountMap; + } + + /** + * 获取成功推送统计信息 + * + * @return 统计信息 + */ + public ConcurrentHashMap getPushSuccessMap() { + return pushSuccessMap; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/PushStrategy.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/PushStrategy.java new file mode 100644 index 0000000..7c7f0f1 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/PushStrategy.java @@ -0,0 +1,40 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.push; + +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.persistence.base.BaseUPRequest; + +/** + * 推送策略接口 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +public interface PushStrategy { + + /** + * 执行推送 + * + * @param request 上行请求 + * @param messageJson 消息JSON字符串 + */ + IoTPushResult execute(BaseUPRequest request, String messageJson); + + /** + * 是否支持该推送类型 + * + * @return 是否支持 + */ + boolean isSupported(); +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/PushStrategyManager.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/PushStrategyManager.java new file mode 100644 index 0000000..c299d14 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/PushStrategyManager.java @@ -0,0 +1,249 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.push; + +import cn.hutool.json.JSONUtil; +import cn.universal.core.message.UPRequest; +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.dm.device.service.push.processor.PushRetryProcessor; +import cn.universal.dm.device.service.push.processor.PushStatisticsProcessor; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.entity.bo.UPPushBO; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 推送策略管理器 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +@Slf4j +@Component +public class PushStrategyManager { + + @Autowired private HttpPushStrategy httpPushStrategy; + + @Autowired private MqttPushStrategy mqttPushStrategy; + + @Autowired private KafkaPushStrategy kafkaPushStrategy; + + @Autowired private RocketMQPushStrategy rocketMQPushStrategy; + + @Autowired private UPProcessorManager upProcessorManager; + + @Autowired private PushStatisticsProcessor pushStatisticsProcessor; + + @Autowired private PushRetryProcessor pushRetryProcessor; + + /** + * 执行推送 + * + * @param request 上行请求 + * @param config 推送配置 + * @return 推送结果列表 + */ + public List executePush(BaseUPRequest request, UPPushBO config) { + if (request == null || config == null) { + log.warn("[推送策略管理器] 请求或配置为空,跳过推送"); + return null; + } + + String messageJson = JSONUtil.toJsonStr((UPRequest) request); + log.debug("[推送策略管理器] 开始推送消息: {}", request.getIotId()); + + List results = new java.util.ArrayList<>(); + + // HTTP 推送 + if (config.getHttp() != null && config.getHttp().isEnable()) { + httpPushStrategy.setConfig(config.getHttp()); + if (httpPushStrategy.isSupported()) { + IoTPushResult result = executeSinglePush(httpPushStrategy, request, messageJson, "HTTP"); + if (result != null) { + results.add(result); + } + } + } + + // MQTT 推送 + if (config.getMqtt() != null && config.getMqtt().isEnable()) { + mqttPushStrategy.setConfig(config.getMqtt()); + if (mqttPushStrategy.isSupported()) { + IoTPushResult result = executeSinglePush(mqttPushStrategy, request, messageJson, "MQTT"); + if (result != null) { + results.add(result); + } + } + } + + // Kafka 推送 + if (config.getKafka() != null && config.getKafka().isEnable()) { + kafkaPushStrategy.setConfig(config.getKafka()); + if (kafkaPushStrategy.isSupported()) { + IoTPushResult result = executeSinglePush(kafkaPushStrategy, request, messageJson, "Kafka"); + if (result != null) { + results.add(result); + } + } + } + + // RocketMQ 推送 + if (config.getRocketMQ() != null && config.getRocketMQ().isEnable()) { + rocketMQPushStrategy.setConfig(config.getRocketMQ()); + if (rocketMQPushStrategy.isSupported()) { + IoTPushResult result = + executeSinglePush(rocketMQPushStrategy, request, messageJson, "RocketMQ"); + if (result != null) { + results.add(result); + } + } + } + + // 异步处理推送结果 + CompletableFuture.runAsync( + () -> { + processPushResults(results, request); + }); + + // 执行推送后处理器 + upProcessorManager.executeAfterPush(List.of(request), results); + + log.debug("[推送策略管理器] 推送完成: {}", request.getIotId()); + return results; + } + + /** 执行单个推送策略 */ + private IoTPushResult executeSinglePush( + PushStrategy strategy, BaseUPRequest request, String messageJson, String channel) { + long startTime = System.currentTimeMillis(); + + try { + IoTPushResult result = strategy.execute(request, messageJson); + + // 补充推送结果信息 + if (result != null) { + result.setDeviceId(request.getIotId()); + result.setProductKey(request.getProductKey()); + result.setChannel(channel); + result.setResponseTime(System.currentTimeMillis() - startTime); + result.setRequestId(request.getIotId() + "_" + System.currentTimeMillis()); + } + + return result; + } catch (Exception e) { + log.error("[推送策略管理器] {} 推送失败: {}", channel, request.getIotId(), e); + + // 创建失败结果 - 从设备配置中获取平台信息 + String platform = null; + if (request.getIoTDeviceDTO() != null) { + platform = request.getIoTDeviceDTO().getThirdPlatform(); + } + + return IoTPushResult.failed( + platform, // 从设备配置中获取平台信息 + request.getProductKey(), + request.getIotId(), + channel, + messageJson, + e.getMessage(), + "PUSH_ERROR"); + } + } + + /** 处理推送结果 */ + private void processPushResults(List results, BaseUPRequest request) { + if (results == null || results.isEmpty()) { + return; + } + + for (IoTPushResult result : results) { + try { + // 只处理非空结果 + if (result != null) { + // 处理失败推送 + if (!result.isOk()) { + pushRetryProcessor.addToRetryQueue(result); + pushRetryProcessor.recordFailedPush(result); + log.warn( + "[推送策略管理器] 推送失败,已加入重试队列: deviceId={}, channel={}, error={}", + result.getDeviceId(), + result.getChannel(), + result.getErrorMessage()); + } else { + log.debug( + "[推送策略管理器] 推送成功: deviceId={}, channel={}, responseTime={}ms", + result.getDeviceId(), + result.getChannel(), + result.getResponseTime()); + } + } + } catch (Exception e) { + log.error( + "[推送策略管理器] 处理推送结果失败: deviceId={}, channel={}", + result.getDeviceId(), + result.getChannel(), + e); + } + } + } + + /** + * 批量推送 + * + * @param requests 请求列表 + * @param config 推送配置 + * @return 所有推送结果 + */ + public List executeBatchPush(List requests, UPPushBO config) { + if (requests == null || requests.isEmpty()) { + log.warn("[推送策略管理器] 请求列表为空,跳过批量推送"); + return null; + } + + log.debug("[推送策略管理器] 开始批量推送,消息数量: {}", requests.size()); + + // 执行推送前处理 + List processedRequests = upProcessorManager.executeBeforePush(requests); + + List allResults = new java.util.ArrayList<>(); + + for (BaseUPRequest request : processedRequests) { + try { + List results = executePush(request, config); + if (results != null) { + allResults.addAll(results); + } + } catch (Exception e) { + log.error("[推送策略管理器] 单条消息推送失败: {}", request.getIotId(), e); + + // 创建失败结果 + IoTPushResult failedResult = + IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), // 平台信息暂时设为null,后续可以从配置中获取 + request.getProductKey(), + request.getIotId(), + "BATCH_PUSH", + JSONUtil.toJsonStr(request), + e.getMessage(), + "BATCH_PUSH_ERROR"); + allResults.add(failedResult); + } + } + + log.info("[推送策略管理器] 批量推送完成,消息数量: {}, 结果数量: {}", requests.size(), allResults.size()); + return allResults; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/README.md b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/README.md new file mode 100644 index 0000000..27fd179 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/README.md @@ -0,0 +1,299 @@ +# 推送系统架构说明 + +## 概述 + +本推送系统基于现有的`UPProcessorManager` +架构实现,支持多种推送渠道(HTTP、MQTT、Kafka、RocketMQ),具备统计、重试等完整功能,同时保持架构的一致性和可扩展性。 + +## 核心组件 + +### 1. IoTPushResult - 推送结果实体 + +```java +@Data +@Builder +public class IoTPushResult { + private boolean ok; // 推送是否成功 + private ThirdPlatform platform; // 第三方平台类型 + private String productKey; // 产品Key + private String deviceId; // 设备ID + private String channel; // 推送渠道 + private String messageContent; // 推送消息内容 + private LocalDateTime pushTime; // 推送时间 + private Long responseTime; // 响应时间(毫秒) + private String errorMessage; // 错误信息 + private String errorCode; // 错误代码 + private Integer retryCount; // 重试次数 + private Integer maxRetryCount; // 最大重试次数 + private PushStatus status; // 推送状态 + private String configId; // 推送配置ID + private String requestId; // 请求ID +} +``` + +### 2. UPPushBO - 推送配置扩展 + +- **功能**: 扩展现有的`UPPushBO`,添加统计和重试配置 +- **新增配置**: + - `StatisticsConfig`: 统计配置(启用状态、保留时间、异步处理) + - `RetryConfig`: 重试配置(启用状态、最大次数、间隔时间) + +### 3. PushStatisticsProcessor - 推送统计处理器 + +- **架构**: 基于`UPProcessor`接口实现 +- **功能**: 统计每天不同渠道、不同产品的推送数据 +- **统计维度**: + - 总体统计(成功/失败/总数/响应时间) + - 渠道统计(按HTTP、MQTT等渠道) + - 产品统计(按产品Key) + - 平台统计(按第三方平台) +- **存储**: Redis Hash结构,支持TTL自动过期 +- **执行时机**: 推送后处理(`afterPush`方法) + +### 4. PushRetryProcessor - 推送重试处理器 + +- **架构**: 基于`UPProcessor`接口实现 +- **功能**: 处理推送失败后的重试机制 +- **特性**: + - 可配置最大重试次数(默认3次) + - 定时重试任务(每5分钟执行) + - 手动重试功能 + - 重试队列管理 + - 自动清理过期重试记录 +- **执行时机**: 推送后处理(`afterPush`方法) + +### 5. PushStrategyManager - 推送策略管理器(升级版) + +- **功能**: 统一管理所有推送策略,集成统计和重试功能 +- **特性**: + - 支持多种推送渠道(HTTP、MQTT、Kafka、RocketMQ) + - 集成`UPProcessorManager`架构 + - 异步处理推送结果 + - 支持批量推送 + - 自动执行推送前/后处理器 + +## 架构优势 + +### 1. 基于现有架构 + +- ✅ 复用`UPProcessorManager`的处理器机制 +- ✅ 扩展现有的`UPPushBO`配置 +- ✅ 保持架构一致性 +- ✅ 减少新增节点 + +### 2. 插件化设计 + +- ✅ 统计和重试功能作为处理器插件 +- ✅ 支持优先级排序 +- ✅ 易于扩展新功能 +- ✅ 支持条件过滤 + +### 3. 配置驱动 + +- ✅ 通过`UPPushBO`统一配置 +- ✅ 支持各渠道独立配置 +- ✅ 统计和重试参数可配置 +- ✅ 运行时动态调整 + +## 使用示例 + +### 基本推送 + +```java + +@Autowired +private PushStrategyManager pushStrategyManager; + +// 执行推送 +List results = pushStrategyManager.executePush(request, config); + +// 批量推送 +List batchResults = pushStrategyManager.executeBatchPush(requests, config); +``` + +### 获取统计信息 + +```java +// 获取今日推送统计 +PushStatisticsProcessor.PushStatistics stats = + pushStatisticsProcessor.getStatistics(LocalDate.now()); + +// 获取渠道统计 +PushStatisticsProcessor.ChannelStatistics channelStats = + pushStatisticsProcessor.getChannelStatistics(LocalDate.now(), "HTTP"); +``` + +### 重试功能 + +```java +// 手动重试 +pushRetryProcessor.manualRetry("deviceId","HTTP","productKey"); + +// 获取重试次数 +Integer retryCount = pushRetryProcessor.getRetryCount("deviceId", "HTTP"); + +// 重置重试计数 +pushRetryProcessor. + +resetRetryCount("deviceId","HTTP"); +``` + +## 配置说明 + +### UPPushBO 配置示例 + +```java +UPPushBO config = UPPushBO.builder() + .http(HttpPushConfig.builder() + .url("http://example.com/push") + .enable(true) + .support(true) + .build()) + .mqtt(MqttPushConfig.builder() + .url("tcp://localhost:1883") + .topic("platform/push") + .enable(true) + .support(true) + .build()) + .statistics(UPPushBO.StatisticsConfig.builder() + .enable(true) + .retentionDays(30) + .async(true) + .build()) + .retry(UPPushBO.RetryConfig.builder() + .enable(true) + .maxCount(3) + .intervalMinutes(5) + .build()) + .build(); +``` + +## Redis Key 结构 + +### 统计相关 + +- `push:stats:2025-01-09` - 总体统计 +- `push:channel:2025-01-09:HTTP` - 渠道统计 +- `push:product:2025-01-09:productKey` - 产品统计 +- `push:platform:2025-01-09:ctwing` - 平台统计 + +### 重试相关 + +- `push:retry:queue:deviceId:channel` - 重试队列 +- `push:retry:count:deviceId:channel` - 重试计数 +- `push:failed:2025-01-09` - 失败推送记录 + +## 处理器执行流程 + +### 推送前处理 + +1. `UPProcessorManager.executeBeforePush()` 执行所有处理器 +2. 按优先级排序执行 +3. 支持条件过滤(`supports`方法) + +### 推送执行 + +1. 根据配置执行各渠道推送 +2. 记录推送结果到`IoTPushResult` +3. 异步处理推送结果 + +### 推送后处理 + +1. `UPProcessorManager.executeAfterPush()` 执行所有处理器 +2. `PushStatisticsProcessor` 记录统计信息 +3. `PushRetryProcessor` 处理失败重试 + +## 定时任务 + +### 重试任务 + +- **频率**: 每5分钟执行一次 +- **功能**: 自动重试失败的推送 +- **实现**: `PushRetryProcessor.retryFailedPushes()` + +### 清理任务 + +- **重试清理**: 每天凌晨2点执行 +- **功能**: 清理过期的重试记录和监控数据 +- **实现**: `PushRetryProcessor.cleanupExpiredRetryRecords()` + +## 扩展指南 + +### 添加新的统计维度 + +1. 在`PushStatisticsProcessor`中添加新的统计方法 +2. 定义相应的统计数据结构 +3. 在`afterPush`方法中调用统计方法 + +### 添加新的重试策略 + +1. 在`PushRetryProcessor`中实现新的重试逻辑 +2. 配置重试参数到`UPPushBO.RetryConfig` +3. 在定时任务中调用重试方法 + +### 添加新的处理器 + +1. 实现`UPProcessor`接口 +2. 配置优先级(`getOrder`方法) +3. 实现条件过滤(`supports`方法) +4. 在`afterPush`方法中实现业务逻辑 + +## 性能优化 + +### 异步处理 + +- 推送结果统计采用异步处理 +- 重试逻辑采用异步处理 +- 减少对主流程的影响 + +### Redis优化 + +- 使用Hash结构存储统计数据 +- 设置合理的TTL自动过期 +- 定期清理过期数据 + +### 批量操作 + +- 支持批量推送 +- 批量统计处理 +- 减少网络开销 + +## 监控指标 + +### 业务指标 + +- 推送成功率 +- 平均响应时间 +- 失败推送数量 +- 重试次数 + +### 系统指标 + +- Redis连接状态 +- 处理器执行时间 +- 队列长度 +- 内存使用情况 + +## 最佳实践 + +1. **配置优化**: 根据实际业务需求调整重试次数和间隔 +2. **监控告警**: 设置合理的告警阈值,及时发现问题 +3. **数据清理**: 定期清理过期数据,避免Redis内存占用过高 +4. **性能调优**: 根据推送量调整异步处理参数 +5. **故障演练**: 定期进行故障演练,验证系统容错能力 + +## 架构对比 + +### 原有架构 + +- 独立的推送策略管理器 +- 分散的统计和重试逻辑 +- 配置管理不统一 + +### 新架构优势 + +- ✅ 基于现有`UPProcessorManager`架构 +- ✅ 统一的配置管理(`UPPushBO`) +- ✅ 插件化的统计和重试功能 +- ✅ 更好的可扩展性和维护性 +- ✅ 减少代码重复和架构复杂度 \ No newline at end of file diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/RocketMQPushStrategy.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/RocketMQPushStrategy.java new file mode 100644 index 0000000..d130e81 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/RocketMQPushStrategy.java @@ -0,0 +1,87 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.push; + +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.entity.bo.UPPushBO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * RocketMQ推送策略实现 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +@Slf4j +@Component +public class RocketMQPushStrategy implements PushStrategy { + + private UPPushBO.RocketMQPushConfig rocketMQConfig; + + public void setConfig(UPPushBO.RocketMQPushConfig rocketMQConfig) { + this.rocketMQConfig = rocketMQConfig; + } + + @Override + public IoTPushResult execute(BaseUPRequest request, String messageJson) { + if (rocketMQConfig == null) { + log.warn("[RocketMQ推送] 配置为空,跳过推送"); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "RocketMQ", + messageJson, + "配置为空", + "CONFIG_NULL"); + } + + try { + log.info("[RocketMQ推送] 推送消息: {}", request.getIotId()); + + // TODO: 实现具体的RocketMQ推送逻辑 + // 这里可以集成RocketMQ客户端,比如: + // - RocketMQ Spring Boot Starter + // - Apache RocketMQ Client + // - Alibaba RocketMQ Client + // rocketMQTemplate.syncSend("platform-topic", messageJson); + + // 暂时返回成功(实际实现时需要根据RocketMQ推送结果返回) + return IoTPushResult.success( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "RocketMQ", + messageJson, + System.currentTimeMillis()); + + } catch (Exception e) { + log.error("[RocketMQ推送] 推送失败: {}", request.getIotId(), e); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "RocketMQ", + messageJson, + "推送异常: " + e.getMessage(), + "PUSH_EXCEPTION"); + } + } + + @Override + public boolean isSupported() { + return rocketMQConfig != null && rocketMQConfig.isSupport() && rocketMQConfig.isEnable(); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/RuleEngineProcessor.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/RuleEngineProcessor.java new file mode 100644 index 0000000..240b50a --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/RuleEngineProcessor.java @@ -0,0 +1,93 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.push; + +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import java.util.List; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 规则引擎处理器 - 消息过滤和规则检查 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +@Slf4j +@Component +public class RuleEngineProcessor implements UPProcessor { + + @Override + public String getName() { + return "RuleEngineProcessor"; + } + + @Override + public String getDescription() { + return "规则引擎处理器"; + } + + @Override + public int getOrder() { + return 200; // 在格式转换之后执行 + } + + @Override + public List beforePush(List upRequests) { + log.debug("[规则引擎处理器] 开始处理 {} 条消息", upRequests.size()); + + // 应用业务规则过滤 + List filteredRequests = + upRequests.stream().filter(this::applyBusinessRules).collect(Collectors.toList()); + + int filteredCount = upRequests.size() - filteredRequests.size(); + if (filteredCount > 0) { + log.info("[规则引擎处理器] 过滤掉 {} 条不符合规则的消息", filteredCount); + } + + log.debug("[规则引擎处理器] 处理完成,剩余 {} 条消息", filteredRequests.size()); + return filteredRequests; + } + + /** + * 应用业务规则 + * + * @param request 请求对象 + * @return 是否通过规则检查 + */ + private boolean applyBusinessRules(BaseUPRequest request) { + IoTDeviceDTO deviceDTO = request.getIoTDeviceDTO(); + + // 规则1:设备必须存在且有效 + if (deviceDTO == null) { + log.debug("[规则引擎] 设备不存在,过滤消息: {}", request.getIotId()); + return false; + } + + // 规则2:应用必须启用 + if (deviceDTO.isAppDisable()) { + log.debug("[规则引擎] 应用已禁用,过滤消息: {}", request.getIotId()); + return false; + } + + // 规则3:设备必须在线 + if (deviceDTO.getState() == null || !deviceDTO.getState()) { + log.debug("[规则引擎] 设备离线,过滤消息: {}", request.getIotId()); + return false; + } + + return true; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/SubscribePushStrategy.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/SubscribePushStrategy.java new file mode 100644 index 0000000..ffdcc8f --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/SubscribePushStrategy.java @@ -0,0 +1,180 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.push; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.Header; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.entity.IoTDeviceSubscribe; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.List; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * HTTP订阅推送策略实现 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +@Slf4j +@Component +public class SubscribePushStrategy implements PushStrategy { + + /** HTTP请求超时时间(毫秒) */ + private static final Integer HTTP_TIME_OUT = 1200; + + /** URL黑名单缓存 */ + private static final Cache blockUrl = + Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).maximumSize(256).build(); + + /** 超过失败次数则入黑名单 */ + private static final Integer BLOCK_COUNT = 30; + + private static final String CACHE_NOTICE = "DNotice:"; + + @Override + public IoTPushResult execute(BaseUPRequest request, String messageJson) { + + // 订阅的推送 + try { + Object devSubObj = request.getDevSubscribe(); + if (devSubObj != null) { + List subscribes = (List) devSubObj; + for (IoTDeviceSubscribe subscribe : subscribes) { + if (StrUtil.isBlank(subscribe.getUrl())) { + log.warn("[HTTP订阅推送] URL为空,跳过推送"); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "Subscribe", + messageJson, + "URL为空", + "URL_EMPTY"); + } + + // 检查URL是否被冻结 + if (isUrlBlocked(subscribe.getUrl())) { + log.warn("[HTTP订阅推送] URL {} 因多次推送失败已被冻结", subscribe.getUrl()); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "Subscribe", + messageJson, + "URL已被冻结", + "URL_BLOCKED"); + } + + // 创建HTTP请求 + HttpRequest httpRequest = HttpUtil.createPost(subscribe.getUrl()); + httpRequest.timeout(HTTP_TIME_OUT); + + // 添加时间戳和签名 + String timestamp = String.valueOf(System.currentTimeMillis()); + // String signature = String.valueOf((timestamp + httpConfig.getUrl()).hashCode()); + httpRequest.header("X-Timestamp", timestamp); + // httpRequest.header("X-Signature", signature); + httpRequest.header(Header.CONTENT_TYPE, "application/json"); + + // 设置请求体 + httpRequest.body(messageJson); + + // 执行请求 + HttpResponse response = httpRequest.execute(); + String result = response.body(); + + // 检查响应状态 + if (response == null || response.getStatus() != 200) { + log.warn( + "[HTTP订阅推送] 推送失败, url={}, status={}, response={}", + subscribe.getUrl(), + response != null ? response.getStatus() : "null", + result); + countFail(subscribe.getUrl()); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "Subscribe", + messageJson, + "HTTP状态码错误: " + (response != null ? response.getStatus() : "null"), + "HTTP_ERROR"); + } + + log.info("[HTTP订阅推送] 推送成功, url={}, deviceId={}", subscribe.getUrl(), request.getIotId()); + removeSuccess(subscribe.getUrl()); + } + } + + // 如果没有订阅或所有订阅都成功,返回成功 + return IoTPushResult.success( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "Subscribe", + messageJson, + System.currentTimeMillis()); + + } catch (Exception e) { + log.error("[HTTP订阅推送] 推送失败: {}", request.getIotId(), e); + return IoTPushResult.failed( + request.getIoTDeviceDTO().getThirdPlatform(), + request.getProductKey(), + request.getIotId(), + "Subscribe", + messageJson, + "推送异常: " + e.getMessage(), + "PUSH_EXCEPTION"); + } + } + + @Override + public boolean isSupported() { + return true; + } + + /** 检查URL是否被冻结 */ + private boolean isUrlBlocked(String url) { + return blockUrl.getIfPresent(url) != null && blockUrl.getIfPresent(url) >= BLOCK_COUNT; + } + + /** 记录失败次数 */ + private void countFail(String url) { + int failure = blockUrl.getIfPresent(url) == null ? 0 : (blockUrl.getIfPresent(url) + 1); + if (failure > BLOCK_COUNT) { + // 5分钟只推送一次 + if (blockUrl.getIfPresent(CACHE_NOTICE + url) != null) { + log.warn("地址:{} 推送失败超过阈值", url); + blockUrl.put(CACHE_NOTICE + url, 0); + } + } else { + blockUrl.put(url, failure); + } + } + + /** 移除成功记录 */ + private void removeSuccess(String url) { + if (blockUrl.getIfPresent(url) != null) { + blockUrl.invalidate(url); + blockUrl.invalidate(CACHE_NOTICE + url); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/UPProcessor.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/UPProcessor.java new file mode 100644 index 0000000..85a3042 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/UPProcessor.java @@ -0,0 +1,77 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.push; + +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.persistence.base.BaseUPRequest; +import java.util.List; + +/** + * 上行消息处理器接口,用于插件化扩展 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +public interface UPProcessor { + + /** + * 处理器名称 + * + * @return 处理器名称 + */ + String getName(); + + /** + * 处理器描述 + * + * @return 处理器描述 + */ + String getDescription(); + + /** + * 处理器优先级,数字越小优先级越高 + * + * @return 优先级 + */ + int getOrder(); + + /** + * 推送前处理 + * + * @param upRequests 上行请求列表 + * @return 处理后的请求列表 + */ + default List beforePush(List upRequests) { + return upRequests; + } + + /** + * 推送后处理(带推送结果) + * + * @param upRequests 上行请求列表 + * @param pushResults 推送结果列表 + */ + default void afterPush(List upRequests, List pushResults) { + // 默认空实现 + } + + /** + * 是否支持处理指定类型的请求 + * + * @param request 请求对象 + * @return 是否支持 + */ + default boolean supports(T request) { + return true; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/UPProcessorManager.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/UPProcessorManager.java new file mode 100644 index 0000000..c4b27b5 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/UPProcessorManager.java @@ -0,0 +1,134 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.push; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.persistence.base.BaseUPRequest; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 上行消息处理器管理器 + * + * @version 1.0 @Author Aleo + * @since 2025/1/9 + */ +@Slf4j +@Component +public class UPProcessorManager { + + /** + * 获取所有处理器并按优先级排序 + * + * @param 请求类型 + * @return 处理器列表 + */ + @SuppressWarnings("unchecked") + public List> getProcessors() { + Map processorMap = SpringUtil.getBeansOfType(UPProcessor.class); + if (processorMap.isEmpty()) { + return List.of(); + } + + return processorMap.values().stream() + .map(processor -> (UPProcessor) processor) + .sorted(Comparator.comparingInt(UPProcessor::getOrder)) + .collect(Collectors.toList()); + } + + /** + * 执行推送前处理 + * + * @param upRequests 上行请求列表 + * @param 请求类型 + * @return 处理后的请求列表 + */ + public List executeBeforePush(List upRequests) { + if (CollectionUtil.isEmpty(upRequests)) { + return upRequests; + } + + List> processors = getProcessors(); + if (processors.isEmpty()) { + return upRequests; + } + + List processedRequests = upRequests; + for (UPProcessor processor : processors) { + try { + // 过滤出支持的请求 + List supportedRequests = + processedRequests.stream().filter(processor::supports).collect(Collectors.toList()); + + if (!supportedRequests.isEmpty()) { + log.debug( + "[UP处理器][推送前] 执行处理器: {}, 处理 {} 条消息", processor.getName(), supportedRequests.size()); + + List result = processor.beforePush(supportedRequests); + if (result != null) { + processedRequests = result; + } + } + } catch (Exception e) { + log.error("[UP处理器][推送前] 处理器 {} 执行异常", processor.getName(), e); + } + } + + return processedRequests; + } + + /** + * 执行推送后处理(带推送结果) + * + * @param upRequests 上行请求列表 + * @param pushResults 推送结果列表 + * @param 请求类型 + */ + public void executeAfterPush( + List upRequests, List pushResults) { + if (CollectionUtil.isEmpty(upRequests)) { + return; + } + + List> processors = getProcessors(); + if (processors.isEmpty()) { + return; + } + + for (UPProcessor processor : processors) { + try { + // 过滤出支持的请求 + List supportedRequests = + upRequests.stream().filter(processor::supports).collect(Collectors.toList()); + + if (!supportedRequests.isEmpty()) { + log.debug( + "[UP处理器][推送后] 执行处理器: {}, 处理 {} 条消息, 结果数量: {}", + processor.getName(), + supportedRequests.size(), + pushResults != null ? pushResults.size() : 0); + + processor.afterPush(supportedRequests, pushResults); + } + } catch (Exception e) { + log.error("[UP处理器][推送后] 处理器 {} 执行异常", processor.getName(), e); + } + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/processor/PushRetryProcessor.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/processor/PushRetryProcessor.java new file mode 100644 index 0000000..873d745 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/processor/PushRetryProcessor.java @@ -0,0 +1,674 @@ +package cn.universal.dm.device.service.push.processor; + +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.dm.device.service.push.UPProcessor; +import cn.universal.persistence.base.BaseUPRequest; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 推送重试处理器 基于UPProcessor架构实现推送重试功能 + * + * @author Aleo + * @version 2.0 + * @since 2025/1/9 + */ +@Slf4j +@Component +public class PushRetryProcessor implements UPProcessor { + + @Autowired private StringRedisTemplate redisTemplate; + + @Autowired private PushStatisticsProcessor pushStatisticsProcessor; + + // 配置参数 + @Value("${push.retry.redis.timeout:5}") + private int redisTimeoutSeconds; + + // Redis Key 前缀 + private static final String RETRY_QUEUE_KEY_PREFIX = "push:retry:queue:"; + private static final String RETRY_COUNT_KEY_PREFIX = "push:retry:count:"; + private static final String FAILED_PUSH_KEY_PREFIX = "push:failed:"; + private static final DateTimeFormatter DATETIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + // 默认配置 + private static final int DEFAULT_MAX_RETRY_COUNT = 3; + private static final int DEFAULT_RETRY_INTERVAL_MINUTES = 5; + + // 专用重试线程池 - 使用自定义线程池避免ForkJoinPool耗尽 + // 直接使用默认值初始化,简单可靠 + private final java.util.concurrent.ExecutorService retryExecutor = + java.util.concurrent.Executors.newFixedThreadPool( + 10, // 默认线程池大小 + r -> { + Thread t = new Thread(r, "push-retry-" + System.currentTimeMillis()); + t.setDaemon(true); + return t; + }); + + @Override + public String getName() { + return "PushRetryProcessor"; + } + + @Override + public String getDescription() { + return "推送重试处理器"; + } + + @Override + public int getOrder() { + return 200; // 较低优先级,在统计后执行 + } + + /** 获取线程池状态信息 */ + public String getThreadPoolStatus() { + if (retryExecutor instanceof java.util.concurrent.ThreadPoolExecutor) { + java.util.concurrent.ThreadPoolExecutor executor = + (java.util.concurrent.ThreadPoolExecutor) retryExecutor; + return String.format( + "PushRetry线程池状态: 核心线程数=%d, 最大线程数=%d, 当前线程数=%d, 活跃线程数=%d, 队列大小=%d, 已完成任务数=%d", + executor.getCorePoolSize(), + executor.getMaximumPoolSize(), + executor.getPoolSize(), + executor.getActiveCount(), + executor.getQueue().size(), + executor.getCompletedTaskCount()); + } + return "PushRetry线程池状态: 无法获取详细信息"; + } + + /** 应用关闭时清理资源 */ + @PreDestroy + public void destroy() { + log.info("[推送重试] 开始关闭PushRetryProcessor..."); + if (retryExecutor != null && !retryExecutor.isShutdown()) { + retryExecutor.shutdown(); + try { + if (!retryExecutor.awaitTermination(30, TimeUnit.SECONDS)) { + log.warn("[推送重试] 线程池未能在30秒内正常关闭,强制关闭"); + retryExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + log.warn("[推送重试] 等待线程池关闭时被中断"); + retryExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + log.info("[推送重试] PushRetryProcessor已关闭"); + } + + @Override + public void afterPush(List upRequests, List pushResults) { + if (pushResults == null || pushResults.isEmpty()) { + return; + } + + // 处理失败推送 + for (IoTPushResult result : pushResults) { + if (!result.isOk()) { + addToRetryQueue(result); + recordFailedPush(result); + log.warn( + "[推送重试] 发现失败推送,已加入重试队列: deviceId={}, channel={}, error={}", + result.getDeviceId(), + result.getChannel(), + result.getErrorMessage()); + } + } + } + + /** 添加失败推送到重试队列 */ + public void addToRetryQueue(IoTPushResult result) { + if (result == null || result.isOk()) { + return; + } + + try { + String retryKey = generateRetryKey(result); + String retryData = buildRetryData(result); + + // 检查重试次数 + String countKey = RETRY_COUNT_KEY_PREFIX + result.getDeviceId() + ":" + result.getChannel(); + Integer currentRetryCount = getRetryCount(countKey); + + if (currentRetryCount >= DEFAULT_MAX_RETRY_COUNT) { + log.warn( + "[推送重试] 设备 {} 渠道 {} 已达到最大重试次数 {}", + result.getDeviceId(), + result.getChannel(), + DEFAULT_MAX_RETRY_COUNT); + return; + } + + // 使用异步处理,避免阻塞ForkJoinPool + CompletableFuture addToQueueFuture = + CompletableFuture.runAsync( + () -> { + try { + // 添加到重试队列 + redisTemplate.opsForList().rightPush(retryKey, retryData); + redisTemplate.expire(retryKey, java.time.Duration.ofDays(1)); + log.debug("[推送重试] Redis添加队列成功: deviceId={}, channel={}", + result.getDeviceId(), result.getChannel()); + } catch (Exception e) { + log.error("[推送重试] Redis添加队列失败: deviceId={}, channel={}", + result.getDeviceId(), result.getChannel(), e); + } + }, + retryExecutor); + + CompletableFuture incrementCountFuture = + CompletableFuture.runAsync( + () -> { + try { + // 增加重试次数 + redisTemplate.opsForValue().increment(countKey); + redisTemplate.expire(countKey, java.time.Duration.ofDays(1)); + log.debug("[推送重试] Redis增加计数成功: deviceId={}, channel={}", + result.getDeviceId(), result.getChannel()); + } catch (Exception e) { + log.error("[推送重试] Redis增加计数失败: deviceId={}, channel={}", + result.getDeviceId(), result.getChannel(), e); + } + }, + retryExecutor); + + // 异步处理结果,不阻塞当前线程 + CompletableFuture.allOf(addToQueueFuture, incrementCountFuture) + .thenRun(() -> { + log.info( + "[推送重试] 添加失败推送到重试队列成功: deviceId={}, channel={}, retryCount={}", + result.getDeviceId(), + result.getChannel(), + currentRetryCount + 1); + }) + .exceptionally(throwable -> { + log.error( + "[推送重试] 添加失败推送到重试队列异常: deviceId={}, channel={}", + result.getDeviceId(), + result.getChannel(), + throwable); + return null; + }); + } catch (Exception e) { + log.error( + "[推送重试] 添加失败推送到重试队列失败: deviceId={}, channel={}", + result.getDeviceId(), + result.getChannel(), + e); + } + } + + /** 定时重试任务 每5分钟执行一次 */ + @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) + public void retryFailedPushes() { + log.debug("[推送重试] 开始执行重试任务"); + + try { + // 从重试队列获取所有待重试的推送 + List retryPushes = getRetryQueuePushes(); + + for (IoTPushResult retryPush : retryPushes) { + CompletableFuture.runAsync( + () -> { + try { + retryPush(retryPush); + } catch (Exception e) { + log.error( + "[推送重试] 重试推送失败: deviceId={}, channel={}", + retryPush.getDeviceId(), + retryPush.getChannel(), + e); + } + }, + retryExecutor); + } + } catch (Exception e) { + log.error("[推送重试] 执行重试任务失败", e); + } + } + + /** 重试单个推送 */ + private void retryPush(IoTPushResult failedPush) { + String countKey = + RETRY_COUNT_KEY_PREFIX + failedPush.getDeviceId() + ":" + failedPush.getChannel(); + Integer currentRetryCount = getRetryCount(countKey); + + if (currentRetryCount >= DEFAULT_MAX_RETRY_COUNT) { + log.warn( + "[推送重试] 设备 {} 渠道 {} 已达到最大重试次数,跳过重试,并清理重试队列和计数", + failedPush.getDeviceId(), + failedPush.getChannel()); + // 直接删除重试计数和队列 + String retryKey = generateRetryKey(failedPush); + redisTemplate.delete(java.util.Arrays.asList(countKey, retryKey)); + return; + } + + log.info( + "[推送重试] 开始重试推送: deviceId={}, channel={}, retryCount={}", + failedPush.getDeviceId(), + failedPush.getChannel(), + currentRetryCount + 1); + + try { + // 构建重试的推送结果 + IoTPushResult retryResult = + IoTPushResult.retry(failedPush, "Retry attempt " + (currentRetryCount + 1)); + retryResult.setMaxRetryCount(DEFAULT_MAX_RETRY_COUNT); + + // 记录重试统计 + pushStatisticsProcessor.recordPushResult(retryResult); + + // 执行实际推送(这里需要根据你的推送策略管理器调用) + boolean pushSuccess = executeActualPush(retryResult); + + if (pushSuccess) { + // 重试成功,清理重试队列和计数 + log.info( + "[推送重试] 重试推送成功: deviceId={}, channel={}", + failedPush.getDeviceId(), + failedPush.getChannel()); + + // 清理重试数据 + String retryKey = generateRetryKey(failedPush); + redisTemplate.delete(java.util.Arrays.asList(countKey, retryKey)); + + // 从重试队列中移除 + removeFromRetryQueue(failedPush); + + } else { + // 重试失败,继续保留在队列中 + log.warn( + "[推送重试] 重试推送失败: deviceId={}, channel={}", + failedPush.getDeviceId(), + failedPush.getChannel()); + } + + } catch (Exception e) { + log.error( + "[推送重试] 重试推送异常: deviceId={}, channel={}", + failedPush.getDeviceId(), + failedPush.getChannel(), + e); + + // 如果重试失败,继续添加到重试队列 + if (currentRetryCount < DEFAULT_MAX_RETRY_COUNT) { + addToRetryQueue(failedPush); + } + } + } + + /** 执行实际推送(需要根据你的推送策略管理器实现) */ + private boolean executeActualPush(IoTPushResult retryResult) { + try { + // TODO: 这里需要调用你的推送策略管理器 + // 例如:pushStrategyManager.executePush(originalRequest, config); + // 由于原始请求信息可能已经丢失,这里提供一个框架 + // 实际实现时需要根据业务需求调整 + + log.debug( + "[推送重试] 执行实际推送: deviceId={}, channel={}", + retryResult.getDeviceId(), + retryResult.getChannel()); + + // 临时返回false,表示需要你根据实际情况实现 + return false; + } catch (Exception e) { + log.error( + "[推送重试] 执行实际推送失败: deviceId={}, channel={}", + retryResult.getDeviceId(), + retryResult.getChannel(), + e); + return false; + } + } + + /** 从重试队列中移除 */ + private void removeFromRetryQueue(IoTPushResult result) { + try { + String retryKey = generateRetryKey(result); + String retryData = buildRetryData(result); + + // 从队列中移除指定的数据 + redisTemplate.opsForList().remove(retryKey, 1, retryData); + } catch (Exception e) { + log.error( + "[推送重试] 从重试队列移除失败: deviceId={}, channel={}", + result.getDeviceId(), + result.getChannel(), + e); + } + } + + /** 获取重试队列中的所有推送 */ + private List getRetryQueuePushes() { + List retryPushes = new java.util.ArrayList<>(); + + try { + // 获取所有重试队列的key + String retryQueuePattern = RETRY_QUEUE_KEY_PREFIX + "*"; + + // 使用SCAN命令获取所有重试队列 + org.springframework.data.redis.core.ScanOptions options = + org.springframework.data.redis.core.ScanOptions.scanOptions() + .match(retryQueuePattern) + .count(100) + .build(); + + org.springframework.data.redis.core.Cursor cursor = redisTemplate.scan(options); + + while (cursor.hasNext()) { + String retryKey = cursor.next(); + + // 从队列中获取数据 + List retryDataList = redisTemplate.opsForList().range(retryKey, 0, -1); + + if (retryDataList != null) { + for (String retryData : retryDataList) { + try { + IoTPushResult result = parseRetryData(retryData); + if (result != null) { + retryPushes.add(result); + } + } catch (Exception e) { + log.error("[推送重试] 解析重试数据失败: {}", retryData, e); + } + } + } + } + } catch (Exception e) { + log.error("[推送重试] 获取重试队列失败", e); + } + + return retryPushes; + } + + /** 解析重试数据 */ + private IoTPushResult parseRetryData(String retryData) { + try { + String[] parts = retryData.split("\\|"); + if (parts.length >= 9) { + return IoTPushResult.builder() + .deviceId(parts[0]) + .productKey(parts[1]) + .channel(parts[2]) + .platform(parts[3].isEmpty() ? null : parts[3]) + .messageContent(parts[4]) + .errorMessage(parts[5]) + .errorCode(parts[6]) + .pushTime(LocalDateTime.parse(parts[7], DATETIME_FORMATTER)) + .retryCount(Integer.valueOf(parts[8])) + .ok(false) + .status(IoTPushResult.PushStatus.FAILED) + .build(); + } + } catch (Exception e) { + log.error("[推送重试] 解析重试数据失败: {}", retryData, e); + } + return null; + } + + /** 手动重试指定设备的推送 */ + public void manualRetry(String deviceId, String channel, String productKey) { + String countKey = RETRY_COUNT_KEY_PREFIX + deviceId + ":" + channel; + Integer currentRetryCount = getRetryCount(countKey); + + if (currentRetryCount >= DEFAULT_MAX_RETRY_COUNT) { + log.warn("[推送重试] 设备 {} 渠道 {} 已达到最大重试次数", deviceId, channel); + return; + } + + // 创建重试推送结果 + IoTPushResult retryResult = + IoTPushResult.builder() + .deviceId(deviceId) + .productKey(productKey) + .channel(channel) + .pushTime(LocalDateTime.now()) + .ok(false) + .status(IoTPushResult.PushStatus.RETRYING) + .retryCount(currentRetryCount + 1) + .maxRetryCount(DEFAULT_MAX_RETRY_COUNT) + .errorMessage("Manual retry") + .build(); + + // 记录重试统计 + pushStatisticsProcessor.recordPushResult(retryResult); + + log.info( + "[推送重试] 手动重试推送: deviceId={}, channel={}, retryCount={}", + deviceId, + channel, + currentRetryCount + 1); + } + + /** 获取重试次数 - 同步方法,直接调用Redis */ + private Integer getRetryCount(String countKey) { + try { + // 直接调用Redis,避免异步复杂性 + String count = redisTemplate.opsForValue().get(countKey); + return count != null ? Integer.valueOf(count) : 0; + } catch (Exception e) { + log.error("[推送重试] 获取重试次数失败,key={}", countKey, e); + return 0; + } + } + + /** 生成重试队列Key */ + private String generateRetryKey(IoTPushResult result) { + return RETRY_QUEUE_KEY_PREFIX + result.getDeviceId() + ":" + result.getChannel(); + } + + /** 构建重试数据 */ + private String buildRetryData(IoTPushResult result) { + return String.format( + "%s|%s|%s|%s|%s|%s|%s|%s|%s", + result.getDeviceId(), + result.getProductKey(), + result.getChannel(), + result.getPlatform() != null ? result.getPlatform() : "", + result.getMessageContent(), + result.getErrorMessage(), + result.getErrorCode(), + result.getPushTime().format(DATETIME_FORMATTER), + result.getRetryCount() != null ? result.getRetryCount() : 0); + } + + /** 获取失败推送列表 */ + public List getFailedPushes(LocalDate date) { + try { + String dateKey = date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + String failedKey = FAILED_PUSH_KEY_PREFIX + dateKey; + + // 使用CompletableFuture添加超时处理 + CompletableFuture> future = + CompletableFuture.supplyAsync( + () -> { + return redisTemplate.opsForList().range(failedKey, 0, -1); + }, + retryExecutor); + + List failedDataList = future.get(redisTimeoutSeconds, TimeUnit.SECONDS); + List failedPushes = new java.util.ArrayList<>(); + + if (failedDataList != null) { + for (String failedData : failedDataList) { + try { + String[] parts = failedData.split("\\|"); + if (parts.length >= 8) { + IoTPushResult result = + IoTPushResult.builder() + .deviceId(parts[0]) + .productKey(parts[1]) + .channel(parts[2]) + .platform(parts[3].isEmpty() ? null : parts[3]) + .messageContent(parts[4]) + .errorMessage(parts[5]) + .errorCode(parts[6]) + .pushTime(LocalDateTime.parse(parts[7], DATETIME_FORMATTER)) + .ok(false) + .status(IoTPushResult.PushStatus.FAILED) + .build(); + failedPushes.add(result); + } + } catch (Exception e) { + log.error("[推送重试] 解析失败推送数据失败: {}", failedData, e); + } + } + } + + return failedPushes; + } catch (TimeoutException e) { + log.warn("[推送重试] 获取失败推送列表超时,date={}", date); + return new java.util.ArrayList<>(); + } catch (Exception e) { + log.error("[推送重试] 获取失败推送列表失败,date={}", date, e); + return new java.util.ArrayList<>(); + } + } + + /** 记录失败推送 */ + public void recordFailedPush(IoTPushResult result) { + if (result == null || result.isOk()) { + return; + } + + try { + String dateKey = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + String failedKey = FAILED_PUSH_KEY_PREFIX + dateKey; + String failedData = + String.format( + "%s|%s|%s|%s|%s|%s|%s|%s", + result.getDeviceId(), + result.getProductKey(), + result.getChannel(), + result.getPlatform() != null ? result.getPlatform() : "", + result.getMessageContent(), + result.getErrorMessage(), + result.getErrorCode(), + result.getPushTime().format(DATETIME_FORMATTER)); + + // 使用异步处理,避免阻塞ForkJoinPool + CompletableFuture future = + CompletableFuture.runAsync( + () -> { + try { + redisTemplate.opsForList().rightPush(failedKey, failedData); + redisTemplate.expire(failedKey, java.time.Duration.ofDays(7)); // 失败记录保留7天 + log.debug("[推送重试] 记录失败推送成功: deviceId={}, channel={}", + result.getDeviceId(), result.getChannel()); + } catch (Exception e) { + log.error("[推送重试] 记录失败推送Redis操作失败: deviceId={}, channel={}", + result.getDeviceId(), result.getChannel(), e); + } + }, + retryExecutor); + + // 异步处理结果,不阻塞当前线程 + future.thenRun(() -> { + log.debug("[推送重试] 记录失败推送完成: deviceId={}, channel={}", + result.getDeviceId(), result.getChannel()); + }).exceptionally(throwable -> { + log.error("[推送重试] 记录失败推送异常: deviceId={}, channel={}", + result.getDeviceId(), result.getChannel(), throwable); + return null; + }); + } catch (Exception e) { + log.error("[推送重试] 记录失败推送失败: deviceId={}, channel={}", + result.getDeviceId(), result.getChannel(), e); + } + } + + /** 清理过期的重试记录 每天凌晨2点执行 */ + @Scheduled(cron = "0 0 2 * * ?") + public void cleanupExpiredRetryRecords() { + log.info("[推送重试] 开始清理过期的重试记录"); + + try { + // 清理7天前的重试记录 + LocalDate cleanupDate = LocalDate.now().minusDays(7); + String dateKey = cleanupDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + + // 清理失败推送记录 + String failedKey = FAILED_PUSH_KEY_PREFIX + dateKey; + redisTemplate.delete(failedKey); + + // 清理重试队列 + String retryQueuePattern = RETRY_QUEUE_KEY_PREFIX + "*"; + deleteKeysByPattern(retryQueuePattern); + + // 清理重试计数 + String retryCountPattern = RETRY_COUNT_KEY_PREFIX + "*"; + deleteKeysByPattern(retryCountPattern); + + log.info("[推送重试] 清理过期的重试记录完成"); + } catch (Exception e) { + log.error("[推送重试] 清理过期的重试记录失败", e); + } + } + + /** 获取设备的重试统计 */ + public Integer getRetryCount(String deviceId, String channel) { + String countKey = RETRY_COUNT_KEY_PREFIX + deviceId + ":" + channel; + return getRetryCount(countKey); + } + + /** 重置设备的重试计数 */ + public void resetRetryCount(String deviceId, String channel) { + String countKey = RETRY_COUNT_KEY_PREFIX + deviceId + ":" + channel; + redisTemplate.delete(countKey); + log.info("[推送重试] 重置设备重试计数: deviceId={}, channel={}", deviceId, channel); + } + + /** 检查是否可以进行重试 */ + public boolean canRetry(String deviceId, String channel) { + Integer retryCount = getRetryCount(deviceId, channel); + return retryCount < DEFAULT_MAX_RETRY_COUNT; + } + + /** + * 使用SCAN命令删除匹配模式的键 + * + * @param pattern 键模式 + */ + private void deleteKeysByPattern(String pattern) { + try { + long cursor = 0; + do { + org.springframework.data.redis.core.ScanOptions options = + org.springframework.data.redis.core.ScanOptions.scanOptions() + .match(pattern) + .count(100) + .build(); + + org.springframework.data.redis.core.Cursor cursorResult = + redisTemplate.scan(options); + + while (cursorResult.hasNext()) { + String key = cursorResult.next(); + redisTemplate.delete(key); + } + + cursor = cursorResult.getCursorId(); + } while (cursor != 0); + } catch (Exception e) { + log.error("[推送重试] 删除匹配模式的键失败: pattern={}", pattern, e); + } + } + +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/processor/PushStatisticsProcessor.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/processor/PushStatisticsProcessor.java new file mode 100644 index 0000000..3386517 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/processor/PushStatisticsProcessor.java @@ -0,0 +1,696 @@ +package cn.universal.dm.device.service.push.processor; + +import cn.universal.dm.device.entity.IoTPushResult; +import cn.universal.dm.device.service.push.UPProcessor; +import cn.universal.persistence.base.BaseUPRequest; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 推送统计处理器 基于UPProcessor架构实现推送统计功能 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +@Slf4j +@Component +public class PushStatisticsProcessor implements UPProcessor { + + @Autowired private StringRedisTemplate redisTemplate; + + // 本地原子计数器 - 使用ConcurrentHashMap保证线程安全 + private final Map localCounters = new ConcurrentHashMap<>(); + + // 本地平台统计缓存 + private final Map> localPlatformStats = new ConcurrentHashMap<>(); + + // 本地产品统计缓存 + private final Map> localProductStats = new ConcurrentHashMap<>(); + + // 定时刷新间隔(毫秒) + private static final long FLUSH_INTERVAL = 300000; // 5分钟 = 300000毫秒 + + // 上次刷新时间戳 + private volatile long lastFlushTime = System.currentTimeMillis(); + + private static final String PUSH_PRODUCT_KEY_PREFIX = "push:product:"; + private static final String PUSH_PLATFORM_KEY_PREFIX = "push:platform:"; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + @Override + public String getName() { + return "PushStatisticsProcessor"; + } + + @Override + public String getDescription() { + return "推送统计处理器"; + } + + @Override + public int getOrder() { + return 100; // 较低优先级,在推送后执行 + } + + @Override + public void afterPush(List upRequests, List pushResults) { + if (upRequests == null || upRequests.isEmpty()) { + return; + } + // 异步处理统计 + CompletableFuture.runAsync( + () -> { + try { + // 统计所有推送请求的总数(无论是否有推送结果) + // 包括:需要推送的消息、无需推送的消息、推送失败的消息等 + for (BaseUPRequest request : upRequests) { + recordTotalMessageFromRequest(request); + } + + // 如果有推送结果,统计实际推送数 + // 只统计实际执行了推送操作的消息 + if (pushResults != null && !pushResults.isEmpty()) { + for (IoTPushResult result : pushResults) { + if (result != null) { + recordPushMessage(result); + } + } + } + + log.debug("[推送统计] 完成 {} 条推送请求的统计记录", upRequests.size()); + } catch (Exception e) { + log.error("[推送统计] 统计处理失败", e); + } + }); + } + + /** 记录推送结果统计 */ + public void recordPushResult(IoTPushResult result) { + if (result == null) { + log.debug("[推送统计] 跳过空结果统计"); + return; + } + + // 检查必要字段是否为空 + if (result.getProductKey() == null || result.getChannel() == null) { + log.warn( + "[推送统计] 跳过无效结果统计: productKey={}, channel={}", + result.getProductKey(), + result.getChannel()); + return; + } + + CompletableFuture.runAsync( + () -> { + try { + String productKey = result.getProductKey(); + String channel = result.getChannel(); + String dateKey = LocalDate.now().format(DATE_FORMATTER); + + // 记录到本地缓存 + recordPlatformStatsToLocal(result, dateKey); + recordProductStatsToLocal(result, dateKey, productKey); + + // 记录消息统计 + incrementMessageStats( + channel, + productKey, + result.isOk(), + result.getResponseTime() == null ? 0L : result.getResponseTime()); + + log.debug( + "[推送统计] 记录推送结果: productKey={}, channel={}, success={}, responseTime={}ms", + productKey, + channel, + result.isOk(), + result.getResponseTime()); + + } catch (Exception e) { + log.error("[推送统计] 记录推送结果失败: {}", result, e); + } + }); + } + + /** 记录消息总数统计(无论结果是否为空) */ + private void recordTotalMessage(IoTPushResult result) { + CompletableFuture.runAsync( + () -> { + try { + String productKey = result != null ? result.getProductKey() : "unknown"; + String channel = result != null ? result.getChannel() : "unknown"; + String dateKey = LocalDate.now().format(DATE_FORMATTER); + + // 记录到本地缓存 - 总数统计 + recordPlatformStatsToLocalForTotal(result, dateKey); + recordProductStatsToLocalForTotal(result, dateKey, productKey); + + log.debug("[推送统计] 记录消息总数: productKey={}, channel={}", productKey, channel); + + } catch (Exception e) { + log.error("[推送统计] 记录消息总数失败: {}", result, e); + } + }); + } + + /** 记录推送消息数统计(仅非空结果) */ + private void recordPushMessage(IoTPushResult result) { + if (result == null) { + return; + } + + CompletableFuture.runAsync( + () -> { + try { + String productKey = result.getProductKey(); + String channel = result.getChannel(); + String dateKey = LocalDate.now().format(DATE_FORMATTER); + + // 记录到本地缓存 - 推送数统计 + // 只统计实际执行了推送操作的消息 + recordPlatformStatsToLocalForPush(result, dateKey); + recordProductStatsToLocalForPush(result, dateKey, productKey); + + log.debug( + "[推送统计] 记录推送消息数: productKey={}, channel={}, success={}", + productKey, + channel, + result.isOk()); + + } catch (Exception e) { + log.error("[推送统计] 记录推送消息数失败: {}", result, e); + } + }); + } + + /** 记录消息总数统计(基于请求) */ + private void recordTotalMessageFromRequest(BaseUPRequest request) { + CompletableFuture.runAsync( + () -> { + try { + String productKey = request.getProductKey(); + String channel = getChannelFromRequest(request); + String dateKey = LocalDate.now().format(DATE_FORMATTER); + + // 记录到本地缓存 - 总数统计 + recordPlatformStatsToLocalForTotalFromRequest(request, dateKey); + recordProductStatsToLocalForTotalFromRequest(request, dateKey, productKey); + + log.debug("[推送统计] 记录消息总数: productKey={}, channel={}", productKey, channel); + + } catch (Exception e) { + log.error("[推送统计] 记录消息总数失败: {}", request, e); + } + }); + } + + /** 从请求中获取channel信息 */ + private String getChannelFromRequest(BaseUPRequest request) { + // 从设备DTO中获取thirdPlatform作为channel + if (request.getIoTDeviceDTO() != null && request.getIoTDeviceDTO().getThirdPlatform() != null) { + return request.getIoTDeviceDTO().getThirdPlatform().toLowerCase(); + } + return "unknown"; + } + + /** 从请求中获取platform信息 */ + private String getPlatformFromRequest(BaseUPRequest request) { + // 从设备DTO中获取thirdPlatform作为platform + if (request.getIoTDeviceDTO() != null && request.getIoTDeviceDTO().getThirdPlatform() != null) { + return request.getIoTDeviceDTO().getThirdPlatform(); + } + return "unknown"; + } + + /** 记录平台统计到本地缓存 */ + private void recordPlatformStatsToLocal(IoTPushResult result, String dateKey) { + String platform = result.getPlatform(); + if (platform == null || platform.isEmpty()) { + // 当平台信息为空时,使用默认值而不是跳过统计 + platform = "unknown"; + log.info("[推送统计] 推送结果中缺少平台信息,使用默认值: {}", platform); + } + + String platformKey = dateKey + ":" + platform.toLowerCase(); + Map platformStats = + localPlatformStats.computeIfAbsent(platformKey, k -> new ConcurrentHashMap<>()); + + // 记录总数 + platformStats.computeIfAbsent("total_count", k -> new AtomicLong(0)).incrementAndGet(); + + if (result.isOk()) { + platformStats.computeIfAbsent("success_count", k -> new AtomicLong(0)).incrementAndGet(); + } else { + platformStats.computeIfAbsent("failed_count", k -> new AtomicLong(0)).incrementAndGet(); + } + + // 记录重试次数 + if (result.getRetryCount() != null && result.getRetryCount() > 0) { + platformStats + .computeIfAbsent("retry_count", k -> new AtomicLong(0)) + .addAndGet(result.getRetryCount()); + } + } + + /** 记录产品统计到本地缓存 */ + private void recordProductStatsToLocal(IoTPushResult result, String dateKey, String productKey) { + String productStatsKey = dateKey + ":" + productKey; + Map productStats = + localProductStats.computeIfAbsent(productStatsKey, k -> new ConcurrentHashMap<>()); + + // 记录总数 + productStats.computeIfAbsent("total", k -> new AtomicLong(0)).incrementAndGet(); + + if (result.isOk()) { + productStats.computeIfAbsent("success", k -> new AtomicLong(0)).incrementAndGet(); + } else { + productStats.computeIfAbsent("failed", k -> new AtomicLong(0)).incrementAndGet(); + } + + // 记录重试次数 + if (result.getRetryCount() != null && result.getRetryCount() > 0) { + productStats + .computeIfAbsent("retry", k -> new AtomicLong(0)) + .addAndGet(result.getRetryCount()); + } + } + + /** 记录平台统计到本地缓存 - 总数统计 */ + private void recordPlatformStatsToLocalForTotal(IoTPushResult result, String dateKey) { + String platform = result != null ? result.getPlatform() : "unknown"; + if (platform == null || platform.isEmpty()) { + platform = "unknown"; + } + + String platformKey = dateKey + ":" + platform.toLowerCase(); + Map platformStats = + localPlatformStats.computeIfAbsent(platformKey, k -> new ConcurrentHashMap<>()); + + // 记录总数 + platformStats.computeIfAbsent("total_count", k -> new AtomicLong(0)).incrementAndGet(); + } + + /** 记录产品统计到本地缓存 - 总数统计 */ + private void recordProductStatsToLocalForTotal( + IoTPushResult result, String dateKey, String productKey) { + String productStatsKey = dateKey + ":" + productKey; + Map productStats = + localProductStats.computeIfAbsent(productStatsKey, k -> new ConcurrentHashMap<>()); + + // 记录总数 + productStats.computeIfAbsent("total", k -> new AtomicLong(0)).incrementAndGet(); + } + + /** 记录平台统计到本地缓存 - 总数统计(基于请求) */ + private void recordPlatformStatsToLocalForTotalFromRequest( + BaseUPRequest request, String dateKey) { + String platform = getPlatformFromRequest(request); + if (platform == null || platform.isEmpty()) { + platform = "unknown"; + } + + String platformKey = dateKey + ":" + platform.toLowerCase(); + Map platformStats = + localPlatformStats.computeIfAbsent(platformKey, k -> new ConcurrentHashMap<>()); + + // 记录总数 + platformStats.computeIfAbsent("total_count", k -> new AtomicLong(0)).incrementAndGet(); + } + + /** 记录产品统计到本地缓存 - 总数统计(基于请求) */ + private void recordProductStatsToLocalForTotalFromRequest( + BaseUPRequest request, String dateKey, String productKey) { + String productStatsKey = dateKey + ":" + productKey; + Map productStats = + localProductStats.computeIfAbsent(productStatsKey, k -> new ConcurrentHashMap<>()); + + // 记录总数 + productStats.computeIfAbsent("total", k -> new AtomicLong(0)).incrementAndGet(); + } + + /** 记录平台统计到本地缓存 - 推送数统计 */ + private void recordPlatformStatsToLocalForPush(IoTPushResult result, String dateKey) { + String platform = result.getPlatform(); + if (platform == null || platform.isEmpty()) { + // 当平台信息为空时,使用默认值而不是跳过统计 + platform = "unknown"; + log.info("[推送统计] 推送结果中缺少平台信息,使用默认值: {}", platform); + } + + String platformKey = dateKey + ":" + platform.toLowerCase(); + Map platformStats = + localPlatformStats.computeIfAbsent(platformKey, k -> new ConcurrentHashMap<>()); + + // 记录推送数 + platformStats.computeIfAbsent("push_count", k -> new AtomicLong(0)).incrementAndGet(); + + if (result.isOk()) { + platformStats.computeIfAbsent("success_count", k -> new AtomicLong(0)).incrementAndGet(); + } else { + platformStats.computeIfAbsent("failed_count", k -> new AtomicLong(0)).incrementAndGet(); + } + + // 记录重试次数 + if (result.getRetryCount() != null && result.getRetryCount() > 0) { + platformStats + .computeIfAbsent("retry_count", k -> new AtomicLong(0)) + .addAndGet(result.getRetryCount()); + } + } + + /** 记录产品统计到本地缓存 - 推送数统计 */ + private void recordProductStatsToLocalForPush( + IoTPushResult result, String dateKey, String productKey) { + String productStatsKey = dateKey + ":" + productKey; + Map productStats = + localProductStats.computeIfAbsent(productStatsKey, k -> new ConcurrentHashMap<>()); + + // 记录推送数 + productStats.computeIfAbsent("push", k -> new AtomicLong(0)).incrementAndGet(); + + if (result.isOk()) { + productStats.computeIfAbsent("success", k -> new AtomicLong(0)).incrementAndGet(); + } else { + productStats.computeIfAbsent("failed", k -> new AtomicLong(0)).incrementAndGet(); + } + + // 记录重试次数 + if (result.getRetryCount() != null && result.getRetryCount() > 0) { + productStats + .computeIfAbsent("retry", k -> new AtomicLong(0)) + .addAndGet(result.getRetryCount()); + } + } + + /** 原子自增消息统计 */ + private void incrementMessageStats( + String channel, String productKey, boolean isSuccess, long responseTime) { + LocalDate today = LocalDate.now(); + String dateKey = today.format(DATE_FORMATTER); + + // 消息总数 + String totalKey = + String.format( + "dashboard:metric:%s:all:%s:%s:message_total", + dateKey, channel != null ? channel : "all", productKey != null ? productKey : "all"); + incrementMetric(totalKey, 1); + + // 成功/失败消息数 + String successKey = + String.format( + "dashboard:metric:%s:all:%s:%s:message_%s", + dateKey, + channel != null ? channel : "all", + productKey != null ? productKey : "all", + isSuccess ? "success" : "failed"); + incrementMetric(successKey, 1); + + // 响应时间统计 + if (responseTime > 0) { + String responseTimeKey = + String.format( + "dashboard:metric:%s:all:%s:%s:response_time_total", + dateKey, channel != null ? channel : "all", productKey != null ? productKey : "all"); + String responseCountKey = + String.format( + "dashboard:metric:%s:all:%s:%s:response_count", + dateKey, channel != null ? channel : "all", productKey != null ? productKey : "all"); + + incrementMetric(responseTimeKey, responseTime); + incrementMetric(responseCountKey, 1); + } + } + + /** 原子自增统计 */ + private void incrementMetric(String metricKey, long increment) { + AtomicLong counter = localCounters.computeIfAbsent(metricKey, k -> new AtomicLong(0)); + counter.addAndGet(increment); + + log.debug( + "[推送统计] 本地计数器自增: key={}, increment={}, current={}", metricKey, increment, counter.get()); + } + + /** 定时刷新到Redis(每5分钟执行一次) */ + @Scheduled(fixedRate = FLUSH_INTERVAL) + public void flushToRedis() { + long currentTime = System.currentTimeMillis(); + if (currentTime - lastFlushTime < FLUSH_INTERVAL) { + return; // 避免重复执行 + } + + log.debug("[推送统计] 开始定时刷新到Redis"); + + try { + int flushedCount = 0; + + // 刷新平台统计 + flushedCount += flushPlatformStatsToRedis(); + + // 刷新产品统计 + flushedCount += flushProductStatsToRedis(); + + // 刷新消息统计 + flushedCount += flushMessageStatsToRedis(); + + lastFlushTime = currentTime; + log.info("[推送统计] 定时刷新完成,刷新了 {} 个指标", flushedCount); + + } catch (Exception e) { + log.error("[推送统计] 定时刷新到Redis失败", e); + } + } + + /** 刷新平台统计到Redis */ + private int flushPlatformStatsToRedis() { + int flushedCount = 0; + + for (Map.Entry> entry : localPlatformStats.entrySet()) { + String platformKey = entry.getKey(); + Map stats = entry.getValue(); + + String[] parts = platformKey.split(":"); + if (parts.length != 2) { + continue; + } + + String dateKey = parts[0]; + String platform = parts[1]; + String redisKey = PUSH_PLATFORM_KEY_PREFIX + dateKey + ":" + platform; + + try { + for (Map.Entry statEntry : stats.entrySet()) { + String field = statEntry.getKey(); + AtomicLong counter = statEntry.getValue(); + + long currentValue = counter.get(); + if (currentValue > 0) { + // 使用Redis HINCRBY原子操作 + redisTemplate.opsForHash().increment(redisKey, field, currentValue); + + // 重置本地计数器 + counter.addAndGet(-currentValue); + + flushedCount++; + log.debug( + "[推送统计] 刷新平台统计到Redis: key={}, field={}, value={}", redisKey, field, currentValue); + } + } + + // 设置过期时间(30天) + redisTemplate.expire(redisKey, java.time.Duration.ofDays(30)); + + } catch (Exception e) { + log.error("[推送统计] 刷新平台统计失败: {}", platformKey, e); + } + } + + return flushedCount; + } + + /** 刷新产品统计到Redis */ + private int flushProductStatsToRedis() { + int flushedCount = 0; + + for (Map.Entry> entry : localProductStats.entrySet()) { + String productStatsKey = entry.getKey(); + Map stats = entry.getValue(); + + String[] parts = productStatsKey.split(":"); + if (parts.length != 2) { + continue; + } + + String dateKey = parts[0]; + String productKey = parts[1]; + String redisKey = PUSH_PRODUCT_KEY_PREFIX + dateKey + ":" + productKey; + + try { + for (Map.Entry statEntry : stats.entrySet()) { + String field = statEntry.getKey(); + AtomicLong counter = statEntry.getValue(); + + long currentValue = counter.get(); + if (currentValue > 0) { + // 使用Redis HINCRBY原子操作 + redisTemplate.opsForHash().increment(redisKey, field, currentValue); + + // 重置本地计数器 + counter.addAndGet(-currentValue); + + flushedCount++; + log.debug( + "[推送统计] 刷新产品统计到Redis: key={}, field={}, value={}", redisKey, field, currentValue); + } + } + + // 设置过期时间(30天) + redisTemplate.expire(redisKey, java.time.Duration.ofDays(30)); + + } catch (Exception e) { + log.error("[推送统计] 刷新产品统计失败: {}", productStatsKey, e); + } + } + + return flushedCount; + } + + /** 刷新消息统计到Redis */ + private int flushMessageStatsToRedis() { + int flushedCount = 0; + + for (Map.Entry entry : localCounters.entrySet()) { + String metricKey = entry.getKey(); + AtomicLong counter = entry.getValue(); + + long currentValue = counter.get(); + if (currentValue > 0) { + // 使用Redis INCRBY原子操作 + Long newValue = redisTemplate.opsForValue().increment(metricKey, currentValue); + + // 重置本地计数器 + counter.addAndGet(-currentValue); + + // 设置过期时间(7天) + redisTemplate.expire(metricKey, java.time.Duration.ofDays(7)); + + flushedCount++; + log.debug( + "[推送统计] 刷新指标到Redis: key={}, value={}, newTotal={}", metricKey, currentValue, newValue); + } + } + + return flushedCount; + } + + /** 强制刷新到Redis */ + public void forceFlushToRedis() { + log.info("[推送统计] 强制刷新到Redis"); + flushToRedis(); + } + + /** 获取本地计数器状态 */ + public Map getLocalCounterStatus() { + Map status = new ConcurrentHashMap<>(); + for (Map.Entry entry : localCounters.entrySet()) { + status.put(entry.getKey(), entry.getValue().get()); + } + return status; + } + + /** 获取本地平台统计状态 */ + public Map> getLocalPlatformStatsStatus() { + Map> status = new ConcurrentHashMap<>(); + for (Map.Entry> entry : localPlatformStats.entrySet()) { + Map platformStatus = new ConcurrentHashMap<>(); + for (Map.Entry statEntry : entry.getValue().entrySet()) { + platformStatus.put(statEntry.getKey(), statEntry.getValue().get()); + } + status.put(entry.getKey(), platformStatus); + } + return status; + } + + /** 获取本地产品统计状态 */ + public Map> getLocalProductStatsStatus() { + Map> status = new ConcurrentHashMap<>(); + for (Map.Entry> entry : localProductStats.entrySet()) { + Map productStatus = new ConcurrentHashMap<>(); + for (Map.Entry statEntry : entry.getValue().entrySet()) { + productStatus.put(statEntry.getKey(), statEntry.getValue().get()); + } + status.put(entry.getKey(), productStatus); + } + return status; + } + + /** 清理本地计数器 */ + public void clearLocalCounters() { + localCounters.clear(); + localPlatformStats.clear(); + localProductStats.clear(); + log.info("[推送统计] 本地计数器已清理"); + } + + /** 获取统计管理器状态 */ + public Map getManagerStatus() { + Map status = new ConcurrentHashMap<>(); + status.put("localCounterSize", localCounters.size()); + status.put("localPlatformStatsSize", localPlatformStats.size()); + status.put("localProductStatsSize", localProductStats.size()); + status.put("lastFlushTime", lastFlushTime); + status.put("flushInterval", FLUSH_INTERVAL); + status.put("localCounters", getLocalCounterStatus()); + status.put("localPlatformStats", getLocalPlatformStatsStatus()); + status.put("localProductStats", getLocalProductStatsStatus()); + return status; + } + + /** 获取活跃的产品key列表 */ + public List getActiveProductKeys() { + List productKeys = new ArrayList<>(); + String dateKey = LocalDate.now().format(DATE_FORMATTER); + + // 从本地产品统计缓存中获取活跃的产品key + for (String key : localProductStats.keySet()) { + // key格式:dateKey:productKey + String[] parts = key.split(":"); + if (parts.length == 2 && parts[0].equals(dateKey)) { + String productKey = parts[1]; + productKeys.add(productKey); + } + } + + log.info("[推送统计] 从本地缓存获取到 {} 个活跃产品key: {}", productKeys.size(), productKeys); + return productKeys; + } + + /** 获取活跃的平台列表 */ + public List getActivePlatforms() { + List platforms = new ArrayList<>(); + String dateKey = LocalDate.now().format(DATE_FORMATTER); + + // 从本地平台统计缓存中获取活跃的平台 + for (String key : localPlatformStats.keySet()) { + // key格式:dateKey:platform + String[] parts = key.split(":"); + if (parts.length == 2 && parts[0].equals(dateKey)) { + String platform = parts[1]; + platforms.add(platform); + } + } + + log.debug("[推送统计] 从本地缓存获取到 {} 个活跃平台: {}", platforms.size(), platforms); + return platforms; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/AccountDisableTask.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/AccountDisableTask.java new file mode 100644 index 0000000..be3f7e4 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/AccountDisableTask.java @@ -0,0 +1,42 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.task; + +import cn.universal.persistence.mapper.IoTUserMapper; +import jakarta.annotation.Resource; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class AccountDisableTask { + + private final String KEY = "job:accountDisable"; + @Resource private StringRedisTemplate stringRedisTemplate; + @Resource private IoTUserMapper iotUserMapper; + + @Scheduled(cron = "0 20 1 * * ? ") + public void doAccountDisable() { + // 分布式锁,只执行1次,必须设置失效时间 + boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY, "0", 5, TimeUnit.MINUTES); + if (!flag) { + return; + } + log.info("开始执行账号禁用扫描"); + int rows = iotUserMapper.doAccountDisable(); + log.info("结束账号禁用扫描,本次禁用{}个账号", rows); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/IoTDeviceLogClearTask.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/IoTDeviceLogClearTask.java new file mode 100644 index 0000000..1711ba9 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/IoTDeviceLogClearTask.java @@ -0,0 +1,183 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.task; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.universal.common.constant.IoTConstant; +import cn.universal.dm.device.service.impl.IoTDeviceService; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import cn.universal.persistence.base.IoTDeviceLifeCycle; +import cn.universal.persistence.dto.IoTDeviceLogMaxStorageTime; +import cn.universal.persistence.mapper.IoTDeviceLogMapper; +import cn.universal.persistence.mapper.IoTDeviceLogMetadataMapper; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import jakarta.annotation.Resource; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 日志清除任务 + * + * @version 1.0 @Author Aleo + * @since 2023/2/15 + */ +@Component +@Slf4j +public class IoTDeviceLogClearTask { + + private final String KEY = "job:devLogClear"; + + @Value("${shard.log.table.number:1}") + private int logTableNumber; + + @Resource private StringRedisTemplate stringRedisTemplate; + + @Resource private IoTProductDeviceService iotProductDeviceService; + + @Resource private IoTDeviceMapper ioTDeviceMapper; + + @Resource(name = "ioTDeviceActionAfterService") + private IoTDeviceLifeCycle ioTDeviceLifeCycle; + + @Resource private IoTDeviceService iotDeviceService; + + @Resource private IoTDeviceLogMapper ioTDeviceLogMapper; + + @Resource private IoTDeviceLogMetadataMapper ioTDeviceLogMetadataMapper; + + /** 加载第三方平台的生命周期支持情况 */ + private Map> lifecycleSupportedConfig = new HashMap<>(); + + @Scheduled(cron = "0 10 1 * * ? ") + public void doLogDelWithOutCondition() { + Long t1 = System.currentTimeMillis(); + + try { + // 分布式锁,只执行1次,必须设置失效时间 + boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY, "0", 5, TimeUnit.MINUTES); + if (!flag) { + return; + } + log.info("开始执行日志清除扫描"); + + int days = IoTConstant.DEFAULT_LOG_MAX_STORAGE_TIME; + // 计算出超过过期的unix时间戳 + DateTime offsetDay = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, -days); + Long time = offsetDay.getTime() / 1000L; + // 此处tablesIndex=-1 是因为表名问题 + for (int tablesIndex = -1; tablesIndex < logTableNumber; tablesIndex++) { + // 开始删除iot_device_log表 + Long id = ioTDeviceLogMapper.queryLogIdByTime(tablesIndex, time); + int total = 0; + if (id != null && id > 0L) { + int count; + do { + count = ioTDeviceLogMapper.deleteLogById(tablesIndex, id); + total += count; + } while (count != 0); + log.info("iot_device_log 任务清除,表index={},条数={},节点时间={}", tablesIndex, total, offsetDay); + } + // 开始删除 iot_device_log_metadata 表 + Long metaId = ioTDeviceLogMetadataMapper.queryLogMetaIdByTime(tablesIndex, time); + int totalMeta = 0; + if (metaId != null && metaId > 0L) { + int metaCount; + do { + metaCount = ioTDeviceLogMetadataMapper.deleteLogMetaById(tablesIndex, metaId); + totalMeta += metaCount; + } while (metaCount != 0); + log.info( + "iot_device_log_metadata 任务清除,表index={},条数={},节点时间={}", + tablesIndex, + totalMeta, + offsetDay); + } + } + } catch (Exception e) { + log.error("日志清除任务执行异常={}", e); + } + Long t2 = System.currentTimeMillis(); + + log.info("结束执行日志清除扫描,耗时={}", (t2 - t1) / 1000L); + } + + /** 暂时停用 */ + // @Scheduled(cron = "0 30 2 * * ? ") + public void doLogDel() { + try { + // 分布式锁,只执行1次,必须设置失效时间 + boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY, "0", 5, TimeUnit.MINUTES); + if (!flag) { + return; + } + log.info("开始执行日志清除扫描"); + // 查询所有产品的列表 + List logMaxStorageList = + iotProductDeviceService.getProductLogMaxStorage(); + if (CollectionUtil.isNotEmpty(logMaxStorageList)) { + // 开始按照不同产品 + for (IoTDeviceLogMaxStorageTime s : logMaxStorageList) { + String productKey = s.getProductKey(); + int days = s.getDays(); + // 计算出超过过期的unix时间戳 + DateTime offsetDay = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, -days); + Long time = offsetDay.getTime() / 1000L; + // 此处tablesIndex=-1 是因为表名问题 + for (int tablesIndex = -1; tablesIndex < logTableNumber; tablesIndex++) { + // 开始删除iot_device_log表 + Long id = + ioTDeviceLogMapper.queryLogIdByProductKeyAndTime(tablesIndex, productKey, time); + if (id != null && id > 0L) { + int count = ioTDeviceLogMapper.deleteLogByTask(tablesIndex, productKey, id); + log.info( + "iot_device_log 任务清除,表index={},productKey={},条数={},节点时间={}", + tablesIndex, + productKey, + count, + offsetDay); + } + // 开始删除 iot_device_log_metadata 表 + Long metaId = + ioTDeviceLogMetadataMapper.queryLogMetaIdByProductKeyAndTime( + tablesIndex, productKey, time); + if (metaId != null && metaId > 0L) { + int metaCount = + ioTDeviceLogMetadataMapper.deleteLogMetaByTask(tablesIndex, productKey, metaId); + log.info( + "iot_device_log_metadata 任务清除,表index={},productKey={},条数={},节点时间={}", + tablesIndex, + productKey, + metaCount, + offsetDay); + } + } + } + } + } catch (Exception e) { + log.error("日志清除任务执行异常={}", e); + } + log.info("结束执行日志清除扫描"); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/IoTDeviceOffOnlineTask.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/IoTDeviceOffOnlineTask.java new file mode 100644 index 0000000..6ede07e --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/IoTDeviceOffOnlineTask.java @@ -0,0 +1,142 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.task; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant.DevLifeCycle; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import cn.universal.persistence.base.IoTDeviceLifeCycle; +import cn.universal.persistence.dto.IoTDeviceOfflineThesholdBO; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import jakarta.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 离线定制任务 + * + * @version 1.0 @Author Aleo + * @since 2023/2/9 + */ +@Component +@Slf4j +public class IoTDeviceOffOnlineTask { + + /** 离线定制任务redis-key */ + private final String KEY = "job:Offline:task:"; + + @Resource private StringRedisTemplate stringRedisTemplate; + + @Resource private IoTProductDeviceService iotProductDeviceService; + + @Resource private IoTDeviceMapper ioTDeviceMapper; + + @Resource(name = "ioTDeviceActionAfterService") + private IoTDeviceLifeCycle ioTDeviceLifeCycle; + + /** 加载第三方平台的生命周期支持情况 */ + private Map> lifecycleSupportedConfig = new HashMap<>(); + + @Autowired + void setLifecycleConfig( + @Value("${universal.iot.third.lifecycle.supported}") String lifecyclesupported) { + if (StrUtil.isBlank(lifecyclesupported)) { + return; + } + JSONObject obj = JSONUtil.parseObj(lifecyclesupported); + obj.keySet().stream() + .forEach( + s -> { + lifecycleSupportedConfig.put( + s, + obj.getJSONArray(s).stream().map(c -> c.toString()).collect(Collectors.toSet())); + }); + } + + /** 校验其他平台是否支持设备上线离线的生命周期 */ + public boolean thirdSupport(String platform, DevLifeCycle devLifeCycle) { + if (lifecycleSupportedConfig.containsKey(platform)) { + return lifecycleSupportedConfig.get(platform).contains(devLifeCycle.name()); + } + return false; + } + + @Scheduled(cron = "0 0/35 * * * ?") // 每35分钟执行一次 + public void doOfflineTask() { + try { + boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY, "0", 5, TimeUnit.MINUTES); + if (!flag) { + return; + } + log.debug("开始执行设备离线任务"); + // 查询所有产品的离线配置项 + List productOfflineThresholds = + iotProductDeviceService.getProductOfflineThresholds(); + if (CollectionUtil.isNotEmpty(productOfflineThresholds)) { + // 开始按照不同产品 + for (IoTDeviceOfflineThesholdBO s : productOfflineThresholds) { + if (s == null) { + log.warn("getOfflineThreshold null s={}", productOfflineThresholds); + continue; + } + long currentSeconds = DateUtil.currentSeconds(); + // 单位分钟,转换为秒 + long configThresholds = s.getOfflineThreshold() * 60; + // 差额 + long difference = currentSeconds - configThresholds; + // 最后一次通信时间(online_time)比差额还小,则离线 + List deviceIds = + ioTDeviceMapper.selectOfflineThresholdIotIds(s.getProductKey(), difference + ""); + if (CollectionUtil.isEmpty(deviceIds)) { + continue; + } + // 第三方不支持生命周期的,进入方法 + if (!thirdSupport(s.getPlatform(), DevLifeCycle.offline)) { + deviceIds.forEach( + deviceId -> { + // 执行离线事件 + log.info( + "productKey={} ,deviceId={} ,当前时间={},设定离线值(min)={},与当前差额={} 满足条件", + s.getProductKey(), + deviceId, + currentSeconds, + configThresholds, + difference); + ioTDeviceLifeCycle.offline(s.getProductKey(), deviceId); + }); + } else { + log.info("有第三方的配置,productKey={}", s.getProductKey()); + } + } + } + } catch (Exception e) { + log.error("设备生命周期执行异常={}", e); + } + + log.debug("结束执行设备离线任务"); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceAddParamIntercept.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceAddParamIntercept.java new file mode 100644 index 0000000..7d22324 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceAddParamIntercept.java @@ -0,0 +1,102 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.wrapper; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant.DownCmd; +import cn.universal.common.domain.R; +import cn.universal.common.exception.IoTErrorCode; +import cn.universal.common.exception.IoTException; +import cn.universal.core.message.DownRequest; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import cn.universal.persistence.base.IoTDownWrapper; +import cn.universal.persistence.entity.IoTProduct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 设备添加的预处理,参数校验,设备编号和产品校验 + * + * @version 1.0 @Author Aleo + * @since 2023/1/20 + */ +@Service("ioTDeviceAddParamIntercept") +@Slf4j +public class IoTDeviceAddParamIntercept implements IoTDownWrapper { + + private String CUSTOM_FIELD = "customField"; + + private String STOCK_DEVICE_CHECK = "stockDeviceCheck"; + private String RELATED_PRODUCT_KEY = "canNotRelateProductKey"; + + @Resource private IoTProductDeviceService iotProductDeviceService; + + @Override + public R beforeDownAction(IoTProduct product, Object data, DownRequest downRequest) { + // 参数拦截 + paramIntercept(product, data, downRequest); + + return null; + } + + /** + * 拦截自定义参数是否规范 + * + * @param product + * @param data + * @param downRequest + */ + private void paramIntercept(IoTProduct product, Object data, DownRequest downRequest) { + if (downRequest.isAllowInsert()) { + return; + } + if (product != null + && StrUtil.isNotBlank(product.getThirdConfiguration()) + && DownCmd.DEV_ADD.equals(downRequest.getCmd())) { + boolean flag = false; + String result = null; + try { + JSONObject jsonObject = JSONUtil.parseObj(product.getThirdConfiguration()); + JSONArray customFields = jsonObject.getJSONArray(CUSTOM_FIELD); + if (jsonObject == null || customFields == null || customFields.size() <= 0) { + return; + } + StringBuilder builder = new StringBuilder("产品[" + product.getName() + "]data中的必填参数不能为空,"); + for (Object obj : customFields) { + JSONObject object = (JSONObject) obj; + Object fieldValue = BeanUtil.getFieldValue(data, object.getStr("id")); + if (fieldValue == null) { + builder + .append(object.getStr("id")) + .append("(") + .append(object.getStr("name")) + .append(")") + .append(","); + flag = true; + } + } + result = builder.substring(0, builder.length() - 1); + } catch (Exception e) { + log.warn("校验CUSTOM_FIELD字段出错={}", e); + } + if (flag) { + throw new IoTException(result, IoTErrorCode.DEV_ADD_ERROR.getCode()); + } + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceAutoInsertIntercept.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceAutoInsertIntercept.java new file mode 100644 index 0000000..eb6e663 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceAutoInsertIntercept.java @@ -0,0 +1,136 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.wrapper; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.constant.IoTConstant.ERROR_CODE; +import cn.universal.common.domain.R; +import cn.universal.core.message.DownRequest; +import cn.universal.persistence.base.IoTDownWrapper; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.SupportMapAreasMapper; +import jakarta.annotation.Resource; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** 设备自动注册 */ +@Service("ioTDeviceAutoInsertIntercept") +@Slf4j +public class IoTDeviceAutoInsertIntercept implements IoTDownWrapper { + + @Resource private IoTDeviceMapper ioTDeviceMapper; + @Resource private SupportMapAreasMapper supportMapAreasMapper; + + @Value("${iot.register.auto.unionId}") + private String autoRegister; + + @Value("${iot.register.auto.latitude}") + private String latitude; + + @Value("${iot.register.auto.longitude}") + private String longitude; + + @Override + public R beforeDownAction(IoTProduct product, Object data, DownRequest downRequest) { + // 产品为空或者没有开启自动注册直接略过 + if (ObjectUtil.isEmpty(product) + || !JSONUtil.parseObj(product.getConfiguration()) + .getBool(IoTConstant.ALLOW_INSERT, false)) { + return null; + } + R r = null; + switch (downRequest.getCmd()) { + case DEV_ADD: + r = updateIoTDevice(product, downRequest, data); + break; + case DEV_DEL: + r = devRestore(downRequest, data); + break; + } + return r; + } + + // 恢复设备拥有者 + private R devRestore(DownRequest downRequest, Object data) { + IoTDevice ioTDevice = + IoTDevice.builder() + .productKey(downRequest.getProductKey()) + .deviceId(downRequest.getDeviceId()) + .build(); + // 访问数据库获取设备信息 + IoTDevice dev = ioTDeviceMapper.selectOne(ioTDevice); + ioTDevice.setId(dev.getId()); + ioTDevice.setDeviceName(downRequest.getDeviceId()); + ioTDevice.setCreatorId(autoRegister); + ioTDevice.setCoordinate(StrUtil.join(",", longitude, latitude)); + ioTDevice.setApplication(""); + int row = ioTDeviceMapper.updateDevInstance(ioTDevice); + if (row == 0) { + return R.error(ERROR_CODE.DEV_DEL_ERROR.getCode(), ERROR_CODE.DEV_DEL_ERROR.getName()); + } + return R.ok("删除成功"); + } + + // 更新设备拥有者 + private R updateIoTDevice(IoTProduct product, DownRequest downRequest, Object data) { + JSONObject object = JSONUtil.parseObj(data); + IoTDevice ioTDevice = + IoTDevice.builder() + .productKey(downRequest.getProductKey()) + .creatorId(autoRegister) + .deviceId(downRequest.getDeviceId()) + .build(); + // 访问数据库获取设备信息 + IoTDevice dev = ioTDeviceMapper.selectOne(ioTDevice); + if (ObjectUtil.isEmpty(dev)) { + return null; + } + // 修改设备信息 + ioTDevice.setId(dev.getId()); + ioTDevice.setCreatorId(downRequest.getAppUnionId()); + ioTDevice.setCreateTime(System.currentTimeMillis() / 1000); + ioTDevice.setDeviceName(object.getStr("deviceName")); + ioTDevice.setApplication(downRequest.getApplicationId()); + ioTDevice.setDetail(downRequest.getDetail()); + ioTDevice.setExtDeviceId(object.getStr("extDeviceId")); + if (StrUtil.isNotBlank(object.getStr("latitude")) + && StrUtil.isNotBlank(object.getStr("longitude"))) { + String latitude = object.getStr("latitude"); + String longitude = object.getStr("longitude"); + ioTDevice.setCoordinate(StrUtil.join(",", longitude, latitude)); + } + int row = ioTDeviceMapper.updateDevInstance(ioTDevice); + // 组件返回字段 + if (row <= 0) { + return R.error(ERROR_CODE.DEV_ADD_ERROR.getCode(), ERROR_CODE.DEV_ADD_ERROR.getName()); + } + Map result = new HashMap<>(); + result.put("iotId", dev.getIotId()); + result.put("areasId", ioTDevice.getAreasId() == null ? "" : ioTDevice.getAreasId()); + if (StrUtil.isNotBlank(product.getMetadata())) { + result.put("metadata", JSONUtil.parseObj(product.getMetadata())); + } + result.put("productKey", downRequest.getProductKey()); + result.put("deviceNode", product.getDeviceNode()); + return R.ok(result); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceDownFunctionWrapper.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceDownFunctionWrapper.java new file mode 100644 index 0000000..b1b5df3 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceDownFunctionWrapper.java @@ -0,0 +1,138 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.wrapper; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.domain.R; +import cn.universal.core.message.DownRequest; +import cn.universal.dm.device.service.impl.IoTCacheRemoveService; +import cn.universal.persistence.base.IoTDownWrapper; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTDeviceShadow; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.mapper.IoTDeviceShadowMapper; +import cn.universal.persistence.shadow.Shadow; +import cn.universal.persistence.shadow.State; +import jakarta.annotation.Resource; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +@Service("iotDeviceDownFunctionWrapper") +@Slf4j +public class IoTDeviceDownFunctionWrapper implements IoTDownWrapper { + + @Resource private IoTCacheRemoveService iotCacheRemoveService; + + @Resource private IoTDeviceShadowMapper ioTDeviceShadowMapper; + + @Resource private StringRedisTemplate stringRedisTemplate; + + // 保存功能期望值 + @Override + public R beforeFunctionOrConfigDown( + IoTProduct product, IoTDevice ioTDevice, DownRequest downRequest) { + if (downRequest.getFunction() == null) { + return null; + } + String function = downRequest.getFunction().get("function").toString(); + JSONObject data = JSONUtil.parseObj(downRequest.getFunction().get("data")); + // 当方法是set开头且data里不为空 保存data内的数据到影子期望值 + if (function.startsWith("set") && ObjectUtil.isNotEmpty(data)) { + // TODO: 判断物模型属性是否有可读写的 + + doShadow(product, ioTDevice, data); + } + return null; + } + + private void doShadow(IoTProduct product, IoTDevice ioTDevice, JSONObject data) { + IoTDeviceShadow ioTDeviceShadow = + IoTDeviceShadow.builder() + .deviceId(ioTDevice.getDeviceId()) + .productKey(ioTDevice.getProductKey()) + .iotId(ioTDevice.getIotId()) + .build(); + ioTDeviceShadow = ioTDeviceShadowMapper.selectOne(ioTDeviceShadow); + // 影子不存在时创建 + Boolean flag = + stringRedisTemplate + .opsForValue() + .setIfAbsent("doCreateShadow:" + ioTDevice.getIotId(), "1", 10, TimeUnit.MINUTES); + if (ObjectUtil.isNull(ioTDeviceShadow) && Boolean.TRUE.equals(flag)) { + ioTDeviceShadow = + IoTDeviceShadow.builder() + .iotId(ioTDevice.getIotId()) + .extDeviceId(ioTDevice.getExtDeviceId()) + .deviceId(ioTDevice.getDeviceId()) + .activeTime(new Date()) + .onlineTime(new Date()) + .updateDate(new Date()) + .lastTime(new Date()) + .build(); + State state = State.builder().desired(new JSONObject()).reported(new JSONObject()).build(); + State metadata = State.builder().desired(new JSONObject()).reported(new JSONObject()).build(); + Shadow shadow = + Shadow.builder() + .state(state) + .metadata(metadata) + .timestamp(DateUtil.currentSeconds()) + .version(1L) + .build(); + ioTDeviceShadow.setMetadata(JSONUtil.toJsonStr(shadow)); + ioTDeviceShadowMapper.insert(ioTDeviceShadow); + // 代理调用内部方法,清除缓存 + iotCacheRemoveService.removeDevInstanceBOCache(); + } + + Shadow shadow = null; + if (StrUtil.isBlank(ioTDeviceShadow.getMetadata())) { + shadow = Shadow.builder().timestamp(DateUtil.currentSeconds()).version(1L).build(); + } else { + shadow = JSONUtil.toBean(ioTDeviceShadow.getMetadata(), Shadow.class); + } + doDesired(shadow, ioTDevice, data); + ioTDeviceShadow.setMetadata(JSONUtil.toJsonStr(shadow)); + ioTDeviceShadow.setLastTime(new Date()); + ioTDeviceShadow.setUpdateDate(new Date()); + ioTDeviceShadowMapper.updateByPrimaryKeySelective(ioTDeviceShadow); + } + + private void doDesired(Shadow shadow, IoTDevice ioTDevice, JSONObject data) { + // 设置期望值 + shadow.getState().getDesired().putAll(data); + // 设置期望值时间 + JSONObject metaTime = new JSONObject(); + Timestamp timestamp = new Timestamp(DateUtil.currentSeconds()); + + for (String key : data.keySet()) { + metaTime.set(key, timestamp); + } + shadow.getMetadata().getDesired().putAll(metaTime); + } + + @AllArgsConstructor + @Getter + private class Timestamp { + + private Long timestamp; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceShareIntercept.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceShareIntercept.java new file mode 100644 index 0000000..dd38674 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceShareIntercept.java @@ -0,0 +1,160 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.wrapper; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant.MessageType; +import cn.universal.common.domain.R; +import cn.universal.common.exception.IoTErrorCode; +import cn.universal.core.message.DownRequest; +import cn.universal.dm.device.service.impl.IoTDeviceSubscribeService; +import cn.universal.persistence.base.IoTDownWrapper; +import cn.universal.persistence.dto.IoTDeviceSubscribeBO; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.IoTUserApplication; +import cn.universal.persistence.entity.SupportMapAreas; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.IoTUserApplicationMapper; +import cn.universal.persistence.mapper.SupportMapAreasMapper; +import jakarta.annotation.Resource; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** 设备分享,实际上是绑定了订阅 */ +@Service("ioTDeviceShareIntercept") +@Slf4j +public class IoTDeviceShareIntercept implements IoTDownWrapper { + + @Resource private IoTDeviceSubscribeService iotDeviceSubscribeService; + + @Resource private IoTUserApplicationMapper iotUserApplicationMapper; + + @Resource private IoTDeviceMapper ioTDeviceMapper; + + @Resource private SupportMapAreasMapper supportMapAreasMapper; + + @Override + public R beforeDownAction(IoTProduct product, Object data, DownRequest downRequest) { + // 非设备分享,直接返回 + if (!downRequest.isDeviceReuse()) { + return null; + } + IoTDevice ioTDevice = + ioTDeviceMapper.selectOne( + IoTDevice.builder() + .productKey(downRequest.getProductKey()) + .deviceId(downRequest.getDeviceId()) + .build()); + switch (downRequest.getCmd()) { + case DEV_ADD: + if (Objects.isNull(ioTDevice)) { + return R.error( + IoTErrorCode.DEV_MASTER_NOT_EXIST_ERROR.getCode(), + IoTErrorCode.DEV_MASTER_NOT_EXIST_ERROR.getName()); + } + IoTUserApplication applicationAdd = + iotUserApplicationMapper.selectIotUserApplicationById(downRequest.getApplicationId()); + if (applicationAdd == null + || (StrUtil.isBlank(applicationAdd.getNotifyUrl()) + && StrUtil.isBlank(applicationAdd.getUpTopic()))) { + return R.error(IoTErrorCode.DATA_CAN_NOT_NULL.getCode(), "添加失败"); + } + // 添加订阅 + R rAdd = + iotDeviceSubscribeService.doSubscribe( + ioTDevice.getIotId(), + downRequest.getProductKey(), + applicationAdd.getUnionId(), + applicationAdd.getAppUniqueId(), + IoTDeviceSubscribeBO.builder() + .msgType(MessageType.ALL.name()) + .url(applicationAdd.getNotifyUrl()) + .build()); + if (!R.SUCCESS.equals(rAdd.getCode())) { + return rAdd; + } + // 组件返回字段 + Map resultAdd = new HashMap<>(); + resultAdd.put("iotId", ioTDevice.getIotId()); + resultAdd.put("areasId", ioTDevice.getAreasId() == null ? "" : ioTDevice.getAreasId()); + if (StrUtil.isNotBlank(product.getMetadata())) { + resultAdd.put("metadata", JSONUtil.parseObj(product.getMetadata())); + } + resultAdd.put("productKey", downRequest.getProductKey()); + resultAdd.put("deviceNode", product.getDeviceNode()); + return R.ok(resultAdd); + case DEV_DEL: + if (Objects.isNull(ioTDevice)) { + return R.ok(); + } + // 删除订阅 + IoTUserApplication applicationDel = + iotUserApplicationMapper.selectIotUserApplicationById(downRequest.getApplicationId()); + if (applicationDel == null) { + return R.error(IoTErrorCode.DATA_CAN_NOT_NULL.getCode(), "操作失败"); + } + R rDel = + iotDeviceSubscribeService.deleteSubscribe( + ioTDevice.getIotId(), applicationDel.getUnionId(), applicationDel.getAppUniqueId()); + if (!R.SUCCESS.equals(rDel.getCode())) { + return rDel; + } + return R.ok(); + case DEV_UPDATE: + if (Objects.isNull(ioTDevice)) { + return R.error(IoTErrorCode.DEV_MASTER_NOT_EXIST_ERROR.getCode(), "主设备不存在"); + } + JSONObject location = JSONUtil.parseObj(data); + SupportMapAreas supportMapAreas = + supportMapAreasMapper.selectMapAreas( + location.getStr("longitude"), location.getStr("latitude")); + if (supportMapAreas == null) { + log.info( + "查询区域id为空,lot={},lat={}", location.getStr("longitude"), location.getStr("latitude")); + } else { + ioTDevice.setAreasId(supportMapAreas.getId()); + } + // 组件返回字段 + Map resultUp = new HashMap<>(); + resultUp.put("deviceId", ioTDevice.getDeviceId()); + resultUp.put("areasId", ioTDevice.getAreasId() == null ? "" : ioTDevice.getAreasId()); + return R.ok(resultUp); + case DEV_FUNCTION: + if (Objects.isNull(ioTDevice)) { + return R.error(IoTErrorCode.DEV_MASTER_NOT_EXIST_ERROR.getCode(), "主设备不存在"); + } + // 权限判断 + IoTUserApplication applicationFun = + iotUserApplicationMapper.selectIotUserApplicationById(downRequest.getApplicationId()); + if (applicationFun == null) { + return R.error(IoTErrorCode.DATA_CAN_NOT_NULL.getCode(), "操作失败"); + } + if (StrUtil.isNotBlank(applicationFun.getScope())) { + JSONObject scope = JSONUtil.parseObj(applicationFun.getScope()); + if (StrUtil.isNotBlank(scope.getStr("functionDown"))) { + return null; + } + } + return R.error("权限不足"); + default: + log.debug("复用设备处理下行未匹配到方法"); + return R.error("操作失败"); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceUPIntercept.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceUPIntercept.java new file mode 100644 index 0000000..3a12902 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceUPIntercept.java @@ -0,0 +1,136 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.wrapper; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.constant.IoTConstant.MessageType; +import cn.universal.dm.device.constant.DeviceManagerConstant; +import cn.universal.persistence.base.BaseUPRequest; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.SupportMapAreas; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.SupportMapAreasMapper; +import jakarta.annotation.Resource; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import tk.mybatis.mapper.entity.Example; + +/** + * 设备上行消息单独处理 + * + * @version 1.0 @Author Aleo + * @since 2023/9/21 + */ +@Service("ioTDeviceUPIntercept") +@Slf4j +public class IoTDeviceUPIntercept { + + @Resource private IoTDeviceMapper ioTDeviceMapper; + @Resource private SupportMapAreasMapper supportMapAreasMapper; + + @Async + public void messageProcess(BaseUPRequest upRequest) { + if (upRequest == null || upRequest.getMessageType() == null) { + return; + } + MessageType messageType = upRequest.getMessageType(); + switch (messageType) { + case PROPERTIES: + properties(upRequest); + break; + case EVENT: + event(upRequest); + break; + default: + break; + } + } + + private void event(BaseUPRequest baseUPRequest) { + try { + } catch (Exception e) { + log.error("device UP messageProcess [event] error={}", ExceptionUtil.getRootCauseMessage(e)); + } + } + + private void properties(BaseUPRequest baseUPRequest) { + Map properties = baseUPRequest.getProperties(); + if (MapUtil.isEmpty(properties)) { + return; + } + try { + propertiesGeoPoint(baseUPRequest, properties); + propertiesICCID(baseUPRequest, properties); + } catch (Exception e) { + log.error( + "device UP messageProcess [properties] error={}", ExceptionUtil.getRootCauseMessage(e)); + } + } + + /** 处理定位设备经纬度 */ + private void propertiesGeoPoint(BaseUPRequest baseUPRequest, Map properties) { + JSONObject cfg = baseUPRequest.getIoTDeviceDTO().getProductConfig(); + if (cfg != null + && cfg.containsKey(IoTConstant.IS_GPS_PRODUCT) + && cfg.getBool(IoTConstant.IS_GPS_PRODUCT) + && properties.containsKey(DeviceManagerConstant.COORDINATE)) { + String geoPoint = (String) properties.get(DeviceManagerConstant.COORDINATE); + log.info("处理定位类经纬度数据处理,deviceId={},coordinate={}", baseUPRequest.getDeviceId(), geoPoint); + if (StrUtil.isNotBlank(geoPoint)) { + IoTDevice ioTDevice = new IoTDevice(); + ioTDevice.setIotId(baseUPRequest.getIotId()); + ioTDevice.setCoordinate(geoPoint); + String[] coors = geoPoint.split(","); + SupportMapAreas supportMapAreas = supportMapAreasMapper.selectMapAreas(coors[0], coors[1]); + if (supportMapAreas == null) { + log.info("查询区域id为空,lot={},lat={}", coors[0], coors[1]); + } else { + ioTDevice.setAreasId(supportMapAreas.getId()); + } + Example example = new Example(IoTDevice.class); + example.createCriteria().andEqualTo("iotId", baseUPRequest.getIotId()); + ioTDeviceMapper.updateByExampleSelective(ioTDevice, example); + } + } + } + + /** 保存设备解析出来的iccid到设备表 */ + private void propertiesICCID(BaseUPRequest baseUPRequest, Map properties) { + if (properties.containsKey("iccid") || properties.containsKey("ICCID")) { + String iccid = (String) properties.get("iccid"); + if (StrUtil.isBlank(iccid)) { + iccid = (String) properties.get("ICCID"); + } + if (StrUtil.isBlank(iccid)) { + return; + } + Example example = new Example(IoTDevice.class); + example.createCriteria().andEqualTo("iotId", baseUPRequest.getIotId()); + IoTDevice ioTDevice = ioTDeviceMapper.selectOneByExample(example); + JSONObject object = new JSONObject(); + if (StrUtil.isNotBlank(ioTDevice.getConfiguration())) { + object = JSONUtil.parseObj(ioTDevice.getConfiguration()); + } + object.set("iccid", iccid); + ioTDevice.setConfiguration(JSONUtil.toJsonStr(object)); + ioTDeviceMapper.updateDevInstance(ioTDevice); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTSubDeviceAddIntercept.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTSubDeviceAddIntercept.java new file mode 100644 index 0000000..f246715 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTSubDeviceAddIntercept.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.wrapper; + +import cn.hutool.core.util.StrUtil; +import cn.universal.common.constant.IoTConstant.DeviceNode; +import cn.universal.common.constant.IoTConstant.DownCmd; +import cn.universal.common.domain.R; +import cn.universal.core.message.DownRequest; +import cn.universal.dm.device.service.impl.IoTProductDeviceService; +import cn.universal.persistence.base.IoTDownWrapper; +import cn.universal.persistence.entity.IoTProduct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** 子设备逻辑校验 */ +@Service("ioTSubDeviceAddIntercept") +@Slf4j +public class IoTSubDeviceAddIntercept implements IoTDownWrapper { + + @Resource private IoTProductDeviceService iotProductDeviceService; + + @Override + public R beforeDownAction(IoTProduct product, Object data, DownRequest downRequest) { + /*返回的R不为空,会影响后续流程,此处只做参数的补充*/ + if (product == null) { + return null; + } + if (downRequest.getCmd() == null) { + return null; + } + if (DownCmd.DEV_ADD.equals(downRequest.getCmd()) + || DownCmd.DEV_ADDS.equals(downRequest.getCmd())) { + return doGwSubDeviceAdd(product, downRequest); + } + + return null; + } + + private R doGwSubDeviceAdd(IoTProduct product, DownRequest downRequest) { + if (!DeviceNode.GATEWAY_SUB_DEVICE.name().equals(product.getDeviceNode())) { + return null; + } + // 如果是网关子设备,则判断是否绑定了网关 + if (StrUtil.isNotBlank(product.getGwProductKey())) { + downRequest.setGwProductKey(product.getGwProductKey()); + } else { + return R.error("添加网关子设备,请先绑定网关"); + } + return null; + } + + private void doAddAction() {} +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/util/DistributedLockUtil.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/util/DistributedLockUtil.java new file mode 100644 index 0000000..d1f3d4c --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/util/DistributedLockUtil.java @@ -0,0 +1,150 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ +package cn.universal.dm.device.util; + +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 分布式锁工具类 + * 提供便捷的锁操作方法 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +@Component +public class DistributedLockUtil { + + @Autowired + private RedissonClient redissonClient; + + /** + * 尝试获取锁并执行操作 + * + * @param lockKey 锁的键 + * @param waitTime 等待获取锁的时间 + * @param leaseTime 锁的持有时间 + * @param timeUnit 时间单位 + * @param supplier 要执行的操作 + * @param 返回值类型 + * @return 操作结果,如果获取锁失败返回null + */ + public T tryLockAndExecute(String lockKey, long waitTime, long leaseTime, TimeUnit timeUnit, Supplier supplier) { + RLock lock = redissonClient.getLock(lockKey); + + try { + boolean lockAcquired = lock.tryLock(waitTime, leaseTime, timeUnit); + if (lockAcquired) { + try { + log.debug("成功获取分布式锁: {}", lockKey); + return supplier.get(); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + log.debug("释放分布式锁: {}", lockKey); + } + } + } else { + log.warn("获取分布式锁失败: {}", lockKey); + return null; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("获取分布式锁被中断: {}", lockKey); + return null; + } catch (Exception e) { + log.error("执行分布式锁操作异常: {}", lockKey, e); + return null; + } + } + + /** + * 尝试获取锁并执行操作(无返回值) + * + * @param lockKey 锁的键 + * @param waitTime 等待获取锁的时间 + * @param leaseTime 锁的持有时间 + * @param timeUnit 时间单位 + * @param runnable 要执行的操作 + * @return 是否成功执行 + */ + public boolean tryLockAndExecute(String lockKey, long waitTime, long leaseTime, TimeUnit timeUnit, Runnable runnable) { + RLock lock = redissonClient.getLock(lockKey); + + try { + boolean lockAcquired = lock.tryLock(waitTime, leaseTime, timeUnit); + if (lockAcquired) { + try { + log.debug("成功获取分布式锁: {}", lockKey); + runnable.run(); + return true; + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + log.debug("释放分布式锁: {}", lockKey); + } + } + } else { + log.warn("获取分布式锁失败: {}", lockKey); + return false; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("获取分布式锁被中断: {}", lockKey); + return false; + } catch (Exception e) { + log.error("执行分布式锁操作异常: {}", lockKey, e); + return false; + } + } + + /** + * 检查锁是否被持有 + * + * @param lockKey 锁的键 + * @return 是否被持有 + */ + public boolean isLocked(String lockKey) { + RLock lock = redissonClient.getLock(lockKey); + return lock.isLocked(); + } + + /** + * 检查锁是否被当前线程持有 + * + * @param lockKey 锁的键 + * @return 是否被当前线程持有 + */ + public boolean isHeldByCurrentThread(String lockKey) { + RLock lock = redissonClient.getLock(lockKey); + return lock.isHeldByCurrentThread(); + } + + /** + * 强制释放锁(谨慎使用) + * + * @param lockKey 锁的键 + */ + public void forceUnlock(String lockKey) { + RLock lock = redissonClient.getLock(lockKey); + if (lock.isLocked()) { + lock.forceUnlock(); + log.warn("强制释放分布式锁: {}", lockKey); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/test/java/cn/universal/manager/device/DevicePropertiesTest.java b/cn-universal-framework/cn-universal-dm/src/test/java/cn/universal/manager/device/DevicePropertiesTest.java new file mode 100644 index 0000000..44544c0 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/test/java/cn/universal/manager/device/DevicePropertiesTest.java @@ -0,0 +1,55 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.manager.device; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.core.metadata.AbstractFunctionMetadata; +import cn.universal.core.metadata.DeviceMetadata; +import cn.universal.dm.device.entity.IoTDevicePropertiesBO; +import java.util.Optional; +import org.junit.Test; + +/** + * @version 1.0 @Author Aleo + * @since 2025/11/19 + */ +public class DevicePropertiesTest { + + @Test + public void metaProperties() { + String str = + "{\"events\":[{\"id\":\"offline\",\"name\":\"下线\",\"expands\":{\"level\":\"warn\"}," + + "\"valueType\":{\"type\":\"string\",\"expands\":{\"maxLength\":\"50\"}}}]," + + "\"properties\":[{\"id\":\"smokeConcentration\",\"name\":\"烟雾浓度\",\"valueType\":{\"type\":\"int\"," + + "\"unit\":\"%\"}},{\"id\":\"greateValue\",\"name\":\"是否超标\",\"valueType\":{\"type\":\"boolean\"}," + + "\"description\":\"是或者否\"},{\"id\":\"onlineStatus\",\"name\":\"开启状态\",\"valueType\":{\"type\":\"enum\"," + + "\"elements\":[{\"id\":\"open\",\"value\":\"0\",\"text\":\"开启\"},{\"id\":\"close\",\"value\":\"1\"," + + "\"text\":\"关闭\"}]}}],\"functions\":[{\"id\":\"ConfigurationFunction\",\"name\":\"下发配置信息\",\"output\":{}," + + "\"inputs\":[{\"id\":\"deviceRestart\",\"name\":\"设备模式\",\"valueType\":{\"type\":\"enum\"," + + "\"elements\":[{\"text\":\"制冷\",\"value\":\"0\",\"id\":0},{\"id\":2,\"value\":\"1\",\"text\":\"制热\"}," + + "{\"id\":3,\"value\":\"2\",\"text\":\"通风\"}]},\"description\":\"设备重启为1有效\"}]}],\"tags\":[]}"; + JSONObject json = JSONUtil.parseObj(str); + DeviceMetadata data = new DeviceMetadata(json); + Optional deviceIOTFunction = + data.getFunction("ConfigurationFunction"); + System.out.println(JSONUtil.toJsonStr(deviceIOTFunction.get().getInputs())); + IoTDevicePropertiesBO entity = new IoTDevicePropertiesBO(); + entity.withValue(data.getPropertyOrNull("onlineStatus"), 1); + System.out.println(JSONUtil.toJsonStr(entity)); + + IoTDevicePropertiesBO greateValue = new IoTDevicePropertiesBO(); + greateValue.withValue(data.getPropertyOrNull("greateValue"), "true"); + System.out.println(JSONUtil.toJsonStr(greateValue)); + } +} diff --git a/cn-universal-framework/cn-universal-exception/pom.xml b/cn-universal-framework/cn-universal-exception/pom.xml new file mode 100644 index 0000000..e1a9211 --- /dev/null +++ b/cn-universal-framework/cn-universal-exception/pom.xml @@ -0,0 +1,20 @@ + + + cn-universal-exception + 4.0.0 + + cn-universal-framework + cn.universal.iot + 1.5-SNAPSHOT + + + + + cn.universal.iot + cn-universal-common + ${project.version} + + + \ No newline at end of file diff --git a/cn-universal-framework/cn-universal-oss/pom.xml b/cn-universal-framework/cn-universal-oss/pom.xml new file mode 100644 index 0000000..3c2b704 --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/pom.xml @@ -0,0 +1,40 @@ + + + + cn-universal-framework + cn.universal.iot + 1.5-SNAPSHOT + + 4.0.0 + + cn-universal-oss + + + cn.universal.iot + cn-universal-core + ${project.version} + + + cn.universal.iot + cn-universal-persistence + ${project.version} + + + com.aliyun.oss + aliyun-sdk-oss + 3.13.0 + + + commons-logging + commons-logging + + + + + org.springframework + spring-web + + + \ No newline at end of file diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/entity/SysOss.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/entity/SysOss.java new file mode 100644 index 0000000..0862f8b --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/entity/SysOss.java @@ -0,0 +1,80 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** OSS云存储对象 @Author Lion Li */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@Builder +@AllArgsConstructor +@Table(name = "sys_oss") +public class SysOss implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 云存储主键 */ + // @TableId(value = "oss_id", type = IdType.AUTO) + @Id private Long ossId; + + /** 文件名 */ + @Column(name = "file_name") + private String fileName; + + /** 原名 */ + @Column(name = "original_name") + private String originalName; + + /** 文件后缀名 */ + @Column(name = "file_suffix") + private String fileSuffix; + + /** URL地址 */ + @Column(name = "url") + private String url; + + /** 创建时间 */ + // @TableField(fill = FieldFill.INSERT) + @Column(name = "create_time") + private Date createTime; + + /** 上传人 */ + // @TableField(fill = FieldFill.INSERT) + @Column(name = "create_by") + private String createBy; + + /** 更新时间 */ + // @TableField(fill = FieldFill.INSERT_UPDATE) + @Column(name = "update_time") + private Date updateTime; + + /** 更新人 */ + // @TableField(fill = FieldFill.INSERT_UPDATE) + @Column(name = "update_by") + private String updateBy; + + /** 服务商 */ + @Column(name = "third") + private String service; +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/entity/bo/SysOssBo.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/entity/bo/SysOssBo.java new file mode 100644 index 0000000..24ca749 --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/entity/bo/SysOssBo.java @@ -0,0 +1,67 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.entity.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** OSS云存储分页查询对象 sys_oss @Author Lion Li */ +@Data +@Schema(description = "OSS云存储分页查询对象") +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class SysOssBo { + + /** 分页大小 */ + @Schema(description = "分页大小") + private Integer pageSize; + + /** 当前页数 */ + @Schema(description = "当前页数") + private Integer pageNum; + + /** 排序列 */ + @Schema(description = "排序列") + private String orderByColumn; + + /** 排序的方向desc或者asc */ + private String isAsc; + + /** 文件名 */ + @Schema(description = "文件名") + private String fileName; + + /** 原名 */ + @Schema(description = "原名") + private String originalName; + + /** 文件后缀名 */ + @Schema(description = "文件后缀名") + private String fileSuffix; + + /** URL地址 */ + @Schema(description = "URL地址") + private String url; + + /** 服务商 */ + @Schema(description = "服务商") + private String service; + + /** 创建者 */ + @Schema(description = "创建者") + private String createBy; +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/entity/vo/DevCameraVideoVo.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/entity/vo/DevCameraVideoVo.java new file mode 100644 index 0000000..3a2bfcb --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/entity/vo/DevCameraVideoVo.java @@ -0,0 +1,73 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.entity.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "dev_camera_video") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class DevCameraVideoVo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @Id private Long id; + + /** 设备序列号 */ + @Column(name = "device_id") + private String deviceId; + + /** 录像地址 */ + @Column(name = "url") + private String url; + + /** 录像标识 */ + @Column(name = "video_index") + private int videoIndex; + + /** 录像状态 */ + @Column(name = "status") + private String status; + + /** 创建人 */ + @Column(name = "create_by") + private String createBy; + + /** 总数 */ + private String total; + + /** 创建时间 */ + @Column(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @Column(name = "begin_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date beginTime; + + @Column(name = "end_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/entity/vo/SysOssVo.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/entity/vo/SysOssVo.java new file mode 100644 index 0000000..907f694 --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/entity/vo/SysOssVo.java @@ -0,0 +1,57 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Date; +import lombok.Data; + +/** OSS云存储视图对象 sys_oss @Author Lion Li */ +@Data +@Schema(description = "OSS云存储视图对象") +public class SysOssVo { + + private static final long serialVersionUID = 1L; + + /** 云存储主键 */ + @Schema(description = "云存储主键") + private Long ossId; + + /** 文件名 */ + @Schema(description = "文件名") + private String fileName; + + /** 原名 */ + @Schema(description = "原名") + private String originalName; + + /** 文件后缀名 */ + @Schema(description = "文件后缀名") + private String fileSuffix; + + /** URL地址 */ + @Schema(description = "URL地址") + private String url; + + /** 创建时间 */ + @Schema(description = "创建时间") + private Date createTime; + + /** 上传人 */ + @Schema(description = "上传人") + private String createBy; + + /** 服务商 */ + @Schema(description = "服务商") + private String service; +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/mapper/SysOssMapper.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/mapper/SysOssMapper.java new file mode 100644 index 0000000..a358b0b --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/mapper/SysOssMapper.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.mapper; + +import cn.universal.ossm.entity.SysOss; +import cn.universal.persistence.common.BaseMapper; +import java.util.Collection; +import java.util.List; + +/** 文件上传 数据层 @Author Lion Li */ +public interface SysOssMapper extends BaseMapper { + + List listByIds(Collection ids); + + List listByUrls(String[] urls); + + int removeByIds(Collection ids); + + int removeByUrls(String[] ids); +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/constant/CloudConstant.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/constant/CloudConstant.java new file mode 100644 index 0000000..d787e30 --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/constant/CloudConstant.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.oss.constant; + +/** 云存储常量 @Author Lion Li */ +public class CloudConstant { + + /** 云存储配置KEY */ + public static final String CLOUD_STORAGE_CONFIG_KEY = "sys.oss.cloudStorageService"; + + /** 预览列表资源开关Key */ + public static final String PEREVIEW_LIST_RESOURCE_KEY = "sys.oss.previewListResource"; +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/entity/UploadResult.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/entity/UploadResult.java new file mode 100644 index 0000000..ea6b21f --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/entity/UploadResult.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.oss.entity; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** 上传返回体 @Author Lion Li */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class UploadResult { + + /** 文件路径 */ + private String url; + + /** 文件名 */ + private String filename; +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/enumd/CloudServiceEnumd.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/enumd/CloudServiceEnumd.java new file mode 100644 index 0000000..4e2694b --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/enumd/CloudServiceEnumd.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.oss.enumd; + +import cn.universal.ossm.oss.service.impl.AliyunCloudStorageServiceImpl; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** 云存储服务商枚举 @Author Lion Li */ +@Getter +@AllArgsConstructor +public enum CloudServiceEnumd { + + /** 七牛云 */ + // QINIU("qiniu", QiniuCloudStorageServiceImpl.class), + + /** 阿里云 */ + ALIYUN("aliyun", AliyunCloudStorageServiceImpl.class); + + /** 腾讯云 */ + // QCLOUD("qcloud", QcloudCloudStorageServiceImpl.class), + + /** minio */ + // MINIO("minio", MinioCloudStorageServiceImpl.class); + + private final String value; + + private final Class serviceClass; + + public static Class getServiceClass(String value) { + for (CloudServiceEnumd clazz : values()) { + if (clazz.getValue().equals(value)) { + return clazz.getServiceClass(); + } + } + return null; + } +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/enumd/PolicyType.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/enumd/PolicyType.java new file mode 100644 index 0000000..c4f3212 --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/enumd/PolicyType.java @@ -0,0 +1,34 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.oss.enumd; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** minio策略配置 @Author Lion Li */ +@Getter +@AllArgsConstructor +public enum PolicyType { + + /** 只读 */ + READ("read-only"), + + /** 只写 */ + WRITE("write-only"), + + /** 读写 */ + READ_WRITE("read-write"); + + /** 类型 */ + private final String type; +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/exception/OssException.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/exception/OssException.java new file mode 100644 index 0000000..85e73ba --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/exception/OssException.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.oss.exception; + +/** OSS异常类 @Author Lion Li */ +public class OssException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public OssException(String msg) { + super(msg); + } +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/factory/OssFactory.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/factory/OssFactory.java new file mode 100644 index 0000000..2d4fe61 --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/factory/OssFactory.java @@ -0,0 +1,50 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.oss.factory; + +import cn.hutool.core.lang.Assert; +import cn.universal.common.utils.SpringUtils; +import cn.universal.ossm.oss.enumd.CloudServiceEnumd; +import cn.universal.ossm.oss.service.ICloudStorageService; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** 文件上传Factory @Author Lion Li */ +public class OssFactory { + + // private static ISysConfigService sysConfigService; + // + // static { + // OssFactory.sysConfigService = SpringUtils.getBean(ISysConfigService.class); + // } + + private static final Map SERVICES = new ConcurrentHashMap<>(); + + public static ICloudStorageService instance() { + // String type = sysConfigService.selectConfigByKey(CloudConstant.CLOUD_STORAGE_CONFIG_KEY); + return instance("aliyun"); + } + + public static ICloudStorageService instance(String type) { + ICloudStorageService service = SERVICES.get(type); + if (service == null) { + service = (ICloudStorageService) SpringUtils.getBean(CloudServiceEnumd.getServiceClass(type)); + } + return service; + } + + public static void register(String type, ICloudStorageService iCloudStorageService) { + Assert.notNull(type, "type can't be null"); + SERVICES.put(type, iCloudStorageService); + } +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/properties/CloudStorageProperties.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/properties/CloudStorageProperties.java new file mode 100644 index 0000000..0c4c4df --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/properties/CloudStorageProperties.java @@ -0,0 +1,128 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.oss.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** OSS云存储 配置属性 @Author Lion Li */ +@Data +@Component +@ConfigurationProperties(prefix = "cloud-storage") +public class CloudStorageProperties { + + private Boolean previewListImage; + + private QiniuProperties qiniu; + + private AliyunProperties aliyun; + + private QcloudProperties qcloud; + + private MinioProperties minio; + + /** 阿里云 配置属性 @Author Lion Li */ + @Data + @NoArgsConstructor + public static class AliyunProperties { + + /** 阿里云绑定的域名 */ + private String endpoint; + + /** 阿里云路径前缀 */ + private String prefix; + + /** 阿里云AccessKeyId */ + private String accessKeyId; + + /** 阿里云AccessKeySecret */ + private String accessKeySecret; + + /** 阿里云BucketName */ + private String bucketName; + } + + /** Minio 配置属性 @Author Lion Li */ + @Data + @NoArgsConstructor + public static class MinioProperties { + + /** minio域名 */ + private String endpoint; + + /** minio ACCESS_KEY */ + private String accessKey; + + /** minio SECRET_KEY */ + private String secretKey; + + /** minio 存储空间名 */ + private String bucketName; + } + + /** 腾讯云COS 配置属性 @Author Lion Li */ + @Data + @NoArgsConstructor + public static class QcloudProperties { + + /** 腾讯云绑定的域名 */ + private String endpoint; + + /** 腾讯云路径前缀 */ + private String prefix; + + /** 腾讯云SecretId */ + private String secretId; + + /** 腾讯云SecretKey */ + private String secretKey; + + /** 腾讯云BucketName */ + private String bucketName; + + /** 七牛是否使用https */ + private Boolean isHttps; + + /** 腾讯云COS所属地区 */ + private String region; + } + + /** 七牛云 配置属性 @Author Lion Li */ + @Data + @NoArgsConstructor + public static class QiniuProperties { + + /** 七牛绑定的域名 */ + private String domain; + + /** 七牛路径前缀 */ + private String prefix; + + /** 七牛ACCESS_KEY */ + private String accessKey; + + /** 七牛SECRET_KEY */ + private String secretKey; + + /** 七牛存储空间名 */ + private String bucketName; + + /** 七牛存储区域 */ + private String region; + + /** 七牛是否使用https */ + private Boolean isHttps; + } +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/service/ICloudStorageService.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/service/ICloudStorageService.java new file mode 100644 index 0000000..eb558e1 --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/service/ICloudStorageService.java @@ -0,0 +1,79 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.oss.service; + +import cn.universal.ossm.oss.entity.UploadResult; +import java.io.InputStream; + +/** 云存储服务接口 @Author Lion Li */ +public interface ICloudStorageService { + + void createBucket(); + + /** 获取服务商类型 */ + String getServiceType(); + + /** + * 文件路径 + * + * @param prefix 前缀 + * @param suffix 后缀 + * @return 返回上传路径 + */ + String getPath(String prefix, String suffix); + + /** + * 文件上传 + * + * @param data 文件字节数组 + * @param path 文件路径,包含文件名 + * @return 返回http地址 + */ + UploadResult upload(byte[] data, String path, String contentType); + + /** + * 文件删除 + * + * @param path 文件路径,包含文件名 + */ + void delete(String path); + + /** + * 文件上传 + * + * @param data 文件字节数组 + * @param suffix 后缀 + * @return 返回http地址 + */ + UploadResult uploadSuffix(byte[] data, String suffix, String contentType); + + /** + * 文件上传 + * + * @param inputStream 字节流 + * @param path 文件路径,包含文件名 + * @return 返回http地址 + */ + UploadResult upload(InputStream inputStream, String path, String contentType); + + /** + * 文件上传 + * + * @param inputStream 字节流 + * @param suffix 后缀 + * @return 返回http地址 + */ + UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType); + + String getEndpointLink(); +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/service/abstractd/AbstractCloudStorageService.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/service/abstractd/AbstractCloudStorageService.java new file mode 100644 index 0000000..b0dcdd3 --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/service/abstractd/AbstractCloudStorageService.java @@ -0,0 +1,71 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.oss.service.abstractd; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.universal.ossm.oss.entity.UploadResult; +import cn.universal.ossm.oss.service.ICloudStorageService; +import java.io.InputStream; +import java.util.Date; +import org.springframework.beans.factory.InitializingBean; + +/** 云存储(支持七牛、阿里云、腾讯云、minio) @Author Lion Li */ +public abstract class AbstractCloudStorageService + implements ICloudStorageService, InitializingBean { + + @Override + public abstract void createBucket(); + + @Override + public abstract String getServiceType(); + + @Override + public String getPath(String prefix, String suffix) { + // 生成uuid + String uuid = IdUtil.fastSimpleUUID(); + // 文件路径 + String path = DateUtil.format(new Date(), "yyyyMMdd") + "/" + uuid; + if (StrUtil.isNotBlank(prefix)) { + path = prefix + "/" + path; + } + return path + suffix; + } + + @Override + public abstract UploadResult upload(byte[] data, String path, String contentType); + + @Override + public abstract void delete(String path); + + @Override + public UploadResult upload(InputStream inputStream, String path, String contentType) { + byte[] data = IoUtil.readBytes(inputStream); + return this.upload(data, path, contentType); + } + + @Override + public abstract UploadResult uploadSuffix(byte[] data, String suffix, String contentType); + + @Override + public abstract UploadResult uploadSuffix( + InputStream inputStream, String suffix, String contentType); + + @Override + public abstract void afterPropertiesSet() throws Exception; + + @Override + public abstract String getEndpointLink(); +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/service/impl/AliyunCloudStorageServiceImpl.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/service/impl/AliyunCloudStorageServiceImpl.java new file mode 100644 index 0000000..a82b94d --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/oss/service/impl/AliyunCloudStorageServiceImpl.java @@ -0,0 +1,136 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.oss.service.impl; + +import cn.hutool.core.util.StrUtil; +import cn.universal.ossm.oss.entity.UploadResult; +import cn.universal.ossm.oss.enumd.CloudServiceEnumd; +import cn.universal.ossm.oss.exception.OssException; +import cn.universal.ossm.oss.factory.OssFactory; +import cn.universal.ossm.oss.properties.CloudStorageProperties; +import cn.universal.ossm.oss.service.abstractd.AbstractCloudStorageService; +import com.aliyun.oss.ClientConfiguration; +import com.aliyun.oss.OSSClient; +import com.aliyun.oss.common.auth.DefaultCredentialProvider; +import com.aliyun.oss.model.CannedAccessControlList; +import com.aliyun.oss.model.CreateBucketRequest; +import com.aliyun.oss.model.ObjectMetadata; +import com.aliyun.oss.model.PutObjectRequest; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +/** 阿里云存储 @Author Lion Li */ +@Lazy +@Service +public class AliyunCloudStorageServiceImpl extends AbstractCloudStorageService + implements InitializingBean { + + private final OSSClient client; + private final CloudStorageProperties.AliyunProperties properties; + + @Autowired + public AliyunCloudStorageServiceImpl(CloudStorageProperties properties) { + this.properties = properties.getAliyun(); + try { + ClientConfiguration configuration = new ClientConfiguration(); + DefaultCredentialProvider credentialProvider = + new DefaultCredentialProvider( + this.properties.getAccessKeyId(), this.properties.getAccessKeySecret()); + client = new OSSClient(this.properties.getEndpoint(), credentialProvider, configuration); + createBucket(); + } catch (Exception e) { + throw new IllegalArgumentException("阿里云存储配置错误! 请检查系统配置!"); + } + } + + @Override + public void createBucket() { + try { + String bucketName = properties.getBucketName(); + if (client.doesBucketExist(bucketName)) { + return; + } + CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName); + createBucketRequest.setCannedACL(CannedAccessControlList.PublicRead); + client.createBucket(createBucketRequest); + } catch (Exception e) { + throw new OssException("创建Bucket失败, 请核对阿里云配置信息"); + } + } + + @Override + public String getServiceType() { + return CloudServiceEnumd.ALIYUN.getValue(); + } + + @Override + public UploadResult upload(byte[] data, String path, String contentType) { + return upload(new ByteArrayInputStream(data), path, contentType); + } + + @Override + public UploadResult upload(InputStream inputStream, String path, String contentType) { + try { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(contentType); + client.putObject( + new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata)); + } catch (Exception e) { + throw new OssException("上传文件失败,请检查阿里云配置信息"); + } + return new UploadResult().setUrl(getEndpointLink() + "/" + path).setFilename(path); + } + + @Override + public void delete(String path) { + path = path.replace(getEndpointLink() + "/", ""); + try { + client.deleteObject(properties.getBucketName(), path); + } catch (Exception e) { + throw new OssException("上传文件失败,请检查阿里云配置信息"); + } + } + + @Override + public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) { + return upload(data, getPath(properties.getPrefix(), suffix), contentType); + } + + @Override + public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) { + return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType); + } + + @Override + public void afterPropertiesSet() throws Exception { + OssFactory.register(getServiceType(), this); + } + + @Override + public String getEndpointLink() { + String endpoint = properties.getEndpoint(); + StringBuilder sb = new StringBuilder(endpoint); + if (StrUtil.containsAnyIgnoreCase(endpoint, "http://")) { + sb.insert(7, properties.getBucketName() + "."); + } else if (StrUtil.containsAnyIgnoreCase(endpoint, "https://")) { + sb.insert(8, properties.getBucketName() + "."); + } else { + throw new OssException("Endpoint配置错误"); + } + return sb.toString(); + } +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/IDevCameraSnapshotService.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/IDevCameraSnapshotService.java new file mode 100644 index 0000000..b4c77fe --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/IDevCameraSnapshotService.java @@ -0,0 +1,16 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.service; + +/** 文件上传 服务层 @Author Lion Li */ +public interface IDevCameraSnapshotService {} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/IDevCameraVideoService.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/IDevCameraVideoService.java new file mode 100644 index 0000000..c3ff5e3 --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/IDevCameraVideoService.java @@ -0,0 +1,16 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.service; + +/** 文件上传 服务层 @Author Lion Li */ +public interface IDevCameraVideoService {} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/ISysOssService.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/ISysOssService.java new file mode 100644 index 0000000..a0a9b9f --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/ISysOssService.java @@ -0,0 +1,105 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.service; + +import cn.universal.ossm.entity.SysOss; +import cn.universal.ossm.entity.bo.SysOssBo; +import java.io.InputStream; +import java.util.Collection; +import java.util.List; +import org.springframework.web.multipart.MultipartFile; + +/** + * 文件上传服务接口 + * + *

提供IoT平台中文件存储和管理功能,包括: - 文件上传(支持MultipartFile和InputStream) - 文件信息查询和分页 - 文件删除和批量删除 - 视频文件特殊处理 + * + *

支持多种文件类型,包括普通文件、视频文件等 @Author Lion Li + */ +public interface ISysOssService { + + /** + * 根据OSS ID查询文件信息 + * + *

通过文件在OSS系统中的唯一标识获取文件的详细信息 + * + * @param ossId OSS文件ID + * @return 文件信息,如果不存在返回null + */ + SysOss getById(Long ossId); + + /** + * 分页查询文件列表 + * + *

根据查询条件分页获取文件列表,支持多种查询条件 + * + * @param sysOss 查询条件对象,包含分页参数和查询条件 + * @return 文件列表 + */ + List queryPageList(SysOssBo sysOss); + + /** + * 上传文件(MultipartFile方式) + * + *

处理Web表单上传的文件,自动关联到指定用户 适用于Web界面的文件上传功能 + * + * @param file 上传的文件对象 + * @param unionId 用户唯一标识,用于关联文件所有者 + * @return 上传后的文件信息 + */ + SysOss upload(MultipartFile file, String unionId); + + /** + * 上传文件(InputStream方式) + * + *

处理流式文件上传,适用于程序内部的文件上传 支持任意类型的文件上传 + * + * @param inputStream 文件输入流 + * @param fileName 文件名 + * @return 上传后的文件信息 + */ + SysOss uploadStream(InputStream inputStream, String fileName); + + /** + * 上传视频文件(InputStream方式) + * + *

专门处理视频文件的上传,可能包含视频特有的处理逻辑 如视频格式验证、缩略图生成等 + * + * @param inputStream 视频文件输入流 + * @param fileName 视频文件名 + * @return 上传后的视频文件信息 + */ + SysOss uploadVideoStream(InputStream inputStream, String fileName); + + /** + * 批量删除文件(带验证) + * + *

根据文件ID列表批量删除文件,支持删除前的业务验证 验证失败的文件不会被删除 + * + * @param ids 要删除的文件ID集合 + * @param isValid 是否进行业务验证 + * @return 删除操作是否成功 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 根据URL批量删除文件(带验证) + * + *

根据文件URL列表批量删除文件,支持删除前的业务验证 适用于通过文件访问地址进行删除的场景 + * + * @param urls 要删除的文件URL数组 + * @param isValid 是否进行业务验证 + * @return 删除操作是否成功 + */ + Boolean deleteWithValidByUrls(String[] urls, Boolean isValid); +} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/impl/DevCameraShapshotImpl.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/impl/DevCameraShapshotImpl.java new file mode 100644 index 0000000..fa91ae5 --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/impl/DevCameraShapshotImpl.java @@ -0,0 +1,20 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.service.impl; + +import cn.universal.ossm.service.IDevCameraSnapshotService; +import org.springframework.stereotype.Service; + +/** 文件上传 服务层实现 @Author Lion Li */ +@Service +public class DevCameraShapshotImpl implements IDevCameraSnapshotService {} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/impl/DevCameraVideoImpl.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/impl/DevCameraVideoImpl.java new file mode 100644 index 0000000..61a3083 --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/impl/DevCameraVideoImpl.java @@ -0,0 +1,20 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.service.impl; + +import cn.universal.ossm.service.IDevCameraVideoService; +import org.springframework.stereotype.Service; + +/** 文件上传 服务层实现 @Author Lion Li */ +@Service +public class DevCameraVideoImpl implements IDevCameraVideoService {} diff --git a/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/impl/SysOssServiceImpl.java b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/impl/SysOssServiceImpl.java new file mode 100644 index 0000000..4204471 --- /dev/null +++ b/cn-universal-framework/cn-universal-oss/src/main/java/cn/universal/ossm/service/impl/SysOssServiceImpl.java @@ -0,0 +1,163 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.ossm.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.universal.common.exception.IoTException; +import cn.universal.ossm.entity.SysOss; +import cn.universal.ossm.entity.bo.SysOssBo; +import cn.universal.ossm.mapper.SysOssMapper; +import cn.universal.ossm.oss.entity.UploadResult; +import cn.universal.ossm.oss.factory.OssFactory; +import cn.universal.ossm.oss.service.ICloudStorageService; +import cn.universal.ossm.service.ISysOssService; +import jakarta.annotation.Resource; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +/** 文件上传 服务层实现 @Author Lion Li */ +@Service +public class SysOssServiceImpl implements ISysOssService { + + @Resource private SysOssMapper sysOssMapper; + + @Value("${codec.path:cn-universal}") + private String prePath; + + @Override + public SysOss getById(Long ossId) { + return sysOssMapper.selectByPrimaryKey(ossId); + } + + @Override + public List queryPageList(SysOssBo bo) { + SysOss sysOss = BeanUtil.toBean(bo, SysOss.class); + return sysOssMapper.select(sysOss); + } + + @Override + public SysOss upload(MultipartFile file, String unionId) { + if (file.getOriginalFilename() == null) { + throw new IoTException("文件名为空!"); + } + String originalfileName = + prePath + file.getOriginalFilename().replace(".", RandomUtil.randomString(8) + "."); + String suffix = + StrUtil.sub(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length()); + ICloudStorageService storage = OssFactory.instance(); + UploadResult uploadResult; + try { + uploadResult = storage.upload(file.getBytes(), originalfileName, file.getContentType()); + } catch (IOException e) { + throw new IoTException("文件读取异常!!!", e); + } + // 保存文件信息 + SysOss oss = + new SysOss() + .setUrl(uploadResult.getUrl()) + .setFileSuffix(suffix) + .setFileName(uploadResult.getFilename()) + .setOriginalName(originalfileName) + .setService(storage.getServiceType()) + .setCreateBy(unionId) + .setCreateTime(new Date()); + sysOssMapper.insert(oss); + return oss; + } + + @Override + public SysOss uploadStream(InputStream inputStream, String fileName) { + ICloudStorageService storage = OssFactory.instance(); + UploadResult uploadResult; + try { + uploadResult = + storage.upload( + inputStream, "snapshot/" + fileName, fileName.substring(fileName.lastIndexOf("."))); + } catch (Exception e) { + throw new IoTException("文件读取异常!!!", e); + } + // 保存文件信息 + SysOss oss = + new SysOss() + .setUrl(uploadResult.getUrl()) + .setOriginalName(fileName) + .setFileSuffix(fileName.substring(fileName.lastIndexOf("."))) + .setFileName(fileName) + .setService(storage.getServiceType()) + .setCreateTime(new Date()); + sysOssMapper.insert(oss); + return oss; + } + + @Override + public SysOss uploadVideoStream(InputStream inputStream, String fileName) { + ICloudStorageService storage = OssFactory.instance(); + UploadResult uploadResult; + try { + uploadResult = + storage.upload( + inputStream, "video/" + fileName, fileName.substring(fileName.lastIndexOf("."))); + } catch (Exception e) { + throw new IoTException("文件读取异常!!!", e); + } + // 保存文件信息 + SysOss oss = + new SysOss() + .setUrl(uploadResult.getUrl()) + .setOriginalName(fileName) + .setFileSuffix(fileName.substring(fileName.lastIndexOf("."))) + .setFileName(fileName) + .setService(storage.getServiceType()) + .setCreateTime(new Date()); + sysOssMapper.insert(oss); + return oss; + } + + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // 做一些业务上的校验,判断是否需要校验 + } + List list = sysOssMapper.listByIds(ids); + for (SysOss sysOss : list) { + ICloudStorageService storage = OssFactory.instance(sysOss.getService()); + storage.delete(sysOss.getUrl()); + } + return sysOssMapper.removeByIds(ids) > 0; + } + + @Override + public Boolean deleteWithValidByUrls(String[] urls, Boolean isValid) { + if (urls == null || urls.length <= 0) { + return false; + } + + if (isValid) { + // 做一些业务上的校验,判断是否需要校验 + } + List list = sysOssMapper.listByUrls(urls); + for (SysOss sysOss : list) { + ICloudStorageService storage = OssFactory.instance(sysOss.getService()); + storage.delete(sysOss.getUrl()); + } + return sysOssMapper.removeByUrls(urls) > 0; + } +} diff --git a/cn-universal-framework/cn-universal-rocketmq/pom.xml b/cn-universal-framework/cn-universal-rocketmq/pom.xml new file mode 100644 index 0000000..c7ae96a --- /dev/null +++ b/cn-universal-framework/cn-universal-rocketmq/pom.xml @@ -0,0 +1,95 @@ + + + + cn-universal-framework + cn.universal.iot + 1.5-SNAPSHOT + + 4.0.0 + + cn-universal-rocketmq + + + jar + + + + true + + + + + cn-universal-persistence + cn.universal.iot + ${project.version} + + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + + org.apache.rocketmq + rocketmq-tools + true + + + ch.qos.logback + logback-classic + + + ch.qos.logback + logback-core + + + org.apache.tomcat + annotations-api + + + + + + cn.hutool + hutool-all + compile + + + cn-universal-core + cn.universal.iot + ${project.version} + + + + ch.qos.logback + logback-core + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 21 + 21 + + + + + org.apache.maven.plugins + maven-jar-plugin + + true + + + + + + \ No newline at end of file diff --git a/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/DynamicRocketMQConsumer.java b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/DynamicRocketMQConsumer.java new file mode 100644 index 0000000..8a49d1f --- /dev/null +++ b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/DynamicRocketMQConsumer.java @@ -0,0 +1,141 @@ +// package cn.universal.rocketmq; +// +// import cn.universal.core.config.InstanceIdProvider; +// import lombok.extern.slf4j.Slf4j; +// import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +// import org.apache.rocketmq.spring.annotation.SelectorType; +// import org.apache.rocketmq.spring.core.RocketMQListener; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.stereotype.Component; +// +/// ** +// * 动态RocketMQ消费者配置 根据SQL92状态自动选择过滤策略 +// * +// * @version 1.0 @Author Aleo +// * @since 2025/1/27 +// */ +// @Slf4j +// @Component +// public class DynamicRocketMQConsumer { +// +// @Autowired private RocketMQFilterStrategy filterStrategy; +// +// /** 产品刷新配置 - 动态过滤策略 根据SQL92状态自动选择SQL92或Tag过滤 */ +// @Component +// @RocketMQMessageListener( +// topic = "${platform.rocketmq.product.flush.topic}", +// consumerGroup = "${rocketmq.producer.group}-reload-dynamic") +// public static class DynamicProductFlushConfigure implements RocketMQListener { +// +// @Autowired private InstanceIdProvider instanceIdProvider; +// +// @Override +// public void onMessage(String message) { +// try { +// // 应用层过滤:检查是否为自己的消息 +// if (message != null && message.contains("sourceId")) { +// String sourceId = extractSourceId(message); +// if (sourceId != null && instanceIdProvider.isOwnMessage(sourceId)) { +// log.debug("跳过自己发出的产品刷新消息: sourceId={}", sourceId); +// return; +// } +// } +// +// log.info("接收到产品刷新消息:{}", message); +// // 处理消息逻辑 +// +// } catch (Exception e) { +// log.error("处理产品刷新消息失败: message={}, error={}", message, e.getMessage(), e); +// } +// } +// +// private String extractSourceId(String message) { +// try { +// int startIndex = message.indexOf("\"sourceId\":\""); +// if (startIndex != -1) { +// startIndex += 12; +// int endIndex = message.indexOf("\"", startIndex); +// if (endIndex != -1) { +// return message.substring(startIndex, endIndex); +// } +// } +// } catch (Exception e) { +// log.debug("提取sourceId失败: {}", e.getMessage()); +// } +// return null; +// } +// } +// +// /** 智能消费者工厂 根据过滤策略动态创建消费者配置 */ +// public static class SmartConsumerFactory { +// +// /** +// * 创建智能消费者配置 +// * +// * @param topic 主题 +// * @param consumerGroup 消费者组 +// * @param filterStrategy 过滤策略 +// * @return 消费者配置 +// */ +// public static ConsumerConfig createConsumerConfig( +// String topic, String consumerGroup, RocketMQFilterStrategy filterStrategy) { +// ConsumerConfig config = new ConsumerConfig(); +// config.setTopic(topic); +// config.setConsumerGroup(consumerGroup); +// +// if (filterStrategy.isSql92Enabled()) { +// config.setSelectorType(SelectorType.SQL92); +// config.setSelectorExpression(filterStrategy.getSql92Expression()); +// log.info("使用SQL92过滤: topic={}, expression={}", topic, config.getSelectorExpression()); +// } else { +// config.setSelectorType(SelectorType.TAG); +// config.setSelectorExpression(filterStrategy.getTagExpression()); +// log.info("使用Tag过滤: topic={}, expression={}", topic, config.getSelectorExpression()); +// } +// +// return config; +// } +// } +// +// /** 消费者配置类 */ +// public static class ConsumerConfig { +// +// private String topic; +// private String consumerGroup; +// private SelectorType selectorType; +// private String selectorExpression; +// +// // getters and setters +// public String getTopic() { +// return topic; +// } +// +// public void setTopic(String topic) { +// this.topic = topic; +// } +// +// public String getConsumerGroup() { +// return consumerGroup; +// } +// +// public void setConsumerGroup(String consumerGroup) { +// this.consumerGroup = consumerGroup; +// } +// +// public SelectorType getSelectorType() { +// return selectorType; +// } +// +// public void setSelectorType(SelectorType selectorType) { +// this.selectorType = selectorType; +// } +// +// public String getSelectorExpression() { +// return selectorExpression; +// } +// +// public void setSelectorExpression(String selectorExpression) { +// this.selectorExpression = selectorExpression; +// } +// } +// } diff --git a/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQAdminToolConfig.java b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQAdminToolConfig.java new file mode 100644 index 0000000..9cdea7b --- /dev/null +++ b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQAdminToolConfig.java @@ -0,0 +1,54 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.rocketmq; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Role; + +@Configuration +@Slf4j +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class RocketMQAdminToolConfig { + + @Value("${rocketmq.name-server}") + private String nameServer; // RocketMQ的Nameserver地址 + + // + // @Value("{rocketmq.producer.group:iot_group}") + // private String defaultMQProduct; + // + // @Bean + // public RocketMQTemplate rocketMQTemplate() { + // RocketMQTemplate template = new RocketMQTemplate(); + // DefaultMQProducer producer = new DefaultMQProducer(defaultMQProduct); + // producer.setNamesrvAddr(nameServer); + // template.setProducer(producer); + // return template; + // } + + @Bean + @Lazy + public DefaultMQAdminExt rocketMQAdmin() throws MQClientException { + DefaultMQAdminExt admin = new DefaultMQAdminExt(); + admin.setNamesrvAddr(nameServer); + admin.start(); + return admin; + } +} diff --git a/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQAutoConfig.java b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQAutoConfig.java new file mode 100644 index 0000000..715d44e --- /dev/null +++ b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQAutoConfig.java @@ -0,0 +1,69 @@ +package cn.universal.rocketmq; + +import cn.universal.core.config.InstanceIdProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; + +/** + * RocketMQ自动配置 在应用启动时自动检测和配置过滤策略 + * + * @version 1.0 @Author Aleo + * @since 2025/1/27 + */ +@Configuration +@Slf4j +public class RocketMQAutoConfig { + + @Autowired private RocketMQFilterStrategy filterStrategy; + + @Autowired private InstanceIdProvider instanceIdProvider; + + /** 应用启动完成后自动检测SQL92状态 */ + @EventListener(ApplicationReadyEvent.class) + public void onApplicationReady() { + log.info("=== RocketMQ过滤策略自动配置 ==="); + + try { + // 获取当前策略 + RocketMQFilterStrategy.FilterStrategy strategy = filterStrategy.getCurrentStrategy(); + log.info("当前过滤策略: {}", strategy.getDescription()); + log.info("实例ID: {}", instanceIdProvider.getInstanceId()); + + // 输出配置信息 + if (strategy == RocketMQFilterStrategy.FilterStrategy.SQL92) { + log.info("SQL92过滤表达式: {}", filterStrategy.getSql92Expression()); + log.info("✅ SQL92过滤已启用,将使用高性能的SQL92过滤"); + } else { + log.info("Tag过滤表达式: {}", filterStrategy.getTagExpression()); + log.info("✅ Tag过滤已启用,将使用兼容性更好的Tag过滤"); + } + + log.info("=== 过滤策略配置完成 ==="); + + } catch (Exception e) { + log.error("过滤策略配置失败: {}", e.getMessage(), e); + // 设置默认策略为Tag过滤 + filterStrategy.setSql92Enabled(false); + log.info("已设置默认策略为Tag过滤"); + } + } + + /** 手动切换过滤策略的方法 可以通过JMX或其他方式调用 */ + public void switchToSql92() { + filterStrategy.setSql92Enabled(true); + log.info("已切换到SQL92过滤策略"); + } + + public void switchToTag() { + filterStrategy.setSql92Enabled(false); + log.info("已切换到Tag过滤策略"); + } + + /** 获取当前策略信息 */ + public String getCurrentStrategyInfo() { + return filterStrategy.getStrategyInfo(); + } +} diff --git a/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQConfig.java b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQConfig.java new file mode 100644 index 0000000..0d16436 --- /dev/null +++ b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQConfig.java @@ -0,0 +1,79 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.rocketmq; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.acl.common.AclClientRPCHook; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +@Slf4j +@Configuration +public class RocketMQConfig { + + @Value("${rocketmq.name-server}") + private String nameServer; + + @Value("${rocketmq.producer.group}") + private String producerGroup; + + @Value("${rocketmq.producer.access-key}") // 新增:从配置读取 AccessKey + private String accessKey; + + @Value("${rocketmq.producer.secret-key}") // 新增:从配置读取 SecretKey + private String secretKey; + + @Value("${rocketmq.producer.send-message-timeout:10000}") + private int sendMessageTimeout; + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public RocketMQTemplate rocketMQTemplate() { + RocketMQTemplate template = new RocketMQTemplate(); + + // 关键步骤:创建 RPCHook 注入密钥 + RPCHook rpcHook = + new AclClientRPCHook( + new SessionCredentials(accessKey, secretKey) // 使用密钥初始化 + ); + + // 使用带 RPCHook 的构造函数 + DefaultMQProducer producer = new DefaultMQProducer(producerGroup, rpcHook); // 修改此行 + producer.setNamesrvAddr(nameServer); + producer.setSendMsgTimeout(sendMessageTimeout); + + // 优化连接配置(保持原有配置) + producer.setRetryTimesWhenSendAsyncFailed(2); + producer.setRetryTimesWhenSendFailed(3); + producer.setCompressMsgBodyOverHowmuch(4096); + producer.setMaxMessageSize(4194304); // 4MB + producer.setClientCallbackExecutorThreads(4); + producer.setHeartbeatBrokerInterval(30000); + producer.setPersistConsumerOffsetInterval(5000); + + template.setProducer(producer); + log.info( + "RocketMQ producer configured: nameServer={}, group={}, timeout={}ms", + nameServer, + producerGroup, + sendMessageTimeout); + return template; + } +} diff --git a/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQFilterStrategy.java b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQFilterStrategy.java new file mode 100644 index 0000000..b2d9010 --- /dev/null +++ b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQFilterStrategy.java @@ -0,0 +1,169 @@ +package cn.universal.rocketmq; + +import cn.universal.core.config.InstanceIdProvider; +import jakarta.annotation.PostConstruct; +import java.util.concurrent.atomic.AtomicBoolean; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.SelectorType; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * RocketMQ过滤策略管理器 自动检测SQL92是否开启,并选择相应的过滤方案 + * + * @version 1.0 @Author Aleo + * @since 2025/1/27 + */ +@Component +@Slf4j +public class RocketMQFilterStrategy { + + @Autowired private RocketMQTemplate rocketMQTemplate; + + @Autowired private InstanceIdProvider instanceIdProvider; + + private final AtomicBoolean sql92Enabled = new AtomicBoolean(false); + private final AtomicBoolean initialized = new AtomicBoolean(false); + + @PostConstruct + public void init() { + detectSql92Support(); + } + + /** 检测SQL92是否支持 */ + private void detectSql92Support() { + try { + // 尝试发送一个测试消息来检测SQL92支持 + // 如果SQL92不支持,会在启动时抛出异常 + log.info("正在检测RocketMQ SQL92支持..."); + + // 这里可以通过检查RocketMQ的配置或者尝试创建SQL92消费者来检测 + // 由于检测比较复杂,我们提供一个配置开关 + String sql92EnabledConfig = System.getProperty("rocketmq.sql92.enabled", "false"); + boolean enabled = Boolean.parseBoolean(sql92EnabledConfig); + + if (enabled) { + sql92Enabled.set(true); + log.info("SQL92过滤已启用"); + } else { + sql92Enabled.set(false); + log.info("SQL92过滤未启用,将使用Tag过滤"); + } + + } catch (Exception e) { + log.warn("SQL92检测失败,默认使用Tag过滤: {}", e.getMessage()); + sql92Enabled.set(false); + } finally { + initialized.set(true); + } + } + + /** + * 获取当前过滤策略 + * + * @return 过滤策略 + */ + public FilterStrategy getCurrentStrategy() { + if (!initialized.get()) { + detectSql92Support(); + } + + if (sql92Enabled.get()) { + return FilterStrategy.SQL92; + } else { + return FilterStrategy.TAG; + } + } + + /** + * 获取SQL92过滤表达式 + * + * @return SQL92过滤表达式 + */ + public String getSql92Expression() { + return "sourceId != '" + instanceIdProvider.getInstanceId() + "'"; + } + + /** + * 获取Tag过滤表达式 + * + * @return Tag过滤表达式 + */ + public String getTagExpression() { + return "!instance-" + instanceIdProvider.getInstanceId(); + } + + /** + * 获取当前实例的Tag + * + * @return 实例Tag + */ + public String getInstanceTag() { + return "instance-" + instanceIdProvider.getInstanceId(); + } + + /** + * 获取SelectorType + * + * @return SelectorType + */ + public SelectorType getSelectorType() { + return sql92Enabled.get() ? SelectorType.SQL92 : SelectorType.TAG; + } + + /** + * 获取SelectorExpression + * + * @return SelectorExpression + */ + public String getSelectorExpression() { + return sql92Enabled.get() ? getSql92Expression() : getTagExpression(); + } + + /** + * 手动设置SQL92启用状态 + * + * @param enabled 是否启用 + */ + public void setSql92Enabled(boolean enabled) { + sql92Enabled.set(enabled); + log.info("手动设置SQL92过滤: {}", enabled ? "启用" : "禁用"); + } + + /** + * 检查SQL92是否启用 + * + * @return true表示启用 + */ + public boolean isSql92Enabled() { + return sql92Enabled.get(); + } + + /** + * 获取过滤策略信息 + * + * @return 策略信息 + */ + public String getStrategyInfo() { + FilterStrategy strategy = getCurrentStrategy(); + return String.format( + "当前过滤策略: %s, 实例ID: %s", strategy.name(), instanceIdProvider.getSimpleInstanceId()); + } + + /** 过滤策略枚举 */ + public enum FilterStrategy { + SQL92("SQL92过滤"), + TAG("Tag过滤"); + + private final String description; + + FilterStrategy(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } +} diff --git a/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQHealthIndicator.java b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQHealthIndicator.java new file mode 100644 index 0000000..0c63f40 --- /dev/null +++ b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQHealthIndicator.java @@ -0,0 +1,45 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.rocketmq; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +/** RocketMQ 健康检查 @Author Aleo */ +@Slf4j +@Component +public class RocketMQHealthIndicator implements HealthIndicator { + @Autowired(required = false) + private RocketMQService rocketMQService; + + @Override + public Health health() { + if (rocketMQService == null) { + return Health.down().withDetail("status", "RocketMQ connection failed").build(); + } + try { + boolean isHealthy = rocketMQService.rocketmqNormal(); + if (isHealthy) { + return Health.up().withDetail("status", "RocketMQ is healthy").build(); + } else { + return Health.down().withDetail("status", "RocketMQ connection failed").build(); + } + } catch (Exception e) { + log.error("RocketMQ health check failed", e); + return Health.down().withDetail("error", e.getMessage()).build(); + } + } +} diff --git a/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQMonitor.java b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQMonitor.java new file mode 100644 index 0000000..baff663 --- /dev/null +++ b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQMonitor.java @@ -0,0 +1,31 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.rocketmq; + +import java.util.HashMap; +import java.util.Set; + +/** + * @version 1.0 @Author Aleo + * @since 2023/6/29 + */ +public interface RocketMQMonitor { + + /** + * 检查消费组信息 + * + * @param defaultTopic + * @return + */ + HashMap> queryDefaultTopicExistConsumer(String defaultTopic); +} diff --git a/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQMonitorService.java b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQMonitorService.java new file mode 100644 index 0000000..aa06b81 --- /dev/null +++ b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQMonitorService.java @@ -0,0 +1,58 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.rocketmq; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +/** RocketMQ 监控服务 @Author Aleo */ +@Slf4j +@Service +public class RocketMQMonitorService { + + private final RocketMQService rocketMQService; + private int consecutiveFailures = 0; + private static final int FAILURE_THRESHOLD = 5; + + public RocketMQMonitorService(RocketMQService rocketMQService) { + this.rocketMQService = rocketMQService; + } + + /** 监控 RocketMQ 连接状态 */ + @Scheduled(fixedRate = 300000) // 每30秒检查一次 + public void monitorConnection() { + boolean isHealthy = rocketMQService.rocketmqNormal(); + + if (!isHealthy) { + consecutiveFailures++; + log.warn("RocketMQ connection unhealthy, consecutive failures: {}", consecutiveFailures); + + if (consecutiveFailures >= FAILURE_THRESHOLD) { + log.error("RocketMQ connection failed {} times, triggering alert", FAILURE_THRESHOLD); + // 这里可以添加告警逻辑 + sendAlert(); + } + } else { + if (consecutiveFailures > 0) { + log.info("RocketMQ connection recovered after {} failures", consecutiveFailures); + } + consecutiveFailures = 0; + } + } + + private void sendAlert() { + // 实现告警逻辑,比如发送钉钉、邮件等 + log.error("RocketMQ connection alert: Server may be down or network issues"); + } +} diff --git a/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQService.java b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQService.java new file mode 100644 index 0000000..fcadeea --- /dev/null +++ b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/RocketMQService.java @@ -0,0 +1,352 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.rocketmq; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import cn.universal.core.base.monitor.NetMonitor; +import cn.universal.core.config.InstanceIdProvider; +import cn.universal.core.constant.IotConstant; +import cn.universal.persistence.base.IotUPRocketMQAdapter; +import jakarta.annotation.Resource; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.GroupList; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.apache.rocketmq.spring.support.RocketMQHeaders; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Role; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +/** + * RocketMQ消息服务类 + * + *

提供IoT平台中消息队列的核心功能,包括: - 同步/异步消息发送 - 顺序消息发送(FIFO) - 延迟消息发送 - 事务消息支持 - 消息监控和健康检查 - 消费者连接状态监控 + * + *

实现了IotUPRocketMQAdapter和RocketMQMonitor接口, 为IoT平台提供统一的消息队列服务 + * + * @version 1.0 @Author Aleo + * @since 2023/6/28 + */ +@Service("rocketMQService") +@Slf4j +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class RocketMQService implements IotUPRocketMQAdapter, RocketMQMonitor { + + @Resource private RocketMQTemplate rocketMQTemplate; + + @Autowired private InstanceIdProvider instanceIdProvider; + + @Value("${platform.rocketmq.topic}") + private String defaultTopic; + + @Value("${platform.rocketmq.heartbeat.topic}") + private String heartTopic; + + @Value("${platform.rocketmq.logConsumerGroup:log}") + private String logConsumerGroup; + + @Value("${rocketmq.name-server}") + private String nameServer; + + @Resource private DefaultMQAdminExt rocketMQAdmin; + + /** + * 检查RocketMQ连接状态 + * + *

通过监控器检查RocketMQ服务器的连接状态 用于判断消息队列服务是否可用 + * + * @return true表示连接正常,false表示连接异常 + */ + public boolean rocketmqNormal() { + Map connectionStatus = NetMonitor.getConnectionStatus(); + if (ObjectUtil.isNotNull(connectionStatus.get(nameServer)) + && !connectionStatus.get(nameServer)) { + log.warn("rocketmq is not connected"); + return false; + } + return true; + } + + /** + * 同步发送消息 + * + *

同步方式发送消息到指定主题,会等待发送结果 适用于对消息发送结果有严格要求场景 + * + * @param topic 消息主题,如果为空则使用默认主题 + * @param message 要发送的消息对象 + */ + @Override + public Object send(String topic, String message) { + return send(topic, message, instanceIdProvider.getInstanceId()); + } + + @Override + public Object send(String topic, String message, String tag) { + String instanceId = instanceIdProvider.getInstanceId(); + String tp = StrUtil.isBlank(topic) ? defaultTopic : topic; + MessageBuilder builder = + MessageBuilder.withPayload(message).setHeader(IotConstant.CURRENT_INSTANCE_ID, instanceId); + if (tag != null) { + builder.setHeader(RocketMQHeaders.TAGS, tag); + } + Message msg = builder.build(); + log.info("发送消息: topic={}, tag={}, sourceId={}, message={}", topic, tag, instanceId, message); + SendResult result = rocketMQTemplate.syncSend(tp, msg, 8000); + log.info( + "同步发送成功: msgId={},status={}", + result.getMsgId(), + result == null ? "" : result.getSendStatus().name()); + return result; + } + + @Override + public void sendAsync(String topic, String message, String tag) { + String tp = StrUtil.isBlank(topic) ? defaultTopic : topic; + String instanceId = instanceIdProvider.getInstanceId(); + MessageBuilder builder = + MessageBuilder.withPayload(message).setHeader(IotConstant.CURRENT_INSTANCE_ID, instanceId); + if (tag != null) { + builder.setHeader(RocketMQHeaders.TAGS, tag); + } + Message msg = builder.build(); + log.info("发送消息: topic={}, tag={}, sourceId={}, message={}", topic, tag, instanceId, message); + try { + rocketMQTemplate.asyncSend( + topic, + msg, + new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + log.info("sendAsync onSuccess= {}", sendResult); + } + + @Override + public void onException(Throwable e) { + log.error( + "rocketmq send sendAsync ={} message={}", + ExceptionUtil.getSimpleMessage(e), + JSONUtil.toJsonStr(message)); + } + }); + } catch (Exception e) { + log.error( + "rocketmq send failure ={} message={}", + ExceptionUtil.getSimpleMessage(e), + JSONUtil.toJsonStr(message)); + } + } + + /** + * 异步发送消息 + * + *

异步方式发送消息到指定主题,不等待发送结果 通过回调函数处理发送成功或失败的情况 + * + * @param topic 消息主题,如果为空则使用默认主题 + * @param message 要发送的消息对象 + */ + @Override + public void sendAsync(String topic, String message) { + sendAsync(topic, message, null); + } + + @Override + public void sendFifo(String topic, String orderKey, String message, String tag) { + String tp = StrUtil.isBlank(topic) ? defaultTopic : topic; + try { + String instanceId = instanceIdProvider.getInstanceId(); + MessageBuilder builder = + MessageBuilder.withPayload(message) + .setHeader(IotConstant.CURRENT_INSTANCE_ID, instanceId); + if (tag != null) { + builder.setHeader(RocketMQHeaders.TAGS, tag); + } + Message msg = builder.build(); + rocketMQTemplate.asyncSendOrderly( + tp, + msg, + orderKey, + new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + log.info("sendFifo onSuccess= {}", sendResult); + } + + @Override + public void onException(Throwable e) { + log.error( + "rocketmq sendFifo sendAsync ={} message={}", + ExceptionUtil.getSimpleMessage(e), + JSONUtil.toJsonStr(message)); + } + }); + } catch (Exception e) { + log.error( + "rocketmq send sendAsync ={} message={}", + ExceptionUtil.getSimpleMessage(e), + JSONUtil.toJsonStr(message)); + } + } + + /** + * 发送顺序消息(FIFO) + * + *

根据orderKey确保相同key的消息按顺序发送到同一个队列 适用于需要保证消息顺序的场景 + * + * @param topic 消息主题,如果为空则使用默认主题 + * @param orderKey 顺序键,相同key的消息会按顺序处理 + * @param message 要发送的消息对象 + */ + @Override + public void sendFifo(String topic, String orderKey, String message) { + sendFifo(topic, orderKey, message, null); + } + + /** + * 定时检测RocketMQ连接状态 + * + *

通过发送心跳消息检测RocketMQ服务的可用性 需要在RocketMQ控制台创建heartTopic主题 仅在生产环境启用,使用随机分钟数避免同时执行 + */ + @Profile("prod") + @Scheduled( + cron = "0 #{T(java.util.concurrent.ThreadLocalRandom).current().nextInt(1, 11)} * * * ?") + public void heartbeat() { + String message = UUID.randomUUID().toString(); + try { + rocketMQTemplate.asyncSend( + heartTopic, + message, + new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + log.info("rocketmq heartbeat onSuccess= {}", sendResult); + NetMonitor.freshConnectionStatus(nameServer, Boolean.TRUE); + } + + @Override + public void onException(Throwable throwable) { + log.info("rocketmq heartbeat onException= {}", ExceptionUtil.getRootCause(throwable)); + NetMonitor.freshConnectionStatus(nameServer, Boolean.FALSE); + } + }); + } catch (Exception e) { + log.error("rocketmq heartbeat send failure ={}", ExceptionUtil.getRootCause(e)); + NetMonitor.freshConnectionStatus(nameServer, Boolean.FALSE); + } + // NetMonitor.freshConnectionStatus(IOT_ROCKETMQ_EXIST_CONSUMER, + // checkIotConsumer(defaultTopic)); + } + + /** + * 查询主题的消费者信息 + * + *

获取指定主题的所有消费者组和客户端连接信息 用于监控消息消费状态和连接情况 + * + * @param defaultTopic 要查询的主题 + * @return 消费者组和客户端ID的映射关系 + */ + @Override + public HashMap> queryDefaultTopicExistConsumer(String defaultTopic) { + HashMap> consumerMap = new HashMap<>(); + try { + GroupList groupList = rocketMQAdmin.queryTopicConsumeByWho(defaultTopic); + for (String groupName : groupList.getGroupList()) { + ConsumerConnection consumerConnection = + rocketMQAdmin.examineConsumerConnectionInfo(groupName); + Set set = new HashSet<>(); + for (Connection connection : consumerConnection.getConnectionSet()) { + String clientId = connection.getClientId(); + set.add(clientId); + log.info("groupName={},其他信息={}", groupName, clientId); + } + + consumerMap.put(groupName, set); + } + log.info("rocketmq ,主题={},消费组信息={}", defaultTopic, consumerMap); + } catch (Exception err) { + log.info("op=getClientConnection_error" + err); + } + return consumerMap; + } + + /** + * 检查消费组是否存在 + * + *

检查指定主题是否有活跃的消费者组 如果没有消费者,则不推送消息以避免消息丢失 + * + * @param defaultTopic 要检查的主题 + * @return true表示有消费者,false表示没有消费者 + */ + private boolean checkIotConsumer(String defaultTopic) { + HashMap> consumerMap = queryDefaultTopicExistConsumer(defaultTopic); + return CollectionUtil.isNotEmpty(consumerMap); + } + + /** + * 刷新连接状态 + * + *

更新指定服务器的连接状态到监控器 + * + * @param serverURI 服务器URI + * @param flag 连接状态标志 + */ + private void freFreshConnectionStatus(String serverURI, Boolean flag) { + NetMonitor.freshConnectionStatus(serverURI, flag); + } + + /** + * 发送延迟消息 + * + *

发送延迟指定秒数的消息 适用于需要延迟处理的业务场景 + * + * @param topic 消息主题,如果为空则使用默认主题 + * @param message 要发送的消息对象 + * @param secondes 延迟秒数 + */ + @Override + public void sendDelaySeconds(String topic, Object message, long secondes) { + String tp = StrUtil.isBlank(topic) ? defaultTopic : topic; + try { + String instanceId = instanceIdProvider.getInstanceId(); + MessageBuilder builder = + MessageBuilder.withPayload(message) + .setHeader(IotConstant.CURRENT_INSTANCE_ID, instanceId); + Message msg = builder.build(); + SendResult sendResult = rocketMQTemplate.syncSendDelayTimeSeconds(tp, msg, secondes); + log.info("rocketmq 发送结果={}", sendResult); + } catch (Exception e) { + log.error( + "rocketmq send failure ={} message={}", + ExceptionUtil.getSimpleMessage(e), + JSONUtil.toJsonStr(message)); + } + } +} diff --git a/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/consumer/MultiFilterConsumer.java b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/consumer/MultiFilterConsumer.java new file mode 100644 index 0000000..53aeb0b --- /dev/null +++ b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/consumer/MultiFilterConsumer.java @@ -0,0 +1,190 @@ +// package cn.universal.rocketmq.consumer; +// +// import cn.hutool.json.JSONObject; +// import cn.hutool.json.JSONUtil; +// import cn.universal.protocol.tcp.util.RocketMQMessageUtil; +// import lombok.extern.slf4j.Slf4j; +// import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +// import org.apache.rocketmq.spring.core.RocketMQListener; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.stereotype.Component; +// +/// ** +// * 多过滤方案消费者示例 +// * 展示不同的消息过滤方案 +// * +// * @version 1.0 +// * @Author Aleo +// * @since 2025/1/27 +// */ +// @Component +// @Slf4j +// public class MultiFilterConsumer { +// +// @Autowired +// private InstanceIdProvider instanceIdProvider; +// +// @Autowired +// private RocketMQMessageUtil rocketMQMessageUtil; +// +// /** +// * 方案1: Tag过滤消费者(推荐) +// * 使用Tag过滤,性能好,配置简单 +// */ +// @Component +// @RocketMQMessageListener( +// topic = "tcp-message-topic", +// consumerGroup = "tcp-message-consumer-tag", +// selectorExpression = "!instance-*" // 排除所有instance-开头的Tag +// ) +// public static class TagFilterConsumer implements RocketMQListener { +// +// @Autowired +// private InstanceIdProvider instanceIdProvider; +// +// @Override +// public void onMessage(String message) { +// try { +// // 应用层过滤作为兜底方案 +// if (message != null && message.contains(IotConstant.CURRENT_INSTANCE_ID)) { +// String sourceId = extractSourceId(message); +// if (sourceId != null && instanceIdProvider.isOwnMessage(sourceId)) { +// log.debug("Tag过滤:跳过自己发出的消息: sourceId={}", sourceId); +// return; +// } +// } +// +// log.info("Tag过滤消费者收到消息: {}", message); +// // 处理消息 +// } catch (Exception e) { +// log.error("Tag过滤消费者处理消息失败: message={}, error={}", message, e.getMessage(), e); +// } +// } +// +// private String extractSourceId(String message) { +// try { +// int startIndex = message.indexOf("\"sourceId\":\""); +// if (startIndex != -1) { +// startIndex += 12; +// int endIndex = message.indexOf("\"", startIndex); +// if (endIndex != -1) { +// return message.substring(startIndex, endIndex); +// } +// } +// } catch (Exception e) { +// log.debug("提取sourceId失败: {}", e.getMessage()); +// } +// return null; +// } +// } +// +// /** +// * 方案2: 应用层过滤消费者 +// * 接收所有消息,在应用层过滤 +// */ +// @Component +// @RocketMQMessageListener( +// topic = "tcp-message-topic", +// consumerGroup = "tcp-message-consumer-app" +// ) +// public static class AppFilterConsumer implements RocketMQListener { +// +// @Autowired +// private InstanceIdProvider instanceIdProvider; +// +// @Override +// public void onMessage(String message) { +// try { +// // 解析消息 +// JSONObject messageJson = JSONUtil.parseObj(message); +// String sourceId = messageJson.getStr(IotConstant.CURRENT_INSTANCE_ID); +// +// // 应用层过滤:检查是否为自己的消息 +// if (instanceIdProvider.isOwnMessage(sourceId)) { +// log.debug("应用层过滤:跳过自己发出的消息: sourceId={}", sourceId); +// return; +// } +// +// log.info("应用层过滤消费者收到来自其他实例的消息: sourceId={}", sourceId); +// // 处理消息 +// +// } catch (Exception e) { +// log.error("处理消息失败: message={}, error={}", message, e.getMessage(), e); +// } +// } +// } +// +// /** +// * 方案3: 混合过滤消费者 +// * 使用Tag预过滤 + 应用层精确过滤 +// */ +// @Component +// @RocketMQMessageListener( +// topic = "tcp-message-topic", +// consumerGroup = "tcp-message-consumer-hybrid", +// selectorExpression = "business" // 只接收business标签的消息 +// ) +// public static class HybridFilterConsumer implements RocketMQListener { +// +// @Autowired +// private InstanceIdProvider instanceIdProvider; +// +// @Override +// public void onMessage(String message) { +// try { +// // 解析消息 +// JSONObject messageJson = JSONUtil.parseObj(message); +// String sourceId = messageJson.getStr(IotConstant.CURRENT_INSTANCE_ID); +// +// // 应用层精确过滤 +// if (instanceIdProvider.isOwnMessage(sourceId)) { +// log.debug("混合过滤:跳过自己发出的消息: sourceId={}", sourceId); +// return; +// } +// +// log.info("混合过滤消费者收到业务消息: sourceId={}", sourceId); +// // 处理消息 +// +// } catch (Exception e) { +// log.error("处理消息失败: message={}, error={}", message, e.getMessage(), e); +// } +// } +// } +// +// /** +// * 方案4: 广播消费者 +// * 接收所有消息,包括自己的(适用于某些特殊场景) +// */ +// @Component +// @RocketMQMessageListener( +// topic = "tcp-message-topic", +// consumerGroup = "tcp-message-consumer-broadcast", +// messageModel = org.apache.rocketmq.spring.annotation.MessageModel.BROADCASTING +// ) +// public static class BroadcastConsumer implements RocketMQListener { +// +// @Autowired +// private InstanceIdProvider instanceIdProvider; +// +// @Override +// public void onMessage(String message) { +// try { +// JSONObject messageJson = JSONUtil.parseObj(message); +// String sourceId = messageJson.getStr(IotConstant.CURRENT_INSTANCE_ID); +// +// log.info("广播消费者收到消息: sourceId={}, 当前实例={}", +// sourceId, instanceIdProvider.getSimpleInstanceId()); +// +// // 广播模式下可能需要处理自己的消息 +// if (instanceIdProvider.isOwnMessage(sourceId)) { +// log.info("广播模式:处理自己发出的消息"); +// } +// +// // 处理消息 +// +// } catch (Exception e) { +// log.error("处理消息失败: message={}, error={}", message, e.getMessage(), e); +// } +// } +// } +// } diff --git a/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/consumer/TcpMessageConsumer.java b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/consumer/TcpMessageConsumer.java new file mode 100644 index 0000000..f9f473c --- /dev/null +++ b/cn-universal-framework/cn-universal-rocketmq/src/main/java/cn/universal/rocketmq/consumer/TcpMessageConsumer.java @@ -0,0 +1,129 @@ +// package cn.universal.rocketmq.consumer; +// +// import cn.hutool.json.JSONObject; +// import cn.hutool.json.JSONUtil; +// import cn.universal.core.config.InstanceIdProvider; +// import cn.universal.protocol.tcp.util.RocketMQMessageUtil; +// import cn.universal.rocketmq.RocketMQFilterStrategy; +// import lombok.extern.slf4j.Slf4j; +// import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +// import org.apache.rocketmq.spring.annotation.SelectorType; +// import org.apache.rocketmq.spring.core.RocketMQListener; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.stereotype.Component; +// +/// ** +// * TCP消息消费者 用于处理来自其他实例的TCP相关消息 +// * +// * @version 1.0 +// * @Author Aleo +// * @since 2025/1/27 +// */ +// @Component +// @Slf4j +// @RocketMQMessageListener( +// topic = "tcp-message-topic", +// consumerGroup = "tcp-message-consumer-group", +// selectorType = SelectorType.SQL92, // 默认使用SQL92 +// selectorExpression = "sourceId != '${instanceIdProvider.instanceId}'" +// ) +// public class TcpMessageConsumer implements RocketMQListener { +// +// @Autowired +// private InstanceIdProvider instanceIdProvider; +// +// @Autowired +// private RocketMQFilterStrategy filterStrategy; +// +// @Autowired +// private RocketMQMessageUtil rocketMQMessageUtil; +// +// @Override +// public void onMessage(String message) { +// try { +// // 记录当前过滤策略 +// log.debug("当前过滤策略: {}", filterStrategy.getStrategyInfo()); +// +// // 解析消息 +// JSONObject messageJson = JSONUtil.parseObj(message); +// String sourceId = messageJson.getStr(IotConstant.CURRENT_INSTANCE_ID); +// String messageType = messageJson.getStr("messageType"); +// String payload = messageJson.getStr("payload"); +// +// // 应用层过滤作为兜底方案(防止SQL92配置错误) +// if (instanceIdProvider.isOwnMessage(sourceId)) { +// log.debug("跳过自己发出的消息: sourceId={}, messageType={}, 策略={}", +// sourceId, messageType, filterStrategy.getCurrentStrategy().name()); +// return; +// } +// +// log.info("收到来自其他实例的消息: sourceId={}, messageType={}, instanceId={}, 策略={}", +// sourceId, messageType, instanceIdProvider.getSimpleInstanceId(), +// filterStrategy.getCurrentStrategy().name()); +// +// // 根据消息类型处理 +// processMessageByType(messageType, payload, sourceId); +// +// } catch (Exception e) { +// log.error("处理消息失败: message={}, error={}", message, e.getMessage(), e); +// } +// } +// +// /** +// * 根据消息类型处理消息 +// * +// * @param messageType 消息类型 +// * @param payload 消息内容 +// * @param sourceId 消息源ID +// */ +// private void processMessageByType(String messageType, String payload, String sourceId) { +// switch (messageType) { +// case "DEVICE_CONNECT": +// handleDeviceConnect(payload, sourceId); +// break; +// case "DEVICE_DISCONNECT": +// handleDeviceDisconnect(payload, sourceId); +// break; +// case "DEVICE_MESSAGE": +// handleDeviceMessage(payload, sourceId); +// break; +// case "PRODUCT_CONFIG_UPDATE": +// handleProductConfigUpdate(payload, sourceId); +// break; +// default: +// log.warn("未知的消息类型: messageType={}", messageType); +// } +// } +// +// /** +// * 处理设备连接消息 +// */ +// private void handleDeviceConnect(String payload, String sourceId) { +// log.info("处理设备连接消息: payload={}, 来自实例={}", payload, sourceId); +// // 你的设备连接处理逻辑 +// } +// +// /** +// * 处理设备断开消息 +// */ +// private void handleDeviceDisconnect(String payload, String sourceId) { +// log.info("处理设备断开消息: payload={}, 来自实例={}", payload, sourceId); +// // 你的设备断开处理逻辑 +// } +// +// /** +// * 处理设备消息 +// */ +// private void handleDeviceMessage(String payload, String sourceId) { +// log.info("处理设备消息: payload={}, 来自实例={}", payload, sourceId); +// // 你的设备消息处理逻辑 +// } +// +// /** +// * 处理产品配置更新消息 +// */ +// private void handleProductConfigUpdate(String payload, String sourceId) { +// log.info("处理产品配置更新消息: payload={}, 来自实例={}", payload, sourceId); +// // 你的产品配置更新处理逻辑 +// } +// } diff --git a/cn-universal-framework/pom.xml b/cn-universal-framework/pom.xml new file mode 100644 index 0000000..278f571 --- /dev/null +++ b/cn-universal-framework/pom.xml @@ -0,0 +1,23 @@ + + + cn-universal-framework + 4.0.0 + pom + + cn-universal + cn.universal.iot + 1.5-SNAPSHOT + + + + cn-universal-common + cn-universal-core + cn-universal-exception + cn-universal-config + cn-universal-oss + cn-universal-dm + + + \ No newline at end of file diff --git a/cn-universal-infrastructure/cn-universal-cache/pom.xml b/cn-universal-infrastructure/cn-universal-cache/pom.xml new file mode 100644 index 0000000..67dfad5 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-cache/pom.xml @@ -0,0 +1,32 @@ + + + cn-universal-cache + 4.0.0 + + cn-universal-infrastructure + cn.universal.iot + 1.5-SNAPSHOT + + + + + com.github.ben-manes.caffeine + caffeine + + + org.aspectj + aspectjweaver + + + cn.universal.iot + cn-universal-common + ${project.version} + + + org.springframework.boot + spring-boot-starter-data-redis + + + \ No newline at end of file diff --git a/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/annotation/MultiLevelCacheable.java b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/annotation/MultiLevelCacheable.java new file mode 100644 index 0000000..0783b96 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/annotation/MultiLevelCacheable.java @@ -0,0 +1,71 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.cache.annotation; + +import cn.universal.cache.strategy.CacheStrategy; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.core.annotation.AliasFor; + +/** + * 多级缓存注解 支持L1本地缓存和L2分布式缓存的多级缓存策略 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Cacheable +public @interface MultiLevelCacheable { + + /** 缓存名称 */ + @AliasFor(annotation = Cacheable.class, attribute = "cacheNames") + String[] cacheNames() default {}; + + /** 缓存键 */ + @AliasFor(annotation = Cacheable.class, attribute = "key") + String key() default ""; + + /** 键生成器 */ + @AliasFor(annotation = Cacheable.class, attribute = "keyGenerator") + String keyGenerator() default ""; + + /** 缓存管理器 */ + @AliasFor(annotation = Cacheable.class, attribute = "cacheManager") + String cacheManager() default "multiLevelCacheManager"; + + /** 条件表达式 */ + @AliasFor(annotation = Cacheable.class, attribute = "condition") + String condition() default ""; + + /** 排除条件 */ + @AliasFor(annotation = Cacheable.class, attribute = "unless") + String unless() default ""; + + /** L1缓存过期时间(秒) */ + long l1Expire() default 300; + + /** L2缓存过期时间(秒) */ + long l2Expire() default 3600; + + /** 是否启用多级缓存 */ + boolean multiLevel() default true; + + /** 缓存策略 */ + CacheStrategy strategy() default CacheStrategy.WRITE_THROUGH; +} diff --git a/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/aspect/MultiLevelCacheAspect.java b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/aspect/MultiLevelCacheAspect.java new file mode 100644 index 0000000..d5a5006 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/aspect/MultiLevelCacheAspect.java @@ -0,0 +1,203 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.cache.aspect; + +import cn.universal.cache.annotation.MultiLevelCacheable; +import cn.universal.cache.config.MultiLevelCacheProperties; +import cn.universal.cache.impl.MultiLevelCache; +import java.lang.reflect.Method; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +/** + * 多级缓存切面 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +@Aspect +@Component +public class MultiLevelCacheAspect { + + @Autowired private CacheManager cacheManager; + + @Autowired private MultiLevelCacheProperties properties; + + private final ExpressionParser parser = new SpelExpressionParser(); + private final KeyGenerator keyGenerator = new SimpleKeyGenerator(); + + @Around("@annotation(multiLevelCacheable)") + public Object around(ProceedingJoinPoint joinPoint, MultiLevelCacheable multiLevelCacheable) + throws Throwable { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + + // 解析缓存名称 + String[] cacheNames = multiLevelCacheable.cacheNames(); + if (cacheNames.length == 0) { + log.warn("缓存名称未配置: {}", method.getName()); + return joinPoint.proceed(); + } + + // 解析缓存键 + String key = resolveKey(multiLevelCacheable, method, joinPoint.getArgs()); + if (key == null) { + log.warn("缓存键解析失败: {}", method.getName()); + return joinPoint.proceed(); + } + + // 检查条件 + if (!evaluateCondition(multiLevelCacheable.condition(), method, joinPoint.getArgs())) { + log.debug("缓存条件不满足,跳过缓存: {}", method.getName()); + return joinPoint.proceed(); + } + + // 获取缓存 + Cache cache = cacheManager.getCache(cacheNames[0]); + if (cache == null) { + log.warn("缓存未找到: {}", cacheNames[0]); + return joinPoint.proceed(); + } + + // 尝试从缓存获取 + Cache.ValueWrapper cachedValue = cache.get(key); + if (cachedValue != null) { + log.debug("缓存命中: {} -> {}", key, cacheNames[0]); + return cachedValue.get(); + } + + // 缓存未命中,执行方法 + log.debug("缓存未命中,执行方法: {} -> {}", key, cacheNames[0]); + Object result = joinPoint.proceed(); + + // 检查排除条件 + if (result != null + && !evaluateUnless(multiLevelCacheable.unless(), method, joinPoint.getArgs(), result)) { + // 设置缓存过期时间(如果支持) + setCacheExpiration(cache, key, multiLevelCacheable); + + // 写入缓存 + cache.put(key, result); + log.debug("缓存写入: {} -> {}", key, cacheNames[0]); + } + + return result; + } + + /** 解析缓存键 */ + private String resolveKey(MultiLevelCacheable annotation, Method method, Object[] args) { + String key = annotation.key(); + String keyGeneratorName = annotation.keyGenerator(); + + if (StringUtils.hasText(key)) { + // 使用SpEL表达式 + return evaluateExpression(key, method, args, String.class); + } else if (StringUtils.hasText(keyGeneratorName)) { + // 使用键生成器 + return keyGeneratorName + ":" + keyGenerator.generate(null, method, args); + } else { + // 使用默认键生成器 + return keyGenerator.generate(null, method, args).toString(); + } + } + + /** 评估条件表达式 */ + private boolean evaluateCondition(String condition, Method method, Object[] args) { + if (!StringUtils.hasText(condition)) { + return true; + } + + Boolean result = evaluateExpression(condition, method, args, Boolean.class); + return result != null && result; + } + + /** 评估排除条件表达式 */ + private boolean evaluateUnless(String unless, Method method, Object[] args, Object result) { + if (!StringUtils.hasText(unless)) { + return false; + } + + Boolean resultValue = evaluateExpression(unless, method, args, result, Boolean.class); + return resultValue != null && resultValue; + } + + /** 评估SpEL表达式 */ + private T evaluateExpression( + String expression, Method method, Object[] args, Class expectedType) { + return evaluateExpression(expression, method, args, null, expectedType); + } + + /** 评估SpEL表达式(带结果) */ + private T evaluateExpression( + String expression, Method method, Object[] args, Object result, Class expectedType) { + try { + Expression exp = parser.parseExpression(expression); + EvaluationContext context = new StandardEvaluationContext(); + + // 设置方法参数 + String[] paramNames = getParameterNames(method); + for (int i = 0; i < args.length && i < paramNames.length; i++) { + context.setVariable(paramNames[i], args[i]); + } + + // 设置结果 + if (result != null) { + context.setVariable("result", result); + } + + return exp.getValue(context, expectedType); + } catch (Exception e) { + log.error("表达式评估失败: {}", expression, e); + return null; + } + } + + /** 获取方法参数名 */ + private String[] getParameterNames(Method method) { + String[] names = new String[method.getParameterCount()]; + for (int i = 0; i < names.length; i++) { + names[i] = "p" + i; + } + return names; + } + + /** 设置缓存过期时间 */ + private void setCacheExpiration(Cache cache, Object key, MultiLevelCacheable annotation) { + // 这里可以根据具体的缓存实现来设置过期时间 + // 由于Spring Cache接口没有直接提供设置过期时间的方法, + // 需要在具体的缓存实现中处理 + if (cache instanceof MultiLevelCache) { + MultiLevelCache multiLevelCache = (MultiLevelCache) cache; + + // 可以通过反射或其他方式设置过期时间 + // 这里暂时跳过,因为需要在缓存实现中支持 + log.debug("缓存过期时间设置: L1={}s, L2={}s", annotation.l1Expire(), annotation.l2Expire()); + } + } +} diff --git a/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/config/CacheConfig.java b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/config/CacheConfig.java new file mode 100644 index 0000000..ce52f12 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/config/CacheConfig.java @@ -0,0 +1,324 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.cache.config; + +import cn.universal.cache.manager.MultiLevelCacheManager; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.SimpleCacheErrorHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.BatchStrategies; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * 多级缓存配置 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +@Configuration +@EnableConfigurationProperties(MultiLevelCacheProperties.class) +@ConditionalOnProperty( + prefix = "cache.multi-level", + name = "enabled", + havingValue = "true", + matchIfMissing = true) +public class CacheConfig { + + private final MultiLevelCacheProperties properties; + + public CacheConfig(MultiLevelCacheProperties properties) { + this.properties = properties; + } + + /** 创建全局Jackson配置的Redis序列化器,与RedisConfig保持一致 解决序列化配置不一致导致的类型冲突问题 */ + @Bean + public GenericJackson2JsonRedisSerializer redisJsonSerializer(ObjectMapper objectMapper) { + // 使用全局配置的ObjectMapper,确保与RedisConfig的一致性 + ObjectMapper cacheObjectMapper = objectMapper.copy(); + + // 启用类型信息,与RedisConfig保持一致 + cacheObjectMapper.activateDefaultTyping( + LaissezFaireSubTypeValidator.instance, + ObjectMapper.DefaultTyping.NON_FINAL, + JsonTypeInfo.As.PROPERTY); + + // 全局忽略未知字段 + cacheObjectMapper.configure( + com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + // 防御循环引用 + cacheObjectMapper.disable(SerializationFeature.FAIL_ON_SELF_REFERENCES); + + // 配置时间模块 + cacheObjectMapper.registerModule(new JavaTimeModule()); + cacheObjectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + // 全局配置:忽略所有未知字段 + cacheObjectMapper.addMixIn(Object.class, IgnoreUnknownPropertiesMixin.class); + + // 增强反序列化支持 + cacheObjectMapper.configure( + com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, + false); + cacheObjectMapper.configure( + com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, + false); + cacheObjectMapper.configure( + com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false); + cacheObjectMapper.configure( + com.fasterxml.jackson.databind.DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, + true); + cacheObjectMapper.configure( + com.fasterxml.jackson.databind.DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, + true); + + // 支持内部类反序列化 + cacheObjectMapper.configure( + com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false); + + // 配置序列化特性 + cacheObjectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + cacheObjectMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); + + log.info("✅ 统一Redis序列化器已配置:与RedisConfig保持一致,启用类型信息,增强反序列化支持"); + + return new GenericJackson2JsonRedisSerializer(cacheObjectMapper); + } + + /** L1缓存管理器(Caffeine) */ + @Bean("l1CacheManager") + @ConditionalOnProperty(prefix = "cache.multi-level", name = "enabled", havingValue = "true") + public CacheManager l1CacheManager() { + if (!properties.getL1().isEnabled()) { + log.warn("L1缓存已禁用"); + return null; + } + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + cacheManager.setCaffeine( + Caffeine.newBuilder() + .expireAfterWrite(properties.getL1().getExpireAfterWrite(), TimeUnit.SECONDS) + .expireAfterAccess(properties.getL1().getExpireAfterAccess(), TimeUnit.SECONDS) + .initialCapacity(properties.getL1().getInitialCapacity()) + .maximumSize(properties.getL1().getMaximumSize())); + + log.info( + "L1缓存管理器初始化完成,过期时间:{}秒,最大容量:{}", + properties.getL1().getExpireAfterWrite(), + properties.getL1().getMaximumSize()); + + return cacheManager; + } + + /** 修复 L2 缓存管理器:强制启用 SCAN 策略,使用统一的序列化配置 */ + @Bean("l2CacheManager") + @ConditionalOnProperty(prefix = "cache.multi-level", name = "enabled", havingValue = "true") + public CacheManager l2CacheManager( + RedisConnectionFactory redisConnectionFactory, + GenericJackson2JsonRedisSerializer redisJsonSerializer) { + if (!properties.getL2().isEnabled()) { + log.warn("L2缓存已禁用"); + return null; + } + + RedisCacheConfiguration config = + RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofSeconds(properties.getL2().getExpireAfterWrite())) + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(redisJsonSerializer)) + .disableCachingNullValues(); + + // 关键修复:注入 SCAN 策略的 RedisCacheWriter [1,6](@ref) + RedisCacheWriter cacheWriter = + RedisCacheWriter.lockingRedisCacheWriter( + redisConnectionFactory, BatchStrategies.scan(1000) // 每批扫描 1000 个键 + ); + + RedisCacheManager cacheManager = + RedisCacheManager.builder(cacheWriter) // 使用自定义 CacheWriter + .cacheDefaults(config) + .transactionAware() + .build(); + + log.info("✅ L2缓存管理器已启用SCAN策略,批次大小:1000,过期时间:{}秒", properties.getL2().getExpireAfterWrite()); + return cacheManager; + } + + // /** + // * L2缓存管理器(Redis) + // */ + // @Bean("l2CacheManager") + // @ConditionalOnProperty(prefix = "cache.multi-level", name = "enabled", havingValue = "true") + // public CacheManager l2CacheManager(RedisConnectionFactory redisConnectionFactory) { + // if (!properties.getL2().isEnabled()) { + // log.warn("L2缓存已禁用"); + // return null; + // } + // + // RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + // .entryTtl(Duration.ofSeconds(properties.getL2().getExpireAfterWrite())) + // .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new + // StringRedisSerializer())) + // .serializeValuesWith( + // RedisSerializationContext.SerializationPair.fromSerializer(new + // GenericJackson2JsonRedisSerializer())) + // .disableCachingNullValues(); + // + // // 强制使用 SCAN 策略,避免 KEYS 命令 + // RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) + // .cacheDefaults(config) + // .transactionAware() + // .withInitialCacheConfigurations(java.util.Map.of( + // "default", config + // )) + // .build(); + // + // log.info("L2缓存管理器初始化完成,过期时间:{}秒,键前缀:{},使用SCAN策略", + // properties.getL2().getExpireAfterWrite(), properties.getL2().getKeyPrefix()); + // + // return cacheManager; + // } + + /** 多级缓存管理器(主缓存管理器) 只有当多级缓存启用时才创建 */ + @Bean("multiLevelCacheManager") + @Primary + @ConditionalOnProperty(prefix = "cache.multi-level", name = "enabled", havingValue = "true") + public CacheManager multiLevelCacheManager() { + MultiLevelCacheManager cacheManager = new MultiLevelCacheManager(); + cacheManager.setProperties(properties); + log.info("多级缓存管理器初始化完成"); + return cacheManager; + } + + /** 默认缓存管理器(当多级缓存禁用时使用) 使用统一的序列化配置 */ + @Bean("defaultCacheManager") + @Primary + @ConditionalOnProperty(prefix = "cache.multi-level", name = "enabled", havingValue = "false") + public CacheManager defaultCacheManager( + RedisConnectionFactory redisConnectionFactory, + GenericJackson2JsonRedisSerializer redisJsonSerializer) { + RedisCacheConfiguration config = + RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofSeconds(3600)) // 默认1小时 + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(redisJsonSerializer)) + .disableCachingNullValues(); + + RedisCacheManager cacheManager = + RedisCacheManager.builder(redisConnectionFactory) + .cacheDefaults(config) + .transactionAware() + .build(); + + log.info("默认缓存管理器初始化完成(多级缓存已禁用)"); + return cacheManager; + } + + /** 全局缓存异常处理器,防止缓存异常影响主业务 */ + @Bean + public CacheErrorHandler errorHandler() { + return new SimpleCacheErrorHandler() { + @Override + public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) { + // 1. 区分反序列化错误和其他错误 + if (exception instanceof SerializationException) { + log.error("[Cache] 反序列化失败 → 触发降级查询. Key={}, Cache={}", key, cache.getName(), exception); + // 关键降级:返回null使Spring Cache调用原方法 + } + // 其他错误仅记录日志 + log.warn("[Cache] Get操作失败: {} Key={}", exception.getMessage(), key); + } + + @Override + public void handleCachePutError( + RuntimeException exception, Cache cache, Object key, Object value) { + // 写入失败时仅告警(防止坏数据污染缓存) + log.warn("[Cache] Put操作失败: {} Key={}", exception.getMessage(), key); + } + + @Override + public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) { + // 清除失败不影响主流程 + log.warn("[Cache] Evict操作失败: {} Key={}", exception.getMessage(), key); + } + + @Override + public void handleCacheClearError(RuntimeException exception, Cache cache) { + log.warn("[Cache] Clear操作失败: {}", exception.getMessage()); + // 识别KEYS命令阻塞问题(如Redis集群限制) + if (exception.getMessage().contains("KEYS")) { + log.error("Redis禁止KEYS命令! 请改用SCAN策略[8](@ref)"); + } + } + }; + } +} + +/** 全局忽略未知字段的Mixin */ +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class IgnoreUnknownPropertiesMixin {} + +/** 安全的对象反序列化器,处理反序列化异常 */ +@Slf4j +class SafeObjectDeserializer extends StdDeserializer { + + public SafeObjectDeserializer() { + super(Object.class); + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + try { + // 尝试正常反序列化 + return ctxt.readValue(p, Object.class); + } catch (Exception e) { + log.warn("反序列化失败,返回null: {}", e.getMessage()); + // 反序列化失败时返回null,而不是抛出异常 + return null; + } + } +} diff --git a/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/config/MultiLevelCacheProperties.java b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/config/MultiLevelCacheProperties.java new file mode 100644 index 0000000..5b69efc --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/config/MultiLevelCacheProperties.java @@ -0,0 +1,91 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.cache.config; + +import cn.universal.cache.strategy.CacheStrategy; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 多级缓存配置属性 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Data +@ConfigurationProperties(prefix = "cache.multi-level") +public class MultiLevelCacheProperties { + + /** 多级缓存总开关 true: 启用多级缓存(L1 + L2) false: 禁用多级缓存,使用默认的 Spring Cache 策略 */ + private boolean enabled = true; + + /** L1缓存配置(本地缓存) */ + private L1Config l1 = new L1Config(); + + /** L2缓存配置(分布式缓存) */ + private L2Config l2 = new L2Config(); + + /** 默认配置 */ + private DefaultConfig defaults = new DefaultConfig(); + + /** L1缓存配置 */ + @Data + public static class L1Config { + + /** 写入后过期时间(秒) */ + private long expireAfterWrite = 300; + + /** 访问后过期时间(秒) */ + private long expireAfterAccess = 300; + + /** 初始容量 */ + private int initialCapacity = 100; + + /** 最大容量 */ + private int maximumSize = 5000; + + /** 是否启用L1缓存 */ + private boolean enabled = true; + } + + /** L2缓存配置 */ + @Data + public static class L2Config { + + /** 写入后过期时间(秒) */ + private long expireAfterWrite = 3600; + + /** 键前缀 */ + private String keyPrefix = "mlc:"; + + /** 是否启用L2缓存 */ + private boolean enabled = true; + } + + /** 默认配置 */ + @Data + public static class DefaultConfig { + + /** 默认缓存策略 */ + private CacheStrategy strategy = CacheStrategy.WRITE_THROUGH; + + /** 是否启用多级缓存 */ + private boolean enableMultiLevel = true; + + /** 默认L1过期时间(秒) */ + private long defaultL1Expire = 300; + + /** 默认L2过期时间(秒) */ + private long defaultL2Expire = 3600; + } +} diff --git a/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/example/MultiLevelCacheExample.java b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/example/MultiLevelCacheExample.java new file mode 100644 index 0000000..4c12274 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/example/MultiLevelCacheExample.java @@ -0,0 +1,232 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.cache.example; + +import cn.universal.cache.annotation.MultiLevelCacheable; +import cn.universal.cache.strategy.CacheStrategy; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 多级缓存使用示例 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +@Service +public class MultiLevelCacheExample { + + private final Map userDatabase = new HashMap<>(); + private final Map productDatabase = new HashMap<>(); + + public MultiLevelCacheExample() { + // 初始化示例数据 + userDatabase.put("user1", new User("user1", "张三", 25)); + userDatabase.put("user2", new User("user2", "李四", 30)); + userDatabase.put("user3", new User("user3", "王五", 28)); + + productDatabase.put("prod1", new Product("prod1", "iPhone 15", 5999.0)); + productDatabase.put("prod2", new Product("prod2", "MacBook Pro", 12999.0)); + productDatabase.put("prod3", new Product("prod3", "AirPods Pro", 1999.0)); + } + + /** 基本使用示例 L1缓存5分钟,L2缓存1小时 */ + @MultiLevelCacheable( + cacheNames = "user_info", + key = "#userId", + l1Expire = 300, + l2Expire = 3600, + strategy = CacheStrategy.WRITE_THROUGH) + public User getUserById(String userId) { + log.info("从数据库查询用户: {}", userId); + return userDatabase.get(userId); + } + + /** 条件缓存示例 只有当userId不为空且结果不为null时才缓存 */ + @MultiLevelCacheable( + cacheNames = "user_info", + key = "#userId", + condition = "#userId != null", + unless = "#result == null", + l1Expire = 600, + l2Expire = 7200) + public User getUserByIdWithCondition(String userId) { + log.info("从数据库查询用户(条件缓存): {}", userId); + return userDatabase.get(userId); + } + + /** 产品信息缓存示例 使用WRITE_BEHIND策略,异步写入L2缓存 */ + @MultiLevelCacheable( + cacheNames = "product_info", + key = "#productId", + l1Expire = 1800, + l2Expire = 86400, + strategy = CacheStrategy.WRITE_BEHIND) + public Product getProductById(String productId) { + log.info("从数据库查询产品: {}", productId); + return productDatabase.get(productId); + } + + /** 复杂键生成示例 使用多个参数生成缓存键 */ + @MultiLevelCacheable( + cacheNames = "user_product", + key = "#userId + ':' + #productId", + l1Expire = 900, + l2Expire = 3600) + public UserProduct getUserProduct(String userId, String productId) { + log.info("从数据库查询用户产品关系: {} - {}", userId, productId); + User user = userDatabase.get(userId); + Product product = productDatabase.get(productId); + if (user != null && product != null) { + return new UserProduct(user, product); + } + return null; + } + + /** 只使用L2缓存的示例 适合内存受限的场景 */ + @MultiLevelCacheable( + cacheNames = "product_list", + key = "'all_products'", + l1Expire = 0, // 禁用L1缓存 + l2Expire = 1800, + strategy = CacheStrategy.WRITE_AROUND) + public Map getAllProducts() { + log.info("从数据库查询所有产品"); + return new HashMap<>(productDatabase); + } + + /** 用户实体类 */ + public static class User { + + private String id; + private String name; + private int age; + + public User(String id, String name, int age) { + this.id = id; + this.name = name; + this.age = age; + } + + // Getters and Setters + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public String toString() { + return "User{id='" + id + "', name='" + name + "', age=" + age + "}"; + } + } + + /** 产品实体类 */ + public static class Product { + + private String id; + private String name; + private double price; + + public Product(String id, String name, double price) { + this.id = id; + this.name = name; + this.price = price; + } + + // Getters and Setters + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + @Override + public String toString() { + return "Product{id='" + id + "', name='" + name + "', price=" + price + "}"; + } + } + + /** 用户产品关系实体类 */ + public static class UserProduct { + + private User user; + private Product product; + + public UserProduct(User user, Product product) { + this.user = user; + this.product = product; + } + + // Getters and Setters + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Product getProduct() { + return product; + } + + public void setProduct(Product product) { + this.product = product; + } + + @Override + public String toString() { + return "UserProduct{user=" + user + ", product=" + product + "}"; + } + } +} diff --git a/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/impl/MultiLevelCache.java b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/impl/MultiLevelCache.java new file mode 100644 index 0000000..6ae51ed --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/impl/MultiLevelCache.java @@ -0,0 +1,369 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.cache.impl; + +import cn.universal.cache.config.MultiLevelCacheProperties; +import cn.universal.cache.statistics.CacheStatistics; +import cn.universal.cache.strategy.CacheStrategy; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.Cache; +import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; + +/** + * 多级缓存实现 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +public class MultiLevelCache implements Cache { + + private final String name; + private final Cache l1Cache; + private final Cache l2Cache; + private final MultiLevelCacheProperties properties; + private final Executor asyncExecutor; + private final CacheStatistics statistics = new CacheStatistics(); + + public MultiLevelCache( + String name, Cache l1Cache, Cache l2Cache, MultiLevelCacheProperties properties) { + this.name = name; + this.l1Cache = l1Cache; + this.l2Cache = l2Cache; + this.properties = properties; + this.asyncExecutor = + Executors.newFixedThreadPool( + 2, + r -> { + Thread t = new Thread(r, "multi-level-cache-async-" + name); + t.setDaemon(true); + return t; + }); + } + + @Override + public String getName() { + return name; + } + + @Override + public Object getNativeCache() { + return this; + } + + @Override + public ValueWrapper get(Object key) { + if (key == null) { + return null; + } + String cacheKey = key.toString(); + try { + if (l1Cache != null) { + ValueWrapper l1Value = l1Cache.get(key); + if (l1Value != null) { + statistics.recordL1Hit(); + log.debug("L1缓存命中: {} -> {}", cacheKey, name); + return l1Value; + } else { + statistics.recordL1Miss(); + } + } + } catch (Exception e) { + log.warn("L1缓存 get 异常: {} -> {}", cacheKey, name, e); + } + try { + if (l2Cache != null) { + ValueWrapper l2Value = l2Cache.get(key); + if (l2Value != null) { + statistics.recordL2Hit(); + if (l1Cache != null) { + try { + l1Cache.put(key, l2Value.get()); + log.debug("L2缓存命中,回填L1: {} -> {}", cacheKey, name); + } catch (Exception e) { + log.warn("L1缓存回填异常: {} -> {}", cacheKey, name, e); + } + } + return l2Value; + } else { + statistics.recordL2Miss(); + } + } + } catch (Exception e) { + log.warn("L2缓存 get 异常: {} -> {}", cacheKey, name, e); + } + log.debug("缓存未命中: {} -> {}", cacheKey, name); + return null; + } + + @Override + public T get(Object key, Class type) { + try { + ValueWrapper value = get(key); + if (value != null && type.isInstance(value.get())) { + return type.cast(value.get()); + } + } catch (Exception e) { + log.warn("缓存 get(Class) 异常: {} -> {}", key, name, e); + } + return null; + } + + @Override + public T get(Object key, Callable valueLoader) { + if (key == null) { + return null; + } + String cacheKey = key.toString(); + try { + ValueWrapper value = get(key); + if (value != null) { + @SuppressWarnings("unchecked") + T result = (T) value.get(); + return result; + } + } catch (Exception e) { + log.warn("缓存 get(Callable) 异常: {} -> {}", cacheKey, name, e); + } + try { + T loadedValue = valueLoader.call(); + if (loadedValue != null) { + try { + put(key, loadedValue); + log.debug("使用valueLoader加载数据并缓存: {} -> {}", cacheKey, name); + } catch (Exception e) { + log.warn("缓存 put 异常: {} -> {}", cacheKey, name, e); + } + } + return loadedValue; + } catch (Exception e) { + log.error("使用valueLoader加载数据失败: {} -> {}", cacheKey, name, e); + return null; + } + } + + @Override + public void put(Object key, Object value) { + if (key == null || value == null) { + return; + } + String cacheKey = key.toString(); + CacheStrategy strategy = properties.getDefaults().getStrategy(); + try { + switch (strategy) { + case WRITE_THROUGH: + if (l1Cache != null) { + try { + l1Cache.put(key, value); + statistics.recordL1Put(); + } catch (Exception e) { + log.warn("L1 put 异常: {} -> {}", cacheKey, name, e); + } + } + if (l2Cache != null) { + try { + l2Cache.put(key, value); + statistics.recordL2Put(); + } catch (Exception e) { + log.warn("L2 put 异常: {} -> {}", cacheKey, name, e); + } + } + log.debug("WRITE_THROUGH策略写入: {} -> {}", cacheKey, name); + break; + case WRITE_BEHIND: + if (l1Cache != null) { + try { + l1Cache.put(key, value); + statistics.recordL1Put(); + } catch (Exception e) { + log.warn("L1 put 异常: {} -> {}", cacheKey, name, e); + } + } + if (l2Cache != null) { + CompletableFuture.runAsync( + () -> { + try { + l2Cache.put(key, value); + statistics.recordL2Put(); + log.debug("WRITE_BEHIND策略异步写入L2: {} -> {}", cacheKey, name); + } catch (Exception e) { + log.warn("异步写入L2缓存失败: {} -> {}", cacheKey, name, e); + } + }, + asyncExecutor); + } + log.debug("WRITE_BEHIND策略写入L1: {} -> {}", cacheKey, name); + break; + case WRITE_AROUND: + if (l2Cache != null) { + try { + l2Cache.put(key, value); + statistics.recordL2Put(); + log.debug("WRITE_AROUND策略写入L2: {} -> {}", cacheKey, name); + } catch (Exception e) { + log.warn("L2 put 异常: {} -> {}", cacheKey, name, e); + } + } + break; + } + } catch (Exception e) { + log.warn("缓存 put 异常: {} -> {}", cacheKey, name, e); + } + } + + @Override + public ValueWrapper putIfAbsent(Object key, Object value) { + if (key == null || value == null) { + return null; + } + try { + ValueWrapper existing = get(key); + if (existing != null) { + return existing; + } + put(key, value); + return new SimpleValueWrapper(value); + } catch (Exception e) { + log.warn("缓存 putIfAbsent 异常: {} -> {}", key, name, e); + return null; + } + } + + @Override + public void evict(Object key) { + if (key == null) { + return; + } + String cacheKey = key.toString(); + try { + if (l1Cache != null) { + try { + l1Cache.evict(key); + statistics.recordL1Evict(); + } catch (Exception e) { + log.warn("L1 evict 异常: {} -> {}", cacheKey, name, e); + } + } + if (l2Cache != null) { + try { + l2Cache.evict(key); + statistics.recordL2Evict(); + } catch (Exception e) { + log.warn("L2 evict 异常: {} -> {}", cacheKey, name, e); + } + } + log.debug("缓存驱逐: {} -> {}", cacheKey, name); + } catch (Exception e) { + log.warn("缓存 evict 异常: {} -> {}", cacheKey, name, e); + } + } + + @Override + public void clear() { + try { + if (l1Cache != null) { + try { + l1Cache.clear(); + log.debug("L1缓存清空成功: {}", name); + } catch (Exception e) { + log.warn("L1缓存清空失败: {}", name, e); + } + } + if (l2Cache != null) { + try { + if (l2Cache.getNativeCache() instanceof RedisTemplate) { + RedisTemplate redisTemplate = (RedisTemplate) l2Cache.getNativeCache(); + String pattern = name + "::*"; + RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); + try { + Set keys = new HashSet<>(); + Cursor cursor = + connection.scan(ScanOptions.scanOptions().match(pattern).count(100).build()); + while (cursor.hasNext()) { + keys.add(cursor.next()); + } + if (!keys.isEmpty()) { + connection.del(keys.toArray(new byte[0][])); + log.debug("L2缓存使用SCAN清空成功,删除{}个键: {}", keys.size(), name); + } else { + log.debug("L2缓存没有找到匹配的键: {}", name); + } + } finally { + connection.close(); + } + } else { + l2Cache.clear(); + log.debug("L2缓存使用默认方式清空成功: {}", name); + } + } catch (Exception e) { + log.warn("L2缓存清空失败,尝试使用默认方式: {}", name, e); + try { + l2Cache.clear(); + log.debug("L2缓存降级清空成功: {}", name); + } catch (Exception fallbackException) { + log.error("L2缓存降级清空也失败: {}", name, fallbackException); + } + } + } + log.info("缓存清空操作完成: {}", name); + } catch (Exception e) { + log.error("缓存清空操作失败: {}", name, e); + } + } + + /** 获取L1缓存 */ + public Cache getL1Cache() { + return l1Cache; + } + + /** 获取L2缓存 */ + public Cache getL2Cache() { + return l2Cache; + } + + /** 预热缓存 */ + public void warmUp(Object key, Object value) { + if (key == null || value == null) { + return; + } + + String cacheKey = key.toString(); + + // 预热L1缓存 + if (l1Cache != null) { + l1Cache.put(key, value); + log.debug("L1缓存预热: {} -> {}", cacheKey, name); + } + + // 预热L2缓存 + if (l2Cache != null) { + l2Cache.put(key, value); + log.debug("L2缓存预热: {} -> {}", cacheKey, name); + } + } + + /** 获取缓存统计信息 */ + public CacheStatistics getCacheStatistics() { + return statistics; + } +} diff --git a/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/manager/MultiLevelCacheManager.java b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/manager/MultiLevelCacheManager.java new file mode 100644 index 0000000..b7ecf6e --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/manager/MultiLevelCacheManager.java @@ -0,0 +1,138 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.cache.manager; + +import cn.universal.cache.config.MultiLevelCacheProperties; +import cn.universal.cache.impl.MultiLevelCache; +import cn.universal.cache.statistics.CacheStatistics; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +/** + * 多级缓存管理器 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +public class MultiLevelCacheManager implements CacheManager { + + private final ConcurrentMap caches = new ConcurrentHashMap<>(); + + @Autowired(required = false) + @Qualifier("l1CacheManager") + private CacheManager l1CacheManager; + + @Autowired(required = false) + @Qualifier("l2CacheManager") + private CacheManager l2CacheManager; + + private MultiLevelCacheProperties properties; + + public void setProperties(MultiLevelCacheProperties properties) { + this.properties = properties; + } + + @Override + public Cache getCache(String name) { + return caches.computeIfAbsent(name, this::createMultiLevelCache); + } + + @Override + public Collection getCacheNames() { + return caches.keySet(); + } + + /** 创建多级缓存 */ + private MultiLevelCache createMultiLevelCache(String name) { + Cache l1Cache = null; + Cache l2Cache = null; + + // 获取L1缓存 + if (l1CacheManager != null) { + l1Cache = l1CacheManager.getCache(name); + log.debug("L1缓存创建成功: {}", name); + } else { + log.warn("L1缓存管理器未配置,跳过L1缓存"); + } + + // 获取L2缓存 + if (l2CacheManager != null) { + l2Cache = l2CacheManager.getCache(name); + log.debug("L2缓存创建成功: {}", name); + } else { + log.warn("L2缓存管理器未配置,跳过L2缓存"); + } + + // 如果两个缓存都未配置,抛出异常 + if (l1Cache == null && l2Cache == null) { + throw new IllegalStateException("至少需要配置一个缓存管理器(L1或L2)"); + } + + MultiLevelCache multiLevelCache = new MultiLevelCache(name, l1Cache, l2Cache, properties); + log.info( + "多级缓存创建成功: {}, L1: {}, L2: {}", + name, + l1Cache != null ? "启用" : "禁用", + l2Cache != null ? "启用" : "禁用"); + + return multiLevelCache; + } + + /** 清除指定缓存 */ + public void evictCache(String name) { + MultiLevelCache cache = caches.remove(name); + if (cache != null) { + cache.clear(); + log.info("缓存已清除: {}", name); + } + } + + /** 清除所有缓存 */ + public void evictAllCaches() { + caches.values().forEach(Cache::clear); + caches.clear(); + log.info("所有缓存已清除"); + } + + /** 获取指定缓存的统计信息 */ + public CacheStatistics getCacheStatistics(String name) { + MultiLevelCache cache = caches.get(name); + if (cache != null) { + return cache.getCacheStatistics(); + } + throw new IllegalArgumentException("缓存不存在: " + name); + } + + /** 获取所有缓存的统计信息 */ + public Map getCacheStatistics() { + Map statistics = new HashMap<>(); + for (Map.Entry entry : caches.entrySet()) { + statistics.put(entry.getKey(), entry.getValue().getCacheStatistics()); + } + return statistics; + } + + /** 清除所有缓存(别名方法) */ + public void clearAll() { + evictAllCaches(); + } +} diff --git a/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/package-info.java b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/package-info.java new file mode 100644 index 0000000..351494e --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/package-info.java @@ -0,0 +1,11 @@ +/* + * + * Copyright (c) 2025, IoT-universal. All Rights Reserved. + * + * @Description: 本文件由 AleoXin 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: AleoXin + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ package cn.universal.cache; diff --git a/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/statistics/CacheStatistics.java b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/statistics/CacheStatistics.java new file mode 100644 index 0000000..a65dc09 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/statistics/CacheStatistics.java @@ -0,0 +1,170 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.cache.statistics; + +import java.util.concurrent.atomic.AtomicLong; +import lombok.extern.slf4j.Slf4j; + +/** + * 缓存统计信息 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +public class CacheStatistics { + + private final AtomicLong l1Hits = new AtomicLong(0); + private final AtomicLong l1Misses = new AtomicLong(0); + private final AtomicLong l1Puts = new AtomicLong(0); + private final AtomicLong l1Evicts = new AtomicLong(0); + + private final AtomicLong l2Hits = new AtomicLong(0); + private final AtomicLong l2Misses = new AtomicLong(0); + private final AtomicLong l2Puts = new AtomicLong(0); + private final AtomicLong l2Evicts = new AtomicLong(0); + + /** 记录L1缓存命中 */ + public void recordL1Hit() { + l1Hits.incrementAndGet(); + } + + /** 记录L1缓存未命中 */ + public void recordL1Miss() { + l1Misses.incrementAndGet(); + } + + /** 记录L1缓存写入 */ + public void recordL1Put() { + l1Puts.incrementAndGet(); + } + + /** 记录L1缓存驱逐 */ + public void recordL1Evict() { + l1Evicts.incrementAndGet(); + } + + /** 记录L2缓存命中 */ + public void recordL2Hit() { + l2Hits.incrementAndGet(); + } + + /** 记录L2缓存未命中 */ + public void recordL2Miss() { + l2Misses.incrementAndGet(); + } + + /** 记录L2缓存写入 */ + public void recordL2Put() { + l2Puts.incrementAndGet(); + } + + /** 记录L2缓存驱逐 */ + public void recordL2Evict() { + l2Evicts.incrementAndGet(); + } + + public long getL1Hits() { + return l1Hits.get(); + } + + public long getL1Misses() { + return l1Misses.get(); + } + + public long getL1Puts() { + return l1Puts.get(); + } + + public long getL1Evicts() { + return l1Evicts.get(); + } + + public long getL2Hits() { + return l2Hits.get(); + } + + public long getL2Misses() { + return l2Misses.get(); + } + + public long getL2Puts() { + return l2Puts.get(); + } + + public long getL2Evicts() { + return l2Evicts.get(); + } + + public double getL1HitRate() { + long hits = l1Hits.get(); + long misses = l1Misses.get(); + long total = hits + misses; + return total > 0 ? (double) hits / total : 0.0; + } + + public double getL2HitRate() { + long hits = l2Hits.get(); + long misses = l2Misses.get(); + long total = hits + misses; + return total > 0 ? (double) hits / total : 0.0; + } + + public long getTotalHits() { + return l1Hits.get() + l2Hits.get(); + } + + public long getTotalMisses() { + return l1Misses.get() + l2Misses.get(); + } + + public double getOverallHitRate() { + long totalHits = getTotalHits(); + long totalMisses = getTotalMisses(); + long total = totalHits + totalMisses; + return total > 0 ? (double) totalHits / total : 0.0; + } + + /** 重置统计信息 */ + public void reset() { + l1Hits.set(0); + l1Misses.set(0); + l1Puts.set(0); + l1Evicts.set(0); + l2Hits.set(0); + l2Misses.set(0); + l2Puts.set(0); + l2Evicts.set(0); + } + + @Override + public String toString() { + return String.format( + "CacheStatistics{L1: hits=%d, misses=%d, hitRate=%.2f%%, puts=%d, evicts=%d; " + + "L2: hits=%d, misses=%d, hitRate=%.2f%%, puts=%d, evicts=%d; " + + "Overall: hits=%d, misses=%d, hitRate=%.2f%%}", + l1Hits.get(), + l1Misses.get(), + getL1HitRate() * 100, + l1Puts.get(), + l1Evicts.get(), + l2Hits.get(), + l2Misses.get(), + getL2HitRate() * 100, + l2Puts.get(), + l2Evicts.get(), + getTotalHits(), + getTotalMisses(), + getOverallHitRate() * 100); + } +} diff --git a/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/strategy/CacheStrategy.java b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/strategy/CacheStrategy.java new file mode 100644 index 0000000..6d2a15b --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-cache/src/main/java/cn/universal/cache/strategy/CacheStrategy.java @@ -0,0 +1,31 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.cache.strategy; + +/** + * 缓存策略枚举 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +public enum CacheStrategy { + + /** 直写策略:同步写入所有缓存级别 优点:一致性最好 缺点:写入性能较低 */ + WRITE_THROUGH, + + /** 回写策略:异步写入L2缓存 优点:写入性能最好 缺点:可能存在短暂的数据不一致 */ + WRITE_BEHIND, + + /** 绕写策略:只写L2缓存,L1通过读取回填 优点:内存友好,适合读多写少的场景 缺点:首次读取需要回填L1 */ + WRITE_AROUND +} diff --git a/cn-universal-infrastructure/cn-universal-database/pom.xml b/cn-universal-infrastructure/cn-universal-database/pom.xml new file mode 100644 index 0000000..eb6a74c --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-database/pom.xml @@ -0,0 +1,45 @@ + + + cn-universal-database + 4.0.0 + + cn-universal-infrastructure + cn.universal.iot + 1.5-SNAPSHOT + + + + + + cn.universal.iot + cn-universal-common + ${project.version} + + + + + org.springframework.boot + spring-boot-starter + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + + + + + mysql + mysql-connector-java + + + + + com.alibaba + druid-spring-boot-starter + + + \ No newline at end of file diff --git a/cn-universal-infrastructure/cn-universal-message/pom.xml b/cn-universal-infrastructure/cn-universal-message/pom.xml new file mode 100644 index 0000000..2903af6 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-message/pom.xml @@ -0,0 +1,24 @@ + + + cn-universal-message + 4.0.0 + + cn-universal-infrastructure + cn.universal.iot + 1.5-SNAPSHOT + + + + + cn.universal.iot + cn-universal-common + ${project.version} + + + org.springframework.boot + spring-boot-starter + + + \ No newline at end of file diff --git a/cn-universal-infrastructure/cn-universal-security/pom.xml b/cn-universal-infrastructure/cn-universal-security/pom.xml new file mode 100644 index 0000000..8747a37 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/pom.xml @@ -0,0 +1,80 @@ + + + cn-universal-security + 4.0.0 + + cn-universal-infrastructure + cn.universal.iot + 1.5-SNAPSHOT + + + + + cn.universal.iot + cn-universal-common + ${project.version} + + + + org.springframework.boot + spring-boot-starter-security + + + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-logging + + + org.springframework.security + spring-security-oauth2-authorization-server + + + commons-logging + commons-logging + + + + + org.springframework.boot + spring-boot-starter-undertow + + + cn.universal.iot + cn-universal-persistence + ${project.version} + + + spring-boot-starter-web + + + slf4j-log4j12 + org.slf4j + + + commons-logging + commons-logging + + + org.springframework.boot + spring-boot-starter-tomcat + + + + org.springframework.boot + + + cn.universal.iot + cn-universal-admin + ${project.version} + compile + + + \ No newline at end of file diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/AuthorizationServerConfig.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/AuthorizationServerConfig.java new file mode 100644 index 0000000..b2d1699 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/AuthorizationServerConfig.java @@ -0,0 +1,78 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 AleoXin 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: AleoXin + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security; + +import cn.universal.security.converter.OAuth2CaptchaAuthenticationConverter; +import cn.universal.security.provider.OAuth2CaptchaAuthenticationProvider; +import cn.universal.security.provider.PasswordAuthenticationProvider; +import java.util.ArrayList; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationConverter; + +@Configuration +public class AuthorizationServerConfig { + + @Bean + @Order(2) + public SecurityFilterChain authorizationServerSecurityFilterChain( + HttpSecurity http, + OAuth2CaptchaAuthenticationProvider captchaProvider, + @Autowired(required = false) PasswordAuthenticationProvider passwordProvider, + @Autowired(required = false) AuthenticationConverter passwordConverter, + OAuth2ErrorHandler oauth2ErrorHandler) + throws Exception { + + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + + // 构建认证转换器列表 + var converters = new ArrayList(); + converters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter()); + converters.add(new OAuth2RefreshTokenAuthenticationConverter()); + converters.add(new OAuth2ClientCredentialsAuthenticationConverter()); + + // 添加密码模式转换器(如果可用) + if (passwordConverter != null) { + converters.add(passwordConverter); + } + + // 添加验证码转换器 + converters.add(new OAuth2CaptchaAuthenticationConverter()); + + http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) + .tokenEndpoint( + tokenEndpoint -> { + tokenEndpoint.accessTokenRequestConverter( + new DelegatingAuthenticationConverter(converters)); + tokenEndpoint.authenticationProvider(captchaProvider); + tokenEndpoint.errorResponseHandler(oauth2ErrorHandler); + + // 添加密码模式提供者(如果可用) + if (passwordProvider != null) { + tokenEndpoint.authenticationProvider(passwordProvider); + } + }); + + return http.build(); + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/CustomClientRegistrationService.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/CustomClientRegistrationService.java new file mode 100644 index 0000000..dc9ccf5 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/CustomClientRegistrationService.java @@ -0,0 +1,269 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 AleoXin 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: AleoXin + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security; + +import cn.universal.persistence.entity.OAuth2ClientDetails; +import cn.universal.persistence.mapper.OauthClientDetailsMapper; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +@Service +@Primary +public class CustomClientRegistrationService implements RegisteredClientRepository { + + private static final Logger logger = + LoggerFactory.getLogger(CustomClientRegistrationService.class); + private String DEGAULT_CLIENT_ID = "web"; + private final OauthClientDetailsMapper oauthClientDetailsMapper; + private final InMemoryRegisteredClientRepository fallbackRepository; + + public CustomClientRegistrationService( + @Autowired(required = false) OauthClientDetailsMapper oauthClientDetailsMapper) { + this.oauthClientDetailsMapper = oauthClientDetailsMapper; + + logger.info( + "CustomClientRegistrationService 初始化,oauthClientDetailsMapper: {}", + oauthClientDetailsMapper != null ? "可用" : "不可用"); + + // 创建回退的内存客户端配置,保持与原有系统的兼容性 + RegisteredClient univIotClient = + RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("web") + .clientSecret("{noop}web") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .authorizationGrantType(new AuthorizationGrantType("password")) + .authorizationGrantType(new AuthorizationGrantType("captcha")) + .authorizationGrantType(new AuthorizationGrantType("sms")) + .redirectUri("http://127.0.0.1:8080/login/oauth2/code/cn-universal-client") + .redirectUri("http://127.0.0.1:8080/authorized") + .scope("read") + .scope("write") + .scope("all") + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build()) + .tokenSettings( + TokenSettings.builder() + .accessTokenTimeToLive(Duration.ofHours(1)) + .refreshTokenTimeToLive(Duration.ofDays(30)) + .build()) + .build(); + + this.fallbackRepository = new InMemoryRegisteredClientRepository(univIotClient); + logger.info("内存回退客户端配置已创建,client_id: web"); + } + + @Override + public void save(RegisteredClient registeredClient) { + // 这里可以实现保存逻辑,暂时不实现 + } + + @Override + public RegisteredClient findById(String id) { + logger.debug("查找客户端 by ID: {}", id); + + // 首先尝试从数据库查找,排除web + if (oauthClientDetailsMapper != null && !DEGAULT_CLIENT_ID.equals(id)) { + OAuth2ClientDetails clientDetails = findByClientIdFromDb(id); + if (clientDetails != null) { + logger.debug("从数据库找到客户端: {}", clientDetails.getClientId()); + return convertToRegisteredClient(clientDetails); + } + } + + // 如果数据库中没有找到,回退到内存配置 + logger.debug("数据库中没有找到客户端,回退到内存配置"); + return fallbackRepository.findById(id); + } + + @Override + public RegisteredClient findByClientId(String clientId) { + logger.debug("查找客户端 by ClientId: {}", clientId); + + // 首先尝试从数据库查找 + if (oauthClientDetailsMapper != null && !DEGAULT_CLIENT_ID.equals(clientId)) { + OAuth2ClientDetails clientDetails = findByClientIdFromDb(clientId); + if (clientDetails != null) { + logger.debug( + "从数据库找到客户端: {}, 授权类型: {}", + clientDetails.getClientId(), + clientDetails.getAuthorizedGrantTypes()); + return convertToRegisteredClient(clientDetails); + } else { + logger.debug("数据库中没有找到客户端: {}", clientId); + } + } else { + logger.debug("oauthClientDetailsMapper 不可用,跳过数据库查询"); + } + + // 如果数据库中没有找到,回退到内存配置 + logger.debug("回退到内存配置查找客户端: {}", clientId); + RegisteredClient fallbackClient = fallbackRepository.findByClientId(clientId); + if (fallbackClient != null) { + logger.debug("从内存配置找到客户端: {}", clientId); + } else { + logger.debug("内存配置中也没有找到客户端: {}", clientId); + } + return fallbackClient; + } + + private OAuth2ClientDetails findByClientIdFromDb(String clientId) { + try { + // 使用 selectOne 方法查询 + OAuth2ClientDetails query = new OAuth2ClientDetails(); + query.setClientId(clientId); + OAuth2ClientDetails result = oauthClientDetailsMapper.selectOne(query); + logger.debug("数据库查询结果: {}", result != null ? "找到" : "未找到"); + return result; + } catch (Exception e) { + logger.error("数据库查询异常: {}", e.getMessage(), e); + return null; + } + } + + private RegisteredClient convertToRegisteredClient(OAuth2ClientDetails clientDetails) { + logger.debug("转换客户端配置: {}", clientDetails.getClientId()); + + // 解析授权类型 + Set authorizedGrantTypes = + parseCommaDelimitedString(clientDetails.getAuthorizedGrantTypes()); + + // 解析作用域 + Set scopes = parseCommaDelimitedString(clientDetails.getScope()); + + // 解析重定向URI + Set redirectUris = parseCommaDelimitedString(clientDetails.getWebServerRedirectUri()); + + // 解析权限 + Set authorities = parseCommaDelimitedString(clientDetails.getAuthorities()); + + logger.debug( + "解析结果 - 授权类型: {}, 作用域: {}, 重定向URI: {}", authorizedGrantTypes, scopes, redirectUris); + + // 检查客户端密钥格式 + String clientSecret = clientDetails.getClientSecret(); + logger.debug( + "客户端密钥: {}", + clientSecret != null + ? clientSecret.substring(0, Math.min(10, clientSecret.length())) + "..." + : "null"); + + // 处理不同格式的客户端密钥 + if (clientSecret != null && !DEGAULT_CLIENT_ID.equalsIgnoreCase(clientSecret)) { + if (clientSecret.startsWith("$2a$") + || clientSecret.startsWith("$2b$") + || clientSecret.startsWith("$2y$")) { + // BCrypt 格式,添加 {bcrypt} 前缀 + clientSecret = "{bcrypt}" + clientSecret; + logger.debug( + "添加 {bcrypt} 前缀后的客户端密钥: {}", + clientSecret.substring(0, Math.min(15, clientSecret.length())) + "..."); + } else if (!clientSecret.startsWith("{noop}") + && !clientSecret.startsWith("{bcrypt}") + && !clientSecret.startsWith("{pbkdf2}") + && !clientSecret.startsWith("{scrypt}")) { + // 明文密码,添加 {noop} 前缀 + clientSecret = "{noop}" + clientSecret; + logger.debug( + "添加 {noop} 前缀后的客户端密钥: {}", + clientSecret.substring(0, Math.min(15, clientSecret.length())) + "..."); + } + } + + // 构建客户端设置 + ClientSettings.Builder clientSettingsBuilder = + ClientSettings.builder() + .requireAuthorizationConsent("1".equals(clientDetails.getAutoapprove())); + + // 构建令牌设置 + TokenSettings.Builder tokenSettingsBuilder = TokenSettings.builder(); + if (clientDetails.getAccessTokenValidity() != null) { + tokenSettingsBuilder.accessTokenTimeToLive( + Duration.ofSeconds(clientDetails.getAccessTokenValidity())); + } + if (clientDetails.getRefreshTokenValidity() != null) { + tokenSettingsBuilder.refreshTokenTimeToLive( + Duration.ofSeconds(clientDetails.getRefreshTokenValidity())); + } + + RegisteredClient registeredClient = + RegisteredClient.withId(clientDetails.getClientId()) + .clientId(clientDetails.getClientId()) + .clientSecret(clientSecret) + .clientAuthenticationMethods( + authenticationMethods -> { + if (authorizedGrantTypes.contains("client_credentials")) { + authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); + authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST); + } + }) + .authorizationGrantTypes( + authorizationGrantTypes -> { + if (authorizedGrantTypes.contains("password")) { + authorizationGrantTypes.add(AuthorizationGrantType.PASSWORD); + } + if (authorizedGrantTypes.contains("authorization_code")) { + authorizationGrantTypes.add(AuthorizationGrantType.AUTHORIZATION_CODE); + } + if (authorizedGrantTypes.contains("refresh_token")) { + authorizationGrantTypes.add(AuthorizationGrantType.REFRESH_TOKEN); + } + if (authorizedGrantTypes.contains("client_credentials")) { + authorizationGrantTypes.add(AuthorizationGrantType.CLIENT_CREDENTIALS); + } + }) + .redirectUris(redirectUris::addAll) + .scopes(scopes::addAll) + .clientSettings(clientSettingsBuilder.build()) + .tokenSettings(tokenSettingsBuilder.build()) + .build(); + + logger.debug( + "创建的 RegisteredClient: clientId={}, hasClientSecret={}, authMethods={}, grantTypes={}", + registeredClient.getClientId(), + registeredClient.getClientSecret() != null, + registeredClient.getClientAuthenticationMethods(), + registeredClient.getAuthorizationGrantTypes()); + + return registeredClient; + } + + private Set parseCommaDelimitedString(String value) { + if (!StringUtils.hasText(value)) { + return Collections.emptySet(); + } + return Arrays.stream(value.split(",")) + .map(String::trim) + .filter(StringUtils::hasText) + .collect(Collectors.toSet()); + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/DefaultSecurityConfig.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/DefaultSecurityConfig.java new file mode 100644 index 0000000..3ecb01b --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/DefaultSecurityConfig.java @@ -0,0 +1,373 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security; + +import cn.universal.security.health.IpWhitelistFilter; +import jakarta.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +/** + * 默认安全配置类 + * + *

提供IoT平台的安全配置,包括: - Spring Security 6.x 安全过滤器链配置 - 开发环境和生产环境的安全策略 - JWT认证和授权配置 - CORS跨域配置 - + * IP白名单和访问控制 - OAuth2资源服务器配置 + * + *

支持动态切换开发和生产环境的安全策略, 确保在不同环境下提供适当的安全保护级别。 + * + * @version 1.0 @Author Aleo + * @since 2025/1/1 + */ +@Slf4j +@Configuration +@EnableWebSecurity +public class DefaultSecurityConfig { + + /** 是否启用生产环境安全模式 */ + @Value("${security.production.enabled:false}") + private boolean productionSecurityEnabled; + + /** 允许访问的IP地址列表(逗号分隔) */ + @Value("${security.allowed.ips:}") + private String allowedIps; + + /** 允许访问的主机名列表(逗号分隔) */ + @Value("${security.allowed.hosts:}") + private String allowedHosts; + + /** 是否启用Actuator端点访问 */ + @Value("${security.actuator.enabled:false}") + private boolean actuatorEnabled; + + /** + * 密码编码器 + * + *

使用Spring Security推荐的委托密码编码器, 支持多种密码编码算法 + * + * @return 密码编码器实例 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + + /** + * JWT认证转换器 + * + *

将JWT令牌中的角色信息转换为Spring Security的权限对象 + * + * @return JWT认证转换器 + */ + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + + // 设置权限转换器 + converter.setJwtGrantedAuthoritiesConverter( + jwt -> { + Collection authorities = new ArrayList<>(); + List roles = jwt.getClaimAsStringList("roles"); + if (roles != null) { + for (String role : roles) { + authorities.add(new SimpleGrantedAuthority(role)); + } + } + return authorities; + }); + + return converter; + } + + /** + * 默认安全过滤器链 + * + *

配置Spring Security的主要安全过滤器链, 根据环境配置不同的安全策略 + * + * @param http HTTP安全配置对象 + * @return 安全过滤器链 + * @throws Exception 配置异常 + */ + @Bean + @Order(2) + public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + log.info("开始配置安全过滤器链..."); + log.info("配置安全过滤器链,生产环境安全模式: {}", productionSecurityEnabled); + + if (productionSecurityEnabled) { + log.info("配置生产环境安全..."); + configureProductionSecurity(http); + } else { + log.info("配置开发环境安全..."); + configureDevelopmentSecurity(http); + } + + log.info("安全过滤器链配置完成"); + return http.build(); + } + + /** + * 生产环境安全配置 + * + *

配置生产环境的严格安全策略,包括: - 禁用CSRF保护(API服务) - 启用CORS跨域支持 - 严格的端点访问控制 - IP白名单过滤 - OAuth2资源服务器配置 + * + * @param http HTTP安全配置对象 + * @throws Exception 配置异常 + */ + private void configureProductionSecurity(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .authorizeHttpRequests( + authz -> + authz + // OAuth2相关端点 + .requestMatchers("/oauth2/**", "/oauth/**", "/getCaptchaCode") + .permitAll() + // IoT相关端点 - 不需要权限验证 + .requestMatchers("/iot/**") + .permitAll() + // IoT相关端点 - 不需要权限验证 + .requestMatchers("/magic/**") + .permitAll() + // + .requestMatchers("/dashboard/**") + .permitAll() + .requestMatchers("/emqx/**") + .permitAll() + // 测试接口 + .requestMatchers("/test/**") + .permitAll() + .requestMatchers("/monitor/**") + .permitAll() + // 静态资源 + .requestMatchers( + "/js/**", + "/css/**", + "/images/**", + "/fonts/**", + "/upload/**", + "/MP*.txt", + "/callback/hik") + .permitAll() + // 健康检查端点 + .requestMatchers("/actuator/health") + .permitAll() + // Actuator端点 - 需要特殊权限 + .requestMatchers("/actuator/**") + .access(this::hasActuatorAccess) + // 其他所有请求拒绝 + .anyRequest() + .denyAll()) + .oauth2ResourceServer( + oauth2 -> + oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))) + .httpBasic(basic -> basic.disable()) + .formLogin(form -> form.disable()) + .addFilterBefore(new IpWhitelistFilter(allowedIps), BasicAuthenticationFilter.class); + } + + /** + * 开发环境安全配置 + * + *

配置开发环境的宽松安全策略,便于开发和调试 + * + * @param http HTTP安全配置对象 + * @throws Exception 配置异常 + */ + private void configureDevelopmentSecurity(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .authorizeHttpRequests( + authz -> + authz + .requestMatchers( + "/oauth2/**", + "/oauth/**", + "/api/oauth/**", + "/getCaptchaCode", + "/magic/**", + "/dashboard/**") + .permitAll() + // IoT相关端点 - 不需要权限验证 + .requestMatchers("/iot/**", "/emqx/**") + .permitAll() + // 测试和监控点 + .requestMatchers("/test/**", "/monitor/**") + .permitAll() + // 前端相关 + .requestMatchers( + "/js/**", + "/css/**", + "/images/**", + "/fonts/**", + "/upload/**", + "/MP*.txt", + "/callback/hik") + .permitAll() + // spring + .requestMatchers("/actuator/**") + .permitAll() + // 其他环境 + .anyRequest() + .authenticated()) + // 认证服务器 + .oauth2ResourceServer( + oauth2 -> + oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))) + .httpBasic(basic -> basic.disable()) + .formLogin(form -> form.disable()); + } + + /** + * 检查IP访问权限 + * + *

验证请求IP是否在允许的IP列表中或为内网IP + * + * @param authentication 认证信息提供者 + * @param context 请求授权上下文 + * @return 授权决策 + */ + private AuthorizationDecision hasValidIpAccess( + Supplier authentication, RequestAuthorizationContext context) { + HttpServletRequest request = context.getRequest(); + String clientIp = getClientIpAddress(request); + log.info("检查IP访问权限: {}", clientIp); + + boolean hasAccess = false; + if (allowedIps == null || allowedIps.trim().isEmpty()) { + hasAccess = isInternalNetwork(clientIp); + } else { + List allowedIpList = Arrays.asList(allowedIps.split(",")); + hasAccess = allowedIpList.contains(clientIp) || isInternalNetwork(clientIp); + } + + return new AuthorizationDecision(hasAccess); + } + + /** + * 检查Actuator访问权限 + * + *

验证是否允许访问Actuator监控端点 + * + * @param authentication 认证信息提供者 + * @param context 请求授权上下文 + * @return 授权决策 + */ + private AuthorizationDecision hasActuatorAccess( + Supplier authentication, RequestAuthorizationContext context) { + if (!actuatorEnabled) { + log.warn("Actuator访问被禁用"); + return new AuthorizationDecision(false); + } + + HttpServletRequest request = context.getRequest(); + String clientIp = getClientIpAddress(request); + log.info("检查Actuator访问权限: {}", clientIp); + + // 只允许内网访问Actuator + boolean hasAccess = isInternalNetwork(clientIp); + return new AuthorizationDecision(hasAccess); + } + + /** + * 判断是否为内网IP + * + *

检查IP地址是否属于内网地址段 + * + * @param ip IP地址 + * @return 是否为内网IP + */ + private boolean isInternalNetwork(String ip) { + if (ip == null) { + return false; + } + + // 内网IP段 + return ip.startsWith("127.") + || // 本地回环 + ip.startsWith("10.") + || // A类内网 + ip.startsWith("172.16.") + || // B类内网 + ip.startsWith("172.17.") + || ip.startsWith("172.18.") + || ip.startsWith("172.19.") + || ip.startsWith("172.20.") + || ip.startsWith("172.21.") + || ip.startsWith("172.22.") + || ip.startsWith("172.23.") + || ip.startsWith("172.24.") + || ip.startsWith("172.25.") + || ip.startsWith("172.26.") + || ip.startsWith("172.27.") + || ip.startsWith("172.28.") + || ip.startsWith("172.29.") + || ip.startsWith("172.30.") + || ip.startsWith("172.31.") + || ip.startsWith("192.168."); // C类内网 + } + + /** 获取客户端真实IP */ + private String getClientIpAddress(HttpServletRequest request) { + String xForwardedFor = request.getHeader("X-Forwarded-For"); + if (xForwardedFor != null + && !xForwardedFor.isEmpty() + && !"unknown".equalsIgnoreCase(xForwardedFor)) { + return xForwardedFor.split(",")[0].trim(); + } + + String xRealIp = request.getHeader("X-Real-IP"); + if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) { + return xRealIp; + } + + return request.getRemoteAddr(); + } + + /** CORS配置 */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/KiteUserDetailsService.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/KiteUserDetailsService.java new file mode 100644 index 0000000..6d9a120 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/KiteUserDetailsService.java @@ -0,0 +1,93 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security; + +import cn.universal.admin.system.service.AsyncService; +import cn.universal.common.constant.Constants; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.utils.ServletUtils; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.mapper.IoTUserMapper; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +@Slf4j +@Component(value = "kiteUserDetailsService") +public class KiteUserDetailsService implements UserDetailsService { + + @Resource private IoTUserMapper iotUserMapper; + + @Resource private AsyncService asyncService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + log.info("usernameis:" + username); + + // 首先尝试通过 username 查找用户 + IoTUser iotUser = + IoTUser.builder().username(username).status(IoTConstant.NORMAL.toString()).build(); + try { + iotUser = iotUserMapper.selectOne(iotUser); + + // 如果通过 username 没找到,尝试通过 unionId 查找 + if (iotUser == null) { + iotUser = IoTUser.builder().unionId(username).status(IoTConstant.NORMAL.toString()).build(); + iotUser = iotUserMapper.selectOne(iotUser); + } + + if (iotUser == null) { + HttpServletRequest request = ServletUtils.getRequest(); + asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户名或密码错误", request); + throw new UsernameNotFoundException("the user is not found"); + } else if (IoTConstant.UN_NORMAL.toString().equals(iotUser.getStatus())) { + HttpServletRequest request = ServletUtils.getRequest(); + asyncService.recordLogininfor( + iotUser.getUsername(), Constants.LOGIN_FAIL, "用户已冻结,请联系管理员解冻", request); + throw new UsernameNotFoundException("the user is disabled"); + } + // asyncService.recordLogininfor(iotUser.getUsername(), Constants.LOGIN_SUCCESS, "登录成功"); + // 返回自定义的 KiteUserDetails + List Authorities = new ArrayList<>(); + Authorities.add(new SimpleGrantedAuthority("WEB")); + + // 处理密码编码格式,避免 BCrypt 警告 + String password = iotUser.getPassword(); + // 如果密码不是以 BCrypt 格式开头,添加 {noop} 前缀 + if (password != null + && !password.startsWith("$2a$") + && !password.startsWith("$2b$") + && !password.startsWith("$2y$") + && !password.startsWith("{")) { + password = "{noop}" + password; + } + + // 使用 unionId 作为 User 的 username,这样在 JWT 的 sub 字段中就是 unionId + User user = new User(iotUser.getUnionId(), password, Authorities); + // 更新登录时间 + iotUserMapper.updateLoginDate(iotUser.getId()); + return user; + } catch (Exception e) { + log.error("加载用户详情失败: " + username, e); + throw new UsernameNotFoundException("the user is not found"); + } + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/OAuth2ErrorHandler.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/OAuth2ErrorHandler.java new file mode 100644 index 0000000..d81d9cd --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/OAuth2ErrorHandler.java @@ -0,0 +1,80 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +/** OAuth2 错误处理器 用于格式化 OAuth2 认证失败的错误响应 @Author Aleo */ +@Slf4j +@Component +public class OAuth2ErrorHandler implements AuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure( + HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) + throws IOException { + + log.warn("OAuth2(onAuthenticationFailure) 认证失败: {}", exception.getMessage()); + + // 设置响应头 + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + + // 构建错误响应 + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "invalid_grant"); + errorResponse.put("error_description", getErrorMessage(exception)); + errorResponse.put("timestamp", System.currentTimeMillis()); + errorResponse.put("path", request.getRequestURI()); + + // 写入响应体 + response.getWriter().write(JSONUtil.toJsonStr(errorResponse)); + } + + /** 获取错误信息 */ + private String getErrorMessage(AuthenticationException exception) { + if (exception instanceof BadCredentialsException) { + return exception.getMessage(); + } + + if (exception instanceof OAuth2AuthenticationException) { + OAuth2Error error = ((OAuth2AuthenticationException) exception).getError(); + return error.getDescription() != null ? error.getDescription() : error.getErrorCode(); + } + + if (exception instanceof OAuth2AuthorizationCodeRequestAuthenticationException) { + OAuth2AuthorizationCodeRequestAuthenticationException authException = + (OAuth2AuthorizationCodeRequestAuthenticationException) exception; + OAuth2Error error = authException.getError(); + return error.getDescription() != null ? error.getDescription() : error.getErrorCode(); + } + + // 默认错误信息 + return "认证失败: " + exception.getMessage(); + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/OAuth2ServerConfig.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/OAuth2ServerConfig.java new file mode 100644 index 0000000..efd8385 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/OAuth2ServerConfig.java @@ -0,0 +1,114 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Collection; +import java.util.UUID; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.token.JwtGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; + +@Configuration +public class OAuth2ServerConfig { + + @Bean + public AuthorizationServerSettings AuthorizationServerSettings() { + return AuthorizationServerSettings.builder().issuer("http://localhost:9091").build(); + } + + @Bean + public OAuth2AuthorizationService AuthorizationService() { + return new InMemoryOAuth2AuthorizationService(); + } + + @Bean + public OAuth2TokenGenerator tokenGenerator(JWKSource jwkSource) { + JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource)); + OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); + OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); + return new DelegatingOAuth2TokenGenerator( + jwtGenerator, accessTokenGenerator, refreshTokenGenerator); + } + + @Bean + public JWKSource jwkSource() { + KeyPair keyPair = generateRsaKey(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + RSAKey rsaKey = + new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + JWKSet jwkSet = new JWKSet(rsaKey); + return new ImmutableJWKSet<>(jwkSet); + } + + @Bean + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } + + @Bean + public OAuth2TokenCustomizer jwtCustomizer( + UserDetailsService userDetailsService) { + return context -> { + if (context.getPrincipal() != null + && context.getPrincipal().getPrincipal() + instanceof org.springframework.security.core.userdetails.UserDetails userDetails) { + context.getClaims().claim("username", userDetails.getUsername()); + context.getClaims().claim("unionId", userDetails.getUsername()); + Collection Authorities = userDetails.getAuthorities(); + context + .getClaims() + .claim("roles", Authorities.stream().map(GrantedAuthority::getAuthority).toList()); + } + }; + } + + private static KeyPair generateRsaKey() { + KeyPair keyPair; + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + keyPair = keyPairGenerator.generateKeyPair(); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + return keyPair; + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/PasswordOAuth2Config.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/PasswordOAuth2Config.java new file mode 100644 index 0000000..6417fc4 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/PasswordOAuth2Config.java @@ -0,0 +1,54 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security; + +import cn.universal.persistence.mapper.IoTUserMapper; +import cn.universal.security.converter.PasswordAuthenticationConverter; +import cn.universal.security.provider.PasswordAuthenticationProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.security.web.authentication.AuthenticationConverter; + +/** 密码模式 OAuth2 配置 独立配置,不影响原有的验证码模式 */ +@Configuration +public class PasswordOAuth2Config { + + @Bean + public AuthenticationConverter passwordAuthenticationConverter( + @Autowired(required = false) CustomClientRegistrationService clientRegistrationService) { + if (clientRegistrationService == null) { + return null; // 如果没有数据库配置,返回null + } + return new PasswordAuthenticationConverter(clientRegistrationService); + } + + @Bean + public PasswordAuthenticationProvider passwordAuthenticationProvider( + @Autowired(required = false) CustomClientRegistrationService clientRegistrationService, + @Autowired(required = false) IoTUserMapper iotUserMapper, + @Autowired(required = false) OAuth2TokenGenerator tokenGenerator, + @Autowired(required = false) OAuth2AuthorizationService AuthorizationService) { + if (clientRegistrationService == null + || iotUserMapper == null + || tokenGenerator == null + || AuthorizationService == null) { + return null; // 如果缺少依赖,返回null + } + return new PasswordAuthenticationProvider( + tokenGenerator, clientRegistrationService, + iotUserMapper, AuthorizationService); + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/converter/OAuth2CaptchaAuthenticationConverter.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/converter/OAuth2CaptchaAuthenticationConverter.java new file mode 100644 index 0000000..21d080e --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/converter/OAuth2CaptchaAuthenticationConverter.java @@ -0,0 +1,49 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.converter; + +import cn.universal.security.token.OAuth2CaptchaAuthenticationToken; +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.web.authentication.AuthenticationConverter; + +public class OAuth2CaptchaAuthenticationConverter implements AuthenticationConverter { + + @Override + public Authentication convert(HttpServletRequest request) { + String grantType = request.getParameter("grant_type"); + if (!"captcha".equals(grantType)) { + return null; + } + + // 从 SecurityContext 获取客户端认证信息 + Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); + if (!(clientPrincipal instanceof OAuth2ClientAuthenticationToken)) { + return null; + } + + // 提取请求参数 + Map additionalParameters = new HashMap<>(); + additionalParameters.put("username", request.getParameter("username")); + additionalParameters.put("password", request.getParameter("password")); + additionalParameters.put("code", request.getParameter("code")); + additionalParameters.put("uuid", request.getParameter("uuid")); + additionalParameters.put("grant_type", grantType); + + return new OAuth2CaptchaAuthenticationToken(clientPrincipal, additionalParameters); + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/converter/PasswordAuthenticationConverter.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/converter/PasswordAuthenticationConverter.java new file mode 100644 index 0000000..310a15b --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/converter/PasswordAuthenticationConverter.java @@ -0,0 +1,93 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.converter; + +import cn.universal.security.token.PasswordAuthenticationToken; +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.util.StringUtils; + +public class PasswordAuthenticationConverter implements AuthenticationConverter { + + private final RegisteredClientRepository clientRepository; + + public PasswordAuthenticationConverter(RegisteredClientRepository clientRepository) { + this.clientRepository = clientRepository; + } + + @Override + public Authentication convert(HttpServletRequest request) { + // 只处理 POST 请求 + if (!"POST".equals(request.getMethod())) { + return null; + } + + // 检查 grant_type 参数 + String grantType = request.getParameter("grant_type"); + if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) { + return null; + } + + // 获取客户端ID + String clientId = request.getParameter("client_id"); + if (!StringUtils.hasText(clientId)) { + throw new OAuth2AuthenticationException( + new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "invalid_request", null)); + } + + // 查找注册的客户端 + RegisteredClient registeredClient = this.clientRepository.findByClientId(clientId); + if (registeredClient == null) { + throw new OAuth2AuthenticationException( + new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "invalid_request", null)); + } + + // 验证客户端是否支持密码模式 + if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) { + throw new OAuth2AuthenticationException( + new OAuth2Error(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, "invalid_request", null)); + } + + // 获取用户名和密码 + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) { + throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST)); + } + + // 创建客户端认证令牌 + Authentication clientPrincipal = + new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_POST, null); + + // 提取请求参数 + Map additionalParameters = new HashMap<>(); + additionalParameters.put("username", username); + additionalParameters.put("password", password); + additionalParameters.put("grant_type", grantType); + + return new PasswordAuthenticationToken(clientPrincipal, additionalParameters); + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/health/IpWhitelistFilter.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/health/IpWhitelistFilter.java new file mode 100644 index 0000000..a2049a7 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/health/IpWhitelistFilter.java @@ -0,0 +1,74 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.health; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.util.StringUtils; + +/** IP白名单过滤器 @Author Aleo */ +@Slf4j +public class IpWhitelistFilter extends BasicAuthenticationFilter { + + private final List allowedIps; + + public IpWhitelistFilter(String allowedIps) { + super(null); + this.allowedIps = + StringUtils.hasText(allowedIps) ? Arrays.asList(allowedIps.split(",")) : Arrays.asList(); + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + String clientIp = getClientIpAddress(request); + String requestUri = request.getRequestURI(); + + log.info("IP白名单检查 - IP: {}, URI: {}", clientIp, requestUri); + + // 检查IP白名单 + if (!allowedIps.isEmpty() && !allowedIps.contains(clientIp)) { + log.warn("IP访问被拒绝: {} - {}", clientIp, requestUri); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.getWriter().write("Access Denied: IP not in whitelist"); + return; + } + + chain.doFilter(request, response); + } + + private String getClientIpAddress(HttpServletRequest request) { + String xForwardedFor = request.getHeader("X-Forwarded-For"); + if (xForwardedFor != null + && !xForwardedFor.isEmpty() + && !"unknown".equalsIgnoreCase(xForwardedFor)) { + return xForwardedFor.split(",")[0].trim(); + } + + String xRealIp = request.getHeader("X-Real-IP"); + if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) { + return xRealIp; + } + + return request.getRemoteAddr(); + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/health/NetworkAccessMonitor.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/health/NetworkAccessMonitor.java new file mode 100644 index 0000000..d7b149b --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/health/NetworkAccessMonitor.java @@ -0,0 +1,116 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.health; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +/** 网络访问监控 @Author Aleo */ +@Slf4j +@Component +public class NetworkAccessMonitor implements HandlerInterceptor { + + @Value("${security.production.enabled:false}") + private boolean productionSecurityEnabled; + + @Value("${security.access.monitor.enabled:true}") + private boolean accessMonitorEnabled; + + private final ConcurrentHashMap accessCountMap = new ConcurrentHashMap<>(); + + @Override + public boolean preHandle( + HttpServletRequest request, HttpServletResponse response, Object handler) { + if (!accessMonitorEnabled) { + return true; + } + + String clientIp = getClientIpAddress(request); + String userAgent = request.getHeader("User-Agent"); + String requestUri = request.getRequestURI(); + String method = request.getMethod(); + + // 记录访问日志 + log.info( + "网络访问 - IP: {}, URI: {}, Method: {}, User-Agent: {}", + clientIp, + requestUri, + method, + userAgent); + + // 统计访问次数 + AtomicInteger count = accessCountMap.computeIfAbsent(clientIp, k -> new AtomicInteger(0)); + int currentCount = count.incrementAndGet(); + + // 生产环境下的异常访问监控 + if (productionSecurityEnabled && isExternalNetwork(clientIp)) { + log.warn("检测到外网访问 - IP: {}, 访问次数: {}, URI: {}", clientIp, currentCount, requestUri); + + // 如果外网IP访问次数过多,可以考虑告警 + if (currentCount > 100) { + log.error("外网IP访问频率异常 - IP: {}, 访问次数: {}", clientIp, currentCount); + // 这里可以添加告警逻辑,比如发送钉钉、邮件等 + } + } + + return true; + } + + private String getClientIpAddress(HttpServletRequest request) { + String xForwardedFor = request.getHeader("X-Forwarded-For"); + if (xForwardedFor != null + && !xForwardedFor.isEmpty() + && !"unknown".equalsIgnoreCase(xForwardedFor)) { + return xForwardedFor.split(",")[0].trim(); + } + + String xRealIp = request.getHeader("X-Real-IP"); + if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) { + return xRealIp; + } + + return request.getRemoteAddr(); + } + + private boolean isExternalNetwork(String ip) { + if (ip == null) { + return true; + } + + return !(ip.startsWith("127.") + || ip.startsWith("10.") + || ip.startsWith("172.16.") + || ip.startsWith("172.17.") + || ip.startsWith("172.18.") + || ip.startsWith("172.19.") + || ip.startsWith("172.20.") + || ip.startsWith("172.21.") + || ip.startsWith("172.22.") + || ip.startsWith("172.23.") + || ip.startsWith("172.24.") + || ip.startsWith("172.25.") + || ip.startsWith("172.26.") + || ip.startsWith("172.27.") + || ip.startsWith("172.28.") + || ip.startsWith("172.29.") + || ip.startsWith("172.30.") + || ip.startsWith("172.31.") + || ip.startsWith("192.168.")); + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/health/SecurityConfigValidator.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/health/SecurityConfigValidator.java new file mode 100644 index 0000000..43f8f5c --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/health/SecurityConfigValidator.java @@ -0,0 +1,51 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.health; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +/** 安全配置验证器 @Author Aleo */ +@Slf4j +@Component +public class SecurityConfigValidator implements CommandLineRunner { + + @Value("${security.production.enabled:false}") + private boolean productionSecurityEnabled; + + @Value("${security.allowed.ips:}") + private String allowedIps; + + @Value("${spring.profiles.active:platform}") + private String activeProfile; + + @Override + public void run(String... args) throws Exception { + log.info("=== 安全配置验证 ==="); + log.info("当前环境: {}", activeProfile); + log.info("生产环境安全模式: {}", productionSecurityEnabled); + log.info("允许的IP地址: {}", allowedIps.isEmpty() ? "所有内网IP" : allowedIps); + + if ("prod".equals(activeProfile) && !productionSecurityEnabled) { + log.warn("⚠️ 警告:生产环境未启用安全模式!"); + } + + if (productionSecurityEnabled && allowedIps.isEmpty()) { + log.warn("⚠️ 警告:生产环境安全模式已启用,但未配置允许的IP地址!"); + } + + log.info("=== 安全配置验证完成 ==="); + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/health/SecurityHealthIndicator.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/health/SecurityHealthIndicator.java new file mode 100644 index 0000000..a5a3158 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/health/SecurityHealthIndicator.java @@ -0,0 +1,58 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.health; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +/** 安全健康检查 @Author Aleo */ +@Slf4j +@Component +public class SecurityHealthIndicator implements HealthIndicator { + + @Value("${security.production.enabled:false}") + private boolean productionSecurityEnabled; + + @Value("${security.allowed.ips:}") + private String allowedIps; + + @Value("${security.actuator.enabled:false}") + private boolean actuatorEnabled; + + @Override + public Health health() { + try { + if (productionSecurityEnabled) { + return Health.up() + .withDetail("security_mode", "production") + .withDetail("allowed_ips", allowedIps) + .withDetail("actuator_enabled", actuatorEnabled) + .withDetail("status", "Production security mode enabled") + .build(); + } else { + return Health.up() + .withDetail("security_mode", "development") + .withDetail("allowed_ips", "all") + .withDetail("actuator_enabled", true) + .withDetail("status", "Development mode - relaxed security") + .build(); + } + } catch (Exception e) { + log.error("Security health check failed", e); + return Health.down().withDetail("error", e.getMessage()).build(); + } + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/provider/OAuth2CaptchaAuthenticationProvider.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/provider/OAuth2CaptchaAuthenticationProvider.java new file mode 100644 index 0000000..ed14de4 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/provider/OAuth2CaptchaAuthenticationProvider.java @@ -0,0 +1,262 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 AleoXin 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: AleoXin + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.provider; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.system.service.AsyncService; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.common.constant.Constants; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.utils.RSAUtils; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.security.token.OAuth2CaptchaAuthenticationToken; +import java.security.Principal; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; +import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +public class OAuth2CaptchaAuthenticationProvider implements AuthenticationProvider { + + @Autowired private StringRedisTemplate stringRedisTemplate; + @Autowired private UserDetailsService userDetailsService; + + @Autowired private AsyncService asyncService; + @Autowired private IIotUserService iIotUserService; + @Autowired private OAuth2AuthorizationService authorizationService; + @Autowired private OAuth2TokenGenerator tokenGenerator; + + private static final Integer maxRetryCount = 5; + private static final Integer maxIpRetryCount = 15; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + OAuth2CaptchaAuthenticationToken captchaToken = + (OAuth2CaptchaAuthenticationToken) authentication; + + // 获取客户端信息 + OAuth2ClientAuthenticationToken clientPrincipal = + (OAuth2ClientAuthenticationToken) captchaToken.getPrincipal(); + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + String username = captchaToken.getUsername(); + String password = captchaToken.getPassword(); + String captcha = captchaToken.getCode(); + String uuid = captchaToken.getUuid(); + + // 获取IP地址 + String ip = "unknown"; + Object details = authentication.getDetails(); + if (details instanceof WebAuthenticationDetails) { + ip = ((WebAuthenticationDetails) details).getRemoteAddress(); + } + + // 校验验证码 + if (!StringUtils.hasText(captcha)) { + throw new OAuth2AuthenticationException(new OAuth2Error("invalid_grant", "验证码不能为空", null)); + } + String code = stringRedisTemplate.opsForValue().get("captcha_codes:" + uuid); + if (!StringUtils.hasText(code)) { + throw new OAuth2AuthenticationException(new OAuth2Error("invalid_grant", "验证码已过期", null)); + } + if (!code.equalsIgnoreCase(captcha)) { + throw new OAuth2AuthenticationException(new OAuth2Error("invalid_grant", "验证码输入错误", null)); + } + + // 登录重试次数/IP 限制 + String retryKey = "loginCheck:retryCount:" + username; + String ipKey = "loginCheck:retryIpCount:" + ip; + Integer retryCount = + stringRedisTemplate.opsForValue().get(retryKey) != null + ? Integer.parseInt(stringRedisTemplate.opsForValue().get(retryKey)) + : 0; + Integer ipCount = + stringRedisTemplate.opsForValue().get(ipKey) != null + ? Integer.parseInt(stringRedisTemplate.opsForValue().get(ipKey)) + : 0; + + if (ipCount > maxIpRetryCount) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_grant", "ip已锁定,请一小时后再试", null)); + } + if (retryCount > maxRetryCount) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_grant", "账号已锁定,请15分钟后再试", null)); + } + + try { + String privateKey = stringRedisTemplate.opsForValue().get("RSAPrivateKey"); + if (privateKey != null) { + password = RSAUtils.decrypt(password, privateKey); + } + } catch (Exception e) { + stringRedisTemplate + .opsForValue() + .set(retryKey, String.valueOf(retryCount + 1), 10, TimeUnit.MINUTES); + stringRedisTemplate + .opsForValue() + .set(ipKey, String.valueOf(ipCount + 1), 60, TimeUnit.MINUTES); + throw new OAuth2AuthenticationException(new OAuth2Error("invalid_grant", "密钥已过期", null)); + } + + Matcher matcher = IoTConstant.pattern.matcher(password); + if (!matcher.matches()) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_grant", "密码中必须包含字母、数字、特殊字符,至少8个字符,最多20个字符", null)); + } + + // 独占式登录判断 + IoTUser user = iIotUserService.selectUserByUserName(username); + if (ObjectUtil.isEmpty(user)) { + stringRedisTemplate + .opsForValue() + .set(ipKey, String.valueOf(ipCount + 1), 60, TimeUnit.MINUTES); + throw new OAuth2AuthenticationException(new OAuth2Error("invalid_grant", "用户名或密码错误", null)); + } + if (IoTConstant.UN_NORMAL.toString().equals(user.getStatus())) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_grant", "用户已冻结,请联系管理员解冻", null)); + } + + boolean isExclusive = + "true".equals(JSONUtil.parseObj(user.getCfg()).getStr(IoTConstant.EXCLUSIVE_FIRST_LOGIN)); + if (isExclusive) { + String loginedIp = + stringRedisTemplate.opsForValue().get(IoTConstant.EXCLUSIVE_LOGIN + ":" + username); + if (StrUtil.isNotBlank(loginedIp)) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_grant", "账号已登录,请先退出已登录的账号,IP:" + loginedIp, null)); + } + } + + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + // 注释密码验证,专注解决类型转换问题 + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + if (!passwordEncoder.matches(password, userDetails.getPassword())) { + asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户名或密码错误", null); + stringRedisTemplate + .opsForValue() + .set(retryKey, String.valueOf(retryCount + 1), 10, TimeUnit.MINUTES); + stringRedisTemplate + .opsForValue() + .set(ipKey, String.valueOf(ipCount + 1), 60, TimeUnit.MINUTES); + throw new OAuth2AuthenticationException(new OAuth2Error("invalid_grant", "用户名或密码错误", null)); + } + + // 登录成功 + asyncService.recordLogininfor(username, Constants.LOGIN_SUCCESS, "验证码登录成功", null); + stringRedisTemplate.delete(retryKey); + stringRedisTemplate.delete(ipKey); + + if (isExclusive) { + stringRedisTemplate + .opsForValue() + .set(IoTConstant.EXCLUSIVE_LOGIN + ":" + username, ip, 1800, TimeUnit.SECONDS); + } + + // 生成 OAuth2 令牌 + UsernamePasswordAuthenticationToken userAuth = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + DefaultOAuth2TokenContext.Builder tokenContextBuilder = + DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal(userAuth) + .authorizationServerContext(AuthorizationServerContextHolder.getContext()) + .authorizationGrantType(OAuth2CaptchaAuthenticationToken.CAPTCHA_GRANT_TYPE) + .authorizationGrant(captchaToken); + + OAuth2TokenContext tokenContext = + tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build(); + OAuth2Token generatedAccessToken = tokenGenerator.generate(tokenContext); + if (generatedAccessToken == null) { + throw new OAuth2AuthenticationException(new OAuth2Error("invalid_grant", "请联系管理员", null)); + } + + OAuth2AccessToken accessToken = + new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + generatedAccessToken.getTokenValue(), + generatedAccessToken.getIssuedAt(), + generatedAccessToken.getExpiresAt(), + registeredClient.getScopes()); + + OAuth2RefreshToken refreshToken = null; + if (registeredClient + .getAuthorizationGrantTypes() + .contains(org.springframework.security.oauth2.core.AuthorizationGrantType.REFRESH_TOKEN)) { + tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); + OAuth2Token generatedRefreshToken = tokenGenerator.generate(tokenContext); + if (generatedRefreshToken != null) { + refreshToken = + new OAuth2RefreshToken( + generatedRefreshToken.getTokenValue(), + generatedRefreshToken.getIssuedAt(), + generatedRefreshToken.getExpiresAt()); + } + } + + // 保存授权信息 + OAuth2Authorization.Builder authorizationBuilder = + OAuth2Authorization.withRegisteredClient(registeredClient) + .principalName(userDetails.getUsername()) + .attribute(Principal.class.getName(), userAuth) + .authorizationGrantType(OAuth2CaptchaAuthenticationToken.CAPTCHA_GRANT_TYPE) + .authorizedScopes(registeredClient.getScopes()); + + if (accessToken != null) { + authorizationBuilder.accessToken(accessToken); + } + if (refreshToken != null) { + authorizationBuilder.refreshToken(refreshToken); + } + + OAuth2Authorization authorization = authorizationBuilder.build(); + authorizationService.save(authorization); + + return new OAuth2AccessTokenAuthenticationToken( + registeredClient, clientPrincipal, accessToken, refreshToken); + } + + @Override + public boolean supports(Class authentication) { + return OAuth2CaptchaAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/provider/PasswordAuthenticationProvider.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/provider/PasswordAuthenticationProvider.java new file mode 100644 index 0000000..564710e --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/provider/PasswordAuthenticationProvider.java @@ -0,0 +1,186 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 AleoXin 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: AleoXin + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.provider; + +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.mapper.IoTUserMapper; +import cn.universal.security.token.PasswordAuthenticationToken; +import java.security.Principal; +import java.util.Collections; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; +import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.Assert; + +public class PasswordAuthenticationProvider implements AuthenticationProvider { + + private final OAuth2TokenGenerator tokenGenerator; + private final RegisteredClientRepository clientRepository; + private final IoTUserMapper iotUserMapper; + private final BCryptPasswordEncoder passwordEncoder; + private final OAuth2AuthorizationService authorizationService; + + public PasswordAuthenticationProvider( + OAuth2TokenGenerator tokenGenerator, + RegisteredClientRepository clientRepository, + IoTUserMapper iotUserMapper, + OAuth2AuthorizationService authorizationService) { + Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); + Assert.notNull(clientRepository, "clientRepository cannot be null"); + Assert.notNull(iotUserMapper, "iotUserMapper cannot be null"); + Assert.notNull(authorizationService, "authorizationService cannot be null"); + this.tokenGenerator = tokenGenerator; + this.clientRepository = clientRepository; + this.iotUserMapper = iotUserMapper; + this.authorizationService = authorizationService; + this.passwordEncoder = new BCryptPasswordEncoder(); + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + PasswordAuthenticationToken passwordAuthenticationToken = + (PasswordAuthenticationToken) authentication; + + // 从客户端认证令牌中获取RegisteredClient + OAuth2ClientAuthenticationToken clientPrincipal = + (OAuth2ClientAuthenticationToken) passwordAuthenticationToken.getPrincipal(); + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + // 验证用户名和密码 + String username = passwordAuthenticationToken.getUsername(); + String password = passwordAuthenticationToken.getPassword(); + + if (username == null || password == null) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_request", "用户名或密码不能为空", null)); + } + + // 查询用户 - 使用 selectList 方法 + IoTUser queryUser = new IoTUser(); + queryUser.setUsername(username); + java.util.List users = iotUserMapper.selectList(queryUser); + + if (users == null || users.isEmpty()) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_request", "用户名或密码不能为空", null)); + } + + IoTUser iotUser = users.get(0); + + // 验证密码 + if (!passwordEncoder.matches(password, iotUser.getPassword())) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_request", "用户名或密码不能为空", null)); + } + + // 创建用户详情 + UserDetails userDetails = + User.builder() + .username(iotUser.getUsername()) + .password(iotUser.getPassword()) + .authorities(Collections.singletonList(new SimpleGrantedAuthority("USER"))) + .build(); + + // 创建用户认证令牌 + UsernamePasswordAuthenticationToken userAuth = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + // 生成 OAuth2 令牌 + DefaultOAuth2TokenContext.Builder tokenContextBuilder = + DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal(userAuth) + .authorizationServerContext(AuthorizationServerContextHolder.getContext()) + .authorizationGrantType( + org.springframework.security.oauth2.core.AuthorizationGrantType.PASSWORD) + .authorizationGrant(passwordAuthenticationToken); + + OAuth2TokenContext tokenContext = + tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build(); + OAuth2Token generatedAccessToken = tokenGenerator.generate(tokenContext); + if (generatedAccessToken == null) { + throw new OAuth2AuthenticationException(new OAuth2Error("server_error", "无法生成访问令牌", null)); + } + + OAuth2AccessToken accessToken = + new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + generatedAccessToken.getTokenValue(), + generatedAccessToken.getIssuedAt(), + generatedAccessToken.getExpiresAt(), + registeredClient.getScopes()); + + OAuth2RefreshToken refreshToken = null; + if (registeredClient + .getAuthorizationGrantTypes() + .contains(org.springframework.security.oauth2.core.AuthorizationGrantType.REFRESH_TOKEN)) { + tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); + OAuth2Token generatedRefreshToken = tokenGenerator.generate(tokenContext); + if (generatedRefreshToken != null) { + refreshToken = + new OAuth2RefreshToken( + generatedRefreshToken.getTokenValue(), + generatedRefreshToken.getIssuedAt(), + generatedRefreshToken.getExpiresAt()); + } + } + + // 保存授权信息 + OAuth2Authorization.Builder authorizationBuilder = + OAuth2Authorization.withRegisteredClient(registeredClient) + .principalName(userDetails.getUsername()) + .attribute(Principal.class.getName(), userAuth) + .authorizationGrantType( + org.springframework.security.oauth2.core.AuthorizationGrantType.PASSWORD) + .authorizedScopes(registeredClient.getScopes()); + + if (accessToken != null) { + authorizationBuilder.accessToken(accessToken); + } + if (refreshToken != null) { + authorizationBuilder.refreshToken(refreshToken); + } + + OAuth2Authorization authorization = authorizationBuilder.build(); + authorizationService.save(authorization); + + return new OAuth2AccessTokenAuthenticationToken( + registeredClient, clientPrincipal, accessToken, refreshToken); + } + + @Override + public boolean supports(Class authentication) { + return PasswordAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/provider/SmsAuthenticationProvider.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/provider/SmsAuthenticationProvider.java new file mode 100644 index 0000000..85e1431 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/provider/SmsAuthenticationProvider.java @@ -0,0 +1,147 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.provider; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import cn.universal.admin.system.service.AsyncService; +import cn.universal.admin.system.service.IIotUserService; +import cn.universal.common.constant.Constants; +import cn.universal.common.constant.IoTConstant; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.security.token.SmsAuthenticationToken; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +public class SmsAuthenticationProvider implements AuthenticationProvider { + + @Autowired private StringRedisTemplate stringRedisTemplate; + @Autowired private UserDetailsService userDetailsService; + + @Autowired private AsyncService asyncService; + @Autowired private IIotUserService iIotUserService; + + private static final Integer maxRetryCount = 5; + private static final Integer maxIpRetryCount = 15; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + SmsAuthenticationToken token = (SmsAuthenticationToken) authentication; + String phone = token.getPhone(); + String smsCode = token.getCode(); + + // 获取IP地址 - 避免使用 ServletUtils.getRequest() + String ip = "unknown"; + Object details = authentication.getDetails(); + if (details instanceof WebAuthenticationDetails) { + ip = ((WebAuthenticationDetails) details).getRemoteAddress(); + } + + // 校验短信验证码 + if (!StringUtils.hasText(smsCode)) { + throw new OAuth2AuthenticationException(new OAuth2Error("invalid_grant", "短信验证码不能为空", null)); + } + String code = stringRedisTemplate.opsForValue().get("sms_codes:" + phone); + if (!StringUtils.hasText(code)) { + throw new OAuth2AuthenticationException(new OAuth2Error("invalid_grant", "短信验证码已过期", null)); + } + if (!code.equalsIgnoreCase(smsCode)) { + throw new OAuth2AuthenticationException(new OAuth2Error("invalid_grant", "短信验证码输入错误", null)); + } + + // 登录重试次数/IP 限制 + String retryKey = "loginCheck:retryCount:" + phone; + String ipKey = "loginCheck:retryIpCount:" + ip; + Integer retryCount = + stringRedisTemplate.opsForValue().get(retryKey) != null + ? Integer.parseInt(stringRedisTemplate.opsForValue().get(retryKey)) + : 0; + Integer ipCount = + stringRedisTemplate.opsForValue().get(ipKey) != null + ? Integer.parseInt(stringRedisTemplate.opsForValue().get(ipKey)) + : 0; + + if (ipCount > maxIpRetryCount) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_grant", "ip已锁定,请一小时后再试", null)); + } + if (retryCount > maxRetryCount) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_grant", "账号已锁定,请15分钟后再试", null)); + } + + // 根据手机号查找用户 + IoTUser user = iIotUserService.selectUserByMobile(phone); + if (ObjectUtil.isEmpty(user)) { + stringRedisTemplate + .opsForValue() + .set(ipKey, String.valueOf(ipCount + 1), 60, TimeUnit.MINUTES); + throw new OAuth2AuthenticationException(new OAuth2Error("invalid_grant", "手机号未注册", null)); + } + if (IoTConstant.UN_NORMAL.toString().equals(user.getStatus())) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_grant", "用户已冻结,请联系管理员解冻", null)); + } + + // 独占式登录判断 + boolean isExclusive = + "true".equals(JSONUtil.parseObj(user.getCfg()).getStr(IoTConstant.EXCLUSIVE_FIRST_LOGIN)); + if (isExclusive) { + String loginedIp = + stringRedisTemplate + .opsForValue() + .get(IoTConstant.EXCLUSIVE_LOGIN + ":" + user.getUsername()); + if (StrUtil.isNotBlank(loginedIp)) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_grant", "账号已登录,请先退出已登录的账号,登录ip:" + loginedIp, null)); + } + } + + UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername()); + + // 登录成功 + asyncService.recordLogininfor(user.getUsername(), Constants.LOGIN_SUCCESS, "短信验证码登录成功", null); + stringRedisTemplate.delete(retryKey); + stringRedisTemplate.delete(ipKey); + + if (isExclusive) { + stringRedisTemplate + .opsForValue() + .set(IoTConstant.EXCLUSIVE_LOGIN + ":" + user.getUsername(), ip, 1800, TimeUnit.SECONDS); + } + + UsernamePasswordAuthenticationToken result = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + result.setDetails(token.getDetails()); + return result; + } + + @Override + public boolean supports(Class authentication) { + return SmsAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/CaptchaAuthenticationToken.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/CaptchaAuthenticationToken.java new file mode 100644 index 0000000..b62c113 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/CaptchaAuthenticationToken.java @@ -0,0 +1,58 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.token; + +import org.springframework.security.authentication.AbstractAuthenticationToken; + +public class CaptchaAuthenticationToken extends AbstractAuthenticationToken { + + private final String username; + private final String password; + private final String code; + private final String uuid; + + public CaptchaAuthenticationToken(String username, String password, String code, String uuid) { + super(null); + this.username = username; + this.password = password; + this.code = code; + this.uuid = uuid; + setAuthenticated(false); + } + + @Override + public Object getCredentials() { + return password; + } + + @Override + public Object getPrincipal() { + return username; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getCode() { + return code; + } + + public String getUuid() { + return uuid; + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/OAuth2CaptchaAuthenticationToken.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/OAuth2CaptchaAuthenticationToken.java new file mode 100644 index 0000000..33d0ac9 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/OAuth2CaptchaAuthenticationToken.java @@ -0,0 +1,54 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.token; + +import java.util.Map; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; + +public class OAuth2CaptchaAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { + + public static final AuthorizationGrantType CAPTCHA_GRANT_TYPE = + new AuthorizationGrantType("captcha"); + + private final String username; + private final String password; + private final String code; + private final String uuid; + + public OAuth2CaptchaAuthenticationToken( + Authentication clientPrincipal, Map additionalParameters) { + super(CAPTCHA_GRANT_TYPE, clientPrincipal, additionalParameters); + this.username = (String) additionalParameters.get("username"); + this.password = (String) additionalParameters.get("password"); + this.code = (String) additionalParameters.get("code"); + this.uuid = (String) additionalParameters.get("uuid"); + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getCode() { + return code; + } + + public String getUuid() { + return uuid; + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/OAuth2SmsAuthenticationToken.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/OAuth2SmsAuthenticationToken.java new file mode 100644 index 0000000..a775538 --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/OAuth2SmsAuthenticationToken.java @@ -0,0 +1,41 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.token; + +import java.util.Map; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; + +public class OAuth2SmsAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { + + public static final AuthorizationGrantType SMS_GRANT_TYPE = new AuthorizationGrantType("sms"); + + private final String phone; + private final String code; + + public OAuth2SmsAuthenticationToken( + Authentication clientPrincipal, Map additionalParameters) { + super(SMS_GRANT_TYPE, clientPrincipal, additionalParameters); + this.phone = (String) additionalParameters.get("phone"); + this.code = (String) additionalParameters.get("code"); + } + + public String getPhone() { + return phone; + } + + public String getCode() { + return code; + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/PasswordAuthenticationToken.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/PasswordAuthenticationToken.java new file mode 100644 index 0000000..fa6ad2a --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/PasswordAuthenticationToken.java @@ -0,0 +1,39 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.token; + +import java.util.Map; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; + +public class PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { + + private final String username; + private final String password; + + public PasswordAuthenticationToken( + Authentication clientPrincipal, Map additionalParameters) { + super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters); + this.username = (String) additionalParameters.get("username"); + this.password = (String) additionalParameters.get("password"); + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } +} diff --git a/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/SmsAuthenticationToken.java b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/SmsAuthenticationToken.java new file mode 100644 index 0000000..b8e7d2b --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-security/src/main/java/cn/universal/security/token/SmsAuthenticationToken.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.security.token; + +import org.springframework.security.authentication.AbstractAuthenticationToken; + +public class SmsAuthenticationToken extends AbstractAuthenticationToken { + + private final String phone; + private final String code; + + public SmsAuthenticationToken(String phone, String code) { + super(null); + this.phone = phone; + this.code = code; + setAuthenticated(false); + } + + @Override + public Object getCredentials() { + return code; + } + + @Override + public Object getPrincipal() { + return phone; + } + + public String getPhone() { + return phone; + } + + public String getCode() { + return code; + } +} diff --git a/cn-universal-infrastructure/cn-universal-storage/pom.xml b/cn-universal-infrastructure/cn-universal-storage/pom.xml new file mode 100644 index 0000000..68079be --- /dev/null +++ b/cn-universal-infrastructure/cn-universal-storage/pom.xml @@ -0,0 +1,24 @@ + + + cn-universal-storage + 4.0.0 + + cn-universal-infrastructure + cn.universal.iot + 1.5-SNAPSHOT + + + + + cn.universal.iot + cn-universal-common + ${project.version} + + + org.springframework.boot + spring-boot-starter + + + \ No newline at end of file diff --git a/cn-universal-infrastructure/pom.xml b/cn-universal-infrastructure/pom.xml new file mode 100644 index 0000000..00c5d18 --- /dev/null +++ b/cn-universal-infrastructure/pom.xml @@ -0,0 +1,21 @@ + + + cn-universal-infrastructure + 4.0.0 + pom + + cn-universal + cn.universal.iot + 1.5-SNAPSHOT + + + + cn-universal-database + cn-universal-cache + cn-universal-message + cn-universal-storage + cn-universal-security + + \ No newline at end of file diff --git a/cn-universal-persistence/pom.xml b/cn-universal-persistence/pom.xml new file mode 100644 index 0000000..3cd780b --- /dev/null +++ b/cn-universal-persistence/pom.xml @@ -0,0 +1,145 @@ + + + cn-universal-persistence + 4.0.0 + + cn-universal + cn.universal.iot + 1.5-SNAPSHOT + + + 5.0.1 + true + 21 + 21 + 21 + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + true + + + org.projectlombok + lombok + 1.18.30 + + + + + + mybatis-generator-maven-plugin + + ${basedir}/src/main/resources/generatorConfig.xml + true + true + + + + mysql-connector-java + mysql + ${mysql.jdbc} + + + tk.mybatis + mapper + ${mapper.mybatis} + + + org.mybatis.generator + 1.3.6 + + + + + + + org.springframework.boot + spring-boot-starter-actuator + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper-spring-boot-starter} + + + + mapper-spring-boot-starter + tk.mybatis + ${mapper.mybatis} + + + mysql-connector-java + mysql + + + druid-spring-boot-starter + + + * + org.springframework.boot + + + com.alibaba + + + + lombok + org.projectlombok + + + ru.yandex.clickhouse + clickhouse-jdbc + 0.3.1 + + + commons-logging + commons-logging + + + + + cn.hutool + hutool-all + compile + + + cn-universal-core + cn.universal.iot + ${project.version} + + + + com.github.ben-manes.caffeine + caffeine + 2.8.8 + + + + + org.springframework.boot + spring-boot-starter-validation + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + + + + + com.google.guava + guava + 32.1.3-jre + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/annotation/SensitiveField.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/annotation/SensitiveField.java new file mode 100644 index 0000000..415e808 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/annotation/SensitiveField.java @@ -0,0 +1,26 @@ +package cn.universal.persistence.annotation; + +import cn.universal.persistence.serializer.SensitiveFieldSerializer; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** 敏感字段注解 用于标记需要加密显示的字段 */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@JsonSerialize(using = SensitiveFieldSerializer.class) +public @interface SensitiveField { + + /** 是否完全隐藏(显示为***) */ + boolean hide() default false; + + /** 显示前几位字符 */ + int showPrefix() default 0; + + /** 显示后几位字符 */ + int showSuffix() default 0; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/base/BaseDownRequest.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/BaseDownRequest.java new file mode 100644 index 0000000..ea2d7c4 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/BaseDownRequest.java @@ -0,0 +1,40 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.base; + +import cn.hutool.json.JSONObject; +import cn.universal.core.message.DownRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.entity.IoTProduct; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** */ +@EqualsAndHashCode(callSuper = true) +@AllArgsConstructor +@NoArgsConstructor +@Data +@SuperBuilder +public class BaseDownRequest extends DownRequest { + + private Object params; + + private transient IoTProduct ioTProduct; + + private transient IoTDeviceDTO ioTDeviceDTO; + + private JSONObject data; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/base/BaseUPRequest.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/BaseUPRequest.java new file mode 100644 index 0000000..47dc562 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/BaseUPRequest.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.base; + +import cn.universal.core.message.UPRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.entity.IoTProduct; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * @version 1.0 @Author Aleo + * @since 2023/1/12 + */ +@EqualsAndHashCode(callSuper = true) +@AllArgsConstructor +@NoArgsConstructor +@Data +@SuperBuilder +public class BaseUPRequest extends UPRequest { + + private transient IoTDeviceDTO ioTDeviceDTO; + + private transient IoTProduct ioTProduct; + + /** 指令 */ + private String commandId; + + /** 指令 */ + private Integer commandStatus; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/base/CommonRequest.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/CommonRequest.java new file mode 100644 index 0000000..9923392 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/CommonRequest.java @@ -0,0 +1,289 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.base; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.crypto.digest.MD5; +import cn.hutool.http.Header; +import cn.hutool.http.HttpException; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CommonRequest { + + /** + * http请求超时时间 + */ + private static final Integer HTTP_TIME_OUT = 1200; + + /** + * URL状态信息 + */ + public static class UrlStatus { + + private int failureCount = 0; + private long lastFailureTime = 0; + private long nextAllowTime = 0; + + public UrlStatus() { + } + + public int getFailureCount() { + return failureCount; + } + + public void setFailureCount(int failureCount) { + this.failureCount = failureCount; + } + + public long getLastFailureTime() { + return lastFailureTime; + } + + public void setLastFailureTime(long lastFailureTime) { + this.lastFailureTime = lastFailureTime; + } + + public long getNextAllowTime() { + return nextAllowTime; + } + + public void setNextAllowTime(long nextAllowTime) { + this.nextAllowTime = nextAllowTime; + } + } + + private static String CACHE_NOTICE = "DNotice:"; + + // 存储URL状态信息,30分钟后过期 + private static Cache urlStatusCache = Caffeine.newBuilder() + .expireAfterWrite(30, TimeUnit.MINUTES).maximumSize(256).build(); + + /** + * 分级阈值配置 + */ + private static final int LEVEL_1_THRESHOLD = 3; // 轻微故障阈值 + + private static final int LEVEL_2_THRESHOLD = 6; // 中等故障阈值 + private static final int LEVEL_3_THRESHOLD = 10; // 严重故障阈值 + + /** + * 基础延迟时间(秒) + */ + private static final int BASE_DELAY_SECONDS = 30; + + /** + * 最大延迟时间(分钟) + */ + private static final int MAX_DELAY_MINUTES = 10; + + private static ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + + public static void downTransfer(String url, String body) { + executor.execute(() -> doRequest(url, body)); + } + + public static String downTransferSync(String url, String body) { + return doRequest(url, body); + } + + public static String doRequest(String url, String body) { + return doRequest(url, body, false); + } + + public static String doRequest(String url, String body, boolean needMd5) { + // 检查URL是否被限制访问 + if (isUrlBlocked(url)) { + UrlStatus status = urlStatusCache.getIfPresent(url); + long remainingSeconds = (status.getNextAllowTime() - System.currentTimeMillis()) / 1000; + log.warn("当前地址[{}]因连续失败{}次,暂时限制访问,还需等待{}秒", url, + status.getFailureCount(), remainingSeconds); + return null; + } + + try { + HttpRequest request = HttpUtil.createPost(url); + request.timeout(HTTP_TIME_OUT); + String timestamp = String.valueOf(System.currentTimeMillis()); + String signature = String.valueOf((timestamp + url).hashCode()); + request.header("X-Timestamp", timestamp); + request.header("X-Signature", signature); + request.header(Header.CONTENT_TYPE, "application/json"); + request.body(body); + HttpResponse postBody = request.execute(); + String result = postBody.body(); + if (postBody == null || postBody.getStatus() != 200) { + log.warn("推送第三方失败,url={} body={} 返回={}", url, body, result); + handleFailure(url); + return result; + } + log.info("推送第三方成功={},内容={}", url, body); + handleSuccess(url); + return result; + } catch (HttpException e) { + log.warn("推送第三方失败,url={} body={} 异常={}", url, body, ExceptionUtil.getRootCause(e)); + handleFailure(url); + } + return null; + } + + /** + * 检查URL是否被阻止 + */ + private static boolean isUrlBlocked(String url) { + UrlStatus status = urlStatusCache.getIfPresent(url); + if (status == null) { + return false; + } + + long currentTime = System.currentTimeMillis(); + return currentTime < status.getNextAllowTime(); + } + + /** + * 处理请求成功 + */ + private static void handleSuccess(String url) { + UrlStatus status = urlStatusCache.getIfPresent(url); + if (status != null) { + // 成功后重置失败计数,但保留状态以避免频繁创建对象 + status.setFailureCount(0); + status.setNextAllowTime(0); + log.debug("URL[{}]请求成功,重置失败计数", url); + } + } + + /** + * 处理请求失败 - 智能退避策略 + */ + private static void handleFailure(String url) { + UrlStatus status = urlStatusCache.getIfPresent(url); + if (status == null) { + status = new UrlStatus(); + urlStatusCache.put(url, status); + } + + status.setFailureCount(status.getFailureCount() + 1); + status.setLastFailureTime(System.currentTimeMillis()); + + int failureCount = status.getFailureCount(); + long delayMillis = calculateDelay(failureCount); + status.setNextAllowTime(System.currentTimeMillis() + delayMillis); + + String delayInfo = formatDelayInfo(delayMillis); + log.warn("URL[{}]第{}次失败,采用{}策略,下次可访问时间:{}", url, failureCount, + getStrategyLevel(failureCount), delayInfo); + + // 严重故障时发送通知 + if (failureCount >= LEVEL_3_THRESHOLD) { + sendFailureNotification(url, failureCount); + } + } + + /** + * 计算延迟时间 - 指数退避算法 + */ + private static long calculateDelay(int failureCount) { + if (failureCount <= 1) { + return 0; // 首次失败不延迟 + } + + long delaySeconds; + + if (failureCount <= LEVEL_1_THRESHOLD) { + // 轻微故障:线性增长 30s, 60s, 90s + delaySeconds = failureCount * BASE_DELAY_SECONDS; + } else if (failureCount <= LEVEL_2_THRESHOLD) { + // 中等故障:指数增长,但有上限 + delaySeconds = (long) (BASE_DELAY_SECONDS * Math.pow(2, failureCount - LEVEL_1_THRESHOLD)); + } else { + // 严重故障:固定长时间延迟 + delaySeconds = MAX_DELAY_MINUTES * 60; + } + + // 限制最大延迟时间 + delaySeconds = Math.min(delaySeconds, MAX_DELAY_MINUTES * 60); + + return delaySeconds * 1000; // 转换为毫秒 + } + + /** + * 获取策略级别描述 + */ + private static String getStrategyLevel(int failureCount) { + if (failureCount <= LEVEL_1_THRESHOLD) { + return "轻微故障-线性退避"; + } else if (failureCount <= LEVEL_2_THRESHOLD) { + return "中等故障-指数退避"; + } else { + return "严重故障-长时间限制"; + } + } + + /** + * 格式化延迟信息 + */ + private static String formatDelayInfo(long delayMillis) { + long seconds = delayMillis / 1000; + if (seconds < 60) { + return seconds + "秒后"; + } else { + long minutes = seconds / 60; + long remainingSeconds = seconds % 60; + if (remainingSeconds == 0) { + return minutes + "分钟后"; + } else { + return minutes + "分" + remainingSeconds + "秒后"; + } + } + } + + /** + * 发送失败通知 + */ + private static void sendFailureNotification(String url, int failureCount) { + String noticeKey = CACHE_NOTICE + url; + UrlStatus noticeStatus = urlStatusCache.getIfPresent(noticeKey); + + // 每小时最多发送一次通知 + if (noticeStatus == null || (System.currentTimeMillis() - noticeStatus.getLastFailureTime()) + > TimeUnit.HOURS.toMillis(1)) { + + log.error("严重故障警告:URL[{}]连续失败{}次,已进入长时间限制状态", url, failureCount); + + // 记录通知时间 + UrlStatus newNoticeStatus = new UrlStatus(); + newNoticeStatus.setLastFailureTime(System.currentTimeMillis()); + urlStatusCache.put(noticeKey, newNoticeStatus); + + // TODO: 集成钉钉/企微等通知 + } + } + + private static void md5(String body, boolean needMd5, HttpRequest request) { + if (needMd5) { + String now = DateUtil.now(); + request.header("times", now); + request.header("cn-universal-sign", MD5.create().digestHex(body + now)); + } + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTAction.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTAction.java new file mode 100644 index 0000000..691e9e2 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTAction.java @@ -0,0 +1,16 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.base; + +/** 生命周期接口 */ +public interface IoTAction {} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDeviceExtendDTO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDeviceExtendDTO.java new file mode 100644 index 0000000..13db995 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDeviceExtendDTO.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.base; + +import cn.universal.core.message.DownRequest; + +/** 根据产品自定义扩展实现 */ +public interface IoTDeviceExtendDTO { + + String productKey(); + + /** + * 下行扩展 + * + * @param downRequest + */ + default void downExt(DownRequest downRequest) {} + + /** + * 上行扩展 + * + * @param downRequest + */ + default void upExt(BaseUPRequest downRequest) {} +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDeviceExtendService.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDeviceExtendService.java new file mode 100644 index 0000000..d2e95c6 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDeviceExtendService.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.base; + +import cn.universal.core.message.DownRequest; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.springframework.stereotype.Component; + +@Component +public class IoTDeviceExtendService { + + private final Map deviceExtMap; + + public IoTDeviceExtendService(List deviceExtList) { + deviceExtMap = + deviceExtList.stream() + .collect(Collectors.toMap(IoTDeviceExtendDTO::productKey, Function.identity())); + } + + public void downExt(DownRequest downRequest) { + IoTDeviceExtendDTO ioTDeviceExtendDTO = deviceExtMap.get(downRequest.getProductKey()); + if (Objects.nonNull(ioTDeviceExtendDTO)) { + ioTDeviceExtendDTO.downExt(downRequest); + } + } + + public void upExt(BaseUPRequest upRequest) { + IoTDeviceExtendDTO ioTDeviceExtendDTO = deviceExtMap.get(upRequest.getProductKey()); + if (Objects.nonNull(ioTDeviceExtendDTO)) { + ioTDeviceExtendDTO.upExt(upRequest); + } + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDeviceLifeCycle.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDeviceLifeCycle.java new file mode 100644 index 0000000..e329ed2 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDeviceLifeCycle.java @@ -0,0 +1,121 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.base; + +import cn.universal.common.constant.IoTConstant.DevLifeCycle; +import cn.universal.core.message.DownRequest; +import cn.universal.persistence.dto.IoTDeviceDTO; + +/** + * 设备生命周期接口 + * + *

1.创建设备 + * + *

2.设备上线和下线 + * + *

3.禁用和启用设备 + * + *

4.删除设备 + * + * @version 1.0 @Author Aleo + * @since 2023/1/20 + */ +public interface IoTDeviceLifeCycle extends IoTAction { + + /** + * 设备创建 + * + * @param productKey 产品唯一标识 + * @param deviceId 设备编号 + */ + void create(String productKey, String deviceId, DownRequest downRequest); + + void online(String productKey, String deviceId); + + /** + * 设备离线 + * + * @param productKey 产品key + * @param deviceId 设备编号 + */ + void offline(String productKey, String deviceId); + + /** + * 设备修改 + * + * @param iotId + */ + void update(String iotId); + + /** + * 设备启用 + * + * @param iotId + */ + void enable(String iotId); + + /** + * 设备禁用 + * + * @param iotId + */ + void disable(String iotId); + + /** + * 设备删除 + * + * @param ioTDeviceDTO + */ + void delete(IoTDeviceDTO ioTDeviceDTO, DownRequest downRequest); + + /** + * 判断物模型是否官方处理 + * + * @param ioTDeviceDTO + * @param devLifeCycle + * @return + */ + default boolean thirdSupport(IoTDeviceDTO ioTDeviceDTO, DevLifeCycle devLifeCycle) { + return false; + } + + /** + * 指令下发 + * + *

主要用于统一记录日志,推送消息 + * + * @param ioTDeviceDTO 设备信息 + * @param functions 功能 + */ + default void command(IoTDeviceDTO ioTDeviceDTO, String commandId, Object functions) {} + + /** + * 指令下发 + * + *

主要用于统一记录日志,推送消息 + * + * @param ioTDeviceDTO 设备信息 + * @param functions 功能 + */ + default void commandSuccess(IoTDeviceDTO ioTDeviceDTO, String commandId, Object functions) {} + + /** + * 指令回复 + * + *

主要用于统一记录日志,推送消息 + * + * @param ioTDeviceDTO 设备信息 + * @param resp 功能 + */ + default void commandResp(IoTDeviceDTO ioTDeviceDTO, String commandId, Object resp) {} +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDownAdapter.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDownAdapter.java new file mode 100644 index 0000000..f7b70c1 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDownAdapter.java @@ -0,0 +1,233 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.base; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.constant.IoTConstant.DeviceStatus; +import cn.universal.common.constant.IoTConstant.DownCmd; +import cn.universal.common.domain.R; +import cn.universal.common.utils.ThreadLocalCache; +import cn.universal.core.message.DownRequest; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.SupportMapAreas; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.SupportMapAreasMapper; +import jakarta.annotation.Resource; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** 指令下发包装类 */ +@Slf4j +public abstract class IoTDownAdapter { + + @Resource private StringRedisTemplate stringRedisTemplate; + + @Resource(name = "ioTDeviceActionBeforeService") + private IoTDeviceLifeCycle ioTDeviceLifeCycle; + + @Resource private IoTDeviceExtendService deviceExtTemplate; + + @Resource private SupportMapAreasMapper supportMapAreasMapper; + + @Resource private IoTDeviceMapper ioTDeviceMapper; + + private String CUSTOM_FIELD = "customField"; + + /** + * iot_product + * + *

{third_configuration}字段配置JSON + * + *

customField + * + *

[添加设备]校验->data中是否包含自定义字段 + */ + protected R beforeDownAction(IoTProduct product, Object data, DownRequest downRequest) { + deviceExtTemplate.downExt(downRequest); + Map downWrapperMap = SpringUtil.getBeansOfType(IoTDownWrapper.class); + if (MapUtil.isNotEmpty(downWrapperMap)) { + for (Map.Entry entry : downWrapperMap.entrySet()) { + IoTDownWrapper iotDownWrapper = entry.getValue(); + R r = iotDownWrapper.beforeDownAction(product, data, downRequest); + if (r != null) { + return r; + } + } + } + ioTDeviceLifeCycle.create(downRequest.getProductKey(), downRequest.getDeviceId(), downRequest); + return null; + } + + /** 调用全局功能函数 如果没有匹配到返回null */ + protected R callGlobalFunction(IoTProduct product, IoTDevice ioTDevice, DownRequest downRequest) { + Map downWrapperMap = SpringUtil.getBeansOfType(IoTDownWrapper.class); + if (MapUtil.isNotEmpty(downWrapperMap)) { + for (Map.Entry entry : downWrapperMap.entrySet()) { + IoTDownWrapper iotDownWrapper = entry.getValue(); + R r = iotDownWrapper.beforeFunctionOrConfigDown(product, ioTDevice, downRequest); + if (r != null) { + return r; + } + } + } + return null; + } + + /** 后置处理 */ + protected void finalDown( + Map config, IoTProduct product, DownCmd downCmd, Object data) { + try { + if (product != null + && StrUtil.isNotBlank(product.getThirdConfiguration()) + && DownCmd.DEV_ADD.equals(downCmd)) { + JSONObject jsonObject = JSONUtil.parseObj(product.getThirdConfiguration()); + JSONArray customFields = jsonObject.getJSONArray(CUSTOM_FIELD); + if (jsonObject == null || customFields == null || customFields.size() <= 0) { + return; + } + for (Object obj : customFields) { + JSONObject object = (JSONObject) obj; + Object fieldValue = BeanUtil.getFieldValue(data, object.getStr("id")); + config.put(object.getStr("id"), fieldValue); + } + } + } catch (Exception e) { + log.warn("={}", e); + } + } + + /** 保存发送指令 */ + protected void storeCommand(String productKey, String deviceId, Object data) { + String value = + stringRedisTemplate + .opsForValue() + .get(IoTConstant.REDIS_STORE_COMMAND + ":" + productKey + ":" + deviceId); + if (StrUtil.isBlank(value)) { + stringRedisTemplate + .opsForValue() + .set( + IoTConstant.REDIS_STORE_COMMAND + ":" + productKey + ":" + deviceId, + JSONUtil.toJsonStr(Stream.of(data).collect(Collectors.toList())), + 3, + TimeUnit.DAYS); + } else { + JSONArray ar = JSONUtil.parseArray(value); + JSONArray ne = keepLatestFunction(ar, data); + // ar.add(data); + stringRedisTemplate + .opsForValue() + .set( + IoTConstant.REDIS_STORE_COMMAND + ":" + productKey + ":" + deviceId, + JSONUtil.toJsonStr(ne), + 3, + TimeUnit.DAYS); + } + } + + /** 同一function保留最后一条指令 */ + private JSONArray keepLatestFunction(JSONArray array, Object data) { + JSONArray result = new JSONArray(); + AtomicBoolean isOldData = new AtomicBoolean(false); + array.forEach( + o -> { + String originFunction = JSONUtil.parseObj(o).getJSONObject("function").getStr("function"); + String latestFunction = + JSONUtil.parseObj(data).getJSONObject("function").getStr("function"); + if (originFunction.equals(latestFunction)) { + result.add(data); + isOldData.set(true); + } else { + result.add(o); + } + }); + if (!isOldData.get()) { + result.add(data); + } + return result; + } + + protected Map callSaveDeviceInstance(BaseDownRequest downRequest) { + IoTDevice ioTDevice = + IoTDevice.builder() + .deviceId(downRequest.getDeviceId()) + .createTime(System.currentTimeMillis() / 1000) + .deviceName(downRequest.getData().getStr("deviceName", downRequest.getDeviceId())) + .state(DeviceStatus.offline.getCode()) + .iotId(IdUtil.simpleUUID()) + .application(downRequest.getApplicationId()) + .creatorId(downRequest.getAppUnionId()) + .productName(downRequest.getIoTProduct().getName()) + .detail(downRequest.getDetail()) + .gwProductKey(downRequest.getGwProductKey()) + .extDeviceId(downRequest.getData().getStr("extDeviceId")) + .productKey(downRequest.getProductKey()) + .build(); + + if (StrUtil.isNotBlank(downRequest.getData().getStr("latitude")) + && StrUtil.isNotBlank(downRequest.getData().getStr("longitude"))) { + ioTDevice.setCoordinate( + StrUtil.join( + ",", + downRequest.getData().getStr("longitude"), + downRequest.getData().getStr("latitude"))); + + SupportMapAreas supportMapAreas = + supportMapAreasMapper.selectMapAreas( + downRequest.getData().getStr("longitude"), downRequest.getData().getStr("latitude")); + if (supportMapAreas == null) { + log.info( + "[HTTP下行][区域查询] 查询区域id为空,lon={},lat={}", + downRequest.getData().getStr("longitude"), + downRequest.getData().getStr("latitude")); + } else { + ioTDevice.setAreasId(supportMapAreas.getId()); + } + } + Map config = new HashMap<>(); + finalDown(config, downRequest.getIoTProduct(), downRequest.getCmd(), downRequest.getData()); + ioTDevice.setConfiguration(JSONUtil.toJsonStr(config)); + if (StrUtil.isNotBlank(ThreadLocalCache.localExtDeviceId.get())) { + ioTDevice.setExtDeviceId(ThreadLocalCache.localExtDeviceId.get()); + ThreadLocalCache.localExtDeviceId.remove(); + } + ioTDeviceMapper.insertUseGeneratedKeys(ioTDevice); + // 推送设备创建消息 + ioTDeviceLifeCycle.create(downRequest.getProductKey(), downRequest.getDeviceId(), downRequest); + + // 组件返回字段 + Map result = new HashMap<>(); + result.put("iotId", ioTDevice.getIotId()); + result.put("areasId", ioTDevice.getAreasId() == null ? "" : ioTDevice.getAreasId()); + if (StrUtil.isNotBlank(downRequest.getIoTProduct().getMetadata())) { + result.put("metadata", JSONUtil.parseObj(downRequest.getIoTProduct().getMetadata())); + } + result.put("productKey", downRequest.getProductKey()); + result.put("deviceNode", downRequest.getIoTProduct().getDeviceNode()); + return result; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDownWrapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDownWrapper.java new file mode 100644 index 0000000..e25729d --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTDownWrapper.java @@ -0,0 +1,49 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.base; + +import cn.universal.common.domain.R; +import cn.universal.core.message.DownRequest; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTProduct; + +/** + * @version 1.0 @Author Aleo + * @since 2023/8/10 + */ +public interface IoTDownWrapper { + + /** + * 用于全局调用的处理 + * + * @param product + * @param downRequest + * @return + */ + default R beforeDownAction(IoTProduct product, Object data, DownRequest downRequest) { + return null; + } + + /** + * 用于全局调用的处理 + * + * @param product + * @param ioTDevice + * @param downRequest + * @return + */ + default R beforeFunctionOrConfigDown( + IoTProduct product, IoTDevice ioTDevice, DownRequest downRequest) { + return null; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTProductAction.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTProductAction.java new file mode 100644 index 0000000..ef9b836 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTProductAction.java @@ -0,0 +1,83 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.base; + +import cn.universal.persistence.entity.IoTProduct; + +/** 产品(设备)全Action生命周期接口 */ +public interface IoTProductAction extends IoTAction { + + /** + * 产品创建 + * + * @param productKey 产品唯一标识 + * @param unionId 创建用户 + */ + void create(String productKey, String unionId); + + /** + * 设备上线 + * + * @param product 产品对象 + */ + void update(IoTProduct product); + + /** + * 产品删除 + * + * @param productKey + */ + void delete(String productKey); + + /** + * 产品启用 + * + * @param productKey + */ + void enable(String productKey); + + /** + * 产品禁用 + * + * @param productKey + */ + void disable(String productKey); + + /** + * 产品设置为公共产品 + * + * @param productKey + */ + void publish(String productKey); + + /** + * 物模型创建 + * + * @param productKey + */ + default void metadataCreate(String productKey) {} + + /** + * 物模型更新 + * + * @param productKey + */ + default void metadataUpdate(String productKey) {} + + /** + * 物模型删除 + * + * @param productKey + */ + default void metadataDelete(String productKey) {} +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTUPWrapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTUPWrapper.java new file mode 100644 index 0000000..b9c4e0c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/base/IoTUPWrapper.java @@ -0,0 +1,39 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.base; + +import java.util.List; + +/** + * @version 1.0 @Author Aleo + * @since 2023/8/10 + */ +public interface IoTUPWrapper { + + /** + * 推送前置处理,规则引擎,场景联动 + * + * @param downRequests 消息原文 + */ + default void beforePush(List downRequests) {} + + /** + * mqtt 推送 + * + * @param topic 主题 + * @param message 消息 + */ + default void mqttPush(String topic, String message) {} + + default void tcpPush(String applicationId, String productKey, String message) {} +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/codec/CodecParam.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/codec/CodecParam.java new file mode 100644 index 0000000..bd3e95c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/codec/CodecParam.java @@ -0,0 +1,32 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.codec; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * 请求参数实体 @Author Aleo + * + * @since 2023/11/23 9:10 + */ +@Data +@AllArgsConstructor +public class CodecParam { + + /** 唯一编号 */ + private String codeKey; + + /** 编解码内容 */ + private Object codeBody; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/common/BaseMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/common/BaseMapper.java new file mode 100644 index 0000000..98f5abe --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/common/BaseMapper.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.common; + +import java.io.Serializable; +import tk.mybatis.mapper.common.ConditionMapper; +import tk.mybatis.mapper.common.IdsMapper; +import tk.mybatis.mapper.common.Mapper; +import tk.mybatis.mapper.common.MySqlMapper; + +/** tk.mybatis */ +public interface BaseMapper + extends Mapper, MySqlMapper, ConditionMapper, IdsMapper {} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/common/inteceptor/PerformanceInterceptor.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/common/inteceptor/PerformanceInterceptor.java new file mode 100644 index 0000000..a413260 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/common/inteceptor/PerformanceInterceptor.java @@ -0,0 +1,140 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.common.inteceptor; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.mapping.ParameterMode; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.plugin.Intercepts; +import org.apache.ibatis.plugin.Invocation; +import org.apache.ibatis.plugin.Plugin; +import org.apache.ibatis.plugin.Signature; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.apache.ibatis.type.TypeHandlerRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** MyBatis 性能拦截器,用于输出每条 SQL 语句及其执行时间 */ +@Intercepts({ + @Signature( + type = Executor.class, + method = "query", + args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), + @Signature( + type = Executor.class, + method = "update", + args = {MappedStatement.class, Object.class}) +}) +public class PerformanceInterceptor implements Interceptor { + + private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private final Logger logger = LoggerFactory.getLogger("mybatisPerformanceInterceptor"); + + @Override + public Object intercept(Invocation invocation) throws Throwable { + MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; + Object parameterObject = null; + if (invocation.getArgs().length > 1) { + parameterObject = invocation.getArgs()[1]; + } + String statementId = mappedStatement.getId(); + BoundSql boundSql = mappedStatement.getBoundSql(parameterObject); + Configuration configuration = mappedStatement.getConfiguration(); + String sql = getSql(boundSql, parameterObject, configuration); + long start = System.currentTimeMillis(); + Object result = invocation.proceed(); + long end = System.currentTimeMillis(); + long timing = end - start; + if (logger.isDebugEnabled()) { + logger.debug("[I D]{}", statementId); + logger.debug("[SQL]{}", sql); + logger.debug("[耗时] {} ms", timing); + } + return result; + } + + @Override + public Object plugin(Object target) { + if (target instanceof Executor) { + return Plugin.wrap(target, this); + } + return target; + } + + @Override + public void setProperties(Properties properties) {} + + private String getSql(BoundSql boundSql, Object parameterObject, Configuration configuration) { + String sql = ""; + List parameterMappings = boundSql.getParameterMappings(); + TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); + if (parameterMappings != null) { + try { + sql = boundSql.getSql().replaceAll("[\\s]+", " "); + for (int i = 0; i < parameterMappings.size(); i++) { + ParameterMapping parameterMapping = parameterMappings.get(i); + if (parameterMapping.getMode() != ParameterMode.OUT) { + Object value; + String propertyName = parameterMapping.getProperty(); + if (boundSql.hasAdditionalParameter(propertyName)) { + value = boundSql.getAdditionalParameter(propertyName); + } else if (parameterObject == null) { + value = null; + } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { + value = parameterObject; + } else { + MetaObject metaObject = configuration.newMetaObject(parameterObject); + value = metaObject.getValue(propertyName); + } + sql = replacePlaceholder(sql, value); + } + } + } catch (Exception e) { + logger.error("mybatis intercept error={}", e); + logger.warn("mybatis intercept error ,sql [{}] propertyValue[{}]", sql, parameterObject); + } + } + return sql; + } + + private String replacePlaceholder(String sql, Object propertyValue) { + String result; + if (propertyValue != null) { + if (propertyValue instanceof String) { + // Escape replacement string special characters: $ and \ + String escapedValue = ((String) propertyValue).replace("\\", "\\\\").replace("$", "\\$"); + result = "'" + escapedValue + "'"; + } else if (propertyValue instanceof Date) { + result = "'" + DATE_FORMAT.format(propertyValue) + "'"; + } else { + // Escape replacement string special characters: $ and \ + String escapedValue = propertyValue.toString().replace("\\", "\\\\").replace("$", "\\$"); + result = escapedValue; + } + } else { + result = "null"; + } + return sql.replaceFirst("\\?", result); + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/common/inteceptor/SQenGenId.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/common/inteceptor/SQenGenId.java new file mode 100644 index 0000000..a28e215 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/common/inteceptor/SQenGenId.java @@ -0,0 +1,27 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.common.inteceptor; + +import cn.hutool.core.lang.Snowflake; +import tk.mybatis.mapper.genid.GenId; + +/** 雪花Id生成器 */ +public class SQenGenId implements GenId { + + public static final Snowflake snowflake = new Snowflake(); + + @Override + public Long genId(String s, String s1) { + return snowflake.nextId(); + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/common/inteceptor/TableShardInterceptor.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/common/inteceptor/TableShardInterceptor.java new file mode 100644 index 0000000..0aa145f --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/common/inteceptor/TableShardInterceptor.java @@ -0,0 +1,225 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.common.inteceptor; + +import cn.hutool.extra.spring.SpringUtil; +import cn.universal.persistence.consistent.ITableShardingStrategy; +import cn.universal.persistence.consistent.TableShard; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.util.Map; +import java.util.Properties; +import org.apache.ibatis.binding.MapperMethod; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.plugin.Intercepts; +import org.apache.ibatis.plugin.Invocation; +import org.apache.ibatis.plugin.Plugin; +import org.apache.ibatis.plugin.Signature; +import org.apache.ibatis.reflection.DefaultReflectorFactory; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.ReflectorFactory; +import org.apache.ibatis.reflection.SystemMetaObject; +import org.springframework.stereotype.Component; + +@Component +@Intercepts({ + @Signature( + type = StatementHandler.class, + method = "prepare", + args = {Connection.class, Integer.class}) +}) +public class TableShardInterceptor implements Interceptor { + + private static final ReflectorFactory defaultReflectorFactory = new DefaultReflectorFactory(); + + @Override + public Object intercept(Invocation invocation) throws Throwable { + MetaObject metaObject = getMetaObject(invocation); + BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); + MappedStatement mappedStatement = + (MappedStatement) metaObject.getValue("delegate.mappedStatement"); + + // 获取分表注解 + TableShard tableShard = getTableShard(mappedStatement); + + // 如果method与class都没有TableShard注解或执行方法不存在,执行下一个插件逻辑 + if (tableShard == null) { + return invocation.proceed(); + } + + // 获取值 + String value = tableShard.value(); + // value是否字段名,如果是,需要解析请求参数字段名的值 + boolean fieldFlag = tableShard.fieldFlag(); + + // 如果value为空,说明不需要分表,直接执行 + if (value == null || value.trim().isEmpty()) { + return invocation.proceed(); + } + + if (fieldFlag) { + // 获取请求参数 + Object parameterObject = boundSql.getParameterObject(); + + if (parameterObject instanceof MapperMethod.ParamMap) { // ParamMap类型逻辑处理 + + MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject; + // 根据字段名获取参数值 + Object valueObject = parameterMap.get(value); + if (valueObject == null) { + // 如果找不到参数,直接执行,不进行分表 + return invocation.proceed(); + } + // 替换sql + replaceSql(tableShard, valueObject, metaObject, boundSql); + + } else { // 单参数逻辑 + + // 如果是基础类型抛出异常 + if (isBaseType(parameterObject)) { + throw new RuntimeException("单参数非法,请使用@Param注解"); + } + + if (parameterObject instanceof Map) { + Map parameterMap = (Map) parameterObject; + Object valueObject = parameterMap.get(value); + if (valueObject == null) { + // 如果找不到参数,直接执行,不进行分表 + return invocation.proceed(); + } + // 替换sql + replaceSql(tableShard, valueObject, metaObject, boundSql); + } else { + // 非基础类型对象 + try { + Class parameterObjectClass = parameterObject.getClass(); + Field declaredField = parameterObjectClass.getDeclaredField(value); + declaredField.setAccessible(true); + Object valueObject = declaredField.get(parameterObject); + // 替换sql + replaceSql(tableShard, valueObject, metaObject, boundSql); + } catch (NoSuchFieldException e) { + // 如果找不到字段,直接执行,不进行分表 + return invocation.proceed(); + } + } + } + + } else { // 无需处理parameterField + // 替换sql + replaceSql(tableShard, value, metaObject, boundSql); + } + // 执行下一个插件逻辑 + return invocation.proceed(); + } + + @Override + public Object plugin(Object o) { + // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身, 减少目标被代理的次数 + if (o instanceof StatementHandler) { + return Plugin.wrap(o, this); + } else { + return o; + } + } + + @Override + public void setProperties(Properties properties) {} + + /** 基本数据类型验证,true是,false否 */ + private boolean isBaseType(Object object) { + if (object.getClass().isPrimitive() + || object instanceof String + || object instanceof Integer + || object instanceof Double + || object instanceof Float + || object instanceof Long + || object instanceof Boolean + || object instanceof Byte + || object instanceof Short) { + return true; + } else { + return false; + } + } + + /** 替换sql */ + private void replaceSql( + TableShard tableShard, Object value, MetaObject metaObject, BoundSql boundSql) { + String tableNamePrefix = tableShard.tableNamePrefix(); + // 获取策略class + Class strategyClazz = tableShard.shardStrategy(); + // 从spring ioc容器获取策略类 + + ITableShardingStrategy tableShardStrategy = SpringUtil.getBean(strategyClazz); + // 生成分表名 + String shardTableName = tableShardStrategy.generateTableName(tableNamePrefix, value); + // 获取sql + String sql = boundSql.getSql(); + // 完成表名替换 + metaObject.setValue("delegate.boundSql.sql", sql.replaceAll(tableNamePrefix, shardTableName)); + } + + /** 获取MetaObject对象-mybatis里面提供的一个工具类,类似反射的效果 */ + private MetaObject getMetaObject(Invocation invocation) { + StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); + // MetaObject是mybatis里面提供的一个工具类,类似反射的效果 + MetaObject metaObject = + MetaObject.forObject( + statementHandler, + SystemMetaObject.DEFAULT_OBJECT_FACTORY, + SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, + defaultReflectorFactory); + + return metaObject; + } + + /** 获取分表注解 - 修复版本 */ + private TableShard getTableShard(MappedStatement mappedStatement) throws ClassNotFoundException { + String id = mappedStatement.getId(); + // 获取Class + final String className = id.substring(0, id.lastIndexOf(".")); + // 获取方法名 + final String methodName = id.substring(id.lastIndexOf(".") + 1); + + // 获取Mapper接口类 + Class mapperClass = Class.forName(className); + + // 分表注解 + TableShard tableShard = null; + + // 先查找方法上的注解 + try { + Method[] methods = mapperClass.getMethods(); + for (Method method : methods) { + if (method.getName().equals(methodName)) { + tableShard = method.getAnnotation(TableShard.class); + if (tableShard != null) { + return tableShard; // 找到方法上的注解,直接返回 + } + } + } + } catch (Exception e) { + // 忽略异常,继续查找类上的注解 + } + + // 如果方法没有设置注解,从Mapper接口上面获取TableShard注解 + tableShard = mapperClass.getAnnotation(TableShard.class); + + return tableShard; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/config/MybatisConfig.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/config/MybatisConfig.java new file mode 100644 index 0000000..8e9aa08 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/config/MybatisConfig.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.config; + +import cn.universal.persistence.common.inteceptor.PerformanceInterceptor; +import org.apache.ibatis.plugin.Interceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tk.mybatis.spring.annotation.MapperScan; + +/** 扫描 */ +@Configuration +@MapperScan(value = {"cn.universal.**.mapper"}) +public class MybatisConfig { + + @Bean + Interceptor sqlExplainInterceptor() { + return new PerformanceInterceptor(); + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/consistent/DeviceMetaShardingRouter.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/consistent/DeviceMetaShardingRouter.java new file mode 100644 index 0000000..997157a --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/consistent/DeviceMetaShardingRouter.java @@ -0,0 +1,133 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.consistent; + +import cn.hutool.core.util.ObjectUtil; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import jakarta.annotation.PostConstruct; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** 根据设备iotId的hash值分表 @Author Aleo */ +@Component +public class DeviceMetaShardingRouter implements ITableShardingStrategy { + + private static Integer maxTableShard; + private static Integer virtualNode; + + /** 日志分表是否开启 */ + @Value("${shard.logMeta.enable}") + private Boolean enable; + + /** 日志分表数量 */ + @Value("${shard.logMeta.table.number}") + public void setMaxTableShard(Integer maxTableShard) { + DeviceMetaShardingRouter.maxTableShard = maxTableShard; + } + + /** 每个实际节点对应节点数量 */ + @Value("${shard.logMeta.virtual.number}") + public void setVirtualNode(Integer virtualNode) { + DeviceMetaShardingRouter.virtualNode = virtualNode; + } + + /** key表示日志表序号的hash值,value表示日志表序号 */ + private static SortedMap sortedMap = new TreeMap<>(); + + private static Cache tableNoMap = + Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).maximumSize(2048).build(); + + // static { + // for (int i = 0; i < maxTableShard; i++) { + // for (int j = 0; j <= virtualNode; j++) { + // int hash = getHash("ww048gf52544ea28frece10a1a#" + i + "_" + j); + // sortedMap.put(hash, i); + // } + // } + // } + @PostConstruct + private void initMap() { + for (int i = 0; i < maxTableShard; i++) { + for (int j = 0; j <= virtualNode; j++) { + int hash = getHash("ww048gf52544ea28frece10a1a#" + i + "_" + j); + sortedMap.put(hash, i); + } + } + } + + @Override + public String generateTableName(String tableNamePrefix, Object value) { + if (!enable) { + return tableNamePrefix; + } + verificationTableNamePrefix(tableNamePrefix); + if (ObjectUtil.isEmpty(value)) { + throw new RuntimeException("日志事件表分表异常,设备iotId为空"); + } + String iotId = value.toString(); + // 获取分表序号 + Integer tableNo = tableNoMap.getIfPresent(iotId); + if (tableNo == null) { + tableNo = convertToHash(iotId); + } + return tableNamePrefix + "_" + tableNo; + } + + /** 根据设备iotId转换hash获取分表序号 */ + private int convertToHash(String iotId) { + int hash = getHash(iotId); + SortedMap subMap = sortedMap.tailMap(hash); + if (subMap.isEmpty()) { + // 如果没有比该key的hash值大的,则从第一个node开始 + Integer i = sortedMap.firstKey(); + // 返回对应的日志表序号 + Integer tableNo = sortedMap.get(i); + tableNoMap.put(iotId, tableNo); + return tableNo; + } else { + // 第一个Key就是顺时针过去离node最近的那个结点 + Integer i = subMap.firstKey(); + // 返回对应的日志表序号 + Integer tableNo = subMap.get(i); + tableNoMap.put(iotId, tableNo); + return tableNo; + } + // int hash = mod(getHash(iotId), HASH_RING); + // hash = hash % MAX_TABLE_SHARD; + + // return hash; + } + + /** 计算Hash值, 使用FNV1_32_HASH算法 */ + public static int getHash(String str) { + final int p = 16777619; + int hash = (int) 2166136261L; + for (int i = 0; i < str.length(); i++) { + hash = (hash ^ str.charAt(i)) * p; + } + hash += hash << 13; + hash ^= hash >> 7; + hash += hash << 3; + hash ^= hash >> 17; + hash += hash << 5; + + if (hash < 0) { + hash = Math.abs(hash); + } + return hash; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/consistent/DeviceShardingRouter.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/consistent/DeviceShardingRouter.java new file mode 100644 index 0000000..2a224fa --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/consistent/DeviceShardingRouter.java @@ -0,0 +1,141 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.consistent; + +import cn.hutool.core.util.ObjectUtil; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import jakarta.annotation.PostConstruct; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** 根据设备iotId的hash值分表 */ +@Component +public class DeviceShardingRouter implements ITableShardingStrategy { + + private static Integer maxTableShard; + private static Integer virtualNode; + + /** 日志分表是否开启 */ + @Value("${shard.log.enable}") + private Boolean enable; + + /** 日志分表数量 */ + @Value("${shard.log.table.number}") + public void setMaxTableShard(Integer maxTableShard) { + DeviceShardingRouter.maxTableShard = maxTableShard; + } + + /** 每个实际节点对应节点数量 */ + @Value("${shard.log.virtual.number}") + public void setVirtualNode(Integer virtualNode) { + DeviceShardingRouter.virtualNode = virtualNode; + } + + /** key表示日志表序号的hash值,value表示日志表序号 */ + private static SortedMap sortedMap = new TreeMap<>(); + + private static Cache tableNoMap = + Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).maximumSize(2048).build(); + + // static { + // for (int i = 0; i < maxTableShard; i++) { + // for (int j = 0; j <= virtualNode; j++) { + // int hash = getHash("ww048gf52544ea28frece10a1a#" + i + "_" + j); + // sortedMap.put(hash, i); + // } + // } + // } + @PostConstruct + private void initMap() { + for (int i = 0; i < maxTableShard; i++) { + for (int j = 0; j <= virtualNode; j++) { + int hash = getHash("ww048gf52544ea28frece10a1a#" + i + "_" + j); + sortedMap.put(hash, i); + } + } + // Map s = new HashMap<>(); + // for (int i = 0; i < 10000; i++) { + // String iotId = IdUtil.simpleUUID(); + // int key = convertToHash(iotId); + // int value = (s.get(key) != null ? s.get(key) : 0) + 1; + // s.put(key, value); + // } + // System.out.println("aaaaaaaaaaaaaaaaa==" + JSONUtil.toJsonStr(s)); + } + + @Override + public String generateTableName(String tableNamePrefix, Object value) { + if (!enable) { + return tableNamePrefix; + } + verificationTableNamePrefix(tableNamePrefix); + if (ObjectUtil.isEmpty(value)) { + throw new RuntimeException("日志分表异常,设备iotId为空"); + } + String iotId = value.toString(); + // 获取分表序号 + Integer tableNo = tableNoMap.getIfPresent(iotId); + if (tableNo == null) { + tableNo = convertToHash(iotId); + } + return tableNamePrefix + "_" + tableNo; + } + + /** 根据设备iotId转换hash获取分表序号 */ + private int convertToHash(String iotId) { + int hash = getHash(iotId); + SortedMap subMap = sortedMap.tailMap(hash); + if (subMap.isEmpty()) { + // 如果没有比该key的hash值大的,则从第一个node开始 + Integer i = sortedMap.firstKey(); + // 返回对应的日志表序号 + Integer tableNo = sortedMap.get(i); + tableNoMap.put(iotId, tableNo); + return tableNo; + } else { + // 第一个Key就是顺时针过去离node最近的那个结点 + Integer i = subMap.firstKey(); + // 返回对应的日志表序号 + Integer tableNo = subMap.get(i); + tableNoMap.put(iotId, tableNo); + return tableNo; + } + // int hash = mod(getHash(iotId), HASH_RING); + // hash = hash % MAX_TABLE_SHARD; + + // return hash; + } + + /** 计算Hash值, 使用FNV1_32_HASH算法 */ + public static int getHash(String str) { + final int p = 16777619; + int hash = (int) 2166136261L; + for (int i = 0; i < str.length(); i++) { + hash = (hash ^ str.charAt(i)) * p; + } + hash += hash << 13; + hash ^= hash >> 7; + hash += hash << 3; + hash ^= hash >> 17; + hash += hash << 5; + + if (hash < 0) { + hash = Math.abs(hash); + } + return hash; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/consistent/ITableShardingStrategy.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/consistent/ITableShardingStrategy.java new file mode 100644 index 0000000..e350a32 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/consistent/ITableShardingStrategy.java @@ -0,0 +1,31 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.consistent; + +import cn.hutool.core.util.StrUtil; + +public interface ITableShardingStrategy { + + /** + * @param tableNamePrefix 表前缀名 + * @param value 值 + */ + String generateTableName(String tableNamePrefix, Object value); + + /** 验证tableNamePrefix */ + default void verificationTableNamePrefix(String tableNamePrefix) { + if (StrUtil.isBlank(tableNamePrefix)) { + throw new RuntimeException("tableNamePrefix is null"); + } + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/consistent/TableShard.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/consistent/TableShard.java new file mode 100644 index 0000000..63ddfaf --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/consistent/TableShard.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.consistent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(value = {ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TableShard { + + // 表前缀名 + String tableNamePrefix(); + + // 值 + String value() default ""; + + // 是否是字段名,如果是需要解析请求参数改字段名的值(默认否) + boolean fieldFlag() default false; + + // 对应的分表策略类 + Class shardStrategy(); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/DeviceScanDTO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/DeviceScanDTO.java new file mode 100644 index 0000000..21e995e --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/DeviceScanDTO.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.dto; + +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @version 1.0 @Author Aleo + * @since 2023/7/14 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class DeviceScanDTO { + + private String productKey; + private String qrcode; + + public boolean isEmpty() { + return StrUtil.isEmpty(getProductKey()) || StrUtil.isEmpty(getQrcode()); + } + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class ResultScanDTO { + + private String imei; + private String deviceId; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IndexQueryDTO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IndexQueryDTO.java new file mode 100644 index 0000000..473a919 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IndexQueryDTO.java @@ -0,0 +1,40 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** 首页统计数据 */ +@Data +@Schema +public class IndexQueryDTO { + + @Schema(description = "应用数") + private Integer apps; + + @Schema(description = "产品数") + private Integer product; + + @Schema(description = "规则数") + private Integer rule; + + @Schema(description = "设备总数") + private Integer device; + + @Schema(description = "在线数") + private Integer online; + + @Schema(description = "离线数") + private Integer offline; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTCertificateQueryDTO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTCertificateQueryDTO.java new file mode 100644 index 0000000..b64a070 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTCertificateQueryDTO.java @@ -0,0 +1,17 @@ +package cn.universal.persistence.dto; + +import cn.universal.persistence.query.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = false) +public class IoTCertificateQueryDTO extends BasePage { + + private String name; + private String sslKey; + private String expireStart; + private String expireEnd; + private Integer pageNum = 1; + private Integer pageSize = 10; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceDTO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceDTO.java new file mode 100644 index 0000000..22e902f --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceDTO.java @@ -0,0 +1,108 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.dto; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.constant.IoTConstant.DeviceNode; +import cn.universal.core.metadata.DeviceMetadata; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceDTO { + + /** 归属应用ID */ + private String applicationId; + + private String appId; + private String userId; + + /** 用户唯一标志 */ + private String userUnionId; + + private String upTopic; + private String downTopic; + private String scope; + private String iotId; + private String deviceId; + private String extDeviceId; + private String deviceName; + private DeviceNode deviceNode; + private String productKey; + private String productName; + private String thirdPlatform; + private String gwProductKey; + private String metadata; + private String configuration; + // 在线和离线 + private Boolean state; + + /** 应用状态,true表示禁用 */ + private boolean appDisable; + + /** 首次注册时间 */ + private Integer registryTime; + + private Integer onlineTime; + private List devGroupId; + private JSONObject productConfig; + + /** 是否有影子 */ + private boolean shadow; + + /** 设备配置字段 */ + private String devConfiguration; + + private String areasId; + private String coordinate; + private String detail; + private String storePolicyCfg; + private String storePolicy; + + /** + * 原串 + * + * @return + */ + private String payload; + + @JsonIgnore + public DeviceMetadata getDeviceMetadata() { + if (StrUtil.isNotBlank(metadata)) { + return new DeviceMetadata(new JSONObject(metadata)); + } + return new DeviceMetadata(new JSONObject()); + } + + public JSONObject getProductConfig() { + if (StrUtil.isNotBlank(configuration)) { + productConfig = JSONUtil.parseObj(configuration); + } else { + productConfig = new JSONObject(); + } + return productConfig; + } + + public void clearMetadata() { + setMetadata(null); + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceFunctionHistoryDTO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceFunctionHistoryDTO.java new file mode 100644 index 0000000..290c7b5 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceFunctionHistoryDTO.java @@ -0,0 +1,50 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * @version 1.0 @Author Aleo + * @since 2025/3/21 + */ +@Data +public class IoTDeviceFunctionHistoryDTO implements Serializable { + + private Long id; + private String iotId; + private String productKey; + private String deviceId; + private String deviceName; + + @Schema(description = "指令配置状态 0.待下发;1.下发中;2.已下发") + private Integer downState; + + @Schema(description = "下发结果 0.失败 1.成功") + private Integer downResult; + + private String downError; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private Long taskId; + private Integer retry; + private String command; + private String commandData; + private String thirdPlatform; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceLogMaxStorageTime.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceLogMaxStorageTime.java new file mode 100644 index 0000000..2d77d8d --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceLogMaxStorageTime.java @@ -0,0 +1,33 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @version 1.0 @Author Aleo + * @since 2023/2/15 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class IoTDeviceLogMaxStorageTime { + + private String productKey; + private String platform; + private int days; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceMetadataBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceMetadataBO.java new file mode 100644 index 0000000..d06ad93 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceMetadataBO.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.dto; + +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @version 1.0 @Author Aleo + * @since 2025/9/22 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class IoTDeviceMetadataBO implements Serializable { + + private String companyNo; + private String creatorId; + private String orgId; + private String classifiedId; + private String transportProtocol; + private String point; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceOfflineThesholdBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceOfflineThesholdBO.java new file mode 100644 index 0000000..dad2c0e --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceOfflineThesholdBO.java @@ -0,0 +1,33 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @version 1.0 @Author Aleo + * @since 2023/2/9 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class IoTDeviceOfflineThesholdBO { + + private String productKey; + private String platform; + private int offlineThreshold; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceSubscribeBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceSubscribeBO.java new file mode 100644 index 0000000..bacaeab --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/IoTDeviceSubscribeBO.java @@ -0,0 +1,33 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @version 1.0 @Author Aleo + * @since 2023/4/19 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class IoTDeviceSubscribeBO { + + private String msgType; + private String url; + private String topic; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/LogStorePolicyDTO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/LogStorePolicyDTO.java new file mode 100644 index 0000000..817cd75 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/dto/LogStorePolicyDTO.java @@ -0,0 +1,45 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.dto; + +import java.util.HashMap; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @version 1.0 @Author Aleo + * @since 2023/4/27 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class LogStorePolicyDTO { + @Builder.Default private Map properties = new HashMap(); + @Builder.Default private Map event = new HashMap(); + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class StorePolicy { + + /** 属性英文名 */ + private String id; + + /** 最大存储条数 */ + private int maxStorage; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTCertificate.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTCertificate.java new file mode 100644 index 0000000..89172b5 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTCertificate.java @@ -0,0 +1,58 @@ +package cn.universal.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_certificate") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTCertificate implements Serializable { + + @Id + private Long id; + + @Column(name = "ssl_key") + private String sslKey; + + @Column(name = "name") + private String name; + + @Column(name = "cert_content") + private String certContent; // PEM内容或加密内容 + + @Column(name = "key_content") + private String keyContent; // PEM内容或加密内容 + + @Column(name = "cert_password") + private String certPassword; + + @Column(name = "key_password") + private String keyPassword; + + @Column(name = "expire_time") + private Date expireTime; + + @Column(name = "remark") + private String remark; + + @Column(name = "create_time") + private Date createTime; + + @Column(name = "update_time") + private Date updateTime; + + @Column(name = "create_user") + private String createUser; + + @Column(name = "cert_info") + private String certInfo; // 证书详细信息(JSON) +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDashboardStatistics.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDashboardStatistics.java new file mode 100644 index 0000000..6623cef --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDashboardStatistics.java @@ -0,0 +1,105 @@ +package cn.universal.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import tk.mybatis.mapper.annotation.KeySql; + +/** + * 仪表盘统计实体 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Table(name = "iot_dashboard_statistics") +public class IoTDashboardStatistics implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @KeySql(useGeneratedKeys = true) + private Long id; + + /** + * 统计日期 + */ + @Column(name = "stat_date") + private LocalDate statDate; + + /** + * 产品Key,NULL表示全产品 + */ + @Column(name = "product_key") + private String productKey; + + /** + * 推送渠道,NULL表示全渠道 + */ + @Column(name = "channel") + private String channel; + + /** + * 指标类型 + */ + @Column(name = "metric_type") + private String metricType; + + /** + * 指标值 + */ + @Column(name = "metric_value") + private Long metricValue; + + /** + * 创建时间 + */ + @Column(name = "create_time") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Column(name = "update_time") + private LocalDateTime updateTime; + + /** + * 指标类型枚举 + */ + public enum MetricType { + DEVICE_TOTAL("device_total", "设备总数"), + DEVICE_ONLINE("device_online", "在线设备数"), + MESSAGE_TOTAL("message_total", "消息总数"), + MESSAGE_SUCCESS("message_success", "成功消息数"), + MESSAGE_FAILED("message_failed", "失败消息数"), + MESSAGE_RETRY("message_retry", "重试消息数"), + MESSAGE_PUSH("message_push", "推送消息数"); + + private final String code; + private final String description; + + MetricType(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDevice.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDevice.java new file mode 100644 index 0000000..7d8cdd8 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDevice.java @@ -0,0 +1,195 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.common.annotation.Excel; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_device") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDevice implements Serializable { + + private static final long serialVersionUID = 1L; + @Id + private Long id; + + /** + * 对外设备唯一标识符 + */ + @Column(name = "iot_id") + private String iotId; + + /** + * 设备自身序号 + */ + @Excel(name = "设备序列号") + @Column(name = "device_id") + private String deviceId; + + /** + * 实例名称 + */ + // @Excel(name = "实例名称") + @Column(name = "instance") + private String instance; + + /** + * 归属应用 + */ + @Column(name = "application") + private String application; + + /** + * 激活时间 + */ + @Column(name = "registry_time") + private Integer registryTime; + + /** + * 最后上线时间 + */ + @Column(name = "online_time") + private Long onlineTime; + + /** + * 第三方设备ID唯一标识符 + */ + @Column(name = "ext_device_id") + private String extDeviceId; + + /** + * 别名 + */ + @Column(name = "nick_name") + private String nickName; + + /** + * 设备名称 + */ + @Column(name = "product_name") + private String productName; + + private Long features; + + /** + * 网关产品ProductKey + */ + @Excel(name = "网关产品ProductKey") + @Column(name = "gw_product_key") + private String gwProductKey; + + /** + * 设备密钥 + */ + @Excel(name = "设备密钥") + @Column(name = "device_secret") + private String deviceSecret; + + /** + * 产品key + */ + @Column(name = "product_key") + private String productKey; + + /** + * 设备实例名称 + */ + @Excel(name = "设备名称") + @Column(name = "device_name") + private String deviceName; + + @Column(name = "creator_id") + private String creatorId; + + @Column(name = "creator_name") + private String creatorName; + + /** + * 0-离线,1-在线 + */ + @Excel(name = "在线状态") + private Boolean state; + + /** + * 说明 + */ + @Excel(name = "备注") + private String detail; + + @Column(name = "create_time") + private Long createTime; + + /** + * 派生元数据,有的设备的属性,功能,事件可能会动态的添加 + */ + @Column(name = "derive_metadata") + private String deriveMetadata; + + /** + * 其他配置 + */ + @Column(name = "configuration") + private String configuration; + + /** + * 区域ID + */ + private String areasId; + + /** + * 坐标 + */ + // @Excel(name = "设备坐标") + private String coordinate; + + @Transient + private String deviceNode; + + /** + * 纬度 + */ + @Excel(name = "纬度") + private transient String latitude; + + /** + * 经度 + */ + @Excel(name = "经度") + private transient String longitude; + + /** + * 请求参数 + */ + @Builder.Default + private Map params = new HashMap<>(); + + /** + * 接收额外参数 + */ + @Excel(name = "其他配置") + @Builder.Default + private Map otherParams = new HashMap<>(); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceEvents.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceEvents.java new file mode 100644 index 0000000..6237d5c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceEvents.java @@ -0,0 +1,55 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import java.io.Serializable; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class IoTDeviceEvents implements Serializable { + + /** + * 事件标识 + */ + private String id; + + /** + * 事件名称 + */ + private String name; + + /** + * 事件级别 + */ + private String level; + + /** + * 描述 + */ + private String description; + + /** + * 事件总数 + */ + private String qty; + + /** + * 最新事件上报时间 + */ + private String time; + + // 是否设置存储策略 + private boolean storagePolicy; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceFenceRel.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceFenceRel.java new file mode 100644 index 0000000..ad26a75 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceFenceRel.java @@ -0,0 +1,63 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 设备和围栏中间表 @Author Aleo + * + * @since 2023/8/5 8:51 + */ +@Table(name = "iot_device_fence_rel") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceFenceRel implements Serializable { + + @Id + private Long id; + + /** + * 围栏id + */ + private Long fenceId; + + /** + * 设备唯一标识符 + */ + private String iotId; + + /** + * 设备序列号 + */ + private String deviceId; + + /** + * 创建人 + */ + private String creatorId; + + /** + * 创建时间 + */ + private Date createDate; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceFunction.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceFunction.java new file mode 100644 index 0000000..eb2e9c6 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceFunction.java @@ -0,0 +1,52 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import java.io.Serializable; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class IoTDeviceFunction implements Serializable { + + /** + * 功能标识 + */ + private String id; + + /** + * 功能名称 + */ + private String name; + + /** + * 是否是配置 + */ + private boolean config; + + /** + * 描述 + */ + private String description; + + /** + * 功能来源 + */ + private String source; + + /** + * 输入 + */ + private String inputs; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceFunctionHistory.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceFunctionHistory.java new file mode 100644 index 0000000..1862cbc --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceFunctionHistory.java @@ -0,0 +1,58 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.persistence.common.inteceptor.SQenGenId; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.Data; +import tk.mybatis.mapper.annotation.KeySql; + +/** + * @version 1.0 @Author Aleo + * @since 2025/3/21 + */ +@Data +@Table(name = "iot_device_function_history") +public class IoTDeviceFunctionHistory implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @KeySql(genId = SQenGenId.class) + private Long id; + + private String iotId; + private String productKey; + private String deviceId; + private String deviceName; + + @Schema(description = "指令配置状态 0.待下发;1.下发中;2.已下发") + private Integer downState; + + @Schema(description = "下发结果 0.失败 1.成功") + private Integer downResult; + + private String downError; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private Long taskId; + private Integer retry; + private String extParam; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceFunctionTask.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceFunctionTask.java new file mode 100644 index 0000000..48905f0 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceFunctionTask.java @@ -0,0 +1,62 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.persistence.common.inteceptor.SQenGenId; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import tk.mybatis.mapper.annotation.KeySql; + +/** + * @version 1.0 @Author Aleo + * @since 2025/3/21 + */ +@Table(name = "iot_device_function_task") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceFunctionTask implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @KeySql(genId = SQenGenId.class) + private Long id; + + private String taskName; + private String productKey; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date beginTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + private String creator; + private String creatorId; + private String command; + private String commandData; + + /** + * 状态 0.待执行;1.已执行;2.正在执行 + */ + private Integer status; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceGeoFence.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceGeoFence.java new file mode 100644 index 0000000..4666bc2 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceGeoFence.java @@ -0,0 +1,127 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.persistence.common.inteceptor.SQenGenId; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import tk.mybatis.mapper.annotation.KeySql; + +/** + * 设备围栏 @Author Aleo + * + * @since 2023/8/5 8:47 + */ +@Table(name = "iot_device_geo_fence") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceGeoFence implements Serializable { + + @Id + @KeySql(genId = SQenGenId.class) + private Long id; + + /** + * 围栏名称 + */ + private String name; + + /** + * 围栏状态 0.启用 1.停用 + */ + private Integer status; + + /** + * 触发模式 in.进入 out.离开 all.进入&离开 + */ + private String touchWay; + + /** + * 范围 + */ + private String fence; + + /** + * 类型 circle.圆 polygon.多边形 + */ + private String type; + + /** + * 圆形中心点 + */ + private String point; + + /** + * 半径 + */ + private BigDecimal radius; + + /** + * 创建人 + */ + private String creatorId; + + /** + * 周触发(天) + */ + private String weekTime; + + /** + * 天触发开始时间(时) + */ + private String beginTime; + + /** + * 天触发结束时间(时) + */ + private String endTime; + + /** + * 归属第三方应用 + */ + private String creatorUser; + + @Transient + private List queryUserList; + + /** + * 创建时间 + */ + private Date createDate; + + /** + * 更新时间 + */ + private Date updateDate; + + /** + * 不触发时间 + */ + private String noTriggerTime; + + /** + * 延迟时间 分钟 + */ + private Integer delayTime; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceGroup.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceGroup.java new file mode 100644 index 0000000..100530f --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceGroup.java @@ -0,0 +1,107 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.persistence.common.inteceptor.SQenGenId; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import tk.mybatis.mapper.annotation.KeySql; + +@Table(name = "iot_device_group") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceGroup implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 分组ID,非自增 + */ + @Id + @KeySql(genId = SQenGenId.class) + private Long id; + + /** + * 分组名称 + */ + @Column(name = "group_name") + private String groupName; + + /** + * 分组标识 + */ + @Column(name = "group_code") + private String groupCode; + + /** + * 群组描述 + */ + @Column(name = "group_describe") + private String groupDescribe; + + /** + * 父id + */ + @Column(name = "parent_id") + private Long parentId; + + /** + * 是否有子分组 + */ + @Column(name = "has_child") + private Integer hasChild; + + /** + * 分组级别 + */ + @Column(name = "group_level") + private Integer groupLevel; + + /** + * 激活设备数 + */ + @Column(name = "relat_dev_count") + private Integer relatDevCount; + + /** + * 关联设备树 + */ + @Column(name = "active_dev_count") + private Integer activeDevCount; + + /** + * 创建人 + */ + @Column(name = "creator_id") + private String creatorId; + + /** + * 实例编号 + */ + @Column(name = "instance") + private String instance; + + /** + * 标签 + */ + @Column(name = "tag") + private String tag; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceLog.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceLog.java new file mode 100644 index 0000000..14b9ecf --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceLog.java @@ -0,0 +1,112 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_device_log") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceLog implements Serializable { + + @Id + private Long id; + + /** + * 唯一编码 + */ + @Column(name = "iot_id") + private String iotId; + + /** + * 设备自身序号 + */ + @Column(name = "device_id") + private String deviceId; + + /** + * 第三方设备ID唯一标识符 + */ + // @Transient + @Column(name = "ext_device_id") + private String extDeviceId; + + /** + * 产品ID + */ + @Column(name = "product_key") + private String productKey; + + /** + * 设备名称 + */ + @Column(name = "device_name") + private String deviceName; + + /** + * 消息类型 + */ + @Column(name = "message_type") + private String messageType; + + /** + * 指令ID + */ + @Column(name = "command_id") + private String commandId; + + /** + * 指令ID + */ + @Column(name = "command_status") + private Integer commandStatus; + + /** + * 创建人 + */ + @Column(name = "create_id") + private String createId; + + /** + * 事件名称 + */ + private String event; + + /** + * 实例名称 + */ + private String instance; + + /** + * 创建时间 + */ + @Column(name = "create_time") + private Long createTime; + + /** + * 内容 + */ + private String content; + + private String point; + private static final long serialVersionUID = 1L; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceLogMetadata.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceLogMetadata.java new file mode 100644 index 0000000..a4d4440 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceLogMetadata.java @@ -0,0 +1,83 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_device_log_metadata") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceLogMetadata implements Serializable { + + @Id + private Long id; + + @Column(name = "iot_id") + private String iotId; + + /** + * 产品唯一标识 + */ + @Column(name = "product_key") + private String productKey; + + /** + * 设备名称 + */ + @Column(name = "device_name") + private String deviceName; + + @Column(name = "device_id") + private String deviceId; + + /** + * 消息类型 + */ + @Column(name = "message_type") + private String messageType; + + private String event; + + /** + * 属性 + */ + private String property; + + private String ext1; + + private String ext2; + + private String ext3; + + /** + * 发生时间 + */ + @Column(name = "create_time") + private Integer createTime; + + /** + * 其他 + */ + private String content; + + private static final long serialVersionUID = 1L; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceProperties.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceProperties.java new file mode 100644 index 0000000..ab1fb34 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceProperties.java @@ -0,0 +1,64 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import java.io.Serializable; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class IoTDeviceProperties implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 属性标识 + */ + private String id; + + /** + * 属性名称 + */ + private String name; + + /** + * 数据类型 + */ + private String type; + + /** + * 属性值来源 + */ + private String source; + + /** + * 单位 + */ + private String unit; + + /** + * 枚举键值 + */ + private String elements; + + /** + * 描述 + */ + private String description; + + /** + * 读写 + */ + private String mode; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceProtocol.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceProtocol.java new file mode 100644 index 0000000..9e3de7b --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceProtocol.java @@ -0,0 +1,94 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.core.protocol.support.ProtocolSupportDefinition; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_device_protocol") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IoTDeviceProtocol implements Serializable { + + private static final long serialVersionUID = 1L; + private String name; + private String description; + private Byte state; + @Id + private String id; + private String type; + private String configuration; + private String example; + + public ProtocolSupportDefinition toDefinition() { + ProtocolSupportDefinition definition = new ProtocolSupportDefinition(); + JSONObject object = JSONUtil.parseObj(getConfiguration()); + definition.setConfiguration(object); + definition.setDescription(getDescription()); + definition.setId(getId()); + definition.setName(getName()); + definition.setProvider(object.getStr("provider")); + definition.setType(getType()); + definition.setState(getState()); + if (object != null && object.containsKey("supportMethods")) { + JSONArray jsonArray = object.getJSONArray("supportMethods"); + definition.setSupportMethods( + jsonArray.stream() + .map( + s -> { + return (String) s; + }) + .collect(Collectors.toSet())); + } + return definition; + } + + public ProtocolSupportDefinition toDefinitionNoScript() { + ProtocolSupportDefinition definition = new ProtocolSupportDefinition(); + JSONObject object = JSONUtil.parseObj(getConfiguration()); + // 把几百行的内容删掉 + if (object != null && object.containsKey("location")) { + object.remove("location"); + } + definition.setConfiguration(object); + definition.setDescription(getDescription()); + definition.setId(getId()); + definition.setName(getName()); + definition.setProvider(object.getStr("provider")); + definition.setType(getType()); + definition.setState(getState()); + if (object != null && object.containsKey("supportMethods")) { + JSONArray jsonArray = object.getJSONArray("supportMethods"); + definition.setSupportMethods( + jsonArray.stream() + .map( + s -> { + return (String) s; + }) + .collect(Collectors.toSet())); + } + return definition; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceRuleLog.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceRuleLog.java new file mode 100644 index 0000000..299b670 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceRuleLog.java @@ -0,0 +1,73 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_device_rule_log") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceRuleLog implements Serializable { + + @Id + private Long id; + + /** + * 业务ID + */ + @Column(name = "c_id") + private String cId; + + /** + * 业务名称 + */ + @Column(name = "c_name") + private String cName; + + /** + * 执行状态 + */ + @Column(name = "c_status") + private Byte cStatus; + + /** + * 1-场景联动,2-数据流转 + */ + @Column(name = "c_type") + private Byte cType; + + /** + * 条件 + */ + private String conditions; + + @Column(name = "create_time") + private Date createTime; + + @Column(name = "update_time") + private Date updateTime; + + private String content; + + private static final long serialVersionUID = 1L; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceShadow.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceShadow.java new file mode 100644 index 0000000..df8c12c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceShadow.java @@ -0,0 +1,92 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_device_shadow") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceShadow implements Serializable { + + @Id + private Long id; + + /** + * 本平台设备唯一标识符 + */ + @Column(name = "iot_id") + private String iotId; + + /** + * 产品KEY + */ + @Column(name = "product_key") + private String productKey; + + /** + * 设备自身序号 + */ + @Column(name = "device_id") + private String deviceId; + + /** + * 第三方平台设备ID唯一标识符 + */ + @Column(name = "ext_device_id") + private String extDeviceId; + + /** + * 注册时间 + */ + @Column(name = "active_time") + private Date activeTime; + + /** + * 激活时间 + */ + @Column(name = "online_time") + private Date onlineTime; + + /** + * 最后通信时间 + */ + @Column(name = "last_time") + private Date lastTime; + + /** + * 更新时间 + */ + @Column(name = "update_date") + private Date updateDate; + + /** + * 影子数据 + */ + private String metadata; + + @Column(name = "`instance`") + private String instance; + + private static final long serialVersionUID = 1L; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceSubscribe.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceSubscribe.java new file mode 100644 index 0000000..fa8af2d --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceSubscribe.java @@ -0,0 +1,100 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_device_subscribe") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceSubscribe implements Serializable { + + @Id + private Long id; + + /** + * 消息类别:属性(PROPERTIES),指令(REPLY),事件(EVENT),上下线(EVENT:online,offline),所有 + */ + @Column(name = "msg_type") + private String msgType; + + /** + * 订阅级别:设备级,产品级 + */ + @Column(name = "sub_type") + private String subType; + + /** + * 设备deviceId + */ + @Column(name = "device_id") + private String deviceId; + + /** + * 产品ID或者设备唯一标识 + */ + @Column(name = "product_key") + private String productKey; + + /** + * 产品ID或者设备唯一标识 + */ + @Column(name = "iot_id") + private String iotId; + + /** + * 订阅地址 + */ + private String url; + + /** + * 主题 + */ + private String topic; + + /** + * 创建时间 + */ + @Column(name = "create_date") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createDate; + + /** + * 创建人 + */ + private String creater; + + /** + * 实例编号(应用标识) + */ + @Column(name = "`instance`") + private String instance; + + /** + * 是否启用 + */ + private Boolean enabled; + + private static final long serialVersionUID = 1L; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceTags.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceTags.java new file mode 100644 index 0000000..81ab187 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTDeviceTags.java @@ -0,0 +1,70 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_device_tags") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceTags implements Serializable { + + @Id + private Long id; + + /** + * 设备deviceId + */ + @Column(name = "device_id") + private String deviceId; + + /** + * 产品ID或者设备唯一标识 + */ + @Column(name = "product_key") + private String productKey; + + @Column(name = "iot_id") + private String iotId; + + @Column(name = "create_time") + private Long createTime; + + @Column(name = "`name`") + private String name; + + private String description; + + @Column(name = "`type`") + private String type; + + @Column(name = "`value`") + private String value; + + @Column(name = "`key`") + private String key; + + @Column(name = "`instance`") + private String instance; + + private static final long serialVersionUID = 1L; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTProduct.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTProduct.java new file mode 100644 index 0000000..110ae83 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTProduct.java @@ -0,0 +1,193 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_product") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTProduct implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @Id + private Long id; + + /** + * 产品编号 + */ + @Column(name = "product_id") + private String productId; + + /** + * 产品标签 + */ + @Column(name = "tags") + private String tags; + + /** + * 产品KEY + */ + @Column(name = "product_key") + private String productKey; + + /** + * 产品密钥(1型1密) + */ + @Column(name = "product_secret") + private String productSecret; + + /** + * 第三方平台:lvzhou,ctwing,onenet,alibaba,baidu + */ + @Column(name = "third_platform") + private String thirdPlatform; + + /** + * 第三方平台配置信息 + */ + @Column(name = "third_configuration") + private String thirdConfiguration; + + /** + * 厂商编号 + */ + @Column(name = "company_no") + private String companyNo; + + /** + * 分类ID + */ + @Column(name = "classified_id") + private String classifiedId; + + /** + * 网络组件:关联network表 + */ + @Column(name = "network_union_id") + private String networkUnionId; + + /** + * 设备类型: 网关,设备 + */ + @Column(name = "device_node") + private String deviceNode; + + /** + * 所属项目 + */ + @Column(name = "gw_product_key") + private String gwProductKey; + + /** + * 分类名称 + */ + @Column(name = "classified_name") + private String classifiedName; + + /** + * 消息协议: + */ + @Column(name = "message_protocol") + private String messageProtocol; + + /** + * 名称 + */ + private String name; + + /** + * 创建者id + */ + @Column(name = "creator_id") + private String creatorId; + + /** + * 产品状态 + */ + private Byte state; + + /** + * 说明 + */ + @Column(name = "`describe`") + private String describe; + + /** + * 数据存储策略 + */ + @Column(name = "store_policy") + private String storePolicy; + + /** + * 传输协议: MQTT,COAP,UDP + */ + @Column(name = "transport_protocol") + private String transportProtocol; + + /** + * 图片地址 + */ + @Column(name = "photo_url") + private String photoUrl; + + /** + * 创建时间 + */ + @Column(name = "create_time") + private Long createTime; + + /** + * 协议配置 + */ + private String configuration; + + /** + * 数据存储策略配置 + */ + @Column(name = "store_policy_configuration") + private String storePolicyConfiguration; + + /** + * 物模型 + */ + private String metadata; + + /** + * 第三方平台产品下发信息 + */ + private String thirdDownRequest; + + @Column(name = "update_time") + private Date updateTime; + + @Column(name = "instance") + private String instance; + + @Column(name = "is_deleted") + private Integer isDeleted; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTProductSort.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTProductSort.java new file mode 100644 index 0000000..fc73ce5 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTProductSort.java @@ -0,0 +1,159 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.common.annotation.Excel; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 产品分类对象 iot_product_sort @Author Aleo + * + * @since 2025-12-29 + */ +public class IoTProductSort implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + private String id; + + /** + * 父id + */ + @Excel(name = "父id") + private String parentId; + + /** + * 是否有子节点 + */ + @Excel(name = "是否有子节点") + private Integer hasChild; + + /** + * 标识 + */ + @Excel(name = "标识") + private String identification; + + /** + * 分类名称 + */ + @Excel(name = "分类名称") + private String classifiedName; + + /** + * 说明 + */ + @Excel(name = "说明") + private String description; + + /** + * 创建时间 + */ + @Excel(name = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 创建者 + */ + @Excel(name = "创建者") + private String createBy; + + /** + * 子分类 + */ + private List children = new ArrayList(); + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public static long getSerialVersionUID() { + return serialVersionUID; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public Integer getHasChild() { + return hasChild; + } + + public void setHasChild(Integer hasChild) { + this.hasChild = hasChild; + } + + public String getIdentification() { + return identification; + } + + public void setIdentification(String identification) { + this.identification = identification; + } + + public String getClassifiedName() { + return classifiedName; + } + + public void setClassifiedName(String classifiedName) { + this.classifiedName = classifiedName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getCreateBy() { + return createBy; + } + + public void setCreateBy(String createBy) { + this.createBy = createBy; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTUser.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTUser.java new file mode 100644 index 0000000..ddbf642 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTUser.java @@ -0,0 +1,216 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.annotation.Excel; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_user") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class IoTUser implements Serializable { + + private static final long serialVersionUID = 1L; + @Id + private Long id; + + /** + * 归属组织机构 + */ + @Column(name = "org_id") + @Excel(name = "归属组织机构") + private Long orgId; + + /** + * 用户名 + */ + @Excel(name = "用户名") + private String username; + + /** + * 密码 + */ + @JsonIgnore + @Excel(name = "密码") + private String password; + + /** + * 别名 + */ + @Excel(name = "别名") + private String alias; + + /** + * 用户唯一标识 + */ + @Excel(name = "用户唯一标识") + @Column(name = "union_id") + private String unionId; + + /** + * 邮箱 + */ + @Excel(name = "邮箱") + private String email; + + /** + * 密码加盐 + */ + @Excel(name = "密码加盐") + private String salt; + + /** + * 手机号 + */ + @Excel(name = "手机号") + private String mobile; + + /** + * 账号状态(0正常,1停用) + */ + @Excel(name = "账号状态(0正常,1停用") + private String status; + + /** + * 头像 + */ + @Excel(name = "头像") + private String avatar; + + /** + * 上级用户唯一id + */ + @Excel(name = "上级用户唯一id") + private String parentUnionId; + + /** + * 账号身份 0.超级管理员 1.普通用户 2.子用户 + */ + @Excel(name = "账号身份 0.超级管理员 1.普通用户 2.子用户") + private Integer identity; + + /** + * 登录IP + */ + @Column(name = "login_ip") + @Excel(name = "登录IP") + private String loginIp; + + /** + * 登录时间 + */ + @Column(name = "login_date") + @Excel(name = "登录时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date loginDate; + + /** + * 创建者 + */ + @Column(name = "create_by") + @Excel(name = "创建者") + private String createBy; + + /** + * 创建时间 + */ + @Column(name = "create_date") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "创建时间") + private Date createDate; + + @Column(name = "update_by") + @Excel(name = "更新者") + private String updateBy; + + @Column(name = "update_date") + @Excel(name = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateDate; + + /** + * 备注 + */ + @Excel(name = "备注") + private String remark; + + /** + * 接入数 + */ + @Column(name = "license") + @Excel(name = "接入数") + private Integer license; + + /** + * 接入数 + */ + @Column(name = "license_total_amount") + @Excel(name = "接入数总额度") + private Integer licenseTotalAmount; + + private String cfg; + + @Column(name = "register_from") + @Excel(name = "注册来源") + private String registerFrom; + + /** + * 0-正常,1-删除 + */ + private Integer deleted; + + /** + * 请求参数 + */ + @Builder.Default + private Map params = new HashMap<>(); + + public IoTUser(Long userId) { + this.id = userId; + } + + public boolean isAdmin() { + return identity != null && identity == 0; + } + + public boolean viewAllProduct() { + if (StrUtil.isBlank(cfg)) { + return true; + } + try { + JSONObject object = JSONUtil.parseObj(cfg); + return object.getBool("viewAllProduct", true); + } catch (Exception e) { + return false; + } + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTUserApplication.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTUserApplication.java new file mode 100644 index 0000000..93370b8 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/IoTUserApplication.java @@ -0,0 +1,132 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.common.annotation.Excel; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_user_application") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTUserApplication implements Serializable { + + private static final long serialVersionUID = 1L; + + // @Column(name = "uuid") + // private Long uuid; + + @Column(name = "union_id") + @Excel(name = "用户唯一标识") + private String unionId; + + /** + * 调用凭证APPID + */ + @Column(name = "app_id") + @Excel(name = "调用凭证APPID") + private String appId; + + /** + * MQ上行主题,英文逗号分隔 + */ + @Column(name = "up_topic") + @Excel(name = "MQ上行主题") + private String upTopic; + + /** + * MQ下行主题,英文逗号分隔 + */ + @Column(name = "down_topic") + @Excel(name = "MQ下行主题") + private String downTopic; + + /** + * 调用密钥 + */ + @Column(name = "app_secret") + @Excel(name = "调用密钥") + private String appSecret; + + /** + * 授权结束时间 + */ + @Column(name = "valid_end_date") + @Excel(name = "授权结束时间") + private Date validEndDate; + + /** + * 授权范围 + */ + @Excel(name = "授权范围") + private String scope; + + /** + * 0-正常,1-停用 + */ + @Column(name = "app_status") + @Excel(name = "应用状态 0-正常,1-停用") + private Integer appStatus; + + /** + * 0-正常,1-删除 + */ + @Excel(name = "是否删除 0正常 1删除") + private Integer deleted; + + @Column(name = "notify_url") + @Excel(name = "推送地址") + private String notifyUrl; + + @Column(name = "app_name") + @Excel(name = "应用名称") + private String appName; + + @Id + @Column(name = "app_unique_id") + @Excel(name = "应用唯一标识") + private String appUniqueId; + + @Column(name = "instance") + @Excel(name = "实例名称") + private String instance; + + @Column(name = "remark") + @Excel(name = "描述") + private String remark; + + @Column(name = "create_date") + @Excel(name = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createDate; + + @Column(name = "http_enable") + @Excel(name = "http启用") + private Boolean httpEnable; + + @Column(name = "mqtt_enable") + @Excel(name = "mqtt启用") + private Boolean mqttEnable; + + private String cfg; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/Network.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/Network.java new file mode 100644 index 0000000..f353716 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/Network.java @@ -0,0 +1,76 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_network") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Network implements Serializable { + + @Id + private Integer id; + + /** + * TCP_CLIENT,MQTT_CLIENT,HTTP_CLIENT,WEB_SOCKET_CLIENT + */ + private String type; + + /** + * 唯一标识 + */ + @Column(name = "product_key") + private String productKey; + + @Column(name = "union_id") + private String unionId; + + private String name; + + /** + * 详细描述 + */ + private String description; + + @Column(name = "create_date") + private Date createDate; + + /** + * enable,disable + */ + private Boolean state; + + /** + * 配置内容 + */ + private String configuration; + + /** + * 创建用户 + */ + @Column(name = "create_user") + private String createUser; + + private static final long serialVersionUID = 1L; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/OAuth2ClientDetails.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/OAuth2ClientDetails.java new file mode 100644 index 0000000..596ae86 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/OAuth2ClientDetails.java @@ -0,0 +1,359 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Table(name = "oauth_client_details") +@Entity +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Accessors(chain = true) +public class OAuth2ClientDetails implements Serializable { + + /** + * 客户端标识 + */ + @Id + @Column(name = "client_id") + private String clientId; + + /** + * 资源标识 + */ + @Column(name = "resource_ids") + private String resourceIds; + + /** + * 客户端秘钥 + */ + @Column(name = "client_secret") + private String clientSecret; + + private String scope; + + /** + * 授权类型 Authorization_code:授权码模式 refresh_token:刷新token password:密码模式 implicit:隐私授权模式 + * client_credentials:客户端模式 + */ + @Column(name = "Authorized_grant_types") + private String AuthorizedGrantTypes; + + /** + * 授权成功后回调URL + */ + @Column(name = "web_server_redirect_uri") + private String webServerRedirectUri; + + private String Authorities; + + /** + * 访问令牌的有效时间 + */ + @Column(name = "access_token_validity") + private Integer accessTokenValidity; + + /** + * 刷新令牌的有效时间 + */ + @Column(name = "refresh_token_validity") + private Integer refreshTokenValidity; + + /** + * 额外的信息,备用 + */ + @Column(name = "additional_information") + private String additionalInformation; + + /** + * 自动批准标识:0代表不显示授权页面 1显示授权页面 + */ + private String autoapprove; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 唯一标识 + */ + @Column(name = "iot_union_id") + private String iotUnionId; + + private static final long serialVersionUID = 1L; + + /** + * 获取客户端标识 + * + * @return client_id - 客户端标识 + */ + public String getClientId() { + return clientId; + } + + /** + * 设置客户端标识 + * + * @param clientId 客户端标识 + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * 获取资源标识 + * + * @return resource_ids - 资源标识 + */ + public String getResourceIds() { + return resourceIds; + } + + /** + * 设置资源标识 + * + * @param resourceIds 资源标识 + */ + public void setResourceIds(String resourceIds) { + this.resourceIds = resourceIds; + } + + /** + * 获取客户端秘钥 + * + * @return client_secret - 客户端秘钥 + */ + public String getClientSecret() { + return clientSecret; + } + + /** + * 设置客户端秘钥 + * + * @param clientSecret 客户端秘钥 + */ + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + /** + * @return scope + */ + public String getScope() { + return scope; + } + + /** + * @param scope + */ + public void setScope(String scope) { + this.scope = scope; + } + + /** + * 获取授权类型 Authorization_code:授权码模式 refresh_token:刷新token password:密码模式 implicit:隐私授权模式 + * client_credentials:客户端模式 + * + * @return Authorized_grant_types - 授权类型 Authorization_code:授权码模式 refresh_token:刷新token + * password:密码模式 implicit:隐私授权模式 client_credentials:客户端模式 + */ + public String getAuthorizedGrantTypes() { + return AuthorizedGrantTypes; + } + + /** + * 设置授权类型 Authorization_code:授权码模式 refresh_token:刷新token password:密码模式 implicit:隐私授权模式 + * client_credentials:客户端模式 + * + * @param AuthorizedGrantTypes 授权类型 Authorization_code:授权码模式 refresh_token:刷新token password:密码模式 + * implicit:隐私授权模式 client_credentials:客户端模式 + */ + public void setAuthorizedGrantTypes(String AuthorizedGrantTypes) { + this.AuthorizedGrantTypes = AuthorizedGrantTypes; + } + + /** + * 获取授权成功后回调URL + * + * @return web_server_redirect_uri - 授权成功后回调URL + */ + public String getWebServerRedirectUri() { + return webServerRedirectUri; + } + + /** + * 设置授权成功后回调URL + * + * @param webServerRedirectUri 授权成功后回调URL + */ + public void setWebServerRedirectUri(String webServerRedirectUri) { + this.webServerRedirectUri = webServerRedirectUri; + } + + /** + * @return Authorities + */ + public String getAuthorities() { + return Authorities; + } + + /** + * @param Authorities + */ + public void setAuthorities(String Authorities) { + this.Authorities = Authorities; + } + + /** + * 获取访问令牌的有效时间 + * + * @return access_token_validity - 访问令牌的有效时间 + */ + public Integer getAccessTokenValidity() { + return accessTokenValidity; + } + + /** + * 设置访问令牌的有效时间 + * + * @param accessTokenValidity 访问令牌的有效时间 + */ + public void setAccessTokenValidity(Integer accessTokenValidity) { + this.accessTokenValidity = accessTokenValidity; + } + + /** + * 获取刷新令牌的有效时间 + * + * @return refresh_token_validity - 刷新令牌的有效时间 + */ + public Integer getRefreshTokenValidity() { + return refreshTokenValidity; + } + + /** + * 设置刷新令牌的有效时间 + * + * @param refreshTokenValidity 刷新令牌的有效时间 + */ + public void setRefreshTokenValidity(Integer refreshTokenValidity) { + this.refreshTokenValidity = refreshTokenValidity; + } + + /** + * 获取额外的信息,备用 + * + * @return additional_information - 额外的信息,备用 + */ + public String getAdditionalInformation() { + return additionalInformation; + } + + /** + * 设置额外的信息,备用 + * + * @param additionalInformation 额外的信息,备用 + */ + public void setAdditionalInformation(String additionalInformation) { + this.additionalInformation = additionalInformation; + } + + /** + * 获取自动批准标识:0代表不显示授权页面 1显示授权页面 + * + * @return autoapprove - 自动批准标识:0代表不显示授权页面 1显示授权页面 + */ + public String getAutoapprove() { + return autoapprove; + } + + /** + * 设置自动批准标识:0代表不显示授权页面 1显示授权页面 + * + * @param autoapprove 自动批准标识:0代表不显示授权页面 1显示授权页面 + */ + public void setAutoapprove(String autoapprove) { + this.autoapprove = autoapprove; + } + + /** + * 获取用户名 + * + * @return username - 用户名 + */ + public String getUsername() { + return username; + } + + /** + * 设置用户名 + * + * @param username 用户名 + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * 获取密码 + * + * @return password - 密码 + */ + public String getPassword() { + return password; + } + + /** + * 设置密码 + * + * @param password 密码 + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * 获取唯一标识 + * + * @return iot_union_id - 唯一标识 + */ + public String getIotUnionId() { + return iotUnionId; + } + + /** + * 设置唯一标识 + * + * @param iotUnionId 唯一标识 + */ + public void setIotUnionId(String iotUnionId) { + this.iotUnionId = iotUnionId; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/RuleModel.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/RuleModel.java new file mode 100644 index 0000000..dcbc428 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/RuleModel.java @@ -0,0 +1,114 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.persistence.common.inteceptor.SQenGenId; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import tk.mybatis.mapper.annotation.KeySql; + +/** + * 规则模型 @Author Aleo + * + * @since 2023/1/13 14:28 + */ +@Table(name = "rule_model") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class RuleModel implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @Id + @KeySql(genId = SQenGenId.class) + @Schema(description = "主键ID") + private Long id; + + /** + * 规则名称 + */ + @Schema(description = "规则名称") + @Column(name = "rule_name") + private String ruleName; + + /** + * 数据级别 + */ + @Schema(description = "数据级别") + @Column(name = "data_level") + private String dataLevel; + + /** + * 描述 + */ + @Schema(description = "描述") + @Column(name = "description") + private String description; + + /** + * 状态 + */ + @Schema(description = "状态") + @Column(name = "status") + private String status; + + ; + + /** + * 产品KEY + */ + @Schema(description = "产品KEY") + @Column(name = "product_key") + private String productKey; + + /** + * 规则配置 + */ + @Schema(description = "规则配置") + @Column(name = "config") + private String config; + + /** + * 创建人 + */ + @Schema(description = "创建人") + @Column(name = "creator_id") + private String creatorId; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + @Column(name = "create_time") + private Date createTime; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + @Column(name = "update_time") + private Date updateTime; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/RuleModelInstance.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/RuleModelInstance.java new file mode 100644 index 0000000..53f4000 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/RuleModelInstance.java @@ -0,0 +1,64 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.persistence.common.inteceptor.SQenGenId; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import tk.mybatis.mapper.annotation.KeySql; + +/** + * 规则实例 @Author Aleo + * + * @since 2023/1/13 14:28 + */ +@Table(name = "rule_model_instance") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class RuleModelInstance implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @Id + @KeySql(genId = SQenGenId.class) + private Long id; + + /** + * 模型id + */ + @Column(name = "model_id") + private Long modelId; + + /** + * 关联类型 + */ + @Column(name = "relation_type") + private String relationType; + + /** + * 关联id + */ + @Column(name = "relation_id") + private String relationId; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/RulegoChain.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/RulegoChain.java new file mode 100644 index 0000000..6a133aa --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/RulegoChain.java @@ -0,0 +1,127 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.persistence.common.inteceptor.SQenGenId; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import tk.mybatis.mapper.annotation.KeySql; + +/** + * rulego规则链管理实体 + * + * @author Aleo + * @since 2025/01/15 + */ +@Table(name = "rulego_chain") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class RulegoChain implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @Id + @KeySql(genId = SQenGenId.class) + @Schema(description = "主键ID") + private Long id; + + /** + * rulego规则链ID + */ + @Schema(description = "rulego规则链ID") + @Column(name = "rulego_id") + private String rulegoId; + + /** + * 规则链名称 + */ + @Schema(description = "规则链名称") + @Column(name = "chain_name") + private String chainName; + + /** + * 规则链描述 + */ + @Schema(description = "规则链描述") + @Column(name = "description") + private String description; + + /** + * 创建人unionId + */ + @Schema(description = "创建人unionId") + @Column(name = "creator_id") + private String creatorId; + + /** + * 创建人姓名 + */ + @Schema(description = "创建人姓名") + @Column(name = "creator_name") + private String creatorName; + + /** + * 状态:draft-草稿,deployed-已部署,stopped-已停止 + */ + @Schema(description = "状态") + @Column(name = "status") + private String status; + + /** + * 规则链DSL内容(JSON格式) + */ + @Schema(description = "规则链DSL内容") + @Column(name = "dsl_content") + private String dslContent; + + /** + * 最后同步时间 + */ + @Schema(description = "最后同步时间") + @Column(name = "last_sync_time") + private Date lastSyncTime; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + @Column(name = "create_time") + private Date createTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + @Column(name = "update_time") + private Date updateTime; + + /** + * 是否删除:0-未删除,1-已删除 + */ + @Schema(description = "是否删除") + @Column(name = "deleted") + private Integer deleted; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/RulegoChainLog.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/RulegoChainLog.java new file mode 100644 index 0000000..0a6816e --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/RulegoChainLog.java @@ -0,0 +1,113 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.persistence.common.inteceptor.SQenGenId; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import tk.mybatis.mapper.annotation.KeySql; + +/** + * rulego规则链执行日志实体 + * + * @author Aleo + * @since 2025/01/15 + */ +@Table(name = "rulego_chain_log") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class RulegoChainLog implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @Id + @KeySql(genId = SQenGenId.class) + @Schema(description = "主键ID") + private Long id; + + /** + * rulego规则链ID + */ + @Schema(description = "rulego规则链ID") + @Column(name = "rulego_id") + private String rulegoId; + + /** + * 规则链名称 + */ + @Schema(description = "规则链名称") + @Column(name = "chain_name") + private String chainName; + + /** + * 执行ID + */ + @Schema(description = "执行ID") + @Column(name = "execution_id") + private String executionId; + + /** + * 输入数据 + */ + @Schema(description = "输入数据") + @Column(name = "input_data") + private String inputData; + + /** + * 输出数据 + */ + @Schema(description = "输出数据") + @Column(name = "output_data") + private String outputData; + + /** + * 执行状态:success-成功,failed-失败 + */ + @Schema(description = "执行状态") + @Column(name = "execution_status") + private String executionStatus; + + /** + * 错误信息 + */ + @Schema(description = "错误信息") + @Column(name = "error_message") + private String errorMessage; + + /** + * 执行耗时(毫秒) + */ + @Schema(description = "执行耗时") + @Column(name = "execution_time") + private Long executionTime; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + @Column(name = "create_time") + private Date createTime; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/SceneLinkage.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/SceneLinkage.java new file mode 100644 index 0000000..2dd1662 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/SceneLinkage.java @@ -0,0 +1,90 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.common.annotation.Excel; +import cn.universal.common.domain.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * scene_linkage表 SceneLinkage @Author Aleo + * + * @since 2023-03-01 + */ +@Table(name = "scene_linkage") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class SceneLinkage extends BaseEntity { + + private static final long serialVersionUID = 1L; + + @Id + private Long id; + + /** + * 场景名称 + */ + @Excel(name = "场景名称") + @Column(name = "scene_name") + private String sceneName; + + /** + * 触发条件 all.全部 one.任意一个 + */ + @Excel(name = "触发条件 all.全部 one.任意一个") + @Column(name = "touch") + private String touch; + + /** + * 触发条件 + */ + @Excel(name = "触发条件") + @Column(name = "trigger_condition") + private String triggerCondition; + + /** + * 执行动作 + */ + @Excel(name = "执行动作") + @Column(name = "exec_action") + private String execAction; + + /** + * 沉默周期 + */ + @Excel(name = "沉默周期") + @Column(name = "sleep_cycle") + private Integer sleepCycle; + + /** + * 0启用 1停用 + */ + @Excel(name = "0启用 1停用") + @Column(name = "status") + private Integer status; + + /** + * 设备id + */ + @Excel(name = "设备id") + @Column(name = "dev_id") + private String devId; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/SupportMapAreas.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/SupportMapAreas.java new file mode 100644 index 0000000..c5e6c6d --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/SupportMapAreas.java @@ -0,0 +1,63 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "support_map_areas") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class SupportMapAreas implements Serializable { + + private static final long serialVersionUID = 1L; + @Id + private String id; + + /** + * 父id + */ + private String pid; + + /** + * 地址 + */ + private String name; + + /** + * 详细地址 + */ + private String fullName; + + /** + * 深度 + */ + private String deep; + + /** + * 核心点坐标 + */ + private String location; + + /** + * 范围坐标 + */ + private String polygon; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/SysLogininfor.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/SysLogininfor.java new file mode 100644 index 0000000..b6bbacc --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/SysLogininfor.java @@ -0,0 +1,99 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.common.annotation.Excel; +import cn.universal.common.annotation.Excel.ColumnType; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Id; +import jakarta.persistence.Transient; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 系统访问记录表 sys_logininfor @Author ruoyi + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class SysLogininfor implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Excel(name = "序号", cellType = ColumnType.NUMERIC) + @Id + private Long infoId; + + /** + * 用户账号 + */ + @Excel(name = "用户账号") + private String userName; + + /** + * 登录状态 0成功 1失败 + */ + @Excel(name = "登录状态", readConverterExp = "0=成功,1=失败") + private String status; + + /** + * 登录IP地址 + */ + @Excel(name = "登录地址") + private String ipaddr; + + /** + * 登录地点 + */ + @Excel(name = "登录地点") + private String loginLocation; + + /** + * 浏览器类型 + */ + @Excel(name = "浏览器") + private String browser; + + /** + * 操作系统 + */ + @Excel(name = "操作系统") + private String os; + + /** + * 提示消息 + */ + @Excel(name = "提示消息") + private String msg; + + /** + * 访问时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date loginTime; + + /** + * 请求参数 + */ + @Transient + private Map params = new HashMap<>(); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/SysOperLog.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/SysOperLog.java new file mode 100644 index 0000000..4b62571 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/SysOperLog.java @@ -0,0 +1,147 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity; + +import cn.universal.common.annotation.Excel; +import cn.universal.common.annotation.Excel.ColumnType; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Id; +import jakarta.persistence.Transient; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 操作日志记录表 oper_log @Author ruoyi + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class SysOperLog implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 日志主键 + */ + @Excel(name = "操作序号", cellType = ColumnType.NUMERIC) + @Id + private Long operId; + + /** + * 操作模块 + */ + @Excel(name = "操作模块") + private String title; + + /** + * 业务类型(0其它 1新增 2修改 3删除) + */ + @Excel(name = "业务类型", readConverterExp = "0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据") + private Integer businessType; + + /** + * 业务类型数组 + */ + @Transient + private Integer[] businessTypes; + + /** + * 请求方法 + */ + @Excel(name = "请求方法") + private String method; + + /** + * 请求方式 + */ + @Excel(name = "请求方式") + private String requestMethod; + + /** + * 操作类别(0其它 1后台用户 2手机端用户) + */ + @Excel(name = "操作类别", readConverterExp = "0=其它,1=后台用户,2=手机端用户") + private Integer operatorType; + + /** + * 操作人员 + */ + @Excel(name = "操作人员") + private String operName; + + /** + * 部门名称 + */ + @Excel(name = "部门名称") + private String deptName; + + /** + * 请求url + */ + @Excel(name = "请求地址") + private String operUrl; + + /** + * 操作地址 + */ + @Excel(name = "操作地址") + private String operIp; + + /** + * 操作地点 + */ + @Excel(name = "操作地点") + private String operLocation; + + /** + * 请求参数 + */ + @Excel(name = "请求参数") + private String operParam; + + /** + * 返回参数 + */ + @Excel(name = "返回参数") + private String jsonResult; + + /** + * 操作状态(0正常 1异常) + */ + @Excel(name = "状态", readConverterExp = "0=正常,1=异常") + private Integer status; + + /** + * 错误消息 + */ + @Excel(name = "错误消息") + private String errorMsg; + + /** + * 操作时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date operTime; + + /** + * 请求参数 + */ + @Transient + private Map params = new HashMap<>(); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysConfig.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysConfig.java new file mode 100644 index 0000000..6562787 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysConfig.java @@ -0,0 +1,107 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.admin; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 参数配置表 sys_config @Author ruoyi + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@Builder +@AllArgsConstructor +@Table(name = "sys_config") +public class SysConfig { + + private static final long serialVersionUID = 1L; + + /** + * 参数主键 + */ + @Id + private Long configId; + + /** + * 参数名称 + */ + @Column(name = "config_name") + private String configName; + + /** + * 参数键名 + */ + @Column(name = "config_key") + private String configKey; + + /** + * 参数键值 + */ + @Column(name = "config_value") + private String configValue; + + /** + * 系统内置(Y是 N否) + */ + @Column(name = "config_type") + private String configType; + + /** + * 创建者 + */ + @Column(name = "create_by") + private String createBy; + + /** + * 创建时间 + */ + @Column(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者 + */ + @Column(name = "update_by") + private String updateBy; + + /** + * 更新时间 + */ + @Column(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + /** + * 请求参数 + */ + // @TableField(exist = false) + private Map params = new HashMap<>(); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysDictData.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysDictData.java new file mode 100644 index 0000000..9140195 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysDictData.java @@ -0,0 +1,156 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.admin; + +import cn.universal.common.constant.IoTUserConstants; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 字典数据表 sys_dict_data @Author ruoyi + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@Builder +@AllArgsConstructor +@Table(name = "sys_dict_data") +public class SysDictData implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 字典编码 + */ + // @Excel(name = "字典编码", cellType = Excel.ColumnType.NUMERIC) + @Id + private Long dictCode; + + /** + * 字典排序 + */ + // @Excel(name = "字典排序", cellType = Excel.ColumnType.NUMERIC) + @Column(name = "dict_sort") + private Long dictSort; + + /** + * 字典标签 + */ + // @Excel(name = "字典标签") + @NotBlank(message = "字典标签不能为空") + @Size(min = 0, max = 100, message = "字典标签长度不能超过100个字符") + @Column(name = "dict_label") + private String dictLabel; + + /** + * 字典键值 + */ + // @Excel(name = "字典键值") + @NotBlank(message = "字典键值不能为空") + @Column(name = "dict_value") + @Size(min = 0, max = 100, message = "字典键值长度不能超过100个字符") + private String dictValue; + + /** + * 字典类型 + */ + // @Excel(name = "字典类型") + @NotBlank(message = "字典类型不能为空") + @Column(name = "dict_type") + @Size(min = 0, max = 100, message = "字典类型长度不能超过100个字符") + private String dictType; + + /** + * 样式属性(其他样式扩展) + */ + @Size(min = 0, max = 100, message = "样式属性长度不能超过100个字符") + @Column(name = "css_class") + private String cssClass; + + /** + * 表格字典样式 + */ + @Column(name = "list_class") + private String listClass; + + /** + * 是否默认(Y是 N否) + */ + // @Excel(name = "是否默认", readConverterExp = "Y=是,N=否") + @Column(name = "is_default") + private String isDefault; + + /** + * 状态(0正常 1停用) + */ + // @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** + * 创建者 + */ + // @TableField(fill = FieldFill.INSERT) + @Column(name = "create_by") + private String createBy; + + /** + * 创建时间 + */ + // @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Column(name = "create_time") + private Date createTime; + + /** + * 更新者 + */ + // @TableField(fill = FieldFill.INSERT_UPDATE) + @Column(name = "update_by") + private String updateBy; + + /** + * 更新时间 + */ + // @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Column(name = "update_time") + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + /** + * 请求参数 + */ + // @TableField(exist = false) + private Map params = new HashMap<>(); + + public boolean getDefault() { + return IoTUserConstants.YES.equals(this.isDefault); + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysDictType.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysDictType.java new file mode 100644 index 0000000..1b84dc7 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysDictType.java @@ -0,0 +1,112 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.admin; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 字典类型表 sys_dict_type @Author ruoyi + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@Table(name = "sys_dict_type") +public class SysDictType implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 字典主键 + */ + // @Excel(name = "字典主键", cellType = Excel.ColumnType.NUMERIC) + // @TableId(value = "dict_id", type = IdType.AUTO) + @Id + private Long dictId; + + /** + * 字典名称 + */ + // @Excel(name = "字典名称") + @NotBlank(message = "字典名称不能为空") + @Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符") + @Column(name = "dict_name") + private String dictName; + + /** + * 字典类型 + */ + // @Excel(name = "字典类型") + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型类型长度不能超过100个字符") + @Column(name = "dict_type") + private String dictType; + + /** + * 状态(0正常 1停用) + */ + // @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** + * 创建者 + */ + // @TableField(fill = FieldFill.INSERT) + @Column(name = "create_by") + private String createBy; + + /** + * 创建时间 + */ + // @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Column(name = "create_time") + private Date createTime; + + /** + * 更新者 + */ + // @TableField(fill = FieldFill.INSERT_UPDATE) + @Column(name = "update_by") + private String updateBy; + + /** + * 更新时间 + */ + // @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Column(name = "update_time") + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + /** + * 请求参数 + */ + // @TableField(exist = false) + private Map params = new HashMap<>(); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysMenu.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysMenu.java new file mode 100644 index 0000000..89a0239 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysMenu.java @@ -0,0 +1,162 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.admin; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 菜单权限表 sys_menu @Author ruoyi + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Accessors(chain = true) +@Table(name = "sys_menu") +public class SysMenu implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @Id + private Long menuId; + + /** + * 菜单名称 + */ + @Column(name = "menu_name") + private String menuName; + + /** 父菜单名称 */ + // @Column(name = "parent_name") + // private String parentName; + + /** + * 父菜单ID + */ + @Column(name = "parent_id") + private Long parentId; + + /** + * 显示顺序 + */ + @Column(name = "order_num") + private String orderNum; + + /** + * 路由地址 + */ + private String path; + + /** + * 组件路径 + */ + private String component; + + /** + * 是否为外链(0是 1否) + */ + @Column(name = "is_frame") + private String isFrame; + + /** + * 是否缓存(0缓存 1不缓存) + */ + @Column(name = "is_cache") + private String isCache; + + /** + * 类型(M目录 C菜单 F按钮) + */ + @Column(name = "menu_type") + private String menuType; + + /** + * 显示状态(0显示 1隐藏) + */ + private String visible; + + /** + * 菜单状态(0显示 1隐藏) + */ + private String status; + + /** + * 权限字符串 + */ + private String perms; + + /** + * 菜单图标 + */ + private String icon; + + /** + * 创建者 + */ + @Column(name = "create_by") + private String createBy; + + /** + * 创建时间 + */ + @Column(name = "create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者 + */ + @Column(name = "update_by") + private String updateBy; + + /** + * 更新时间 + */ + @Column(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + /** + * 请求参数 + */ + @Builder.Default + private Map params = new HashMap<>(); + + /** + * 子菜单 + */ + @Builder.Default + private List children = new ArrayList(); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysRole.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysRole.java new file mode 100644 index 0000000..86cc2b0 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysRole.java @@ -0,0 +1,154 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.admin; + +import cn.universal.common.annotation.Excel; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 角色表 sys_role @Author ruoyi + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@AllArgsConstructor +@Builder +@Table(name = "sys_role") +public class SysRole implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 角色ID + */ + @Id + private Long roleId; + + /** + * 角色名称 + */ + @Excel(name = "角色名称") + @Column(name = "role_name") + private String roleName; + + /** + * 角色权限 + */ + @Column(name = "role_key") + private String roleKey; + + /** + * 角色排序 + */ + @Column(name = "role_sort") + private String roleSort; + + /** + * 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限) + */ + @Column(name = "data_scope") + private String dataScope; + + /** + * 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) + */ + @Column(name = "menu_check_strictly") + private Boolean menuCheckStrictly; + + /** + * 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) + */ + @Column(name = "dept_check_strictly") + private Boolean deptCheckStrictly; + + /** + * 角色状态(0正常 1停用) + */ + private String status; + + /** + * 创建者 + */ + @Column(name = "create_by") + private String createBy; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Column(name = "create_time") + private Date createTime; + + /** + * 更新者 + */ + @Column(name = "update_by") + private String updateBy; + + /** + * 更新时间 + */ + @Column(name = "update_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + /** + * 请求参数 + */ + @Builder.Default + private Map params = new HashMap<>(); + + /** + * 用户是否存在此角色标识 默认不存在 + */ + private boolean flag = false; + + /** + * 菜单组 + */ + private Long[] menuIds; + + /** + * 部门组(数据权限) + */ + + // private Long[] deptIds; + public SysRole(Long roleId) { + this.roleId = roleId; + } + + public boolean isAdmin() { + return isAdmin(this.roleId); + } + + public static boolean isAdmin(Long roleId) { + return roleId != null && 1L == roleId; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysRoleMenu.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysRoleMenu.java new file mode 100644 index 0000000..40e61b4 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysRoleMenu.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.admin; + +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 角色和菜单关联 sys_role_menu @Author ruoyi + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@AllArgsConstructor +@Builder +@Table(name = "sys_role_menu") +public class SysRoleMenu implements Serializable { + + private static final long serialVersionUID = 1L; + @Id + private Long uuid; + + /** + * 角色ID + */ + private Long roleId; + + /** + * 菜单ID + */ + private Long menuId; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysUserRole.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysUserRole.java new file mode 100644 index 0000000..2431248 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/SysUserRole.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.admin; + +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 用户和角色关联 sys_user_role @Author ruoyi + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Accessors(chain = true) +@Table(name = "sys_user_role") +public class SysUserRole implements Serializable { + + private static final long serialVersionUID = 1L; + @Id + private Long uuid; + + /** + * 用户ID + */ + private String unionId; + + /** + * 角色ID + */ + private Long roleId; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/TreeSelect.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/TreeSelect.java new file mode 100644 index 0000000..cb435d6 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/TreeSelect.java @@ -0,0 +1,54 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.admin; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * Treeselect树结构实体类 @Author ruoyi + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class TreeSelect implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 节点ID + */ + private Long id; + + /** + * 节点名称 + */ + private String label; + + /** + * 子节点 + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List children; + + public TreeSelect(SysMenu menu) { + this.id = menu.getMenuId(); + this.label = menu.getMenuName(); + this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/vo/MetaVo.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/vo/MetaVo.java new file mode 100644 index 0000000..d54eea0 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/vo/MetaVo.java @@ -0,0 +1,73 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.admin.vo; + +import cn.hutool.core.lang.Validator; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 路由显示信息 @Author ruoyi + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class MetaVo { + + /** + * 设置该路由在侧边栏和面包屑中展示的名字 + */ + private String title; + + /** + * 设置该路由的图标,对应路径src/assets/icons/svg + */ + private String icon; + + /** + * 设置为true,则不会被 缓存 + */ + private boolean noCache; + + /** + * 内链地址(http(s)://开头) + */ + private String link; + + public MetaVo(String title, String icon) { + this.title = title; + this.icon = icon; + } + + public MetaVo(String title, String icon, boolean noCache) { + this.title = title; + this.icon = icon; + this.noCache = noCache; + } + + public MetaVo(String title, String icon, String link) { + this.title = title; + this.icon = icon; + this.link = link; + } + + public MetaVo(String title, String icon, boolean noCache, String link) { + this.title = title; + this.icon = icon; + this.noCache = noCache; + if (Validator.isUrl(link)) { + this.link = link; + } + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/vo/RouterVo.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/vo/RouterVo.java new file mode 100644 index 0000000..cef4d42 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/vo/RouterVo.java @@ -0,0 +1,69 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.admin.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 路由配置信息 @Author ruoyi + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class RouterVo { + + /** + * 路由名字 + */ + private String name; + + /** + * 路由地址 + */ + private String path; + + /** + * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现 + */ + private boolean hidden; + + /** + * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + */ + private String redirect; + + /** + * 组件地址 + */ + private String component; + + /** + * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + */ + private Boolean alwaysShow; + + /** + * 其他元素 + */ + private MetaVo meta; + + /** + * 子路由 + */ + private List children; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/vo/SysDictDataVo.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/vo/SysDictDataVo.java new file mode 100644 index 0000000..9e06abd --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/vo/SysDictDataVo.java @@ -0,0 +1,123 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.admin.vo; + +import cn.universal.common.constant.IoTUserConstants; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 字典数据表 sys_dict_data @Author ruoyi + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SysDictDataVo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 字典编码 + */ + private Long dictCode; + + /** + * 字典排序 + */ + private Long dictSort; + + /** + * 字典标签 + */ + private String dictLabel; + + /** + * 字典键值 + */ + private String dictValue; + + /** + * 字典类型 + */ + private String dictType; + + /** + * 样式属性(其他样式扩展) + */ + private String cssClass; + + /** + * 表格字典样式 + */ + private String listClass; + + /** + * 是否默认(Y是 N否) + */ + private String isDefault; + + /** + * 状态(0正常 1停用) + */ + private String status; + + /** + * 创建者 + */ + private String createBy; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新者 + */ + private String updateBy; + + /** + * 更新时间 + */ + private Date updateTime; + + /** + * 开始时间 + */ + private String startTime; + + /** + * 结束时间 + */ + private String endTime; + + /** + * 备注 + */ + private String remark; + + /** + * 请求参数 + */ + // @TableField(exist = false) + private Map params = new HashMap<>(); + + public boolean getDefault() { + return IoTUserConstants.YES.equals(this.isDefault); + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/vo/SysDictDataVos.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/vo/SysDictDataVos.java new file mode 100644 index 0000000..3dee088 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/admin/vo/SysDictDataVos.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.admin.vo; + +import cn.universal.persistence.entity.admin.SysDictData; +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 字典类型group分组 @Author ruoyi + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SysDictDataVos implements Serializable { + + private static final long serialVersionUID = 1L; + + private String id; + private String name; + private List array; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceBO.java new file mode 100644 index 0000000..cd8520f --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceBO.java @@ -0,0 +1,171 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceBO implements Serializable { + + private static final long serialVersionUID = 1L; + private Long id; + + /** + * 对外设备唯一标识符 + */ + private String iotId; + + /** + * Lora密钥 + */ + private String secretKey; + + /** + * 归属应用 + */ + private String application; + + /** + * 激活时间 + */ + private Integer registryTime; + + /** + * 最后上线时间 + */ + private Long onlineTime; + + /** + * 设备自身序号 + */ + private String deviceId; + + /** + * 第三方设备ID唯一标识符 + */ + private String extDeviceId; + + /** + * 别名 + */ + private String nickName; + + /** + * 设备名称 + */ + private String productName; + + private Long features; + + /** + * 网关产品ProductKey + */ + private String gwProductKey; + + /** + * 设备密钥 + */ + private String deviceSecret; + + /** + * 产品key + */ + private String productKey; + + /** + * 设备实例名称 + */ + private String deviceName; + + private String creatorId; + private String creatorName; + + /** + * 0-离线,1-在线 + */ + private Boolean state; + + /** + * 说明 + */ + private String detail; + + private Long createTime; + + /** + * 派生元数据,有的设备的属性,功能,事件可能会动态的添加 + */ + private String deriveMetadata; + + /** + * 其他配置 + */ + private String configuration; + + /** + * 设备坐标 + */ + private String coordinate; + + /** + * 区域ID + */ + private String areasId; + + /** + * 纬度 + */ + private String latitude; + + /** + * 经度 + */ + private String longitude; + + private String fenceId; + private String online; + private Long devGroupId; + + // private String parentUnionId; + + /** + * 请求参数 + */ + @Builder.Default + private Map params = new HashMap<>(); + + private Boolean hasProtocol; + + /** + * 接收额外参数 + */ + @Builder.Default + private Map otherParams = new HashMap<>(); + + private List devGroupName; + + /** + * 所属父设备名称 + */ + private String parentName; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceFunctionHistoryBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceFunctionHistoryBO.java new file mode 100644 index 0000000..0f21ddf --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceFunctionHistoryBO.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * @version 1.0 @Author Aleo + * @since 2025/3/21 + */ +@Data +public class IoTDeviceFunctionHistoryBO implements Serializable { + + private Long id; + private String iotId; + private String productKey; + private String deviceId; + private String deviceName; + + @Schema(description = "指令配置状态 0.待下发;1.下发中;2.已下发") + private Integer downState; + + @Schema(description = "下发结果 0.失败 1.成功") + private Integer downResult; + + private String downError; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private Long taskId; + private Integer retry; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceFunctionTaskBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceFunctionTaskBO.java new file mode 100644 index 0000000..3c77879 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceFunctionTaskBO.java @@ -0,0 +1,56 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Transient; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * @version 1.0 @Author Aleo + * @since 2025/3/21 + */ +@Data +public class IoTDeviceFunctionTaskBO { + + private String taskName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date beginTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + private String[] ids; + private String command; + private String commandData; + private String productKey; + private String applicationId; + private Long taskId; + private String[] removeIds; + private Boolean chooseAll; + private List extParam; + + /** + * 请求参数 + */ + @Transient + private Map params = new HashMap<>(); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceGroupBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceGroupBO.java new file mode 100644 index 0000000..4183edf --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceGroupBO.java @@ -0,0 +1,62 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceGroupBO { + + private static final long serialVersionUID = 1L; + + /** + * 分组ID,非自增 + */ + private Long id; + + /** + * 设备id + */ + private String[] devIds; + + /** + * 分组名称 + */ + private String groupName; + + /** + * 分组标识 + */ + private String groupCode; + + /** + * 群组描述 + */ + private String groupDescribe; + + /** + * 父id + */ + private Long parentId; + // //账号父id + // private String parentUnionId; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceHistoryBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceHistoryBO.java new file mode 100644 index 0000000..6d2c69d --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceHistoryBO.java @@ -0,0 +1,37 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IoTDeviceHistoryBO { + + private int id; + private String deviceId; + private String deviceName; + private String productKey; + private String coordinate; + private Long firstOnlineTime; + private String creater; + private Long deleteTime; + private Long createTime; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceLogBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceLogBO.java new file mode 100644 index 0000000..9d4258c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceLogBO.java @@ -0,0 +1,112 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import cn.universal.common.domain.BaseEntity; +import jakarta.persistence.Transient; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = false) +public class IoTDeviceLogBO extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 日志ID,非自增 + */ + private Long id; + + /** + * 设备编码 + */ + private String iotId; + + /** + * 设备序列号 + */ + private String deviceId; + + /** + * 第三方设备ID唯一标识符 + */ + @Transient + private String extDeviceId; + + /** + * 产品ID + */ + private String productKey; + + /** + * 设备名称 + */ + private String deviceName; + + /** + * 消息类型 + */ + private String messageType; + + /** + * 公司名称 + */ + private String companyNo; + + /** + * 协议名称 + */ + private String protocol; + + /** + * 节点类型 + */ + private String deviceNode; + + /** + * 设备分类编号 + */ + private String classifiedId; + + /** + * 组织ID + */ + private String orgId; + + /** + * 事件名称 + */ + private String event; + + /** + * 创建人 + */ + private String createId; + + /** + * 经纬度 + */ + private String point; + + /** + * 内容 + */ + private String content; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceProtocolBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceProtocolBO.java new file mode 100644 index 0000000..383e70e --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTDeviceProtocolBO.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_device_protocol") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IoTDeviceProtocolBO implements Serializable { + + private static final long serialVersionUID = 1L; + private String name; + private String description; + private Byte state; + @Id + private String id; + private String type; + private String configuration; + private String fileName; + private String url; + private String jscript; + private String payload; + private Boolean decode; + private Boolean needBs4Decode; + private String example; + + // private String parentUnionId; + +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTGwDeviceBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTGwDeviceBO.java new file mode 100644 index 0000000..4518310 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTGwDeviceBO.java @@ -0,0 +1,31 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import lombok.Data; + +@Data +public class IoTGwDeviceBO { + + /** + * 当前要添加的设备 + */ + private String productKey; + + private String creatorId; + + /** + * 对应网关产品Key + */ + private String gwProductKey; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTProductBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTProductBO.java new file mode 100644 index 0000000..c49610a --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTProductBO.java @@ -0,0 +1,200 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.json.JSONUtil; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_product") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTProductBO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @Id + private Long id; + + /** + * 产品编号 + */ + @Column(name = "product_id") + private String productId; + + /** + * 产品KEY + */ + @Column(name = "product_key") + private String productKey; + + /** + * 产品密钥(1型1密) + */ + @Column(name = "product_secret") + private String productSecret; + + /** + * 第三方平台:lvzhou,ctwing,onenet,alibaba,baidu + */ + @Column(name = "third_platform") + private String thirdPlatform; + + /** + * 第三方平台配置信息 + */ + @Column(name = "third_configuration") + private String thirdConfiguration; + + /** + * 厂商编号 + */ + @Column(name = "company_no") + private String companyNo; + + /** + * 分类ID + */ + @Column(name = "classified_id") + private String classifiedId; + + /** + * 网络组件:关联network表 + */ + @Column(name = "network_union_id") + private String networkUnionId; + + /** + * 设备类型: 网关,设备 + */ + @Column(name = "device_node") + private String deviceNode; + + /** + * 所属项目 + */ + @Column(name = "gw_product_key") + private String gwProductKey; + + /** + * 分类名称 + */ + @Column(name = "classified_name") + private String classifiedName; + + /** + * 消息协议: + */ + @Column(name = "message_protocol") + private String messageProtocol; + + /** + * 名称 + */ + private String name; + + /** + * 创建者id + */ + @Column(name = "creator_id") + private String creatorId; + + /** + * 产品状态 + */ + private Byte state; + + /** + * 说明 + */ + @Column(name = "`describe`") + private String describe; + + /** + * 数据存储策略 + */ + @Column(name = "store_policy") + private String storePolicy; + + /** + * 传输协议: MQTT,COAP,UDP + */ + @Column(name = "transport_protocol") + private String transportProtocol; + + /** + * 图片地址 + */ + @Column(name = "photo_url") + private String photoUrl; + + /** + * 创建时间 + */ + @Column(name = "create_time") + private Long createTime; + + /** + * 协议配置 + */ + private String configuration; + + /** + * 数据存储策略配置 + */ + @Column(name = "store_policy_configuration") + private String storePolicyConfiguration; + + /** + * 物模型 + */ + private String metadata; + + /** + * 电信产品电量模式 + */ + private int powerModel; + + /** + * 电信Edrx时间 + */ + private String lwm2mEdrxTime; + + private String path; + private String jsonStr; + private String type; + private String oldId; + private IoTProductMetadataBO ioTProductMetadataBO; + private String parentUnionId; + private String tags; + + public void beanToJson() { + jsonStr = JSONUtil.parseObj(ioTProductMetadataBO).toString(); + } + + public void jsonToBean() { + ioTProductMetadataBO = BeanUtil.toBean(JSONUtil.parseObj(jsonStr), IoTProductMetadataBO.class); + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTProductImportBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTProductImportBO.java new file mode 100644 index 0000000..9b45e86 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTProductImportBO.java @@ -0,0 +1,214 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import cn.universal.common.annotation.Excel; +import jakarta.persistence.Column; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTProductImportBO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 产品编号 + */ + @Column(name = "product_id") + @Excel(name = "产品编号") + private String productId; + + /** + * 产品KEY + */ + @Column(name = "product_key") + @Excel(name = "产品KEY") + private String productKey; + + /** + * 产品密钥(1型1密) + */ + @Column(name = "product_secret") + private String productSecret; + + /** + * 第三方平台:lvzhou,ctwing,onenet,alibaba,baidu + */ + @Column(name = "third_platform") + @Excel(name = "第三方平台:lvzhou,ctwing,onenet,alibaba,baidu") + private String thirdPlatform; + + /** + * 第三方平台配置信息 + */ + @Column(name = "third_configuration") + @Excel(name = "第三方平台配置信息") + private String thirdConfiguration; + + /** + * 厂商编号 + */ + @Column(name = "company_no") + @Excel(name = "厂商编号") + private String companyNo; + + /** + * 分类ID + */ + @Column(name = "classified_id") + @Excel(name = "分类ID") + private String classifiedId; + + /** + * 网络组件:关联network表 + */ + @Column(name = "network_union_id") + // @Excel(name = "网络组件:关联network表") + private String networkUnionId; + + /** + * 设备类型: 网关,设备 + */ + @Column(name = "device_node") + @Excel(name = "设备类型: 网关,设备") + private String deviceNode; + + /** + * 网关ProductKey + */ + @Column(name = "gw_product_key") + private String gwProductKey; + + /** + * 分类名称 + */ + @Column(name = "classified_name") + // @Excel(name = "分类名称") + private String classifiedName; + + /** + * 消息协议: + */ + @Column(name = "message_protocol") + // @Excel(name = "消息协议") + private String messageProtocol; + + /** + * 名称 + */ + @Excel(name = "名称") + private String name; + + /** + * 创建者id + */ + @Column(name = "creator_id") + private String creatorId; + + /** + * 产品状态 + */ + @Excel(name = "产品状态") + private Byte state; + + /** + * 说明 + */ + @Column(name = "`describe`") + // @Excel(name = "说明") + private String describe; + + /** + * 数据存储策略 + */ + @Column(name = "store_policy") + @Excel(name = "存储策略") + private String storePolicy; + + /** + * 传输协议: MQTT,COAP,UDP + */ + @Column(name = "transport_protocol") + @Excel(name = "传输协议: MQTT,COAP,UDP") + private String transportProtocol; + + /** + * 图片地址 + */ + @Column(name = "photo_url") + @Excel(name = "图片地址") + private String photoUrl; + + /** + * 创建时间 + */ + @Column(name = "create_time") + // @Excel(name = "创建时间") + private Long createTime; + + /** + * 协议配置 + */ + @Excel(name = "协议配置") + private String configuration; + + /** + * 数据存储策略配置 + */ + @Column(name = "store_policy_configuration") + @Excel(name = "数据存储策略配置") + private String storePolicyConfiguration; + + /** + * 物模型 + */ + @Excel(name = "物模型") + private String metadata; + + /** + * 第三方平台产品下发信息 + */ + @Excel(name = "第三方平台产品下发信息") + private String thirdDownRequest; + + /** + * 协议状态 + */ + @Excel(name = "协议状态 0停用 1启用") + private Byte protocolState; + + /** + * 协议类型 + */ + @Excel(name = "协议类型") + private String protocolType; + + /** + * 协议配置 + */ + @Excel(name = "编解码协议配置") + private String protocolConfiguration; + + /** + * 协议编解码示例 + */ + @Excel(name = "协议编解码示例") + private String protocolExample; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTProductMetadataBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTProductMetadataBO.java new file mode 100644 index 0000000..c6f7dca --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTProductMetadataBO.java @@ -0,0 +1,91 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTProductMetadataBO implements Serializable { + + private static final long serialVersionUID = 1L; + + private String id; + private String name; + private String source; + private boolean config; + private Expands expands; + private ValueType valueType; + private List inputs; + private Output output; + private String mode; + private String description; + + @Data + @NoArgsConstructor + public static class Expands { + + private String level; + private String maxLength; + private String readOnly; + private String source; + } + + @Data + @NoArgsConstructor + public static class ValueType { + + private String type; + private Expands expands; + private List elements; + private String unit; + private String min; + private String max; + private String precision; + } + + @Data + @NoArgsConstructor + public static class Elements { + + private String value; + private String text; + } + + @Data + @NoArgsConstructor + public static class Inputs { + + private String id; + private String name; + private ValueType valueType; + private String description; + } + + @Data + @NoArgsConstructor + public static class Output { + + private String id; + private String name; + private ValueType valueType; + private String description; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTProductSortBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTProductSortBO.java new file mode 100644 index 0000000..8617f23 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTProductSortBO.java @@ -0,0 +1,89 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import cn.universal.persistence.entity.IoTProductSort; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.io.Serializable; +import lombok.Data; + +/** + * 产品分类业务对象 + * + * @since 2025-12-29 + */ +@Data +public class IoTProductSortBO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 分类ID(新增时为空) + */ + private String id; + + /** + * 父分类ID + */ + @NotNull(message = "父分类ID不能为空") + private String parentId; + + /** + * 分类名称 + */ + @NotBlank(message = "分类名称不能为空") + private String classifiedName; + + /** + * 标识 + */ + private String identification; + + /** + * 说明 + */ + private String description; + + /** + * 转换为实体对象 + * + * @return DevProductSort实体 + */ + public IoTProductSort toEntity() { + IoTProductSort entity = new IoTProductSort(); + entity.setId(this.id); + entity.setParentId(this.parentId); + entity.setClassifiedName(this.classifiedName); + entity.setIdentification(this.identification); + entity.setDescription(this.description); + return entity; + } + + /** + * 从实体对象转换 + * + * @param entity 实体对象 + * @return 当前BO对象 + */ + public IoTProductSortBO fromEntity(IoTProductSort entity) { + if (entity != null) { + this.id = entity.getId(); + this.parentId = entity.getParentId(); + this.classifiedName = entity.getClassifiedName(); + this.identification = entity.getIdentification(); + this.description = entity.getDescription(); + } + return this; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTUserBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTUserBO.java new file mode 100644 index 0000000..926dc63 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/IoTUserBO.java @@ -0,0 +1,168 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import cn.universal.common.annotation.Excel; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_user") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTUserBO implements Serializable { + + private static final long serialVersionUID = 1L; + @Id + private Long id; + + /** + * 归属组织机构 + */ + @Column(name = "org_id") + private Long orgId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 别名 + */ + private String alias; + + /** + * 用户唯一标识 + */ + @Column(name = "union_id") + private String unionId; + + /** + * 邮箱 + */ + private String email; + + /** + * 密码加盐 + */ + private String salt; + + /** + * 手机号 + */ + private String mobile; + + /** + * 账号状态(0正常,1停用) + */ + private Integer status; + + /** + * 上级用户唯一id + */ + @Excel(name = "上级用户唯一id") + private String parentUnionId; + + /** + * 账号身份 0.超级管理员 1.普通用户 2.子用户 + */ + @Excel(name = "账号身份 0.超级管理员 1.普通用户 2.子用户") + private Integer identity; + + /** + * 头像 + */ + private String avatar; + + /** + * 登录IP + */ + @Column(name = "login_ip") + private String loginIp; + + /** + * 登录时间 + */ + @Column(name = "login_date") + private Date loginDate; + + /** + * 创建者 + */ + @Column(name = "create_by") + private String createBy; + + /** + * 创建时间 + */ + @Column(name = "create_date") + private Date createDate; + + @Column(name = "update_by") + private String updateBy; + + @Column(name = "update_date") + private Date updateDate; + + /** + * 备注 + */ + private String remark; + + /** + * 0-正常,1-删除 + */ + private Integer deleted; + + /** + * 设备接入额度剩余 + */ + private Integer license; + + /** + * 设备接入总数额度 + */ + private Integer licenseTotalAmount; + + private Set roles; + + /** + * 角色组 + */ + private Long[] roleIds; + + /** + * 角色ID + */ + private Long roleId; + + private String cfg; + + @Column(name = "register_from") + private String registerFrom; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/NetworkBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/NetworkBO.java new file mode 100644 index 0000000..db9b169 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/NetworkBO.java @@ -0,0 +1,59 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import cn.universal.persistence.entity.Network; +import java.util.List; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 网络组件业务对象 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NetworkBO extends Network { + + /** + * 网络类型名称 + */ + private String typeName; + + /** + * 是否正在运行 + */ + private boolean running; + + /** + * 状态名称 + */ + private String stateName; + + /** + * 创建时间格式化 + */ + private String createDateStr; + + /** + * 网络类型列表(多个类型) + */ + private List types; + + /** + * 启用/停用状态名称 + */ + private String enableName; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/PasswordBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/PasswordBO.java new file mode 100644 index 0000000..ecfa6f4 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/PasswordBO.java @@ -0,0 +1,26 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import lombok.Data; + +/** + * @version 1.0 @Author Aleo + * @since 2025/3/21 + */ +@Data +public class PasswordBO { + + private String oldPassword; + private String newPassword; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/RuleModelBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/RuleModelBO.java new file mode 100644 index 0000000..1c19fbf --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/RuleModelBO.java @@ -0,0 +1,119 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import cn.universal.persistence.entity.IoTProductSort; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 规则模型查询 @Author Aleo + * + * @since 2023/1/13 15:06 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema +public class RuleModelBO { + + @Schema(description = "id") + private Long id; + + @Schema(description = "规则名称") + private String ruleName; + + @Schema(description = "状态 run.运行中 stop.已停止") + private String status; + + private String productKey; + private String creatorId; + private String iotId; + private List groupIds; + + /** + * 产品分类业务对象 + * + * @since 2025-12-29 + */ + @Data + public static class IoTProductSortBO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 分类ID(新增时为空) + */ + private String id; + + /** + * 父分类ID + */ + @NotNull(message = "父分类ID不能为空") + private String parentId; + + /** + * 分类名称 + */ + @NotBlank(message = "分类名称不能为空") + private String classifiedName; + + /** + * 标识 + */ + private String identification; + + /** + * 说明 + */ + private String description; + + /** + * 转换为实体对象 + * + * @return DevProductSort实体 + */ + public IoTProductSort toEntity() { + IoTProductSort entity = new IoTProductSort(); + entity.setId(this.id); + entity.setParentId(this.parentId); + entity.setClassifiedName(this.classifiedName); + entity.setIdentification(this.identification); + entity.setDescription(this.description); + return entity; + } + + /** + * 从实体对象转换 + * + * @param entity 实体对象 + * @return 当前BO对象 + */ + public IoTProductSortBO fromEntity(IoTProductSort entity) { + if (entity != null) { + this.id = entity.getId(); + this.parentId = entity.getParentId(); + this.classifiedName = entity.getClassifiedName(); + this.identification = entity.getIdentification(); + this.description = entity.getDescription(); + } + return this; + } + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/RulegoChainBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/RulegoChainBO.java new file mode 100644 index 0000000..2658c06 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/RulegoChainBO.java @@ -0,0 +1,78 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * rulego规则链BO + * + * @author Aleo + * @since 2025/01/15 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Schema +public class RulegoChainBO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "主键ID") + private Long id; + + @Schema(description = "rulego规则链ID") + private String rulegoId; + + @Schema(description = "规则链名称") + private String chainName; + + @Schema(description = "规则链描述") + private String description; + + @Schema(description = "创建人unionId") + private String creatorId; + + @Schema(description = "创建人姓名") + private String creatorName; + + @Schema(description = "状态") + private String status; + + @Schema(description = "规则链DSL内容") + private String dslContent; + + @Schema(description = "最后同步时间") + private String lastSyncTime; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "是否删除") + private Integer deleted; + + @Schema(description = "页码") + private Integer pageNum; + + @Schema(description = "页大小") + private Integer pageSize; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/SceneLinkageBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/SceneLinkageBO.java new file mode 100644 index 0000000..c13c182 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/SceneLinkageBO.java @@ -0,0 +1,90 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import cn.universal.common.annotation.Excel; +import cn.universal.common.domain.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.List; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * scene_linkage表 SceneLinkage @Author Aleo + * + * @since 2023-03-01 + */ +@Table(name = "scene_linkage") +@Data +@EqualsAndHashCode(callSuper = false) +public class SceneLinkageBO extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * $column.columnComment + */ + @Id + private Long id; + + /** + * 场景名称 + */ + @Excel(name = "场景名称") + @Column(name = "scene_name") + private String sceneName; + + /** + * 触发条件 all.全部 one.任意一个 + */ + @Excel(name = "触发条件 all.全部 one.任意一个") + @Column(name = "touch") + private String touch; + + /** + * 触发条件 + */ + @Excel(name = "触发条件") + @Column(name = "trigger_condition") + private List triggerCondition; + + /** + * 执行动作 + */ + @Excel(name = "执行动作") + @Column(name = "exec_action") + private List execAction; + + /** + * 沉默周期 + */ + @Excel(name = "沉默周期") + @Column(name = "sleep_cycle") + private Long sleepCycle; + + /** + * 0启用 1停用 + */ + @Excel(name = "0启用 1停用") + @Column(name = "status") + private Integer status; + + /** + * 设备id + */ + @Excel(name = "设备id") + @Column(name = "dev_id") + private String devId; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/TriggerBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/TriggerBO.java new file mode 100644 index 0000000..6e7e89f --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/TriggerBO.java @@ -0,0 +1,182 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.springframework.util.StringUtils; + +/** + * todo @Author Aleo + * + * @since 2025/8/26 18:51 + */ +@Data +@Schema(description = "设备告警规则") +public class TriggerBO { + + // 触发方式,定时,设备 + @Schema(description = "触发方式") + private String trigger = TriggerType.device.name(); + + // trigger为定时任务时的cron表达式 + @Schema(description = "定时触发cron表达式") + private String cron; + + // 类型,属性或者事件. + @Schema(description = "触发消息类型") + private String type; + + @Schema(description = "设备ID") + private String deviceId; + + @Schema(description = "设备名称") + private String deviceName; + + // 物模型属性或者事件的标识 如: fire_alarm + @Schema(description = "物模型表示,如:属性ID,事件ID") + private String modelId; + + // 过滤条件 + @Schema(description = "条件") + private List filters; + + @Schema(description = "动作参数") + private List execData; + + @Schema(description = "通知模板ID") + private String noticeTemplateId; + + @Schema(description = "通知模板名称") + private String noticeTemplateName; + + public String getNoticeTemplateId() { + return noticeTemplateId; + } + + public void setNoticeTemplateId(String noticeTemplateId) { + this.noticeTemplateId = noticeTemplateId; + } + + public String getNoticeTemplateName() { + return noticeTemplateName; + } + + public void setNoticeTemplateName(String noticeTemplateName) { + this.noticeTemplateName = noticeTemplateName; + } + + @AllArgsConstructor + @Getter + public enum MessageType { + // 属性 + properties, + // 事件 + event, + // 功能调用回复 + functions; + } + + @Getter + @AllArgsConstructor + public enum TriggerType implements Serializable { + // 设备消息 + device(Arrays.asList(MessageType.properties, MessageType.event)), + // 定时,定时获取只支持获取设备属性和调用功能. + timer(Arrays.asList(MessageType.properties, MessageType.functions)); + + final List supportMessageTypes; + } + + @Getter + @Setter + public static class ExecData implements Serializable { + + @Schema(description = "参数id") + private String id; + + @Schema(description = "参数名") + private String name; + + @Schema(description = "参数值") + private String params; + } + + @Getter + @Setter + public static class ConditionFilter implements Serializable { + + // 过滤条件key 如: temperature + @Schema(description = "条件key") + private String key; + + // 过滤条件值 + @Schema(description = "值") + private String value; + + // 操作符, 等于,大于,小于.... + @Schema(description = "比对方式") + private String operator = Operator.eq.name(); + + public void validate() { + if (StringUtils.isEmpty(key)) { + throw new IllegalArgumentException("条件key不能为空"); + } + if (StringUtils.isEmpty(value)) { + throw new IllegalArgumentException("条件值不能为空"); + } + } + } + + @AllArgsConstructor + @Getter + public enum Operator { + eq("=="), + not("!="), + gt(">"), + lt("<"), + gte(">="), + lte("<="), + like("like"); + + private final String symbol; + + public Object convert(String value) { + return value; + } + } + + @Getter + @AllArgsConstructor + public enum ExecTriggerType implements Serializable { + device, + notice; + } + + @Schema(description = "动作触发类型: device/notice") + private ExecTriggerType execTriggerType; + + public ExecTriggerType getExecTriggerType() { + return execTriggerType; + } + + public void setExecTriggerType(ExecTriggerType execTriggerType) { + this.execTriggerType = execTriggerType; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/UPPushBO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/UPPushBO.java new file mode 100644 index 0000000..04b1e9e --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/bo/UPPushBO.java @@ -0,0 +1,151 @@ +package cn.universal.persistence.entity.bo; + +import java.io.Serializable; +import lombok.Builder; +import lombok.Data; + +/** + * 上行推送配置BO + * + * @version 1.0 @Author liulihai + * @since 2025/7/12 10:47 + */ +@Data +@Builder +public class UPPushBO implements Serializable { + + private static final long serialVersionUID = 1L; + + // HTTP 推送配置 + private HttpPushConfig http; + + // MQTT 推送配置 + private MqttPushConfig mqtt; + + // Kafka 推送配置 + private KafkaPushConfig kafka; + + // RocketMQ 推送配置 + private RocketMQPushConfig rocketMQ; + + // 统计配置 + private StatisticsConfig statistics; + + // 重试配置 + private RetryConfig retry; + + /** + * HTTP 推送配置 + */ + @Data + @Builder + public static class HttpPushConfig implements Serializable { + + private static final long serialVersionUID = 1L; + private String url; + private boolean enable; + private String header; + private String secret; + private boolean support; + } + + /** + * MQTT 推送配置 + */ + @Data + @Builder + public static class MqttPushConfig implements Serializable { + + private static final long serialVersionUID = 1L; + private String url; + private String topic; + private boolean enable; + private boolean support; + private String password; + private String username; + } + + /** + * Kafka 推送配置 + */ + @Data + @Builder + public static class KafkaPushConfig implements Serializable { + + private static final long serialVersionUID = 1L; + private boolean enable; + private boolean support; + } + + /** + * RocketMQ 推送配置 + */ + @Data + @Builder + public static class RocketMQPushConfig implements Serializable { + + private static final long serialVersionUID = 1L; + private boolean enable; + private boolean support; + } + + /** + * 统计配置 + */ + @Data + @Builder + public static class StatisticsConfig implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 是否启用统计 + */ + private boolean enable = true; + + /** + * 统计数据保留时间(天) + */ + private Integer retentionDays = 30; + + /** + * 异步统计 + */ + private boolean async = true; + } + + /** + * 重试配置 + */ + @Data + @Builder + public static class RetryConfig implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 是否启用重试 + */ + private boolean enable = true; + + /** + * 最大重试次数 + */ + private Integer maxCount = 3; + + /** + * 重试间隔(分钟) + */ + private Integer intervalMinutes = 5; + + /** + * 重试队列保留时间(天) + */ + private Integer queueRetentionDays = 1; + + /** + * 失败记录保留时间(天) + */ + private Integer failedRetentionDays = 7; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/GatewayDeviceVo.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/GatewayDeviceVo.java new file mode 100644 index 0000000..11a9353 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/GatewayDeviceVo.java @@ -0,0 +1,29 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import lombok.Data; + +@Data +public class GatewayDeviceVo { + + private String iotId; + private String productKey; + private String deviceId; + private String deviceName; + private String concatName; + + public String getConcatName() { + return deviceId + "(" + deviceName + ")"; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/InterPhoneInstanceVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/InterPhoneInstanceVO.java new file mode 100644 index 0000000..29ad9d4 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/InterPhoneInstanceVO.java @@ -0,0 +1,102 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class InterPhoneInstanceVO { + + private static final long serialVersionUID = 1L; + + /** + * 设备自身序号 + */ + private String deviceId; + + /** + * 设备实例名称 + */ + private String deviceName; + + /** + * 0-离线,1-在线 + */ + private Integer state; + + /** + * 设备创建时间 + */ + private Long createTime; + + /** + * 设备上线时间 + */ + private Long onlineTime; + + /** + * 经度 + */ + private BigDecimal longitude; + + /** + * 纬度 + */ + private BigDecimal latitude; + + /** + * 经纬度 + */ + private String coordinate; + + /** + * 设备属性值 + */ + @Builder.Default + private Map metadata = new HashMap<>(); + + public void setCoordinate(String coordinate) { + this.coordinate = coordinate; + if (StrUtil.isNotEmpty(coordinate)) { + String[] split = coordinate.split(","); + longitude = new BigDecimal(split[0]); + latitude = new BigDecimal(split[1]); + } + } + + public void setMetadata(String me) { + if (StrUtil.isNotEmpty(me)) { + JSONObject obj = JSONUtil.parseObj(me); + if (Objects.nonNull(obj) && obj.containsKey("state")) { + JSONObject state = obj.getJSONObject("state"); + if (Objects.nonNull(state) && state.containsKey("reported")) { + JSONObject reported = state.getJSONObject("reported"); + metadata = reported; + } + } + } + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceCompanyVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceCompanyVO.java new file mode 100644 index 0000000..1e53dc5 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceCompanyVO.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Data; + +/** + * 公司视图对象 @Author Aleo + * + * @since 2023/1/5 11:35 + */ +@Data +@Schema(description = "公司视图对象") +public class IoTDeviceCompanyVO { + + private static final long serialVersionUID = 1L; + + /** + * 厂家编号 + */ + @Schema(description = "厂家编号") + private String companyNo; + + /** + * 厂家名称 + */ + @Schema(description = "厂家名称") + private String companyName; + + @Schema(description = "设备类型") + private List devices; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceCountVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceCountVO.java new file mode 100644 index 0000000..a887eea --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceCountVO.java @@ -0,0 +1,29 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceCountVO { + + private static final long serialVersionUID = 1L; + private String application; + private String devNum; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceFunctionHistoryVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceFunctionHistoryVO.java new file mode 100644 index 0000000..c966a52 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceFunctionHistoryVO.java @@ -0,0 +1,51 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Id; +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * @version 1.0 @Author Aleo + * @since 2025/3/21 + */ +@Data +public class IoTDeviceFunctionHistoryVO implements Serializable { + + private static final long serialVersionUID = 1L; + @Id + private Long id; + private String iotId; + private String productKey; + private String deviceId; + private String deviceName; + + @Schema(description = "指令配置状态 0.待下发;1.下发中;2.已下发") + private Integer downState; + + @Schema(description = "下发结果 0.失败 1.成功") + private Integer downResult; + + private String downError; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private Long taskId; + private Integer retry; + private String commandData; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceFunctionTaskVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceFunctionTaskVO.java new file mode 100644 index 0000000..4dbd64c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceFunctionTaskVO.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Id; +import java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * @version 1.0 @Author Aleo + * @since 2025/3/21 + */ +@Data +public class IoTDeviceFunctionTaskVO implements Serializable { + + private static final long serialVersionUID = 1L; + @Id + private Long id; + private String taskName; + private String productKey; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date beginTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + private String creator; + private String creatorId; + private String command; + private String commandData; + private Integer successNum; + private Integer totalNum; + private Integer status; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceGeoFenceVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceGeoFenceVO.java new file mode 100644 index 0000000..1798afe --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceGeoFenceVO.java @@ -0,0 +1,124 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import cn.universal.persistence.common.inteceptor.SQenGenId; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import tk.mybatis.mapper.annotation.KeySql; + +/** + * 设备围栏 @Author Aleo + * + * @since 2023/8/5 8:47 + */ +@Table(name = "iot_device_geo_fence") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceGeoFenceVO implements Serializable { + + @Id + @KeySql(genId = SQenGenId.class) + private Long id; + + /** + * 围栏名称 + */ + private String name; + + /** + * 围栏状态 0.启用 1.停用 + */ + private Integer status; + + /** + * 触发模式 in.进入 out.离开 all.进入&离开 + */ + private String touchWay; + + /** + * 范围 + */ + private String fence; + + /** + * 类型 circle.圆 polygon.多边形 + */ + private String type; + + /** + * 圆形中心点 + */ + private String point; + + /** + * 半径 + */ + private BigDecimal radius; + + /** + * 创建人 + */ + private String creatorId; + + /** + * 周触发(天) + */ + private String weekTime; + + /** + * 天触发开始时间(时) + */ + private String beginTime; + + /** + * 天触发结束时间(时) + */ + private String endTime; + + /** + * 归属第三方应用 + */ + private String creatorUser; + + /** + * 创建时间 + */ + private Date createDate; + + /** + * 更新时间 + */ + private Date updateDate; + + private int deviceNum; + + /** + * 不触发时间 + */ + private String noTriggerTime; + + /** + * 延迟时间 分钟 + */ + private Integer delayTime; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceGroupVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceGroupVO.java new file mode 100644 index 0000000..84cfb9b --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceGroupVO.java @@ -0,0 +1,74 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceGroupVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 分组ID,非自增 + */ + private Long id; + + /** + * 分组名称 + */ + private String groupName; + + /** + * 分组标识 + */ + private String groupCode; + + /** + * 群组描述 + */ + private String groupDescribe; + + /** + * 父id + */ + private Long parentId; + + /** + * 是否有子分组 + */ + private Integer hasChild; + + /** + * 分组级别 + */ + private Integer groupLevel; + + /** + * 设备列表 + */ + private List instanceVOS; + + /** + * 分组子集 + */ + private List children; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceLogMetadataVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceLogMetadataVO.java new file mode 100644 index 0000000..45e0bfe --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceLogMetadataVO.java @@ -0,0 +1,61 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "设备属性视图对象") +public class IoTDeviceLogMetadataVO { + + private static final long serialVersionUID = 1L; + + /*-----------------日志信息---------------------*/ + + /** 日志ID,非自增 */ + // private Long id; + + /** + * 设备编码 + */ + private String iotId; + + /** + * 设备序列号 + */ + private String deviceId; + + /** + * 产品ID + */ + private String productKey; + + /** + * 设备名称 + */ + private String deviceName; + + /** + * 消息类型 + */ + private String messageType; + + private String event; + private String property; + private String content; + private String ext1; + private String ext2; + private String ext3; + private Integer createTime; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceLogVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceLogVO.java new file mode 100644 index 0000000..813f3c1 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceLogVO.java @@ -0,0 +1,117 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Transient; +import lombok.Data; + +@Data +@Schema(description = "设备日志视图对象") +public class IoTDeviceLogVO { + + private static final long serialVersionUID = 1L; + + /*-----------------日志信息---------------------*/ + + /** + * 日志ID,非自增 + */ + private Long id; + + /** + * 设备编码 + */ + private String iotId; + + /** + * 设备序列号 + */ + private String deviceId; + + /** + * 第三方设备ID唯一标识符 + */ + @Transient + private String extDeviceId; + + /** + * 产品ID + */ + private String productKey; + + /** + * 设备名称 + */ + private String deviceName; + + /** + * 消息类型 + */ + private String messageType; + + /** + * 公司名称 + */ + private String companyNo; + + /** + * 协议名称 + */ + private String protocol; + + /** + * 节点类型 + */ + private String deviceNode; + + /** + * 设备分类编号 + */ + private String classifiedId; + + /** + * 组织ID + */ + private String orgId; + + /** + * 事件名称 + */ + private String event; + + /** + * 创建人 + */ + private String createId; + + /** + * 实例名称 + */ + private String instance; + + /** + * 经纬度 + */ + private String point; + + /** + * 内容 + */ + private String content; + + /** + * 创建时间 + */ + private Long createTime; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceModelVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceModelVO.java new file mode 100644 index 0000000..cf617b4 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceModelVO.java @@ -0,0 +1,137 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.annotation.Excel; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import lombok.Data; + +/** + * 设备型号视图对象 iot_device_model @Author ruoyi + * + * @since 2025-09-09 + */ +@Data +@Schema(description = "设备型号视图对象") +public class IoTDeviceModelVO { + + private static final long serialVersionUID = 1L; + + /** + * 设备型号 + */ + @Excel(name = "设备型号") + @Schema(description = "设备型号") + private String deviceModel; + + /** + * 型号名称 + */ + @Excel(name = "型号名称") + @Schema(description = "型号名称") + private String deviceModelName; + + /** + * 设备图标 + */ + @Schema(description = "设备图标") + private String icon; + + /** + * 网络协议 + */ + @Excel(name = "网络协议") + @Schema(description = "网络协议") + private String protocol; + + /** + * 产品编号 + */ + @Excel(name = "产品编号") + @Schema(description = "产品编号") + private String productKey; + + /** + * 页面配置 + */ + @Excel(name = "页面配置") + @Schema(description = "页面配置") + private Map pageCtrl; + + @Schema(description = "后台内部扩展字段") + private JSONObject extConf; + + public static long getSerialVersionUID() { + return serialVersionUID; + } + + public String getDeviceModel() { + return deviceModel; + } + + public void setDeviceModel(String deviceModel) { + this.deviceModel = deviceModel; + } + + public String getDeviceModelName() { + return deviceModelName; + } + + public void setDeviceModelName(String deviceModelName) { + this.deviceModelName = deviceModelName; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getProductKey() { + return productKey; + } + + public void setProductKey(String productKey) { + this.productKey = productKey; + } + + public Map getPageCtrl() { + return pageCtrl; + } + + public void setPageCtrl(String pageCtrl) { + this.pageCtrl = JSONUtil.parseObj(pageCtrl); + } + + public JSONObject getExtConf() { + return extConf; + } + + public void setExtConf(String extConf) { + JSONObject map = JSONUtil.parseObj(extConf); + this.extConf = map; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceTypeVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceTypeVO.java new file mode 100644 index 0000000..54fe2d2 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceTypeVO.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Data; + +/** + * 设备类型视图对象 @Author Aleo + * + * @since 2023/1/5 11:36 + */ +@Data +@Schema(description = "设备类型视图对象") +public class IoTDeviceTypeVO { + + private static final long serialVersionUID = 1L; + + /** + * 设备类型 + */ + @Schema(description = "设备类型") + private String deviceType; + + /** + * 设备类型名称 + */ + @Schema(description = "设备类型名称") + private String deviceTypeName; + + @Schema(description = "设备型号") + private List models; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceVO.java new file mode 100644 index 0000000..5a4039e --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTDeviceVO.java @@ -0,0 +1,78 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTDeviceVO { + + private static final long serialVersionUID = 1L; + + /** + * 对外设备唯一标识符 + */ + private String iotId; + + private String gwProductKey; + + /** + * 设备自身序号 + */ + private String deviceId; + + /** + * 派生元数据,有的设备的属性,功能,事件可能会动态的添加 + */ + private String metadata; + + /** + * 产品key + */ + private String productKey; + + /** + * 设备名称 + */ + private String productName; + + /** + * 设备实例名称 + */ + private String deviceName; + + /** + * 别名 + */ + private String nickName; + + private String latlng; + + private String deviceNode; + + private String topic; + private String subMsgType; + private String subType; + private String subUrl; + private String state; + private String deviceTypeName; + private String deviceModel; + private String classifiedName; + private String name; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTProductExportVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTProductExportVO.java new file mode 100644 index 0000000..e29513c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTProductExportVO.java @@ -0,0 +1,222 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import cn.universal.common.annotation.Excel; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTProductExportVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @Id + private Long id; + + /** + * 产品编号 + */ + @Column(name = "product_id") + @Excel(name = "产品编号") + private String productId; + + /** + * 产品KEY + */ + @Column(name = "product_key") + @Excel(name = "产品KEY") + private String productKey; + + /** + * 产品密钥(1型1密) + */ + @Column(name = "product_secret") + private String productSecret; + + /** + * 第三方平台:lvzhou,ctwing,onenet,alibaba,baidu + */ + @Column(name = "third_platform") + @Excel(name = "第三方平台:lvzhou,ctwing,onenet,alibaba,baidu") + private String thirdPlatform; + + /** + * 第三方平台配置信息 + */ + @Column(name = "third_configuration") + @Excel(name = "第三方平台配置信息") + private String thirdConfiguration; + + /** + * 厂商编号 + */ + @Column(name = "company_no") + @Excel(name = "厂商编号") + private String companyNo; + + /** + * 分类ID + */ + @Column(name = "classified_id") + // @Excel(name = "分类ID") + private String classifiedId; + + /** + * 网络组件:关联network表 + */ + @Column(name = "network_union_id") + // @Excel(name = "网络组件:关联network表") + private String networkUnionId; + + /** + * 设备类型: 网关,设备 + */ + @Column(name = "device_node") + @Excel(name = "设备类型: 网关,设备") + private String deviceNode; + + /** + * 所属项目 + */ + @Column(name = "gw_product_key") + private String gwProductKey; + + /** + * 分类名称 + */ + @Column(name = "classified_name") + // @Excel(name = "分类名称") + private String classifiedName; + + /** + * 消息协议: + */ + @Column(name = "message_protocol") + // @Excel(name = "消息协议") + private String messageProtocol; + + /** + * 名称 + */ + @Excel(name = "名称") + private String name; + + /** + * 创建者id + */ + @Column(name = "creator_id") + private String creatorId; + + /** + * 产品状态 + */ + @Excel(name = "产品状态") + private Byte state; + + /** + * 说明 + */ + @Column(name = "`describe`") + // @Excel(name = "说明") + private String describe; + + /** + * 数据存储策略 + */ + @Column(name = "store_policy") + @Excel(name = "存储策略") + private String storePolicy; + + /** + * 传输协议: MQTT,COAP,UDP + */ + @Column(name = "transport_protocol") + @Excel(name = "传输协议: MQTT,COAP,UDP") + private String transportProtocol; + + /** + * 图片地址 + */ + @Column(name = "photo_url") + @Excel(name = "图片地址") + private String photoUrl; + + /** + * 协议配置 + */ + @Column(name = "configuration") + @Excel(name = "产品配置") + private String configuration; + + /** + * 创建时间 + */ + @Column(name = "create_time") + @Excel(name = "创建时间") + private Long createTime; + + /** + * 数据存储策略配置 + */ + @Column(name = "store_policy_configuration") + @Excel(name = "数据存储策略配置") + private String storePolicyConfiguration; + + /** + * 物模型 + */ + @Excel(name = "物模型") + private String metadata; + + /** + * 第三方平台产品下发信息 + */ + @Excel(name = "第三方平台产品下发信息") + private String thirdDownRequest; + + /** + * 协议状态 + */ + @Excel(name = "协议状态 0停用 1启用") + private Byte protocolState; + + /** + * 协议类型 + */ + @Excel(name = "协议类型") + private String protocolType; + + /** + * 协议配置 + */ + @Excel(name = "编解码协议配置") + private String protocolConfiguration; + + /** + * 协议编解码示例 + */ + @Excel(name = "协议编解码示例") + private String example; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTProductSortVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTProductSortVO.java new file mode 100644 index 0000000..241555c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTProductSortVO.java @@ -0,0 +1,159 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import cn.universal.common.annotation.Excel; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 产品分类对象 iot_product_sort @Author Aleo + * + * @since 2025-12-29 + */ +public class IoTProductSortVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + private String id; + + /** + * 父id + */ + @Excel(name = "父id") + private String parentId; + + /** + * 是否有子节点 + */ + @Excel(name = "是否有子节点") + private Integer hasChild; + + /** + * 标识 + */ + @Excel(name = "标识") + private String identification; + + /** + * 分类名称 + */ + @Excel(name = "分类名称") + private String label; + + /** + * 说明 + */ + @Excel(name = "说明") + private String description; + + /** + * 创建时间 + */ + @Excel(name = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 创建者 + */ + @Excel(name = "创建者") + private String createBy; + + /** + * 子分类 + */ + private List children = new ArrayList(); + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public static long getSerialVersionUID() { + return serialVersionUID; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public Integer getHasChild() { + return hasChild; + } + + public void setHasChild(Integer hasChild) { + this.hasChild = hasChild; + } + + public String getIdentification() { + return identification; + } + + public void setIdentification(String identification) { + this.identification = identification; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getCreateBy() { + return createBy; + } + + public void setCreateBy(String createBy) { + this.createBy = createBy; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTProductVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTProductVO.java new file mode 100644 index 0000000..561090f --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTProductVO.java @@ -0,0 +1,36 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import cn.universal.persistence.entity.IoTProduct; +import lombok.Data; + +/** + * @version 1.0 @Author Aleo + * @since 2025/11/16 + */ +@Data +public class IoTProductVO extends IoTProduct { + + private String image; + private int powerModel; + private String lwm2mEdrxTime; + + private Long createTime; + + private int devNum; + + private String storePolicy; + private String type; + private String gwName; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTProtocolVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTProtocolVO.java new file mode 100644 index 0000000..317ebbc --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTProtocolVO.java @@ -0,0 +1,43 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_device_protocol") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IoTProtocolVO implements Serializable { + + private static final long serialVersionUID = 1L; + private String name; + private String description; + private Byte state; + @Id + private String id; + private String type; + private String configuration; + private String example; + private String fileName; + private String url; + private String jscript; + private String needBs4Decode; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTUserApplicationVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTUserApplicationVO.java new file mode 100644 index 0000000..9c51852 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTUserApplicationVO.java @@ -0,0 +1,135 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import cn.universal.common.annotation.Excel; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_user_application") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTUserApplicationVO implements Serializable { + + private static final long serialVersionUID = 1L; + + // @Column(name = "uuid") + // private Long uuid; + + @Column(name = "union_id") + @Excel(name = "用户唯一标识") + private String unionId; + + /** + * 调用凭证APPID + */ + @Column(name = "app_id") + @Excel(name = "调用凭证APPID") + private String appId; + + /** + * MQ上行主题,英文逗号分隔 + */ + @Column(name = "up_topic") + @Excel(name = "MQ上行主题") + private String upTopic; + + /** + * MQ下行主题,英文逗号分隔 + */ + @Column(name = "down_topic") + @Excel(name = "MQ下行主题") + private String downTopic; + + /** + * 调用密钥 + */ + @Column(name = "app_secret") + @Excel(name = "调用密钥") + private String appSecret; + + /** + * 授权结束时间 + */ + @Column(name = "valid_end_date") + @Excel(name = "授权结束时间") + private Date validEndDate; + + /** + * 授权范围 + */ + @Excel(name = "授权范围") + private String scope; + + /** + * 0-正常,1-停用 + */ + @Column(name = "app_status") + @Excel(name = "应用状态 0-正常,1-停用") + private Integer appStatus; + + /** + * 0-正常,1-删除 + */ + @Excel(name = "是否删除 0正常 1删除") + private Integer deleted; + + @Column(name = "notify_url") + @Excel(name = "推送地址") + private String notifyUrl; + + @Column(name = "app_name") + @Excel(name = "应用名称") + private String appName; + + @Id + @Column(name = "app_unique_id") + @Excel(name = "应用唯一标识") + private String appUniqueId; + + @Column(name = "instance") + @Excel(name = "实例名称") + private String instance; + + @Column(name = "remark") + @Excel(name = "描述") + private String remark; + + @Column(name = "http_enable") + @Excel(name = "http启用") + private Boolean httpEnable; + + @Column(name = "mqtt_enable") + @Excel(name = "mqtt启用") + private Boolean mqttEnable; + + @Column(name = "create_date") + @Excel(name = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createDate; + + @Excel(name = "设备数量") + private Integer devNum; + + private String cfg; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTUserVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTUserVO.java new file mode 100644 index 0000000..5c8063e --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/IoTUserVO.java @@ -0,0 +1,173 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "iot_user") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IoTUserVO implements Serializable { + + private static final long serialVersionUID = 1L; + @Id + private Long id; + + /** + * 归属组织机构 + */ + @Column(name = "org_id") + private Long orgId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + @JsonIgnore + private String password; + + /** + * 别名 + */ + private String alias; + + /** + * 用户唯一标识 + */ + @Column(name = "union_id") + private String unionId; + + /** + * 邮箱 + */ + private String email; + + /** + * 密码加盐 + */ + private String salt; + + /** + * 手机号 + */ + private String mobile; + + /** + * 账号状态(0正常,1停用) + */ + private String status; + + /** + * 头像 + */ + private String avatar; + + /** + * 上级用户唯一id + */ + private String parentUnionId; + + /** + * 账号身份 0.超级管理员 1.普通用户 2.子用户 + */ + private Integer identity; + + /** + * 登录IP + */ + @Column(name = "login_ip") + private String loginIp; + + /** + * 登录时间 + */ + @Column(name = "login_date") + private Date loginDate; + + /** + * 创建者 + */ + @Column(name = "create_by") + private String createBy; + + /** + * 创建时间 + */ + @Column(name = "create_date") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createDate; + + @Column(name = "update_by") + private String updateBy; + + @Column(name = "update_date") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateDate; + + /** + * 备注 + */ + private String remark; + + /** + * 0-正常,1-删除 + */ + private Integer deleted; + + private List roleIds; + + /** + * 接入数 + */ + @Column(name = "license") + private Integer license; + + /** + * 接入数 + */ + @Column(name = "license_total_amount") + private Integer licenseTotalAmount; + + /** + * 请求参数 + */ + @Builder.Default + private Map params = new HashMap<>(); + + public IoTUserVO(Long userId) { + this.id = userId; + } + + public boolean isAdmin() { + return identity != null && identity == 0; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/NetworkVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/NetworkVO.java new file mode 100644 index 0000000..81b2b32 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/NetworkVO.java @@ -0,0 +1,58 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import cn.universal.persistence.entity.Network; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 网络组件视图对象 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NetworkVO extends Network { + + /** + * 网络类型名称 + */ + private String typeName; + + /** + * 状态名称 + */ + private String stateName; + + /** + * 创建时间格式化 + */ + private String createDateStr; + + /** + * 配置对象(解析后的JSON) + */ + private Object configObject; + + /** + * 启用/停用状态名称 + */ + private String enableName; + + /** + * 是否正在运行 + */ + private boolean running; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/RuleModelVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/RuleModelVO.java new file mode 100644 index 0000000..3877579 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/RuleModelVO.java @@ -0,0 +1,94 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.persistence.entity.RuleModel; +import cn.universal.persistence.entity.RuleModelInstance; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.beans.BeanUtils; + +/** + * 规则模型 @Author Aleo + * + * @since 2023/1/13 14:28 + */ +@Data +@Schema +@NoArgsConstructor +public class RuleModelVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "主键ID") + private Long id; + + @Schema(description = "规则名称") + private String ruleName; + + /** + * 数据级别 + */ + @Schema(description = "数据级别") + private String dataLevel; + + @Schema(description = "产品KEY") + private String productKey; + + @Schema(description = "描述") + private String description; + + @Schema(description = "状态") + private String status; + + @Schema(description = "规则配置") + private String config; + + @Schema(description = "分组id") + private String groupId; + + @Schema(description = "关联设备id") + private List relationIds; + + @Schema(description = "创建人") + private String creatorId; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + public RuleModelVO(RuleModel ruleModel, List instances) { + BeanUtils.copyProperties(ruleModel, this); + + if ("group".equals(this.dataLevel)) { + this.groupId = instances.get(0).getRelationId(); + } + + if ("device".equals(this.dataLevel)) { + this.relationIds = + instances.stream().map(RuleModelInstance::getRelationId).collect(Collectors.toList()); + } + } + + public JSONObject getConfig() { + return JSONUtil.parseObj(config); + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/RulegoChainVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/RulegoChainVO.java new file mode 100644 index 0000000..64346fa --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/RulegoChainVO.java @@ -0,0 +1,75 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * rulego规则链VO + * + * @author Aleo + * @since 2025/01/15 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Schema +public class RulegoChainVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "主键ID") + private Long id; + + @Schema(description = "rulego规则链ID") + private String rulegoId; + + @Schema(description = "规则链名称") + private String chainName; + + @Schema(description = "规则链描述") + private String description; + + @Schema(description = "创建人unionId") + private String creatorId; + + @Schema(description = "创建人姓名") + private String creatorName; + + @Schema(description = "状态") + private String status; + + @Schema(description = "状态描述") + private String statusDesc; + + @Schema(description = "规则链DSL内容") + private String dslContent; + + @Schema(description = "最后同步时间") + private String lastSyncTime; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "设计器URL") + private String designerUrl; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/WxMiniUserInfoVO.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/WxMiniUserInfoVO.java new file mode 100644 index 0000000..b6b3cf9 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/entity/vo/WxMiniUserInfoVO.java @@ -0,0 +1,85 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.entity.vo; + +import jakarta.persistence.Column; +import java.io.Serializable; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 微信用户信息对象 wx_user_info @Author Aleo + * + * @since 2025-10-19 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class WxMiniUserInfoVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * $column.columnComment + */ + private Long uuid; + + // 用户是否订阅该公众号标识,值为0时,代表此用户没有关注该公众号。 + private Integer subscribe; + + /** + * 用户微信openid + */ + @Column(name = "open_id") + private String openId; + + /** + * 开放平台id + */ + @Column(name = "union_id") + private String unionId; + + /** + * 微信名 + */ + @Column(name = "nick_name") + private String nickName; + + /** + * 公众号id + */ + @Column(name = "app_id") + private String appId; + + /** + * 微信头像 + */ + @Column(name = "head_img_url") + private String headImgUrl; + + @Column(name = "create_time") + private Date createTime; + + @Column(name = "update_time") + private Date updateTime; + + /** + * 手机 + */ + @Column(name = "phone") + private String phone; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTCertificateMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTCertificateMapper.java new file mode 100644 index 0000000..f4ae7a1 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTCertificateMapper.java @@ -0,0 +1,6 @@ +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTCertificate; + +public interface IoTCertificateMapper extends BaseMapper {} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDashboardStatisticsMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDashboardStatisticsMapper.java new file mode 100644 index 0000000..7d73b36 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDashboardStatisticsMapper.java @@ -0,0 +1,102 @@ +package cn.universal.persistence.mapper; + +import cn.universal.persistence.entity.IoTDashboardStatistics; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import tk.mybatis.mapper.common.BaseMapper; + +/** + * 仪表盘统计Mapper接口 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +@Mapper +public interface IoTDashboardStatisticsMapper extends BaseMapper { + + /** + * 根据日期和指标类型查询统计数据 + * + * @param statDate 统计日期 + * @param metricType 指标类型 + * @param productKey 产品Key + * @param channel 渠道 + * @return 统计数据列表 + */ + List selectByDateAndMetric( + @Param("statDate") LocalDate statDate, + @Param("metricType") String metricType, + @Param("productKey") String productKey, + @Param("channel") String channel); + + /** + * 查询指定日期范围的统计数据 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param metricType 指标类型 + * @param productKey 产品Key + * @param channel 渠道 + * @return 统计数据列表 + */ + List selectByDateRange( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("metricType") String metricType, + @Param("productKey") String productKey, + @Param("channel") String channel); + + /** + * 根据唯一键查询统计数据 + * + * @param statDate 统计日期 + * @param productKey 产品Key + * @param channel 渠道 + * @param metricType 指标类型 + * @return 统计数据,如果不存在返回null + */ + IoTDashboardStatistics selectByUniqueKey( + @Param("statDate") LocalDate statDate, + @Param("productKey") String productKey, + @Param("channel") String channel, + @Param("metricType") String metricType); + + /** + * 根据唯一键更新统计数据 + * + * @param statDate 统计日期 + * @param productKey 产品Key + * @param channel 渠道 + * @param metricType 指标类型 + * @param metricValue 指标值 + * @param updateTime 更新时间 + * @return 影响行数 + */ + int updateByUniqueKey( + @Param("statDate") LocalDate statDate, + @Param("productKey") String productKey, + @Param("channel") String channel, + @Param("metricType") String metricType, + @Param("metricValue") Long metricValue, + @Param("updateTime") LocalDateTime updateTime); + + /** + * 插入统计数据 + * + * @param statistics 统计数据 + * @return 影响行数 + */ + int insert(IoTDashboardStatistics statistics); + + /** + * 删除指定日期的所有统计数据 + * + * @param statDate 统计日期 + * @return 删除的记录数 + */ + int deleteByDate(@Param("statDate") LocalDate statDate); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceFenceRelMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceFenceRelMapper.java new file mode 100644 index 0000000..24b2168 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceFenceRelMapper.java @@ -0,0 +1,33 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTDeviceFenceRel; +import cn.universal.persistence.entity.vo.IoTDeviceGeoFenceVO; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +/** + * 设备和围栏中间表 @Author Aleo + * + * @since 2023/8/5 8:54 + */ +public interface IoTDeviceFenceRelMapper extends BaseMapper { + + int deleteFenceInstance(@Param("iotId") String iotId); + + int deleteDeviceIdAndFenceId(@Param("deviceId") String deviceId, @Param("fenceId") Long fenceId); + + List selectFenceByIotId(@Param("iotId") String iotId); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceFunctionHistoryMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceFunctionHistoryMapper.java new file mode 100644 index 0000000..e078cca --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceFunctionHistoryMapper.java @@ -0,0 +1,31 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTDeviceFunctionHistory; +import cn.universal.persistence.entity.bo.IoTDeviceFunctionTaskBO; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +/** + * 设备功能下发任务Mapper接口 + * + * @since 2025-12-30 + */ +public interface IoTDeviceFunctionHistoryMapper extends BaseMapper { + + int retryHistory(@Param("bo") IoTDeviceFunctionTaskBO ioTDeviceFunctionTaskBO); + + void batchInsert(@Param("list") List histories); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceFunctionTaskMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceFunctionTaskMapper.java new file mode 100644 index 0000000..153d2a9 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceFunctionTaskMapper.java @@ -0,0 +1,40 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTDeviceFunctionTask; +import cn.universal.persistence.entity.bo.IoTDeviceFunctionHistoryBO; +import cn.universal.persistence.entity.bo.IoTDeviceFunctionTaskBO; +import cn.universal.persistence.entity.vo.IoTDeviceFunctionHistoryVO; +import cn.universal.persistence.entity.vo.IoTDeviceFunctionTaskVO; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +/** + * 设备功能下发任务Mapper接口 + * + * @since 2025-12-30 + */ +public interface IoTDeviceFunctionTaskMapper extends BaseMapper { + + List selectTaskList( + @Param("bo") IoTDeviceFunctionTaskBO bo, @Param("unionId") String unionId); + + List queryFunctionListByTaskId( + @Param("bo") IoTDeviceFunctionHistoryBO bo, @Param("unionId") String unionId); + + int retryTask(@Param("id") Long taskId); + + IoTDeviceFunctionTask selectOneTask(@Param("bo") IoTDeviceFunctionTask bo); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceGeoFenceMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceGeoFenceMapper.java new file mode 100644 index 0000000..4a314e3 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceGeoFenceMapper.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTDeviceGeoFence; +import cn.universal.persistence.entity.vo.IoTDeviceGeoFenceVO; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +/** + * 设备围栏表 @Author Aleo + * + * @since 2023/8/5 8:54 + */ +public interface IoTDeviceGeoFenceMapper extends BaseMapper { + + List selectByIotId( + @Param("iotId") String iotId, @Param("appUnionId") String appUnionId); + + List selectList( + @Param("ioTDeviceGeoFence") IoTDeviceGeoFence ioTDeviceGeoFence); + + int updateFence(@Param("ioTDeviceGeoFence") IoTDeviceGeoFence ioTDeviceGeoFence); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceGroupMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceGroupMapper.java new file mode 100644 index 0000000..7884f10 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceGroupMapper.java @@ -0,0 +1,83 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTDeviceGroup; +import cn.universal.persistence.entity.vo.IoTDeviceGroupVO; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +/** + * 设备分组Mapper接口 + * + * @since 2025-12-30 + */ +public interface IoTDeviceGroupMapper extends BaseMapper { + + /** + * 查询设备分组 + * + * @param id 设备分组ID + * @return 设备分组 + */ + IoTDeviceGroup selectDevGroupById(Long id); + + /** + * 查询设备分组列表 + * + * @param admin 操作者 + * @return + */ + List selectDevGroupList(String admin); + + /** + * 新增设备分组 + * + * @param ioTDeviceGroup 设备分组 + * @return 结果 + */ + int insertDevGroup(IoTDeviceGroup ioTDeviceGroup); + + /** + * 修改设备分组 + * + * @param ioTDeviceGroup 设备分组 + * @return 结果 + */ + int updateDevGroup(IoTDeviceGroup ioTDeviceGroup); + + /** + * 删除设备分组 + * + * @param id 设备分组ID + * @return 结果 + */ + int deleteDevGroupById(Long id); + + /** + * 根据分组id获取子节点个数 + * + * @param id 设备分组ID + * @return 结果 + */ + int queryChildrenCountById(Long id); + + /** + * 根据设备id查询设备是否绑定这个账号下,10个以上分组 + * + * @param iotId + * @return + */ + int queryDevCountBindGroup(@Param("iotId") String iotId, @Param("admin") String admin); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceLogMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceLogMapper.java new file mode 100644 index 0000000..c0c9bce --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceLogMapper.java @@ -0,0 +1,87 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.consistent.DeviceShardingRouter; +import cn.universal.persistence.consistent.TableShard; +import cn.universal.persistence.entity.IoTDeviceLog; +import cn.universal.persistence.entity.bo.IoTDeviceLogBO; +import cn.universal.persistence.entity.vo.IoTDeviceLogVO; +import cn.universal.persistence.query.LogQuery; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +@TableShard( + tableNamePrefix = "iot_device_log", + value = "iotId", + fieldFlag = true, + shardStrategy = DeviceShardingRouter.class) +public interface IoTDeviceLogMapper extends BaseMapper { + + /** 分页查询设备日志列表 */ + List queryLogPageList(IoTDeviceLogBO bo); + + /** 分页查询设备日志列表 */ + List queryLogPageV2List(LogQuery logQuery); + + /** 根据主键查询 */ + IoTDeviceLogVO queryLogById(LogQuery logQuery); + + /** + * 获取设备事件的统计信息 + * + * @param event 事件id + * @param iotId 设备iotId + */ + List queryEventTotalByEventAndId( + @Param("event") String event, @Param("iotId") String iotId); + + /** 查询最新包含坐标的第二条日志 */ + IoTDeviceLog queryCoordinatesLogByIotId(@Param("iotId") String iotId); + + /** 查询日志ID - 此方法不使用分表 */ + @TableShard( + tableNamePrefix = "iot_device_log", + value = "", + fieldFlag = false, + shardStrategy = DeviceShardingRouter.class) + Long queryLogIdByTime(@Param("tablesIndex") int tablesIndex, @Param("time") Long time); + + @TableShard( + tableNamePrefix = "iot_device_log", + value = "", + fieldFlag = false, + shardStrategy = DeviceShardingRouter.class) + int deleteLogById(@Param("tablesIndex") int tablesIndex, @Param("id") Long id); + + @TableShard( + tableNamePrefix = "iot_device_log", + value = "", + fieldFlag = false, + shardStrategy = DeviceShardingRouter.class) + int deleteLogByTask( + @Param("tablesIndex") int tablesIndex, + @Param("productKey") String productKey, + @Param("id") Long id); + + @TableShard( + tableNamePrefix = "iot_device_log", + value = "", + fieldFlag = false, + shardStrategy = DeviceShardingRouter.class) + Long queryLogIdByProductKeyAndTime( + @Param("tablesIndex") int tablesIndex, + @Param("productKey") String productKey, + @Param("time") Long time); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceLogMetadataMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceLogMetadataMapper.java new file mode 100644 index 0000000..c74d546 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceLogMetadataMapper.java @@ -0,0 +1,72 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTDeviceLogMetadata; +import cn.universal.persistence.entity.vo.IoTDeviceLogMetadataVO; +import cn.universal.persistence.query.LogQuery; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface IoTDeviceLogMetadataMapper extends BaseMapper { + + /** + * 删除属性最大的条数 + * + * @param iotId iotId + * @param maxStorage 最大存储条数 + * @return 影响条数 + */ + int deleteTopPropertiesRecord( + @Param("iotId") String iotId, + @Param("maxStorage") int maxStorage, + @Param("property") String property); + + /** + * 删除属性最大的条数 + * + * @param iotId iotId + * @param maxStorage 最大存储条数 + * @return 影响条数 + */ + int deleteTopEventRecord( + @Param("iotId") String iotId, + @Param("maxStorage") int maxStorage, + @Param("event") String event); + + List selectLogMetaList(LogQuery logQuery); + + /** + * 获取设备事件的统计信息 + * + * @param event 事件id + * @param iotId 设备iotId + */ + List queryEventTotalByEventAndId( + @Param("event") String event, @Param("iotId") String iotId); + + Long queryLogMetaIdByTime(@Param("tablesIndex") int tablesIndex, @Param("time") Long time); + + int deleteLogMetaById(@Param("tablesIndex") int tablesIndex, @Param("id") Long id); + + int deleteLogMetaByTask( + @Param("tablesIndex") int tablesIndex, + @Param("productKey") String productKey, + @Param("id") Long id); + + Long queryLogMetaIdByProductKeyAndTime( + @Param("tablesIndex") int tablesIndex, + @Param("productKey") String productKey, + @Param("time") Long time); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceLogMetadataShardMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceLogMetadataShardMapper.java new file mode 100644 index 0000000..32ac720 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceLogMetadataShardMapper.java @@ -0,0 +1,89 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.consistent.DeviceMetaShardingRouter; +import cn.universal.persistence.consistent.TableShard; +import cn.universal.persistence.entity.IoTDeviceLogMetadata; +import cn.universal.persistence.entity.vo.IoTDeviceLogMetadataVO; +import cn.universal.persistence.query.LogQuery; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +@TableShard( + tableNamePrefix = "iot_device_log_metadata", + value = "iotId", + fieldFlag = true, + shardStrategy = DeviceMetaShardingRouter.class) +public interface IoTDeviceLogMetadataShardMapper extends BaseMapper { + + /** + * 获取属性最大的条数 + * + * @param iotId + * @param maxStorage + * @param property + * @return + */ + Integer getTopPropertiesRecord( + @Param("iotId") String iotId, + @Param("maxStorage") int maxStorage, + @Param("property") String property); + + /** + * 删除属性最大的条数 + * + * @param iotId iotId + * @param topId 待删除的起始id + * @return 影响条数 + */ + int deleteTopPropertiesRecord( + @Param("iotId") String iotId, + @Param("topId") Integer topId, + @Param("property") String property); + + /** + * 获取事件最大的条数 + * + * @param iotId + * @param maxStorage + * @param event + * @return + */ + Integer getTopEventRecord( + @Param("iotId") String iotId, + @Param("maxStorage") int maxStorage, + @Param("event") String event); + + /** + * 删除属性最大的条数 + * + * @param iotId iotId + * @param topId 待删除的起始id + * @return 影响条数 + */ + int deleteTopEventRecord( + @Param("iotId") String iotId, @Param("topId") Integer topId, @Param("event") String event); + + List selectLogMetaList(LogQuery logQuery); + + /** + * 获取设备事件的统计信息 + * + * @param event 事件id + * @param iotId 设备iotId + */ + List queryEventTotalByEventAndId( + @Param("event") String event, @Param("iotId") String iotId); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceLogShardMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceLogShardMapper.java new file mode 100644 index 0000000..0d0a8cc --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceLogShardMapper.java @@ -0,0 +1,68 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.consistent.DeviceShardingRouter; +import cn.universal.persistence.consistent.TableShard; +import cn.universal.persistence.entity.IoTDeviceLog; +import cn.universal.persistence.entity.bo.IoTDeviceLogBO; +import cn.universal.persistence.entity.vo.IoTDeviceLogVO; +import cn.universal.persistence.query.LogQuery; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +@TableShard( + tableNamePrefix = "iot_device_log", + value = "iotId", + fieldFlag = true, + shardStrategy = DeviceShardingRouter.class) +public interface IoTDeviceLogShardMapper extends BaseMapper { + + List queryLogPageV2ByIdList(LogQuery logQuery); + + /** 分页查询设备日志列表 */ + List queryLogPageList(IoTDeviceLogBO bo); + + /** 分页查询设备日志列表 */ + List queryLogPageV2List(LogQuery logQuery); + + /** 根据主键查询 */ + IoTDeviceLogVO queryLogById(LogQuery logQuery); + + /** + * 获取设备事件的统计信息 + * + * @param event 事件id + * @param iotId 设备iotId + */ + List queryEventTotalByEventAndId( + @Param("event") String event, @Param("iotId") String iotId); + + void addFunctionLog(IoTDeviceLogBO bo); + + /** 查询最新包含坐标的第二条日志 */ + IoTDeviceLog queryCoordinatesLogByIotId(@Param("iotId") String iotId); + + /** 查询最新包含坐标的第一条日志 */ + IoTDeviceLog queryLatestCoordinatesLogByIotId(@Param("iotId") String iotId); + + IoTDeviceLog selectOneForCtwing( + @Param("iotId") String iotId, + @Param("commandId") String commandId, + @Param("createTime") Long createTime, + @Param("commandStatus") Integer commandStatus); + + void updateLogByIdForCtwing( + @Param("ioTDeviceLog") IoTDeviceLog ioTDeviceLog, @Param("iotId") String iotId); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceMapper.java new file mode 100644 index 0000000..a1c0c6a --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceMapper.java @@ -0,0 +1,219 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.dto.IoTDeviceDTO; +import cn.universal.persistence.dto.IoTDeviceMetadataBO; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.bo.IoTDeviceBO; +import cn.universal.persistence.entity.bo.IoTDeviceHistoryBO; +import cn.universal.persistence.entity.bo.IoTGwDeviceBO; +import cn.universal.persistence.entity.bo.NetworkBO; +import cn.universal.persistence.entity.vo.GatewayDeviceVo; +import cn.universal.persistence.entity.vo.InterPhoneInstanceVO; +import cn.universal.persistence.entity.vo.IoTDeviceCompanyVO; +import cn.universal.persistence.entity.vo.IoTDeviceCountVO; +import cn.universal.persistence.entity.vo.IoTDeviceModelVO; +import cn.universal.persistence.entity.vo.IoTDeviceVO; +import cn.universal.persistence.query.IoTAPIQuery; +import cn.universal.persistence.query.IoTDeviceQuery; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.ibatis.annotations.Param; + +public interface IoTDeviceMapper extends BaseMapper { + + /** + * deviceId 设备编号,extDeviceId 扩展设备编号,iotId 设备统一编号 + * + * @param map + * @return IoTDeviceDTO + */ + IoTDeviceDTO selectIoTDeviceBO(Map map); + + /** + * 查询设备信息。分页查询 + * + * @param map deviceId 设备编号,extDeviceId 扩展设备编号,iotId 设备统一编号 + * @return IoTDeviceDTO + */ + List selectDevInstanceBOV2List(Map map); + + /** 计算应用设备数量 */ + List countDevByApplication(String appUniqueId); + + /** 查询设备元数据 */ + IoTDeviceMetadataBO selectDevMetadataBo(String iotId); + + List selectList( + @Param("productKey") String productKey, @Param("start") int start, @Param("limit") int limit); + + List selectListByProductKey(@Param("productKey") String productKey); + + IoTDeviceVO apiDeviceInfo(IoTAPIQuery apiQuery); + + List apiDeviceList(IoTAPIQuery apiQuery); + + /** + * 查询设备 + * + * @param id 设备主键 + * @return 设备 + */ + public IoTDevice selectDevInstanceById(String id); + + /** + * 查询设备列表 + * + * @param ioTDevice 设备 + * @return 设备集合 + */ + public List selectDevInstanceList( + @Param("ioTDevice") IoTDevice ioTDevice, @Param("unionId") String unionId); + + public List selectDevInstanceUnList( + @Param("ioTDevice") IoTDevice ioTDevice, @Param("unionId") String unionId); + + List selectDevInstanceForExport( + @Param("ioTDevice") IoTDevice ioTDevice, @Param("unionId") String unionId); + + /** + * 新增设备 + * + * @param ioTDevice 设备 + * @return 结果 + */ + public int insertDevInstance(IoTDevice ioTDevice); + + /** + * 修改设备 + * + * @param ioTDevice 设备 + * @return 结果 + */ + public int updateDevInstance(IoTDevice ioTDevice); + + int bindApp(@Param("id") String[] id, @Param("appUniqueId") String appUniqueId); + + /** 设备绑定应用 */ + int apiBindApp(@Param("appUniqueId") String appUniqueId, @Param("iotId") String iotId); + + /** 设备应用解绑 */ + int apiUnBindApp(@Param("iotId") String iotId); + + /** + * 删除设备 + * + * @param id 设备主键 + * @return 结果 + */ + public int deleteDevInstanceById(String id); + + /** + * 批量删除设备 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteDevInstanceByIds(String[] ids); + + /** 根据分组id获取设备集合 */ + List selectDevListByIds(String groupId); + + int selectDevByAppUniqueId(String[] id); + + /** 查询所以设备型号 */ + List selectAllBySupportChild( + @Param("supportChild") Boolean supportChild, @Param("apps") String apps); + + IoTDevice getOneByDeviceId(@Param("query") IoTDeviceQuery query); + + IoTDevice getOneByIotId(@Param("query") IoTDeviceQuery query); + + /** 根据产品key查型号配置 */ + IoTDeviceModelVO getModelByProductKey(String productKey); + + List selectNotBindDevInstanceList( + @Param("groupId") String groupId, + @Param("dev") IoTDevice ioTDevice, + @Param("unionId") String unionId); + + /** + * 查询满足离线标准的deviceId + * + * @param productKey 产品标识 + * @param difference 时间差额 + */ + List selectOfflineThresholdIotIds( + @Param("productKey") String productKey, @Param("difference") String difference); + + /** 选择离线摄像头 */ + List selectOfflineCamera( + @Param("productKeyList") List productKeyList, + @Param("discardValue") Integer discardValue, + @Param("status") Integer status, + @Param("start") Integer start, + @Param("limit") Integer limit); + + /** 查询设备历史总数 */ + public int selectDevInstanceHistory(IoTDeviceHistoryBO ioTDeviceHistoryBO); + + public int insertDevInstanceHistory(IoTDeviceHistoryBO ioTDeviceHistoryBO); + + /** 修改设备配置字段 */ + public int updateDevConfiguration(Map map); + + List selectFenceDevice(IoTDeviceBO ioTDeviceBO); + + List hikDeviceList(IoTAPIQuery apiQuery); + + void clearDevBindInfo(IoTDevice instance); + + List queryMileSightList(@Param("bo") NetworkBO bo); + + + List getGatewayDeviceList(@Param("bo") IoTGwDeviceBO bo); + + List selectDevInstanceListWithTags( + @Param("ioTDevice") IoTDeviceBO ioTDevice, @Param("unionId") String unionId); + + List listByIds(String[] ids); + + List selectProductListInBatchFunction( + @Param("applicationId") Long applicationId, @Param("unionId") String unionId); + + int checkIotIdUnique(@Param("iotId") String iotId); + + IoTDevice getOneByExtId(@Param("extId") String extId); + + /** 根据productKey统计设备数量 */ + int countByProductKey(String productKey); + + /** 批量刷新设备日志,更新online_time和state,初始化registry_time(如为空) */ + void batchFlushLog(@Param("iotIds") Set iotIds); + + /** 统计所有设备总数 */ + long countAllDevices(); + + /** 统计在线设备总数 */ + long countOnlineDevices(); + + /** 根据创建者统计设备总数 */ + long countDevicesByCreator(@Param("creatorId") String creatorId); + + /** 根据创建者统计在线设备总数 */ + long countOnlineDevicesByCreator(@Param("creatorId") String creatorId); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceProtocolMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceProtocolMapper.java new file mode 100644 index 0000000..56c6acb --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceProtocolMapper.java @@ -0,0 +1,80 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTDeviceProtocol; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface IoTDeviceProtocolMapper extends BaseMapper { + + /** + * 查询设备协议 + * + * @param id 设备协议主键 + * @return 设备协议 + */ + public IoTDeviceProtocol selectDevProtocolById( + @Param("id") String id, @Param("unionId") String unionId); + + public int countByProvider(String provider); + + int insertProtocolList(@Param("list") List ioTDeviceProtocols); + + public List selectDevProtocolByIds(String[] ids); + + /** + * 查询设备协议列表 + * + * @param ioTDeviceProtocol 设备协议 + * @return 设备协议集合 + */ + public List selectDevProtocolList( + @Param("ioTDeviceProtocol") IoTDeviceProtocol ioTDeviceProtocol, + @Param("unionId") String unionId); + + /** + * 新增设备协议 + * + * @param ioTDeviceProtocol 设备协议 + * @return 结果 + */ + public int insertDevProtocol(IoTDeviceProtocol ioTDeviceProtocol); + + /** + * 修改设备协议 + * + * @param ioTDeviceProtocol 设备协议 + * @return 结果 + */ + public int updateDevProtocol(IoTDeviceProtocol ioTDeviceProtocol); + + /** + * 删除设备协议 + * + * @param id 设备协议主键 + * @return 结果 + */ + public int deleteDevProtocolById(String id); + + /** + * 批量删除设备协议 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteDevProtocolByIds(String[] ids); + + IoTDeviceProtocol selectDevProtocolByProductKey(@Param("productKey") String productKey); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceRuleLogMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceRuleLogMapper.java new file mode 100644 index 0000000..5262e1c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceRuleLogMapper.java @@ -0,0 +1,18 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTDeviceRuleLog; + +public interface IoTDeviceRuleLogMapper extends BaseMapper {} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceShadowMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceShadowMapper.java new file mode 100644 index 0000000..38644f0 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceShadowMapper.java @@ -0,0 +1,25 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTDeviceShadow; +import org.apache.ibatis.annotations.Param; + +public interface IoTDeviceShadowMapper extends BaseMapper { + + IoTDeviceShadow getDeviceShadow(String iotId); + + String getShadowMetadata( + @Param("productKey") String productKey, @Param("deviceId") String deviceId); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceSubscribeMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceSubscribeMapper.java new file mode 100644 index 0000000..63b3cd4 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceSubscribeMapper.java @@ -0,0 +1,26 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTDeviceSubscribe; +import java.util.List; + +public interface IoTDeviceSubscribeMapper extends BaseMapper { + + List selectSubscribeBO(IoTDeviceSubscribe ioTDeviceSubscribe); + + List selectSubscribesBO(IoTDeviceSubscribe ioTDeviceSubscribe); + + List selectByMsgAndType(IoTDeviceSubscribe ioTDeviceSubscribe); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceTagsMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceTagsMapper.java new file mode 100644 index 0000000..fbb2d1a --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTDeviceTagsMapper.java @@ -0,0 +1,34 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTDeviceTags; +import java.util.List; + +public interface IoTDeviceTagsMapper extends BaseMapper { + + /** + * 根据分组id查询设备id集合 + * + * @param groupId + * @return + */ + int selectDevIds(String groupId); + + int deleteByValueId(String groupId); + + IoTDeviceTags getOne(String iotId); + + List selectDevGroupName(String iotId); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTProductMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTProductMapper.java new file mode 100644 index 0000000..6e14243 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTProductMapper.java @@ -0,0 +1,144 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTProduct; +import cn.universal.persistence.entity.bo.IoTProductBO; +import cn.universal.persistence.entity.vo.IoTDeviceModelVO; +import cn.universal.persistence.entity.vo.IoTProductExportVO; +import cn.universal.persistence.entity.vo.IoTProductVO; +import cn.universal.persistence.query.IoTAPIQuery; +import cn.universal.persistence.query.IoTProductQuery; +import java.util.List; +import java.util.Map; +import org.apache.ibatis.annotations.Param; +import org.springframework.cache.annotation.Cacheable; + +public interface IoTProductMapper extends BaseMapper { + + List openAPIProductList(IoTAPIQuery iotAPIQuery); + + IoTProductVO apiProductDetail(String productKey); + + /** + * 查询设备产品 + * + * @param id 设备产品主键 + * @return 设备产品 + */ + public IoTProduct selectDevProductById(String id); + + /** + * 查询设备产品列表 + * + * @param ioTProduct 设备产品 + * @return 设备产品集合 + */ + public List selectDevProductList(IoTProduct ioTProduct); + + /** + * 查询设备产品列表 + * + * @param ioTProductQuery 设备产品 + * @return 设备产品集合 + */ + public List selectDevProductV2List(IoTProductQuery ioTProductQuery); + + public List selectDevProductV3List(IoTProductQuery ioTProductQuery); + + public List selectAllDevProductV2List(IoTProductQuery ioTProductQuery); + + /** + * 新增设备产品 + * + * @param ioTProduct 设备产品 + * @return 结果 + */ + public int insertDevProduct(IoTProduct ioTProduct); + + /** + * 修改设备产品 + * + * @param ioTProduct 设备产品 + * @return 结果 + */ + public int updateDevProduct(IoTProduct ioTProduct); + + /** + * 删除设备产品 + * + * @param id 设备产品主键 + * @return 结果 + */ + public int deleteDevProductById(String id); + + /** + * 批量删除设备产品 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteDevProductByIds(String[] ids); + + /** + * 根据产品key查产品 + * + * @param productKey 产品key + * @return 结果 + */ + IoTProduct getProductByProductKey(String productKey); + + /** + * 根据产品key查model配置 + * + * @param productKey 产品key + * @return 结果 + */ + IoTDeviceModelVO getModelByProductKey(String productKey); + + int insertMetadata(IoTProductBO ioTProductBO); + + int deleteMetadata(IoTProductBO ioTProductBO); + + int updateMetadata(IoTProductBO ioTProductBO); + + IoTProductBO getMetadata(IoTProductBO ioTProductBO); + + String selectMetadataByDevId(String devId); + + List countDevNumberByProductKey(@Param("unionId") String unionId); + + List selectDevProductAllList(); + + List selectProductName(String[] productKey); + + List getGatewaySubDeviceList(String productKey); + + String selectNetworkUnionId(String productKey); + + @Cacheable("selectAllEnableNetworkProductKey") + List selectAllEnableNetworkProductKey(); + + /** 统计引用指定network_union_id的产品数量 */ + int countByNetworkUnionId(String networkUnionId); + + /** 模糊搜索productKey和name */ + List> searchProductKeyAndName(IoTProductQuery query); + + /** 根据productKey更新network_union_id */ + int updateNetworkUnionIdByProductKey(String productKey, String networkUnionId); + + /** 根据network_union_id查找productKey */ + String findProductKeyByNetworkUnionId(String networkUnionId); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTProductSortMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTProductSortMapper.java new file mode 100644 index 0000000..a4861e0 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTProductSortMapper.java @@ -0,0 +1,80 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.entity.IoTProductSort; +import java.util.List; + +/** + * 产品分类Mapper接口 @Author Aleo + * + * @since 2025-12-29 + */ +public interface IoTProductSortMapper { + + /** + * 查询产品分类 + * + * @param id 产品分类主键 + * @return 产品分类 + */ + public IoTProductSort selectDevProductSortById(String id); + + /** + * 查询产品分类列表 + * + * @param ioTProductSort 产品分类 + * @return 产品分类集合 + */ + public List selectDevProductSortList(IoTProductSort ioTProductSort); + + /** + * 新增产品分类 + * + * @param ioTProductSort 产品分类 + * @return 结果 + */ + public int insertDevProductSort(IoTProductSort ioTProductSort); + + /** + * 修改产品分类 + * + * @param ioTProductSort 产品分类 + * @return 结果 + */ + public int updateDevProductSort(IoTProductSort ioTProductSort); + + /** + * 删除产品分类 + * + * @param id 产品分类主键 + * @return 结果 + */ + public int deleteDevProductSortById(String id); + + /** + * 批量删除产品分类 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteDevProductSortByIds(String[] ids); + + /** + * 根据父id倒序查出列表 + * + * @param parentId 需要删除的数据主键集合 + * @return 结果 + */ + public List getListByParentId(String parentId); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTUserApplicationMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTUserApplicationMapper.java new file mode 100644 index 0000000..328c9cf --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTUserApplicationMapper.java @@ -0,0 +1,94 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTUserApplication; +import cn.universal.persistence.entity.vo.IoTUserApplicationVO; +import java.util.List; + +/** + * 用户应用信息Mapper接口 + * + * @since 2025-12-30 + */ +public interface IoTUserApplicationMapper extends BaseMapper { + + /** + * 查询用户应用信息 + * + * @param appUniqueId 用户应用信息ID + * @return 用户应用信息 + */ + IoTUserApplication selectIotUserApplicationById(String appUniqueId); + + /** + * 查询用户应用信息 + * + * @param appid 用户应用信息ID + * @return 用户应用信息 + */ + IoTUserApplication selectIotUserApplicationByAppId(String appid); + + List selectIotUserApplicationByUnionId(String unionId); + + /** + * 查询用户应用信息列表 + * + * @param iotUserApplication 用户应用信息 + * @return 用户应用信息集合 + */ + List selectIotUserApplicationList(IoTUserApplication iotUserApplication); + + /** + * 新增用户应用信息 + * + * @param iotUserApplication 用户应用信息 + * @return 结果 + */ + int insertIotUserApplication(IoTUserApplication iotUserApplication); + + /** + * 修改用户应用信息 + * + * @param iotUserApplication 用户应用信息 + * @return 结果 + */ + int updateIotUserApplication(IoTUserApplication iotUserApplication); + + /** + * 删除用户应用信息 + * + * @param appUniqueId 用户应用信息ID + * @return 结果 + */ + int deleteIotUserApplicationById(String appUniqueId); + + /** + * 批量删除用户应用信息 + * + * @param appUniqueId 需要删除的数据ID + * @return 结果 + */ + int deleteIotUserApplicationByIds(String[] appUniqueId); + + /** + * 统计拥有mq地址的应用数量 + * + * @param unionId 用户唯一标识 + * @return 结果 + */ + int countMqtt(String unionId); + + List selectApplicationList(IoTUserApplication application); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTUserMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTUserMapper.java new file mode 100644 index 0000000..c53eb06 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/IoTUserMapper.java @@ -0,0 +1,79 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.IoTUser; +import cn.universal.persistence.entity.bo.IoTUserBO; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface IoTUserMapper extends BaseMapper { + + /** + * 更新登录时间 + * + * @param id 用户ID + */ + int updateLoginDate(Long id); + + List selectList(IoTUser iotUser); + + /** + * 根据条件分页查询未已配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(IoTUserBO user); + + /** + * 通过用户手机号查询用户 + * + * @param mobile 用户手机号 + * @return 用户对象信息 + */ + public IoTUser selectUserByMobile(String mobile); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(IoTUserBO user); + + /** + * 扣除license额度 + * + * @param unionId 用户唯一标识 + */ + public void licenseBuckle(String unionId); + + /** + * 增加license额度 + * + * @param unionId 用户唯一标识 + */ + public void licenseAdd(String unionId); + + /** + * 增加license额度 + * + * @param unionId 用户唯一标识 + */ + public void licenseRecharge( + @Param("unionId") String unionId, @Param("licenseNumber") Integer licenseNumber); + + int doAccountDisable(); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/NetworkMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/NetworkMapper.java new file mode 100644 index 0000000..7a5d47b --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/NetworkMapper.java @@ -0,0 +1,144 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.Network; +import cn.universal.persistence.entity.bo.NetworkBO; +import cn.universal.persistence.entity.vo.NetworkVO; +import cn.universal.persistence.query.NetworkQuery; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +/** + * 网络组件Mapper接口 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +public interface NetworkMapper extends BaseMapper { + + int selectNetWorkByPort(@Param("port") Integer port, @Param("productKey") String productKey); + + /** + * 根据端口查询TCP网络组件(排除指定ID) + * + * @param port 端口 + * @param excludeId 排除的网络组件ID + * @return 网络组件列表 + */ + List selectTcpNetworkByPort( + @Param("port") Integer port, @Param("excludeId") Integer excludeId); + + /** + * 根据主机和用户名查询MQTT网络组件(排除指定ID) + * + * @param host 主机地址 + * @param username 用户名 + * @param excludeId 排除的网络组件ID + * @return 网络组件列表 + */ + List selectMqttNetworkByHostAndUsername( + @Param("host") String host, + @Param("username") String username, + @Param("excludeId") Integer excludeId); + + List selectNetworkListV1(@Param("bo") NetworkBO bo); + + NetworkVO selectById(@Param("id") Long id); + + /** + * 查询网络组件列表 + * + * @param query 查询条件 + * @return 网络组件列表 + */ + List selectNetworkList(NetworkQuery query); + + /** + * 查询网络组件总数 + * + * @param query 查询条件 + * @return 总数 + */ + int selectNetworkCount(NetworkQuery query); + + /** + * 根据ID查询网络组件 + * + * @param id 网络组件ID + * @return 网络组件 + */ + Network selectNetworkById(@Param("id") Integer id); + + /** + * 根据类型和唯一标识查询网络组件 + * + * @param type 类型 + * @param unionId 唯一标识 + * @return 网络组件 + */ + Network selectNetworkByTypeAndUnionId( + @Param("type") String type, @Param("unionId") String unionId); + + /** + * 新增网络组件 + * + * @param network 网络组件 + * @return 影响行数 + */ + int insertNetwork(Network network); + + /** + * 修改网络组件 + * + * @param network 网络组件 + * @return 影响行数 + */ + int updateNetwork(Network network); + + /** + * 删除网络组件 + * + * @param id 网络组件ID + * @return 影响行数 + */ + int deleteNetworkById(@Param("id") Integer id); + + /** + * 批量删除网络组件 + * + * @param ids 网络组件ID数组 + * @return 影响行数 + */ + int deleteNetworkByIds(@Param("ids") Integer[] ids); + + /** + * 更新网络组件状态 + * + * @param id 网络组件ID + * @param state 状态 + * @return 影响行数 + */ + int updateNetworkState(@Param("id") Integer id, @Param("state") Boolean state); + + /** + * 根据端口查询TCP网络组件数量(排除指定ID) + * + * @param port 端口 + * @param excludeId 排除的网络组件ID + * @return 数量 + */ + int selectTcpNetworkByPortCount( + @Param("port") Integer port, @Param("excludeId") Integer excludeId); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/OauthClientDetailsMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/OauthClientDetailsMapper.java new file mode 100644 index 0000000..ed2a8f3 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/OauthClientDetailsMapper.java @@ -0,0 +1,28 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.OAuth2ClientDetails; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface OauthClientDetailsMapper extends BaseMapper { + + @Select("SELECT * FROM oauth_client_details WHERE client_id = #{clientId}") + OAuth2ClientDetails findByClientId(@Param("clientId") String clientId); + + int deleteByClientIds(String[] ids); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/RuleModelInstanceMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/RuleModelInstanceMapper.java new file mode 100644 index 0000000..8746d36 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/RuleModelInstanceMapper.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.RuleModelInstance; + +/** + * 规则实例Mapper @Author Aleo + * + * @since 2023/1/13 14:31 + */ +public interface RuleModelInstanceMapper extends BaseMapper {} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/RuleModelMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/RuleModelMapper.java new file mode 100644 index 0000000..ede34ed --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/RuleModelMapper.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.RuleModel; +import cn.universal.persistence.entity.bo.RuleModelBO; +import cn.universal.persistence.entity.vo.RuleModelVO; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +/** + * 规则模型Mapper @Author Aleo + * + * @since 2023/1/13 14:31 + */ +public interface RuleModelMapper extends BaseMapper { + + /** + * 查询设备相关的规则模型 + * + * @param ruleModelBo + * @return + */ + List selectRuleByBo(@Param("bo") RuleModelBO ruleModelBo); + + /** + * 查询规则模型 + * + * @param ruleModelBo + * @return + */ + List selectRuleListByBo(@Param("bo") RuleModelBO ruleModelBo); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/RulegoChainLogMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/RulegoChainLogMapper.java new file mode 100644 index 0000000..3bb778c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/RulegoChainLogMapper.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.entity.RulegoChainLog; +import java.util.List; +import org.apache.ibatis.annotations.Param; +import tk.mybatis.mapper.common.Mapper; + +/** + * rulego规则链执行日志Mapper接口 + * + * @author Aleo + * @since 2025/01/15 + */ +public interface RulegoChainLogMapper extends Mapper { + + /** + * 根据rulegoId查询执行日志 + * + * @param rulegoId rulego规则链ID + * @param limit 限制条数 + * @return 执行日志列表 + */ + List selectByRulegoId(@Param("rulegoId") String rulegoId, @Param("limit") Integer limit); + + /** + * 根据执行ID查询日志 + * + * @param executionId 执行ID + * @return 执行日志 + */ + RulegoChainLog selectByExecutionId(@Param("executionId") String executionId); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/RulegoChainMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/RulegoChainMapper.java new file mode 100644 index 0000000..794e87c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/RulegoChainMapper.java @@ -0,0 +1,62 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.entity.RulegoChain; +import cn.universal.persistence.entity.bo.RulegoChainBO; +import cn.universal.persistence.entity.vo.RulegoChainVO; +import java.util.List; +import org.apache.ibatis.annotations.Param; +import tk.mybatis.mapper.common.Mapper; + +/** + * rulego规则链Mapper接口 + * + * @author Aleo + * @since 2025/01/15 + */ +public interface RulegoChainMapper extends Mapper { + + /** + * 根据条件查询规则链列表 + * + * @param bo 查询条件 + * @return 规则链列表 + */ + List selectRulegoChainListByBo(@Param("bo") RulegoChainBO bo); + + /** + * 根据rulegoId查询规则链 + * + * @param rulegoId rulego规则链ID + * @return 规则链信息 + */ + RulegoChain selectByRulegoId(@Param("rulegoId") String rulegoId); + + /** + * 根据创建人查询规则链列表 + * + * @param creatorId 创建人ID + * @return 规则链列表 + */ + List selectByCreatorId(@Param("creatorId") String creatorId); + + /** + * 更新DSL内容 + * + * @param rulegoId rulego规则链ID + * @param dslContent DSL内容 + * @return 更新行数 + */ + int updateDslContent(@Param("rulegoId") String rulegoId, @Param("dslContent") String dslContent); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/SceneLinkageMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/SceneLinkageMapper.java new file mode 100644 index 0000000..a9939b5 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/SceneLinkageMapper.java @@ -0,0 +1,80 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.SceneLinkage; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +/** + * 场景联动Mapper接口 @Author Aleo + * + * @since 2023-03-01 + */ +public interface SceneLinkageMapper extends BaseMapper { + + List selectTriggerByDevId(String deviceId); + + List selectTriggerByType(String deviceId); + + /** + * 查询场景联动 + * + * @param id 场景联动ID + * @return 场景联动 + */ + SceneLinkage selectSceneLinkageById(Long id); + + int checkSelf(@Param("id") Long id, @Param("unionId") String unionId); + + /** + * 查询场景联动列表 + * + * @param sceneLinkage 场景联动 + * @return 场景联动集合 + */ + List selectSceneLinkageList(SceneLinkage sceneLinkage); + + /** + * 新增场景联动 + * + * @param sceneLinkage 场景联动 + * @return 结果 + */ + int insertSceneLinkage(SceneLinkage sceneLinkage); + + /** + * 修改场景联动 + * + * @param sceneLinkage 场景联动 + * @return 结果 + */ + int updateSceneLinkage(SceneLinkage sceneLinkage); + + /** + * 删除场景联动 + * + * @param id 场景联动ID + * @return 结果 + */ + int deleteSceneLinkageById(Long id); + + /** + * 批量删除场景联动 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + int deleteSceneLinkageByIds(Long[] ids); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/SupportMapAreasMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/SupportMapAreasMapper.java new file mode 100644 index 0000000..fbea737 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/SupportMapAreasMapper.java @@ -0,0 +1,29 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.SupportMapAreas; +import org.apache.ibatis.annotations.Param; + +public interface SupportMapAreasMapper extends BaseMapper { + + /** + * 根据经纬度查询区域id + * + * @param lon 经度 + * @param lat 维度 + * @return + */ + SupportMapAreas selectMapAreas(@Param("lon") String lon, @Param("lat") String lat); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/SysLogininforMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/SysLogininforMapper.java new file mode 100644 index 0000000..0e4584a --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/SysLogininforMapper.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.SysLogininfor; +import java.util.List; + +/** 系统访问日志情况信息 数据层 @Author ruoyi */ +public interface SysLogininforMapper extends BaseMapper { + + List selectList(SysLogininfor sysLogininfor); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/SysOperLogMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/SysOperLogMapper.java new file mode 100644 index 0000000..29366a4 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/SysOperLogMapper.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.SysOperLog; +import java.util.List; + +/** 系统访问日志情况信息 数据层 @Author ruoyi */ +public interface SysOperLogMapper extends BaseMapper { + + List selectList(SysOperLog sysOperLog); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysConfigMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysConfigMapper.java new file mode 100644 index 0000000..f1a214d --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysConfigMapper.java @@ -0,0 +1,76 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper.admin; + +import cn.universal.persistence.entity.admin.SysConfig; +import java.util.List; + +/** 参数配置 数据层 @Author ruoyi */ +public interface SysConfigMapper { + + /** + * 查询参数配置信息 + * + * @param config 参数配置信息 + * @return 参数配置信息 + */ + public SysConfig selectConfig(SysConfig config); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数配置信息 + */ + public SysConfig checkConfigKeyUnique(String configKey); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 删除参数配置 + * + * @param configId 参数ID + * @return 结果 + */ + public int deleteConfigById(Long configId); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + * @return 结果 + */ + public int deleteConfigByIds(Long[] configIds); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysDictDataMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysDictDataMapper.java new file mode 100644 index 0000000..4abbec1 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysDictDataMapper.java @@ -0,0 +1,27 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper.admin; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.admin.SysDictData; +import cn.universal.persistence.entity.admin.vo.SysDictDataVo; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +/** 字典表 数据层 @Author ruoyi */ +public interface SysDictDataMapper extends BaseMapper { + + List selectDictData(SysDictDataVo sysDictData); + + List selectDictDataByType(@Param("dictType") String dictType); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysDictTypeMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysDictTypeMapper.java new file mode 100644 index 0000000..51d5714 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysDictTypeMapper.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper.admin; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.admin.SysDictType; +import java.util.List; + +/** 字典表 数据层 @Author ruoyi */ +public interface SysDictTypeMapper extends BaseMapper { + + List selectDictTypeList(SysDictType sysDictType); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysMenuMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysMenuMapper.java new file mode 100644 index 0000000..f95a64f --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysMenuMapper.java @@ -0,0 +1,82 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper.admin; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.admin.SysMenu; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +/** 菜单表 数据层 @Author ruoyi */ +// @Mapper +public interface SysMenuMapper extends BaseMapper { + + /** + * 根据用户所有权限 + * + * @return 权限列表 + */ + public List selectMenuPerms(); + + /** 按条件查询系统菜单列表 */ + public List selectMenuList(SysMenu menu); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuListByUserId(SysMenu menu); + + /** + * 根据用户ID查询权限 + * + * @param unionId 用户unionId + * @return 权限列表 + */ + public List selectMenuPermsByUserId(String unionId); + + /** + * 根据用户ID查询菜单 + * + * @return 菜单列表 + */ + public List selectMenuTreeAll(); + + /** + * 根据用户ID查询菜单 + * + * @param unionId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUnionId(@Param("unionId") String unionId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @param menuCheckStrictly 菜单树选择项是否关联显示 + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId( + @Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly); + + /** + * 执行菜单脚本 + * + * @param sql 菜单脚本 + * @return 返回null + */ + Integer execMenuSql(@Param("sql") String sql); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysRoleMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysRoleMapper.java new file mode 100644 index 0000000..75d0612 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysRoleMapper.java @@ -0,0 +1,50 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper.admin; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.admin.SysRole; +import com.github.pagehelper.Page; +import io.lettuce.core.dynamic.annotation.Param; +import java.util.List; + +/** 角色表 数据层 @Author ruoyi */ +// @Mapper +public interface SysRoleMapper extends BaseMapper { + + Page selectPageRoleList(@Param("page") Page page, @Param("role") SysRole role); + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色 + * + * @param unionId 用户unionId + * @return 角色列表 + */ + public List selectRolePermissionByUnionId(String unionId); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param unionId 用户unionId + * @return 选中角色ID列表 + */ + public List selectRoleListByUnionId(String unionId); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysRoleMenuMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysRoleMenuMapper.java new file mode 100644 index 0000000..f8f2b55 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysRoleMenuMapper.java @@ -0,0 +1,20 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper.admin; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.admin.SysRoleMenu; + +/** 角色与菜单关联表 数据层 @Author ruoyi */ +// @Mapper +public interface SysRoleMenuMapper extends BaseMapper {} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysUserRoleMapper.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysUserRoleMapper.java new file mode 100644 index 0000000..7a17015 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/mapper/admin/SysUserRoleMapper.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.mapper.admin; + +import cn.universal.persistence.common.BaseMapper; +import cn.universal.persistence.entity.admin.SysUserRole; + +/** 用户与角色关联表 数据层 @Author ruoyi */ +// @Mapper +public interface SysUserRoleMapper extends BaseMapper { + + public int deleteUserRoleByUserIds(String[] ids); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/message/AbstractIoTMessageAdapter.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/message/AbstractIoTMessageAdapter.java new file mode 100644 index 0000000..d5a62fb --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/message/AbstractIoTMessageAdapter.java @@ -0,0 +1,14 @@ +package cn.universal.persistence.message; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** 标准消息格式适配器 */ +@Component +@Slf4j +public abstract class AbstractIoTMessageAdapter implements IoTMessageAdapter { + + boolean support(String product, String userId, String appUnionId) { + return false; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/message/AliyunIoTMessageAdapter.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/message/AliyunIoTMessageAdapter.java new file mode 100644 index 0000000..6a8eebf --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/message/AliyunIoTMessageAdapter.java @@ -0,0 +1,82 @@ +package cn.universal.persistence.message; + +import cn.hutool.json.JSONUtil; +import cn.universal.persistence.base.BaseDownRequest; +import cn.universal.persistence.base.BaseUPRequest; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** 阿里云IoT平台消息格式适配器 */ +@Component +@Slf4j +public class AliyunIoTMessageAdapter extends AbstractIoTMessageAdapter { + + @Override + public String name() { + return "aliyun"; + } + + @Override + public String formatUpMessage(BaseUPRequest upRequest) { + try { + Map result = new HashMap<>(); + + // 基础字段映射 + result.put("iotId", upRequest.getDeviceId()); + result.put("requestId", upRequest.getRequestId()); + result.put("productKey", upRequest.getProductKey()); + result.put("deviceName", upRequest.getDeviceName()); + result.put("gmtCreate", upRequest.getTime()); + result.put("deviceType", upRequest.getDeviceNode()); + // 处理物模型数据 - 阿里云格式 + Map items = convertProperties(upRequest.getProperties()); + result.put("items", items); + + return JSONUtil.toJsonStr(result); + } catch (Exception e) { + log.error("阿里云上行消息转换失败: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public String formatDownMessage(BaseDownRequest downRequest) { + try { + Map result = new HashMap<>(); + + result.put("id", downRequest.getMsgId()); + result.put("version", "1.0"); + result.put("method", downRequest.getCmd().name()); + result.put("params", downRequest.getFunction()); + + return JSONUtil.toJsonStr(result); + } catch (Exception e) { + log.error("阿里云下行消息转换失败: {}", e.getMessage(), e); + throw e; + } + } + + /** 将属性数据转换为阿里云格式 阿里云格式:{"items":{"propertyId":{"value":"xxx","time":timestamp}}} */ + private Map convertProperties(Map properties) { + Map items = new HashMap<>(); + + if (properties == null || properties.isEmpty()) { + return items; + } + + for (Map.Entry entry : properties.entrySet()) { + String propertyId = entry.getKey(); + Object value = entry.getValue(); + + Map item = new HashMap<>(); + item.put("value", value); + item.put("time", System.currentTimeMillis()); + + items.put(propertyId, item); + } + + return items; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/message/CustomMessageAdapter.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/message/CustomMessageAdapter.java new file mode 100644 index 0000000..4b77e88 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/message/CustomMessageAdapter.java @@ -0,0 +1,26 @@ +package cn.universal.persistence.message; + +import cn.universal.persistence.base.BaseDownRequest; +import cn.universal.persistence.base.BaseUPRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service("CustomMessageAdapter") +public class CustomMessageAdapter extends AbstractIoTMessageAdapter { + + @Override + public String name() { + return "tencent"; + } + + @Override + public String formatUpMessage(BaseUPRequest upRequest) { + return null; + } + + @Override + public String formatDownMessage(BaseDownRequest downRequest) { + return null; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/message/IoTMessageAdapter.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/message/IoTMessageAdapter.java new file mode 100644 index 0000000..8c33bde --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/message/IoTMessageAdapter.java @@ -0,0 +1,34 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ +package cn.universal.persistence.message; + +import cn.universal.persistence.base.BaseDownRequest; +import cn.universal.persistence.base.BaseUPRequest; + +/** + * 消息适配器名称,用于有特定需求的情况 + * + *

用处不大 + */ +public interface IoTMessageAdapter { + + /** + * @return 适配器名称 + */ + String name(); + + /** 格式化上行消息 */ + String formatUpMessage(BaseUPRequest upRequest); + + /** 格式化下行消息 */ + String formatDownMessage(BaseDownRequest downRequest); +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/message/IoTMessageConvertHolder.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/message/IoTMessageConvertHolder.java new file mode 100644 index 0000000..34d0ed7 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/message/IoTMessageConvertHolder.java @@ -0,0 +1,36 @@ +package cn.universal.persistence.message; + +import cn.universal.persistence.base.BaseDownRequest; +import cn.universal.persistence.base.BaseUPRequest; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class IoTMessageConvertHolder implements ApplicationContextAware { + + private Map adapterMap = new HashMap<>(); + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + Map iotMessageAdapterMap = + applicationContext.getBeansOfType(IoTMessageAdapter.class); + log.info("init IoTMessageAdapter third ,[{}]", iotMessageAdapterMap); + adapterMap.forEach((key, value) -> adapterMap.put(value.name(), value)); + } + + /** 格式化上行消息 */ + public String formatUpMessage(BaseUPRequest upRequest) { + return null; + } + + /** 格式化下行消息 */ + public String formatDownMessage(BaseDownRequest downRequest) { + return null; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/message/TencentIoTMessageAdapter.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/message/TencentIoTMessageAdapter.java new file mode 100644 index 0000000..4ef3bad --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/message/TencentIoTMessageAdapter.java @@ -0,0 +1,61 @@ +package cn.universal.persistence.message; + +import cn.hutool.json.JSONUtil; +import cn.universal.persistence.base.BaseDownRequest; +import cn.universal.persistence.base.BaseUPRequest; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service("tencentMessageAdapt") +public class TencentIoTMessageAdapter extends AbstractIoTMessageAdapter { + + @Override + public String name() { + return "tencent"; + } + + @Override + public String formatUpMessage(BaseUPRequest upRequest) { + try { + Map result = new HashMap<>(); + + result.put("msgType", upRequest.getMessageType().name()); + result.put("device_id", upRequest.getDeviceId()); + result.put("product_key", upRequest.getProductKey()); + result.put("device_name", upRequest.getDeviceName()); + result.put("timestamp", upRequest.getTime()); + result.put("event", upRequest.getEvent()); + result.put("eventName", upRequest.getEventName()); + result.put("data", upRequest.getData()); + result.put("properties", upRequest.getProperties()); + result.put("payload", upRequest.getPayload()); + + return JSONUtil.toJsonStr(result); + } catch (Exception e) { + log.error("标准上行消息转换失败: {}", e.getMessage(), e); + throw e; + } + } + + @Override + public String formatDownMessage(BaseDownRequest downRequest) { + try { + Map result = new HashMap<>(); + + result.put("cmd_id", downRequest.getMsgId()); + result.put("cmd_type", downRequest.getCmd().name()); + result.put("device_id", downRequest.getDeviceId()); + result.put("product_key", downRequest.getProductKey()); + result.put("function", downRequest.getFunction()); + result.put("detail", downRequest.getDetail()); + + return JSONUtil.toJsonStr(result); + } catch (Exception e) { + log.error("标准下行消息转换失败: {}", e.getMessage(), e); + throw e; + } + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/page/PageDomain.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/page/PageDomain.java new file mode 100644 index 0000000..a81cf36 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/page/PageDomain.java @@ -0,0 +1,92 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.page; + +import cn.universal.common.utils.StringUtils; + +/** 分页数据 @Author ruoyi */ +public class PageDomain { + + /** 当前记录起始索引 */ + private Integer pageNum; + + /** 每页显示记录数 */ + private Integer pageSize; + + /** 排序列 */ + private String orderByColumn; + + /** 排序的方向desc或者asc */ + private String isAsc = "asc"; + + /** 分页参数合理化 */ + private Boolean reasonable = true; + + public String getOrderBy() { + if (StringUtils.isEmpty(orderByColumn)) { + return ""; + } + return StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc; + } + + public Integer getPageNum() { + return pageNum; + } + + public void setPageNum(Integer pageNum) { + this.pageNum = pageNum; + } + + public Integer getPageSize() { + return pageSize; + } + + public void setPageSize(Integer pageSize) { + this.pageSize = pageSize; + } + + public String getOrderByColumn() { + return orderByColumn; + } + + public void setOrderByColumn(String orderByColumn) { + this.orderByColumn = orderByColumn; + } + + public String getIsAsc() { + return isAsc; + } + + public void setIsAsc(String isAsc) { + if (StringUtils.isNotEmpty(isAsc)) { + // 兼容前端排序类型 + if ("ascending".equals(isAsc)) { + isAsc = "asc"; + } else if ("descending".equals(isAsc)) { + isAsc = "desc"; + } + this.isAsc = isAsc; + } + } + + public Boolean getReasonable() { + if (StringUtils.isNull(reasonable)) { + return Boolean.TRUE; + } + return reasonable; + } + + public void setReasonable(Boolean reasonable) { + this.reasonable = reasonable; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/page/TableDataInfo.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/page/TableDataInfo.java new file mode 100644 index 0000000..9b75263 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/page/TableDataInfo.java @@ -0,0 +1,56 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.page; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.util.List; +import lombok.Data; + +/** 表格分页数据对象 @Author ruoyi */ +@Schema(description = "分页响应对象") +@Data +public class TableDataInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 总记录数 */ + @Schema(description = "总记录数") + private int total; + + /** 列表数据 */ + @Schema(description = "列表数据") + private List rows; + + /** 消息状态码 */ + @Schema(description = "消息状态码") + private int code; + + /** 消息内容 */ + @Schema(description = "消息内容") + private String msg; + + /** 表格数据对象 */ + public TableDataInfo() {} + + /** + * 分页 + * + * @param list 列表数据 + * @param total 总记录数 + */ + public TableDataInfo(List list, int total) { + this.rows = list; + this.total = total; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/page/TableSupport.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/page/TableSupport.java new file mode 100644 index 0000000..0024d12 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/page/TableSupport.java @@ -0,0 +1,49 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.page; + +import cn.universal.common.utils.ServletUtils; + +/** 表格数据处理 @Author ruoyi */ +public class TableSupport { + + /** 当前记录起始索引 */ + public static final String PAGE_NUM = "pageNum"; + + /** 每页显示记录数 */ + public static final String PAGE_SIZE = "pageSize"; + + /** 排序列 */ + public static final String ORDER_BY_COLUMN = "orderByColumn"; + + /** 排序的方向 "desc" 或者 "asc". */ + public static final String IS_ASC = "isAsc"; + + /** 分页参数合理化 */ + public static final String REASONABLE = "reasonable"; + + /** 封装分页对象 */ + public static PageDomain getPageDomain() { + PageDomain pageDomain = new PageDomain(); + pageDomain.setPageNum(ServletUtils.getParameterToInt(PAGE_NUM)); + pageDomain.setPageSize(ServletUtils.getParameterToInt(PAGE_SIZE)); + pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN)); + pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC)); + pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE)); + return pageDomain; + } + + public static PageDomain buildPageRequest() { + return getPageDomain(); + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/query/AjaxResult.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/AjaxResult.java new file mode 100644 index 0000000..295e658 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/AjaxResult.java @@ -0,0 +1,95 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author Aleo + * + * @since 2018年12月17日 上午11:38 + */ +@Data +@Schema(description = "返回统一对象") +public class AjaxResult { + + private static final long serialVersionUID = 1L; + + @Schema(description = "错误编码 正常返回0") + private Integer code; + + @Schema(description = "处理结果信息") + private String msg; + + @Schema(description = "返回结果") + private T data; + + public static final Integer SUCCESS = 0; + public static final Integer ERROR = 500; + public static final String ERROR_MSG = "error"; + public static final String SUCCESS_MSG = "success"; + + private AjaxResult(int code, String msg) { + this.code = code; + this.msg = msg; + } + + private AjaxResult(int code, String msg, T data) { + this.code = code; + this.msg = msg; + this.data = data; + } + + public static AjaxResult success() { + return success(SUCCESS_MSG); + } + + public static AjaxResult success(T data) { + return success(SUCCESS_MSG, data); + } + + public static AjaxResult success(String msg) { + return new AjaxResult<>(SUCCESS, msg, null); + } + + public static AjaxResult success(String msg, T data) { + return new AjaxResult<>(SUCCESS, msg, data); + } + + public AjaxResult() {} + + public static AjaxResult error() { + return error(ERROR_MSG); + } + + public static AjaxResult error(String msg) { + return new AjaxResult<>(ERROR, msg, null); + } + + public static AjaxResult error(String msg, T data) { + return new AjaxResult<>(ERROR, msg, data); + } + + public static AjaxResult error(int code, String msg) { + return new AjaxResult<>(code, msg, null); + } + + public static AjaxResult toAjax(int rows) { + if (rows > 0) { + return success(); + } else { + return error(); + } + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/query/BasePage.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/BasePage.java new file mode 100644 index 0000000..d83b683 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/BasePage.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.query; + +import lombok.Data; + +/** + * @Author Aleo + * + * @since 2018年12月17日 上午10:59 + */ +@Data +public class BasePage { + + private Integer page = 1; + private Integer size = 10; + private Integer pageNum = 1; + private Integer pageSize = 10; + // 特色场景使用 + private Integer halfSize; + + public Integer getHalfSize() { + return pageSize / 2; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/query/DataTransferObject.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/DataTransferObject.java new file mode 100644 index 0000000..d34e017 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/DataTransferObject.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.query; + +import java.io.Serializable; + +/** + * @Author Aleo common of all DTOs + */ +public class DataTransferObject implements Serializable { + + private static final long serialVersionUID = 1L; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/query/IoTAPIQuery.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/IoTAPIQuery.java new file mode 100644 index 0000000..338cc8e --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/IoTAPIQuery.java @@ -0,0 +1,68 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.query; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * @version 1.0 @Author Aleo + * @since 2025/11/15 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = false) +public class IoTAPIQuery extends BasePage { + + private String applicationId; + + private String application; + + private String iotUnionId; + + private String iotId; + + private String productName; + + private String productKey; + + private String deviceName; + + private String deviceId; + + /** 传输协议 */ + private String transportProtocol; + + private String deviceNode; + + private String companyNo; + + /** 产品型号;例如 WS101-470M */ + private String productId; + + /** 经度 */ + private String longitude; + + /** 维度 */ + private String latitude; + + private String gwProductKey; + + /** 备注说明 */ + private String detail; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/query/IoTDeviceQuery.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/IoTDeviceQuery.java new file mode 100644 index 0000000..1af1f4a --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/IoTDeviceQuery.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.query; + +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @version 1.0 @Author Aleo + * @since 2023/1/20 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class IoTDeviceQuery { + + private String iotId; + private String extDeviceId; + private String deviceId; + private String gwProductKey; + private String productKey; + private String thirdPlatform; + + public boolean emptyParams() { + return StrUtil.isBlank(getIotId()) + && StrUtil.isBlank(getExtDeviceId()) + && StrUtil.isBlank(getDeviceId()) + && StrUtil.isBlank(getGwProductKey()) + && StrUtil.isBlank(productKey) + && StrUtil.isBlank(thirdPlatform); + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/query/IoTProductQuery.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/IoTProductQuery.java new file mode 100644 index 0000000..3ee8e10 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/IoTProductQuery.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.query; + +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * @version 1.0 @Author Aleo + * @since 2025/11/15 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class IoTProductQuery extends BasePage { + + private Long id; + private String productId; + private String productKey; + private String networkUnionId; + private String classifiedName; + private String companyNo; + private String messageProtocol; + private String describe; + private String storePolicy; + private String gwProductKey; + private String thirdPlatform; + private String deviceNode; + private String creatorId; + private String name; + private String transportProtocol; + private Integer state; + private String createTime; + private String classifiedId; + private String tags; + + public String[] getTags() { + if (StrUtil.isNotBlank(tags)) { + return tags.split(","); + } + return null; + } + + /** 存在设备 */ + private boolean hasDevice; + + /** 是否自有 */ + private boolean self; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/query/LogQuery.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/LogQuery.java new file mode 100644 index 0000000..d70e0ad --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/LogQuery.java @@ -0,0 +1,66 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.query; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * @version 1.0 @Author Aleo + * @since 2025/11/15 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Builder +public class LogQuery extends BasePage { + + /** 日志ID,非自增 */ + private String id; + + /** 设备编码 */ + private String iotId; + + /** 设备序列号 */ + private String deviceId; + + /** 第三方设备ID唯一标识符 */ + private String extDeviceId; + + /** 产品ID */ + private String productKey; + + /** 设备名称 */ + private String deviceName; + + /** 消息类型 */ + private String messageType; + + /** 事件名称 */ + private String event; + + /** 请求参数 */ + private Map params; + + private Long endCreateTime; + + private Long beginCreateTime; + + /** 属性 */ + private String property; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/query/NetworkQuery.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/NetworkQuery.java new file mode 100644 index 0000000..5e73b9c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/NetworkQuery.java @@ -0,0 +1,49 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.query; + +import java.util.List; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 网络组件查询对象 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NetworkQuery extends BasePage { + + /** 网络类型(单个类型) */ + private String type; + + /** 网络类型列表(多个类型) */ + private List types; + + /** 网络组件名称 */ + private String name; + + /** 产品Key */ + private String productKey; + + /** 状态 */ + private Boolean state; + + /** 唯一标识 */ + private String unionId; + + /** 创建用户 */ + private String createUser; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/query/PageBean.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/PageBean.java new file mode 100644 index 0000000..e6b7a88 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/PageBean.java @@ -0,0 +1,89 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.query; + +import java.util.List; + +/** 分页工具类 @Title: @Description: @Author:刘利海 @Since:2018年9月26日 */ +public class PageBean extends DataTransferObject { + + private static final long serialVersionUID = 1L; + // 总记录数 + private Long totalCount; + // 每页记录数 + private int pageSize; + // 总页数 + private int totalPage; + // 当前页数 + private int currPage; + // 列表数据 + private List list; + + public PageBean() {} + + /** + * 分页 + * + * @param list 列表数据 + * @param totalCount 总记录数 + * @param pageSize 每页记录数 + * @param currPage 当前页数 + */ + public PageBean(List list, Long totalCount, int pageSize, int currPage) { + this.list = list; + this.totalCount = totalCount; + this.pageSize = pageSize; + this.currPage = currPage; + this.totalPage = (int) Math.ceil((double) totalCount / pageSize); + } + + public Long getTotalCount() { + return totalCount; + } + + public void setTotalCount(Long totalCount) { + this.totalCount = totalCount; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public int getTotalPage() { + return totalPage; + } + + public void setTotalPage(int totalPage) { + this.totalPage = totalPage; + } + + public int getCurrPage() { + return currPage; + } + + public void setCurrPage(int currPage) { + this.currPage = currPage; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/query/PageRet.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/PageRet.java new file mode 100644 index 0000000..5a102fe --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/query/PageRet.java @@ -0,0 +1,131 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.query; + +import cn.universal.common.constant.IoTConstant; +import com.github.pagehelper.Page; +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.util.List; +import lombok.Data; +import org.slf4j.MDC; + +/** + * @Author Aleo + * + * @since 2018年12月17日 上午11:38 + */ +@Data +@Schema(description = "分页统一对象") +public class PageRet implements Serializable { + + @Schema(description = "错误编码 正常返回0") + private Integer code; + + @Schema(description = "处理结果信息") + private String msg; + + @Schema(description = "返回结果") + private List data; + + @Schema(description = "页码,从1开始") + private int page; + + @Schema(description = "页面大小") + private int size; + + @Schema(description = "总数") + private long total; + + @Schema(description = "总页数") + private int pages; + + @Schema(description = "请求Id") + private String requestId; + + public static final Integer SUCCESS = 0; + public static final Integer ERROR = 500; + public static final String ERROR_MSG = "error"; + public static final String SUCCESS_MSG = "success"; + + private PageRet(Page page) { + this.code = SUCCESS; + this.msg = SUCCESS_MSG; + this.page = page.getPageNum(); + this.size = page.getPageSize(); + this.total = page.getTotal(); + this.pages = page.getPages(); + this.data = page.getResult(); + this.requestId = MDC.get(IoTConstant.TRACE_ID); + } + + private PageRet(List data, String msg, Integer code) { + this.code = code; + this.msg = msg; + this.data = data; + this.requestId = MDC.get(IoTConstant.TRACE_ID); + } + + public PageRet(List data, int page, int size, long total, int pages) { + this.code = SUCCESS; + this.msg = SUCCESS_MSG; + this.data = data; + this.page = page; + this.size = size; + this.total = total; + this.pages = pages; + this.requestId = MDC.get(IoTConstant.TRACE_ID); + } + + public static PageRet ok(Page page) { + return new PageRet<>(page); + } + + public static PageRet ok(PageBean page) { + return new PageRet<>( + page.getList(), + page.getCurrPage(), + page.getPageSize(), + page.getTotalCount(), + page.getTotalPage()); + } + + public PageRet(PageBean page) { + this.code = SUCCESS; + this.msg = SUCCESS_MSG; + this.page = page.getCurrPage(); + this.size = page.getPageSize(); + this.total = page.getTotalCount(); + this.pages = page.getTotalPage(); + this.data = page.getList(); + this.requestId = MDC.get(IoTConstant.TRACE_ID); + } + + // public static PageRet error(List data){ + // return new PageRet<>(data,ERROR_MSG,ERROR); + // } + // public static PageRet error(){ + // return new PageRet<>(null,ERROR_MSG,ERROR); + // } + // public static PageRet success(List data){ + // return new PageRet<>(data,SUCCESS_MSG,SUCCESS); + // } + // public static PageRet success(){ + // return new PageRet<>(null,SUCCESS_MSG,SUCCESS); + // } + // public static PageRet toAjax(int rows){ + // if(rows>0)return success(); + // else return error(); + // } + +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/serializer/SensitiveFieldSerializer.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/serializer/SensitiveFieldSerializer.java new file mode 100644 index 0000000..776715c --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/serializer/SensitiveFieldSerializer.java @@ -0,0 +1,84 @@ +package cn.universal.persistence.serializer; + +import cn.universal.persistence.annotation.SensitiveField; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import java.io.IOException; + +/** 敏感字段序列化器 自动处理敏感字段的显示 */ +public class SensitiveFieldSerializer extends JsonSerializer + implements ContextualSerializer { + + private SensitiveField sensitiveField; + + public SensitiveFieldSerializer() {} + + public SensitiveFieldSerializer(SensitiveField sensitiveField) { + this.sensitiveField = sensitiveField; + } + + @Override + public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + if (value == null) { + gen.writeNull(); + return; + } + + String maskedValue = maskValue(value); + gen.writeString(maskedValue); + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) + throws JsonMappingException { + if (property != null) { + SensitiveField annotation = property.getAnnotation(SensitiveField.class); + if (annotation != null) { + return new SensitiveFieldSerializer(annotation); + } + } + return this; + } + + /** 脱敏处理 */ + private String maskValue(String value) { + if (value == null || value.isEmpty()) { + return value; + } + + if (sensitiveField.hide()) { + return "***"; + } + + int prefixLen = sensitiveField.showPrefix(); + int suffixLen = sensitiveField.showSuffix(); + + if (prefixLen == 0 && suffixLen == 0) { + // 默认显示前3位和后3位 + if (value.length() <= 6) { + return "***"; + } + return value.substring(0, 3) + "***" + value.substring(value.length() - 3); + } + + if (value.length() <= prefixLen + suffixLen) { + return "***"; + } + + StringBuilder masked = new StringBuilder(); + if (prefixLen > 0) { + masked.append(value.substring(0, prefixLen)); + } + masked.append("***"); + if (suffixLen > 0) { + masked.append(value.substring(value.length() - suffixLen)); + } + + return masked.toString(); + } +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/shadow/Shadow.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/shadow/Shadow.java new file mode 100644 index 0000000..fced1a7 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/shadow/Shadow.java @@ -0,0 +1,40 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.shadow; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 设备影子 + * + * @version 1.0 @Author Aleo + * @since 2025/9/17 + */ +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +public class Shadow { + + /** 状态 */ + private State state; + + private State metadata; + + private Long timestamp; + + private Long version; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/shadow/State.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/shadow/State.java new file mode 100644 index 0000000..44324a4 --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/shadow/State.java @@ -0,0 +1,43 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.shadow; + +import cn.hutool.json.JSONObject; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** 设备影子 */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class State { + + /** + * 设备的预期状态。仅当设备影子文档具有预期状态时,才包含desired部分。 + * + *

应用程序向desired部分写入数据,更新事物的状态,而无需直接连接到该设备。 + */ + private JSONObject desired; + + /** + * 设备的报告状态。设备可以在reported部分写入数据,报告其最新状态。 + * + *

应用程序可以通过读取该参数值,获取设备的状态。 + * + *

JSON文档中也可以不包含reported部分,没有reported部分的文档同样为有效影子JSON文档。 + */ + private JSONObject reported; +} diff --git a/cn-universal-persistence/src/main/java/cn/universal/persistence/util/SnowflakeIdWorker.java b/cn-universal-persistence/src/main/java/cn/universal/persistence/util/SnowflakeIdWorker.java new file mode 100644 index 0000000..bc2645d --- /dev/null +++ b/cn-universal-persistence/src/main/java/cn/universal/persistence/util/SnowflakeIdWorker.java @@ -0,0 +1,167 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.persistence.util; + +/** + * Twitter_Snowflake SnowFlake的结构如下(每部分用-分开): 0 - 0000000000 0000000000 0000000000 0000000000 0 - + * 00000 - 00000 - 000000000000 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0 + * 41位时间戳(毫秒级),注意,41位时间戳不是存储当前时间的时间戳,而是存储时间戳的差值(当前时间戳 - 开始时间戳) + * 得到的值),这里的的开始时间戳,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下面程序SnowflakeIdWorker类的startTime属性)。41位的时间戳,可以使用69年,年T + * = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69 + * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId + * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间戳)产生4096个ID序号 加起来刚好64位,为一个Long型。 + */ +public class SnowflakeIdWorker { + + /** 开始时间戳 (2015-01-01) */ + private final long twepoch = 1420041600000L; + + /** 机器id所占的位数 */ + private final long workerIdBits = 5L; + + /** 数据标识id所占的位数 */ + private final long datacenterIdBits = 5L; + + /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */ + private final long maxWorkerId = -1L ^ (-1L << workerIdBits); + + /** 支持的最大数据标识id,结果是31 */ + private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); + + /** 序列在id中占的位数 */ + private final long sequenceBits = 12L; + + /** 机器ID向左移12位 */ + private final long workerIdShift = sequenceBits; + + /** 数据标识id向左移17位(12+5) */ + private final long datacenterIdShift = sequenceBits + workerIdBits; + + /** 时间戳向左移22位(5+5+12) */ + private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; + + /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */ + private final long sequenceMask = -1L ^ (-1L << sequenceBits); + + /** 工作机器ID(0~31) */ + private long workerId; + + /** 数据中心ID(0~31) */ + private long datacenterId; + + /** 毫秒内序列(0~4095) */ + private long sequence = 0L; + + /** 上次生成ID的时间戳 */ + private long lastTimestamp = -1L; + + // ==============================Constructors===================================== + + /** + * 构造函数 + * + * @param workerId 工作ID (0~31) + * @param datacenterId 数据中心ID (0~31) + */ + public SnowflakeIdWorker(long workerId, long datacenterId) { + if (workerId > maxWorkerId || workerId < 0) { + throw new IllegalArgumentException( + String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); + } + if (datacenterId > maxDatacenterId || datacenterId < 0) { + throw new IllegalArgumentException( + String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); + } + this.workerId = workerId; + this.datacenterId = datacenterId; + } + + // ==============================Methods========================================== + + /** + * 获得下一个ID (该方法是线程安全的) + * + * @return SnowflakeId + */ + public synchronized long nextId() { + long timestamp = timeGen(); + + // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 + if (timestamp < lastTimestamp) { + throw new RuntimeException( + String.format( + "Clock moved backwards. Refusing to generate id for %d milliseconds", + lastTimestamp - timestamp)); + } + + // 如果是同一时间生成的,则进行毫秒内序列 + if (lastTimestamp == timestamp) { + sequence = (sequence + 1) & sequenceMask; + // 毫秒内序列溢出 + if (sequence == 0) { + // 阻塞到下一个毫秒,获得新的时间戳 + timestamp = tilNextMillis(lastTimestamp); + } + } + // 时间戳改变,毫秒内序列重置 + else { + sequence = 0L; + } + + // 上次生成ID的时间戳 + lastTimestamp = timestamp; + + // 移位并通过或运算拼到一起组成64位的ID + return ((timestamp - twepoch) << timestampLeftShift) + | (datacenterId << datacenterIdShift) + | (workerId << workerIdShift) + | sequence; + } + + /** + * 阻塞到下一个毫秒,直到获得新的时间戳 + * + * @param lastTimestamp 上次生成ID的时间戳 + * @return 当前时间戳 + */ + protected long tilNextMillis(long lastTimestamp) { + long timestamp = timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = timeGen(); + } + return timestamp; + } + + /** + * 返回以毫秒为单位的当前时间 + * + * @return 当前时间(毫秒) + */ + protected long timeGen() { + return System.currentTimeMillis(); + } + + // /** + // * 测试 + // */ + // public static void main(String[] args) { + // System.out.println("开始:" + System.currentTimeMillis()); + // SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0); + // for (int i = 0; i < 50; i++) { + // long id = idWorker.nextId(); + // System.out.println(id); + // // System.out.println(Long.toBinaryString(id)); + // } + // System.out.println("结束:" + System.currentTimeMillis()); + // } +} diff --git a/cn-universal-persistence/src/main/resources/application.properties b/cn-universal-persistence/src/main/resources/application.properties new file mode 100644 index 0000000..e69de29 diff --git a/cn-universal-persistence/src/main/resources/generatorConfig.xml b/cn-universal-persistence/src/main/resources/generatorConfig.xml new file mode 100644 index 0000000..8948cde --- /dev/null +++ b/cn-universal-persistence/src/main/resources/generatorConfig.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
\ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTCertificateMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTCertificateMapper.xml new file mode 100644 index 0000000..88d509a --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTCertificateMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDashboardStatisticsMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDashboardStatisticsMapper.xml new file mode 100644 index 0000000..4ee3086 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDashboardStatisticsMapper.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + id + , stat_date, product_key, channel, metric_type, metric_value, create_time, update_time + + + + + + + + + + + + + + UPDATE iot_dashboard_statistics + SET metric_value = #{metricValue}, + update_time = #{updateTime} + WHERE stat_date = #{statDate} + AND (product_key = #{productKey} OR (product_key IS NULL AND #{productKey} IS NULL)) + AND (channel = #{channel} OR (channel IS NULL AND #{channel} IS NULL)) + AND metric_type = #{metricType} + + + + + INSERT INTO iot_dashboard_statistics + (stat_date, product_key, channel, metric_type, metric_value, create_time, update_time) + VALUES (#{statDate}, #{productKey}, #{channel}, #{metricType}, #{metricValue}, #{createTime}, + #{updateTime}) + + + + + DELETE + FROM iot_dashboard_statistics + WHERE stat_date = #{statDate} + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceFenceRelMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceFenceRelMapper.xml new file mode 100644 index 0000000..5c82437 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceFenceRelMapper.xml @@ -0,0 +1,26 @@ + + + + + + delete + from iot_device_fence_rel + where iot_id = #{iotId} + + + delete + from iot_device_fence_rel + where fence_id = #{fenceId} + and device_id = #{deviceId} + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceFunctionHistoryMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceFunctionHistoryMapper.xml new file mode 100644 index 0000000..1d34e7b --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceFunctionHistoryMapper.xml @@ -0,0 +1,25 @@ + + + + + insert into iot_device_function_history (id,product_key, iot_id, device_id, device_name, + down_state,down_result,down_error,task_id,update_time,retry,ext_param) + values + + (#{item.id},#{item.productKey},#{item.iotId},#{item.deviceId},#{item.deviceName},#{item.downState},#{item.downResult} + ,#{item.downError},#{item.taskId},#{item.updateTime},#{item.retry},#{item.extParam}) + + + + + + update iot_device_function_history + set down_state=0 + where task_id=#{bo.taskId} and + id in + + #{id} + + + diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceFunctionTaskMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceFunctionTaskMapper.xml new file mode 100644 index 0000000..feee96f --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceFunctionTaskMapper.xml @@ -0,0 +1,73 @@ + + + + + update iot_device_function_task + set `status`=0 + where id = #{id} + + + + + + + diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceGeoFenceMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceGeoFenceMapper.xml new file mode 100644 index 0000000..65bd003 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceGeoFenceMapper.xml @@ -0,0 +1,106 @@ + + + + + + + + + + update iot_device_geo_fence + + `name` = + #{ioTDeviceGeoFence.name}, + + status = #{ioTDeviceGeoFence.status}, + touch_way = + #{ioTDeviceGeoFence.touchWay}, + + fence = + #{ioTDeviceGeoFence.fence}, + + type = + #{ioTDeviceGeoFence.type}, + + point = + #{ioTDeviceGeoFence.point}, + + radius = + #{ioTDeviceGeoFence.radius}, + + update_date = + #{ioTDeviceGeoFence.updateDate}, + + week_time = + #{ioTDeviceGeoFence.weekTime}, + + + begin_time = + #{ioTDeviceGeoFence.beginTime}, + + end_time = + #{ioTDeviceGeoFence.endTime}, + + no_trigger_time = #{ioTDeviceGeoFence.noTriggerTime},delay_time = + #{ioTDeviceGeoFence.delayTime} + + where id=#{ioTDeviceGeoFence.id} + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceGroupMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceGroupMapper.xml new file mode 100644 index 0000000..67f6a0c --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceGroupMapper.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select id, + group_name, + group_code, + group_describe, + has_child, + group_level, + parent_id, + relat_dev_count, + active_dev_count, + creator_id, + instance, + tag + from iot_device_group + + + + + + + + + + + + insert into iot_device_group + + id, + group_name, + group_code, + group_describe, + parent_id, + has_child, + group_level, + relat_dev_count, + active_dev_count, + creator_id, + instance, + tag, + + + #{id}, + #{groupName}, + #{groupCode}, + #{groupDescribe}, + #{parentId}, + #{hasChild}, + #{groupLevel}, + #{relatDevCount}, + #{activeDevCount}, + #{creatorId}, + #{instance}, + #{tag}, + + + + + update iot_device_group + + group_name = #{groupName}, + group_code = #{groupCode}, + group_describe = #{groupDescribe}, + + parent_id = #{parentId}, + has_child = #{hasChild}, + group_level = #{groupLevel}, + relat_dev_count = #{relatDevCount}, + active_dev_count = #{activeDevCount}, + creator_id = #{creatorId}, + instance = #{instance}, + tag = #{tag}, + + where id = #{id} + + + + delete + from iot_device_group + where id = #{id} + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceLogMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceLogMapper.xml new file mode 100644 index 0000000..c1a0ada --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceLogMapper.xml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DELETE from + + + iot_device_log + + + iot_device_log_${tablesIndex} + + + + + + + + + DELETE from + + + iot_device_log + + + iot_device_log_${tablesIndex} + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceLogMetadataMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceLogMetadataMapper.xml new file mode 100644 index 0000000..2b6fe0a --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceLogMetadataMapper.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + DELETE + FROM iot_device_log_metadata + WHERE id < (SELECT id + FROM (SELECT id + FROM iot_device_log_metadata + WHERE iot_id = #{iotId} + AND property = #{property} + AND message_type = 'PROPERTIES' + ORDER BY id DESC + LIMIT #{maxStorage}) x + order by id asc + limit 1) + AND iot_id = #{iotId} + AND property = #{property}; + + + + DELETE + FROM iot_device_log_metadata + WHERE id < (SELECT id + FROM (SELECT id + FROM iot_device_log_metadata + WHERE iot_id = #{iotId} + AND event = #{event} + AND message_type = 'EVENT' + ORDER BY id DESC + LIMIT #{maxStorage}) x + order by id asc + limit 1) + AND iot_id = #{iotId} + AND event = #{event}; + + + + + + + DELETE from + + + iot_device_log_metadata + + + iot_device_log_metadata_${tablesIndex} + + + + + + + + + DELETE from + + + iot_device_log_metadata + + + iot_device_log_metadata_${tablesIndex} + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceLogMetadataShardMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceLogMetadataShardMapper.xml new file mode 100644 index 0000000..47244f8 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceLogMetadataShardMapper.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + DELETE + FROM iot_device_log_metadata + WHERE id < #{topId} + AND iot_id = #{iotId} + AND property = #{property}; + + + + + + DELETE + FROM iot_device_log_metadata + WHERE id < #{topId} + AND iot_id = #{iotId} + AND event = #{event}; + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceLogShardMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceLogShardMapper.xml new file mode 100644 index 0000000..93aff4a --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceLogShardMapper.xml @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into iot_device_log + + iot_id, + device_id, + ext_device_id, + product_key, + device_name, + message_type, + company_no, + protocol, + device_node, + org_id, + event, + create_id, + instance, + point, + content, + create_time, + + + #{iotId}, + #{deviceId}, + #{extDeviceId}, + #{productKey}, + #{deviceName}, + #{messageType}, + #{companyNo}, + #{protocol}, + #{deviceNode}, + #{orgId}, + #{event}, + #{createId}, + #{instance}, + #{point}, + #{content}, + #{createTime}, + + + + + + + + update iot_device_log + set command_status=1 + where id = #{ioTDeviceLog.id} + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceMapper.xml new file mode 100644 index 0000000..dd8c4ac --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceMapper.xml @@ -0,0 +1,1107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select d.id, + d.iot_id, + d.coordinate, + d.device_id, + d.ext_device_id, + d.derive_metadata, + d.configuration, + d.product_name, + d.nick_name, + d.features, + d.gw_product_key, + d.device_secret, + d.product_key, + d.device_name, + d.creator_id, + d.`instance`, + d.application, + d.creator_name, + d.`state`, + d.detail, + d.create_time, + d.registry_time, + d.online_time, + d.areas_id + from iot_device d + left join iot_user u on u.union_id = d.creator_id + + + + + + + + + + + + + + insert into iot_device + + iot_id, + device_id, + ext_device_id, + derive_metadata, + configuration, + product_name, + nick_name, + features, + gw_product_key, + device_secret, + product_key, + device_name, + creator_id, + instance, + application, + creator_name, + state, + detail, + create_time, + registry_time, + online_time, + areas_id, + + + #{iotId}, + #{deviceId}, + #{extDeviceId}, + #{deriveMetadata}, + #{configuration}, + #{productName}, + #{nickName}, + #{features}, + #{gwProductKey}, + #{deviceSecret}, + #{productKey}, + #{deviceName}, + #{creatorId}, + #{instance}, + #{application}, + #{creatorName}, + #{state}, + #{detail}, + #{createTime}, + #{registryTime}, + #{onlineTime}, + #{areasId}, + + + + + update iot_device + + iot_id = #{iotId}, + device_id = #{deviceId}, + ext_device_id = #{extDeviceId}, + derive_metadata = #{deriveMetadata}, + configuration = #{configuration}, + product_name = #{productName}, + nick_name = #{nickName}, + features = #{features}, + gw_product_key = #{gwProductKey}, + device_secret = #{deviceSecret}, + product_key = #{productKey}, + device_name = #{deviceName}, + creator_id = #{creatorId}, + instance = #{instance}, + application = #{application}, + creator_name = #{creatorName}, + state = #{state}, + detail = #{detail}, + create_time = #{createTime}, + registry_time = #{registryTime}, + online_time = #{onlineTime}, + areas_id = #{areasId}, + coordinate=#{coordinate}, + + where id = #{id} + + + + update iot_device + set application = #{appUniqueId} + where id in + + #{id} + + + + + update iot_device + set application = #{appUniqueId} + where iot_id = #{iotId} + + + update iot_device + set application =null + where iot_id = #{iotId} + + + + + delete + from iot_device + where id = #{id} + + + + delete from iot_device where id in + + #{id} + + + + + + + + + + + + + + + + insert into dev_instance_history + + device_id, + device_Name, + product_key, + coordinate, + first_online_time, + creater, + delete_time, + create_time, + + + #{deviceId}, + #{deviceName}, + #{productKey}, + #{coordinate}, + #{firstOnlineTime}, + #{creater}, + #{deleteTime}, + #{createTime}, + + + + + + + update iot_device + set configuration = #{configuration} + where device_id = #{deviceId} + + + update iot_device + set creator_id=null and creator_name = null and application = null and coordinate = null and + areas_id = null + where id = #{id} + + + + + + + + + + + + + + + + + + + UPDATE iot_device + SET + online_time = UNIX_TIMESTAMP(), + state = 1, + registry_time = IF(registry_time IS NULL, UNIX_TIMESTAMP(), registry_time) + WHERE iot_id IN + + #{id} + + + + + + + + + + + + + + + + diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceProtocolMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceProtocolMapper.xml new file mode 100644 index 0000000..e320284 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceProtocolMapper.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + select pl.id, pl.name, pl.description, pl.state, pl.type, pl.configuration, pl.example + from iot_device_protocol pl + + + + + + + + + + + insert into iot_device_protocol + + id, + name, + description, + state, + type, + configuration, + example, + + + #{id}, + #{name}, + #{description}, + #{state}, + #{type}, + #{configuration}, + #{example}, + + + + + insert into iot_device_protocol (id, name, description, state, type, configuration, example) + values + + (#{item.id},#{item.name},#{item.description},#{item.state},#{item.type},#{item.configuration},#{item.example}) + + + + + update iot_device_protocol + + name = #{name}, + description = #{description}, + state = #{state}, + type = #{type}, + configuration = #{configuration}, + example = #{example}, + + where id = #{id} + + + + delete + from iot_device_protocol + where id = #{id} + + + + delete from iot_device_protocol where id in + + #{id} + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceRuleLogMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceRuleLogMapper.xml new file mode 100644 index 0000000..dc0b8df --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceRuleLogMapper.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceShadowMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceShadowMapper.xml new file mode 100644 index 0000000..9758401 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceShadowMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceSubscribeMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceSubscribeMapper.xml new file mode 100644 index 0000000..95bc79d --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceSubscribeMapper.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceTagsMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceTagsMapper.xml new file mode 100644 index 0000000..bd652cb --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTDeviceTagsMapper.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + DELETE + FROM iot_device_tags + WHERE `value` = #{groupId} + + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTProductMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTProductMapper.xml new file mode 100644 index 0000000..f51ea34 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTProductMapper.xml @@ -0,0 +1,613 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select id, + product_id, + product_key, + product_secret, + third_platform, + third_configuration, + company_no, + classified_id, + configuration, + network_union_id, + device_node, + gw_product_key, + classified_name, + message_protocol, + p.name, + creator_id, + p.describe, + store_policy, + store_policy_configuration, + transport_protocol, + photo_url, + metadata, + create_time, + state + from iot_product p + + + + + + + + + + + + + + + insert into iot_product + + product_id, + product_key, + product_secret, + third_platform, + third_configuration, + company_no, + classified_id, + configuration, + network_union_id, + device_node, + gw_product_key, + classified_name, + message_protocol, + `name`, + creator_id, + `describe`, + store_policy, + store_policy_configuration, + transport_protocol, + photo_url, + metadata, + create_time, + state, + + + #{productId}, + #{productKey}, + #{productSecret}, + #{thirdPlatform}, + #{thirdConfiguration}, + #{companyNo}, + #{classifiedId}, + #{configuration}, + #{networkUnionId}, + #{deviceNode}, + #{gwProductKey}, + #{classifiedName}, + #{messageProtocol}, + #{name}, + #{creatorId}, + #{describe}, + #{storePolicy}, + #{storePolicyConfiguration}, + #{transportProtocol}, + #{photoUrl}, + #{metadata}, + #{createTime}, + #{state}, + + + + + update iot_product p + + product_id = #{productId}, + product_key = #{productKey}, + product_secret = #{productSecret}, + third_platform = #{thirdPlatform}, + third_configuration = #{thirdConfiguration}, + company_no = #{companyNo}, + classified_id = #{classifiedId}, + configuration = #{configuration}, + network_union_id = #{networkUnionId}, + device_node = #{deviceNode}, + gw_product_key = #{gwProductKey}, + classified_name = #{classifiedName}, + message_protocol = #{messageProtocol}, + p.name = #{name}, + creator_id = #{creatorId}, + p.describe = #{describe}, + store_policy = #{storePolicy}, + store_policy_configuration = + #{storePolicyConfiguration}, + + transport_protocol = #{transportProtocol}, + photo_url = #{photoUrl}, + metadata = cast(#{metadata} as json), + create_time = #{createTime}, + state = #{state}, + tags = #{tags}, + + where id = #{id} + + + + delete + from iot_product + where id = #{id} + + + + delete from iot_product where id in + + #{id} + + + + + + + + + update iot_product + set metadata=json_array_append(metadata, #{path}, cast(#{jsonStr} as json)) + where product_key = #{productKey} + + + update iot_product + set metadata=json_remove(metadata, replace( + replace(json_search(metadata, 'one', #{oldId}, null, #{path}), '"', ''), '.id', '')) + where product_key = #{productKey} + + + update iot_product + set metadata=json_set(metadata, replace( + replace(json_search(metadata, 'one', #{oldId}, null, #{path}), '"', ''), '.id', ''), + cast(#{jsonStr} as json)) + where product_key = #{productKey} + + + + + + + + + + + + + + + + UPDATE iot_product + SET network_union_id = #{networkUnionId} + WHERE product_key = #{productKey} + AND is_deleted = 0 + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTProductSortMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTProductSortMapper.xml new file mode 100644 index 0000000..98927b1 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTProductSortMapper.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + select id, + parent_id, + has_child, + identification, + classified_name, + description, + create_time, + create_by + from iot_product_sort + + + + + + + + insert into iot_product_sort + + id, + parent_id, + has_child, + identification, + classified_name, + description, + create_time, + create_by, + + + #{id}, + #{parentId}, + #{hasChild}, + #{identification}, + #{classifiedName}, + #{description}, + #{createTime}, + #{createBy}, + + + + + update iot_product_sort + + parent_id = #{parentId}, + has_child = #{hasChild}, + identification = + #{identification}, + + classified_name = + #{classifiedName}, + + description = #{description}, + create_time = #{createTime}, + create_by = #{createBy}, + + where id = #{id} + + + + delete + from iot_product_sort + where id = #{id} + + + + delete from iot_product_sort where id in + + #{id} + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTUserApplicationMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTUserApplicationMapper.xml new file mode 100644 index 0000000..a71c43d --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTUserApplicationMapper.xml @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select union_id, + app_name, + app_unique_id, + app_id, + up_topic, + down_topic, + notify_url, + valid_end_date, + scope, + app_status, + instance, + remark, + create_date, + deleted, + http_enable, + mqtt_enable, + cfg + from iot_user_application + + + + + + + + + + insert into iot_user_application + + + union_id, + app_name, + app_unique_id, + app_id, + up_topic, + down_topic, + notify_url, + app_secret, + valid_end_date, + scope, + app_status, + instance, + remark, + create_date, + deleted, + + + + #{unionId}, + #{appName}, + #{appUniqueId}, + #{appId}, + #{upTopic}, + #{downTopic}, + #{notifyUrl}, + #{appSecret}, + #{validEndDate}, + #{scope}, + #{appStatus}, + #{instance}, + #{remark}, + #{createDate}, + #{deleted}, + + + + + update iot_user_application + + app_name = #{appName}, + + app_id = #{appId}, + up_topic = #{upTopic}, + down_topic = #{downTopic}, + notify_url = #{notifyUrl}, + app_secret = #{appSecret}, + valid_end_date = #{validEndDate}, + scope = #{scope}, + app_status = #{appStatus}, + instance = #{instance}, + remark = #{remark}, + create_date = #{createDate}, + deleted = #{deleted}, + http_enable = #{httpEnable}, + mqtt_enable = #{mqttEnable}, + cfg = #{cfg}, + + + where app_unique_id = #{appUniqueId} and union_id = #{unionId} + + + + delete + from iot_user_application + where app_unique_id = #{appUniqueId} + + + + delete from iot_user_application where app_unique_id in + + #{appUniqueId} + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/IoTUserMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/IoTUserMapper.xml new file mode 100644 index 0000000..183f3ea --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/IoTUserMapper.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + update iot_user + set login_date=now() + where id = #{id} + + + + + + + + + + + + update iot_user + set license=if(license = 0, 0, license - 1) + where union_id = #{unionId} + + + + update iot_user + set license=license + 1 + where union_id = #{unionId} + + + + update iot_user + set license=license + #{licenseNumber}, + license_total_amount=license_total_amount + #{licenseNumber} + where union_id = #{unionId} + + + update iot_user + set status=1 + where status = 0 + and deleted = 0 + and ((login_date is null and DATEDIFF(NOW(), create_date) >= 90) + or (login_date is not null and DATEDIFF(NOW(), login_date) >= 90)) + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/OauthClientDetailsMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/OauthClientDetailsMapper.xml new file mode 100644 index 0000000..9c57787 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/OauthClientDetailsMapper.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + delete from oauth_client_details where client_id in + + #{id} + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/RuleModelInstanceMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/RuleModelInstanceMapper.xml new file mode 100644 index 0000000..6898ae2 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/RuleModelInstanceMapper.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/RuleModelMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/RuleModelMapper.xml new file mode 100644 index 0000000..7fa4f6c --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/RuleModelMapper.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/SceneLinkageMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/SceneLinkageMapper.xml new file mode 100644 index 0000000..cdeb39b --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/SceneLinkageMapper.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + select id, + scene_name, + touch, + trigger_condition, + exec_action, + sleep_cycle, + create_by, + create_time, + update_by, + update_time, + status, + dev_id + from scene_linkage + + + + + + + + insert into scene_linkage + + scene_name, + touch, + trigger_condition, + exec_action, + sleep_cycle, + create_by, + create_time, + update_by, + update_time, + status, + dev_id, + + + #{sceneName}, + #{touch}, + #{triggerCondition}, + #{execAction}, + #{sleepCycle}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{status}, + #{devId}, + + + + + update scene_linkage + + scene_name = #{sceneName}, + touch = #{touch}, + trigger_condition = + #{triggerCondition}, + + exec_action = #{execAction}, + sleep_cycle = #{sleepCycle}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + status = #{status}, + dev_id = #{devId}, + + where id = #{id} + + + + delete + from scene_linkage + where id = #{id} + + + + delete from scene_linkage where id in + + #{id} + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/SupportMapAreasMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/SupportMapAreasMapper.xml new file mode 100644 index 0000000..eaa920d --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/SupportMapAreasMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/SysLogininforMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/SysLogininforMapper.xml new file mode 100644 index 0000000..e700347 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/SysLogininforMapper.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/SysOperLogMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/SysOperLogMapper.xml new file mode 100644 index 0000000..9460eff --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/SysOperLogMapper.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/admin/NetworkMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/admin/NetworkMapper.xml new file mode 100644 index 0000000..980fe15 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/admin/NetworkMapper.xml @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT n.id, + n.type, + n.union_id, + n.product_key, + n.name, + n.description, + n.create_date, + n.create_user, + n.state, + n.configuration, + CASE + WHEN n.type = 'TCP_CLIENT' THEN 'TCP客户端' + WHEN n.type = 'TCP_SERVER' THEN 'TCP服务端' + WHEN n.type = 'MQTT_CLIENT' THEN 'MQTT客户端' + WHEN n.type = 'MQTT_SERVER' THEN 'MQTT服务端' + ELSE n.type + END as type_name, + CASE + WHEN n.state = 1 THEN '启动' + ELSE '关闭' + END as state_name, + DATE_FORMAT(n.create_date, '%Y-%m-%d %H:%i:%s') as create_date_str + FROM iot_network n + + + + + + + AND n.type IN + + #{typeItem} + + + + AND n.name LIKE CONCAT('%', #{name}, '%') + + + AND n.product_key = #{productKey} + + + AND n.state = #{state} + + + AND n.union_id = #{unionId} + + + AND n.create_user = #{createUser} + + + + + + + + + + + + + + + + + + + INSERT INTO iot_network (type, + union_id, + product_key, + name, + description, + create_date, + create_user, + state, + configuration) + VALUES (#{type}, + #{unionId}, + #{productKey}, + #{name}, + #{description}, + NOW(), + #{createUser}, + #{state}, + #{configuration}) + + + + + UPDATE iot_network + + type = #{type}, + union_id = #{unionId}, + product_key = #{productKey}, + name = #{name}, + description = #{description}, + state = #{state}, + configuration = #{configuration}, + + WHERE id = #{id} + + + + + DELETE + FROM iot_network + WHERE id = #{id} + + + + + DELETE FROM iot_network WHERE id IN + + #{id} + + + + + + UPDATE iot_network + SET state = #{state} + WHERE id = #{id} + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/admin/SysConfigMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/admin/SysConfigMapper.xml new file mode 100644 index 0000000..c977c73 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/admin/SysConfigMapper.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + select config_id, + config_name, + config_key, + config_value, + config_type, + create_by, + create_time, + update_by, + update_time, + remark + from sys_config + + + + + + + and config_id = #{configId} + + + and config_key = #{configKey} + + + + + + + + + + + + insert into sys_config ( + config_name, + config_key, + config_value, + config_type, + create_by, + remark, + create_time + )values( + #{configName}, + #{configKey}, + #{configValue}, + #{configType}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_config + + config_name = #{configName}, + config_key = #{configKey}, + config_value = #{configValue}, + config_type = #{configType}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where config_id = #{configId} + + + + delete + from sys_config + where config_id = #{configId} + + + + delete from sys_config where config_id in + + #{configId} + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/admin/SysDictDataMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/admin/SysDictDataMapper.xml new file mode 100644 index 0000000..8a99a81 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/admin/SysDictDataMapper.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/admin/SysDictTypeMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/admin/SysDictTypeMapper.xml new file mode 100644 index 0000000..0887761 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/admin/SysDictTypeMapper.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/admin/SysMenuMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/admin/SysMenuMapper.xml new file mode 100644 index 0000000..a571419 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/admin/SysMenuMapper.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + select menu_id, + menu_name, + parent_id, + order_num, + path, + component, + is_frame, + is_cache, + menu_type, + visible, + status, + ifnull(perms, '') as perms, + icon, + create_time + from sys_menu + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cn-universal-persistence/src/main/resources/mybatis/admin/SysOssMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/admin/SysOssMapper.xml new file mode 100644 index 0000000..55efe8f --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/admin/SysOssMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + delete from sys_oss where oss_id in + + #{id} + + + + delete from sys_oss where url in + + #{id} + + + diff --git a/cn-universal-persistence/src/main/resources/mybatis/admin/SysRoleMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/admin/SysRoleMapper.xml new file mode 100644 index 0000000..a38bb35 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/admin/SysRoleMapper.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + select distinct r.role_id, + r.role_name, + r.role_key, + r.role_sort, + r.data_scope, + r.menu_check_strictly, + r.dept_check_strictly, + r.status, + r.create_by, + r.create_time, + r.update_by, + r.update_time, + r.remark + from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + + + + + + + + + + + + + diff --git a/cn-universal-persistence/src/main/resources/mybatis/admin/SysRoleMenuMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/admin/SysRoleMenuMapper.xml new file mode 100644 index 0000000..2ee8c7b --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/admin/SysRoleMenuMapper.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/cn-universal-persistence/src/main/resources/mybatis/admin/SysUserRoleMapper.xml b/cn-universal-persistence/src/main/resources/mybatis/admin/SysUserRoleMapper.xml new file mode 100644 index 0000000..17945c3 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/mybatis/admin/SysUserRoleMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + delete from sys_user_role + where union_id in + + #{id} + + + diff --git a/cn-universal-persistence/src/main/resources/sql/db.sql b/cn-universal-persistence/src/main/resources/sql/db.sql new file mode 100644 index 0000000..26a9fb8 --- /dev/null +++ b/cn-universal-persistence/src/main/resources/sql/db.sql @@ -0,0 +1,4310 @@ +/* + Navicat Premium Dump SQL + + Source Server : 194 + Source Server Type : MySQL + Source Server Version : 80406 (8.4.6) + Source Host : 192.168.31.194:3307 + Source Schema : univ + + Target Server Type : MySQL + Target Server Version : 80406 (8.4.6) + File Encoding : 65001 + + Date: 28/08/2025 10:51:25 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for code_protocol +-- ---------------------------- +DROP TABLE IF EXISTS `code_protocol`; +CREATE TABLE `code_protocol` ( + `code_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '唯一key', + `provider_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '协议类型 jar,jscript,magic', + `class_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '协议类型为jar时,编解码类名', + `location` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '编解码网络地址或者内容', + PRIMARY KEY (`code_key`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of code_protocol +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for device_message_routes +-- ---------------------------- +DROP TABLE IF EXISTS `device_message_routes`; +CREATE TABLE `device_message_routes` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品Key', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备ID', + `message_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '消息类型:data/status/command', + `flow_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流程ID', + `priority` int DEFAULT '0' COMMENT '优先级', + `enabled` tinyint DEFAULT '1' COMMENT '是否启用', + `creator_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建者ID', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_route` (`product_key`,`device_id`,`message_type`,`flow_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备消息路由表'; + +-- ---------------------------- +-- Records of device_message_routes +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for gen_table +-- ---------------------------- +DROP TABLE IF EXISTS `gen_table`; +CREATE TABLE `gen_table` ( + `table_id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `table_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '表名称', + `table_comment` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '表描述', + `sub_table_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '关联子表的表名', + `sub_table_fk_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '子表关联的外键名', + `class_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '实体类名称', + `tpl_category` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT 'crud' COMMENT '使用的模板(crud单表操作 tree树表操作)', + `package_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '生成包路径', + `module_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '生成模块名', + `business_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '生成业务名', + `function_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '生成功能名', + `function_author` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '生成功能作者', + `gen_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '生成代码方式(0zip压缩包 1自定义路径)', + `gen_path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '/' COMMENT '生成路径(不填默认项目路径)', + `options` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '其它生成选项', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`table_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='代码生成业务表'; + +-- ---------------------------- +-- Records of gen_table +-- ---------------------------- +BEGIN; +INSERT INTO `gen_table` (`table_id`, `table_name`, `table_comment`, `sub_table_name`, `sub_table_fk_name`, `class_name`, `tpl_category`, `package_name`, `module_name`, `business_name`, `function_name`, `function_author`, `gen_type`, `gen_path`, `options`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (4, 'sys_dict_type', '字典类型表', NULL, NULL, 'SysDictType', 'crud', 'cn.universal.', 'system', 'type', '字典类型', 'iot', '0', '/', NULL, 'iot', '2021-12-23 16:20:55', '', NULL, NULL); +INSERT INTO `gen_table` (`table_id`, `table_name`, `table_comment`, `sub_table_name`, `sub_table_fk_name`, `class_name`, `tpl_category`, `package_name`, `module_name`, `business_name`, `function_name`, `function_author`, `gen_type`, `gen_path`, `options`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (5, 'iot_user_application', '用户应用信息表', NULL, NULL, 'IotUserApplication', 'crud', 'cn.universal.', '用户应用信息', 'application', '用户应用信息', 'iot', '0', '/', '{\"parentMenuId\":\"1\"}', 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11', NULL); +INSERT INTO `gen_table` (`table_id`, `table_name`, `table_comment`, `sub_table_name`, `sub_table_fk_name`, `class_name`, `tpl_category`, `package_name`, `module_name`, `business_name`, `function_name`, `function_author`, `gen_type`, `gen_path`, `options`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (6, 'dev_group', '设备分组表', NULL, NULL, 'DevGroup', 'crud', 'cn.universal.', NULL, 'group', '设备分组', 'iot', '0', '/', NULL, 'iot', '2021-12-30 14:08:05', '', NULL, NULL); +INSERT INTO `gen_table` (`table_id`, `table_name`, `table_comment`, `sub_table_name`, `sub_table_fk_name`, `class_name`, `tpl_category`, `package_name`, `module_name`, `business_name`, `function_name`, `function_author`, `gen_type`, `gen_path`, `options`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (7, 'dev_log', '', NULL, NULL, 'DevLog', 'crud', 'cn.universal.', NULL, 'log', NULL, 'iot', '0', '/', NULL, 'iot', '2022-01-17 10:34:37', '', NULL, NULL); +INSERT INTO `gen_table` (`table_id`, `table_name`, `table_comment`, `sub_table_name`, `sub_table_fk_name`, `class_name`, `tpl_category`, `package_name`, `module_name`, `business_name`, `function_name`, `function_author`, `gen_type`, `gen_path`, `options`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (8, 'scene_linkage', '场景联动', NULL, NULL, 'SceneLinkage', 'crud', 'cn.universal.', 'linkage', 'linkage', '场景联动', 'iot', '0', '/', '{\"parentMenuId\":\"1081\"}', 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20', NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for gen_table_column +-- ---------------------------- +DROP TABLE IF EXISTS `gen_table_column`; +CREATE TABLE `gen_table_column` ( + `column_id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `table_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '归属表编号', + `column_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '列名称', + `column_comment` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '列描述', + `column_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '列类型', + `java_type` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'JAVA类型', + `java_field` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'JAVA字段名', + `is_pk` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '是否主键(1是)', + `is_increment` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '是否自增(1是)', + `is_required` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '是否必填(1是)', + `is_insert` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '是否为插入字段(1是)', + `is_edit` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '是否编辑字段(1是)', + `is_list` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '是否列表字段(1是)', + `is_query` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '是否查询字段(1是)', + `query_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT 'EQ' COMMENT '查询方式(等于、不等于、大于、小于、范围)', + `html_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)', + `dict_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '字典类型', + `sort` int DEFAULT NULL COMMENT '排序', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`column_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=97 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='代码生成业务表字段'; + +-- ---------------------------- +-- Records of gen_table_column +-- ---------------------------- +BEGIN; +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (32, '4', 'dict_id', '字典主键', 'bigint(20)', 'Long', 'dictId', '1', '1', NULL, '1', NULL, NULL, NULL, 'EQ', 'input', '', 1, 'iot', '2021-12-23 16:20:55', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (33, '4', 'dict_name', '字典名称', 'varchar(100)', 'String', 'dictName', '0', '0', NULL, '1', '1', '1', '1', 'LIKE', 'input', '', 2, 'iot', '2021-12-23 16:20:55', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (34, '4', 'dict_type', '字典类型', 'varchar(100)', 'String', 'dictType', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'select', '', 3, 'iot', '2021-12-23 16:20:55', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (35, '4', 'status', '状态(0正常 1停用)', 'char(1)', 'String', 'status', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'radio', '', 4, 'iot', '2021-12-23 16:20:55', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (36, '4', 'create_by', '创建者', 'varchar(64)', 'String', 'createBy', '0', '0', NULL, '1', NULL, NULL, NULL, 'EQ', 'input', '', 5, 'iot', '2021-12-23 16:20:55', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (37, '4', 'create_time', '创建时间', 'datetime', 'Date', 'createTime', '0', '0', NULL, '1', NULL, NULL, NULL, 'EQ', 'datetime', '', 6, 'iot', '2021-12-23 16:20:55', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (38, '4', 'update_by', '更新者', 'varchar(64)', 'String', 'updateBy', '0', '0', NULL, '1', '1', NULL, NULL, 'EQ', 'input', '', 7, 'iot', '2021-12-23 16:20:55', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (39, '4', 'update_time', '更新时间', 'datetime', 'Date', 'updateTime', '0', '0', NULL, '1', '1', NULL, NULL, 'EQ', 'datetime', '', 8, 'iot', '2021-12-23 16:20:55', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (40, '4', 'remark', '备注', 'varchar(500)', 'String', 'remark', '0', '0', NULL, '1', '1', '1', NULL, 'EQ', 'textarea', '', 9, 'iot', '2021-12-23 16:20:55', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (41, '5', 'uuid', NULL, 'bigint(20)', 'Long', 'uuid', '1', '1', '0', '1', '0', '0', '0', 'EQ', 'input', '', 1, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (42, '5', 'union_id', NULL, 'varchar(64)', 'String', 'unionId', '0', '0', '1', '1', '1', '1', '1', 'EQ', 'input', '', 2, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (43, '5', 'app_name', '应用名称', 'varchar(64)', 'String', 'appName', '0', '0', '0', '1', '1', '1', '1', 'LIKE', 'input', '', 3, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (44, '5', 'app_unique_id', '应用标识', 'varchar(128)', 'String', 'appUniqueId', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'input', '', 4, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (45, '5', 'app_id', '调用凭证APPID,必须16位', 'varchar(16)', 'String', 'appId', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'input', '', 5, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (46, '5', 'up_topic', 'MQ上行主题,英文逗号分隔', 'varchar(64)', 'String', 'upTopic', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'input', '', 6, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (47, '5', 'down_topic', 'MQ下行主题,英文逗号分隔', 'varchar(64)', 'String', 'downTopic', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'input', '', 7, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (48, '5', 'notify_url', '回调地址', 'varchar(255)', 'String', 'notifyUrl', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'input', '', 8, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (49, '5', 'app_secret', '调用密钥,必须32位', 'varchar(32)', 'String', 'appSecret', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'input', '', 9, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (50, '5', 'valid_end_date', '授权结束时间', 'datetime', 'Date', 'validEndDate', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'datetime', '', 10, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (51, '5', 'scope', '授权范围', 'varchar(128)', 'String', 'scope', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'input', '', 11, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (52, '5', 'app_status', '0-正常,1-停用', 'tinyint(1) unsigned', 'Integer', 'appStatus', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'radio', '', 12, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (53, '5', 'instance', '实例名称', 'varchar(64)', 'String', 'instance', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'input', '', 13, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (54, '5', 'remark', '描述', 'varchar(128)', 'String', 'remark', '0', '0', '0', '1', '1', '1', '0', 'EQ', 'input', '', 14, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (55, '5', 'create_date', '创建时间', 'datetime', 'Date', 'createDate', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'datetime', '', 15, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (56, '5', 'deleted', '0-正常,1-删除', 'tinyint(1) unsigned', 'Integer', 'deleted', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'input', '', 16, 'iot', '2021-12-30 09:57:00', '', '2021-12-30 09:58:11'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (57, '6', 'id', '分组ID,非自增', 'bigint(20)', 'Long', 'id', '1', '0', NULL, '1', NULL, NULL, NULL, 'EQ', 'input', '', 1, 'iot', '2021-12-30 14:08:05', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (58, '6', 'group_name', '分组名称', 'varchar(128)', 'String', 'groupName', '0', '0', '1', '1', '1', '1', '1', 'LIKE', 'input', '', 2, 'iot', '2021-12-30 14:08:05', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (59, '6', 'group_code', '分组标识', 'varchar(128)', 'String', 'groupCode', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 3, 'iot', '2021-12-30 14:08:05', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (60, '6', 'describe', '群组描述', 'varchar(255)', 'String', 'describe', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 4, 'iot', '2021-12-30 14:08:05', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (61, '6', 'parent_id', '父id', 'bigint(20)', 'Long', 'parentId', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 5, 'iot', '2021-12-30 14:08:05', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (62, '6', 'relat_dev_count', '激活设备数', 'int(10) unsigned zerofill', 'Integer', 'relatDevCount', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 6, 'iot', '2021-12-30 14:08:05', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (63, '6', 'active_dev_count', '关联设备树', 'int(10) unsigned zerofill', 'Integer', 'activeDevCount', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 7, 'iot', '2021-12-30 14:08:05', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (64, '6', 'creator_id', '创建人', 'varchar(255)', 'String', 'creatorId', '0', '0', '1', '1', '1', '1', '1', 'EQ', 'input', '', 8, 'iot', '2021-12-30 14:08:05', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (65, '6', 'instance', '实例编号', 'varchar(64)', 'String', 'instance', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 9, 'iot', '2021-12-30 14:08:05', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (66, '6', 'tag', '标签', 'varchar(128)', 'String', 'tag', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 10, 'iot', '2021-12-30 14:08:05', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (67, '7', 'id', NULL, 'bigint(20) unsigned', 'Long', 'id', '1', '1', NULL, '1', NULL, NULL, NULL, 'EQ', 'input', '', 1, 'iot', '2022-01-17 10:34:37', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (68, '7', 'iot_id', '唯一编码', 'varchar(32)', 'String', 'iotId', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 2, 'iot', '2022-01-17 10:34:37', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (69, '7', 'device_id', '设备自身序号', 'varchar(32)', 'String', 'deviceId', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 3, 'iot', '2022-01-17 10:34:37', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (70, '7', 'ext_device_id', '第三方设备ID唯一标识符', 'varchar(32)', 'String', 'extDeviceId', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 4, 'iot', '2022-01-17 10:34:37', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (71, '7', 'product_key', '产品ID', 'varchar(64)', 'String', 'productKey', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 5, 'iot', '2022-01-17 10:34:37', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (72, '7', 'device_name', '设备名称', 'varchar(32)', 'String', 'deviceName', '0', '0', NULL, '1', '1', '1', '1', 'LIKE', 'input', '', 6, 'iot', '2022-01-17 10:34:37', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (73, '7', 'message_type', '消息类型', 'varchar(20)', 'String', 'messageType', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'select', '', 7, 'iot', '2022-01-17 10:34:37', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (74, '7', 'company_no', NULL, 'varchar(20)', 'String', 'companyNo', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 8, 'iot', '2022-01-17 10:34:37', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (75, '7', 'protocol', '协议', 'varchar(20)', 'String', 'protocol', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 9, 'iot', '2022-01-17 10:34:37', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (76, '7', 'device_node', '节点类型', 'varchar(32)', 'String', 'deviceNode', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 10, 'iot', '2022-01-17 10:34:37', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (77, '7', 'classified_id', NULL, 'varchar(128)', 'String', 'classifiedId', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 11, 'iot', '2022-01-17 10:34:37', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (78, '7', 'org_id', NULL, 'varchar(20)', 'String', 'orgId', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 12, 'iot', '2022-01-17 10:34:38', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (79, '7', 'event', '事件名称', 'varchar(30)', 'String', 'event', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 13, 'iot', '2022-01-17 10:34:38', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (80, '7', 'create_id', NULL, 'varchar(20)', 'String', 'createId', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 14, 'iot', '2022-01-17 10:34:38', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (81, '7', 'instance', '实例名称', 'varchar(64)', 'String', 'instance', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 15, 'iot', '2022-01-17 10:34:38', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (82, '7', 'point', '经纬度', 'varchar(128)', 'String', 'point', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'input', '', 16, 'iot', '2022-01-17 10:34:38', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (83, '7', 'content', '内容', 'text', 'String', 'content', '0', '0', NULL, '1', '1', '1', '1', 'EQ', 'editor', '', 17, 'iot', '2022-01-17 10:34:38', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (84, '7', 'create_time', '创建时间', 'int(10) unsigned zerofill', 'Integer', 'createTime', '0', '0', NULL, '1', NULL, NULL, NULL, 'EQ', 'input', '', 18, 'iot', '2022-01-17 10:34:38', '', NULL); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (85, '8', 'id', NULL, 'bigint(20)', 'Long', 'id', '1', '1', '0', '1', '0', '0', '0', 'EQ', 'input', '', 1, 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (86, '8', 'scene_name', '场景名称', 'varchar(50)', 'String', 'sceneName', '0', '0', '0', '1', '1', '1', '1', 'LIKE', 'input', '', 2, 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (87, '8', 'touch', '触发条件 all.全部 one.任意一个', 'varchar(10)', 'String', 'touch', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'input', '', 3, 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (88, '8', 'trigger_condition', '触发条件', 'text', 'String', 'triggerCondition', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'textarea', '', 4, 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (89, '8', 'exec_action', '执行动作', 'text', 'String', 'execAction', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'textarea', '', 5, 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (90, '8', 'sleep_cycle', '沉默周期', 'int(11)', 'Long', 'sleepCycle', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'input', '', 6, 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (91, '8', 'create_by', NULL, 'varchar(64)', 'String', 'createBy', '0', '0', '0', '1', '0', '0', '0', 'EQ', 'input', '', 7, 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (92, '8', 'create_time', NULL, 'datetime', 'Date', 'createTime', '0', '0', '0', '1', '0', '0', '0', 'EQ', 'datetime', '', 8, 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (93, '8', 'update_by', NULL, 'varchar(64)', 'String', 'updateBy', '0', '0', '0', '1', '1', '0', '0', 'EQ', 'input', '', 9, 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (94, '8', 'update_time', NULL, 'datetime', 'Date', 'updateTime', '0', '0', '0', '1', '1', '0', '0', 'EQ', 'datetime', '', 10, 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (95, '8', 'status', '0启用 1停用', 'tinyint(4)', 'Integer', 'status', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'radio', 'sys_normal_disable', 11, 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20'); +INSERT INTO `gen_table_column` (`column_id`, `table_id`, `column_name`, `column_comment`, `column_type`, `java_type`, `java_field`, `is_pk`, `is_increment`, `is_required`, `is_insert`, `is_edit`, `is_list`, `is_query`, `query_type`, `html_type`, `dict_type`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (96, '8', 'dev_id', '设备id', 'varchar(64)', 'String', 'devId', '0', '0', '0', '1', '1', '1', '1', 'EQ', 'input', '', 12, 'iot', '2022-03-01 10:10:40', '', '2022-03-01 10:15:20'); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_certificate +-- ---------------------------- +DROP TABLE IF EXISTS `iot_certificate`; +CREATE TABLE `iot_certificate` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `ssl_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '证书唯一标识(sslKey)', + `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '证书名称', + `cert_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '证书内容(PEM,Base64或加密)', + `key_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '私钥内容(PEM,Base64或加密)', + `cert_password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '证书密码(如有)', + `key_password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '私钥密码(如有)', + `cert_info` json DEFAULT NULL COMMENT '证书信息', + `expire_time` datetime DEFAULT NULL COMMENT '到期时间', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `create_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '创建者用户ID', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uniq_ssl_key` (`ssl_key`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='物联网产品证书表(内容存数据库)'; + +-- ---------------------------- +-- Records of iot_certificate +-- ---------------------------- +BEGIN; +INSERT INTO `iot_certificate` (`id`, `ssl_key`, `name`, `cert_content`, `key_content`, `cert_password`, `key_password`, `cert_info`, `expire_time`, `remark`, `create_time`, `update_time`, `create_user`) VALUES (3, 'default-tcp', '平台默认12', '-----BEGIN CERTIFICATE-----\nMIIE+zCCA+OgAwIBAgISBsJEJJpCf6bk1ReiwF+n9c2uMA0GCSqGSIb3DQEBCwUA\nMDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD\nEwNSMTEwHhcNMjUwNjI4MjAwODUzWhcNMjUwOTI2MjAwODUyWjAVMRMwEQYDVQQD\nDAoqLmFsZW8ueGluMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsSoj\njURNeVCxNUM1kEkkEeC5x5ueL6u0oXce00xf0uyjMj4PX/Yxr1t/OODisWM9H4VG\nmAsFXzhbqVR2+R6woWDT4JyJqRul24UCvVo/NnjYkB5YJp+k22DScLzBBFTbGxKM\nekHtBfb8Zp8b9n3haSYH9B/ZNSxpei1SAyjtAtWNYZjL4hob4vWcXqllQ3wpUHY6\naLjnatlFgByyDD90BEtrRDwM9WwCSeCHs6vFx1Z1SNHwiGrPHaLRKTqfFBz0EnmI\nIEcFDbHS1Iu0VzYU1RUsaPh6BpebKQv10BQNisKLZbgmLjVGf7iDqZoSZ8anEnqC\ntq5sxxNRNU2yUD0BfQIDAQABo4ICJTCCAiEwDgYDVR0PAQH/BAQDAgWgMB0GA1Ud\nJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQW\nBBS1vsLHaBMI1ecgKZPegSeg3Jw3NTAfBgNVHSMEGDAWgBTFz0ak6vTDwHpslcQt\nsF6SLybjuTAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUHMAKGF2h0dHA6Ly9yMTEu\naS5sZW5jci5vcmcvMCEGA1UdEQQaMBiCCiouYWxlby54aW6CCiouZmlsLmFzaWEw\nEwYDVR0gBAwwCjAIBgZngQwBAgEwLgYDVR0fBCcwJTAjoCGgH4YdaHR0cDovL3Ix\nMS5jLmxlbmNyLm9yZy80Ny5jcmwwggEDBgorBgEEAdZ5AgQCBIH0BIHxAO8AdgDt\nPEvW6AbCpKIAV9vLJOI4Ad9RL+3EhsVwDyDdtz4/4AAAAZe4Xck7AAAEAwBHMEUC\nIQCRU9ghZEoJYsEhSOojDkl3Yh9+KRJv0BAUsObE0g6bOQIgLeQxtRhmkyzxjR8P\nshuMH72YDn8FHB3i9Tl2jDhgTRAAdQAaBP9J0FQdQK/2oMO/8djEZy9O7O4jQGiY\naxdALtyJfQAAAZe4XdHcAAAEAwBGMEQCIGDW96cDRAfAGkJbnnmu4LIFkbDcGucI\nI9l8fsr/C8tpAiBOrKazQ1wRLqQU2F61VEqaCFOxMBHkF4AS8GqpFem57DANBgkq\nhkiG9w0BAQsFAAOCAQEAfDKM0PX1cR2cscaIYi7OQ1LjbMISbOccGhuMoFp57bNA\nSPfaPH76A3ka2KFkpNhky8ZYNXa4GhFxqJgl1kO0+j08x4ir+18P1/W/MGP7zvEa\nrMi0HePTeA7rHOsFFbRSPP0sjrBjbWuqRgaUj5ezLi6b2un8UGW6293nBFbO591V\nyB1sT2H+fP11EDG4yWRCXPO4TPb3JLxk12nENt07Gc/7urFgYprkyXQlLgY6LSKs\nU44baCv8JShbfPzEwrXbJnC4F14hqWtnrb7ysfl6lxyYcGXQnCYv0J3gmh6omVp6\nCa5mbe+0fhw8FkY83d6ufyZ5ZRfJgEs5eHFupUa7mw==\n-----END CERTIFICATE-----\n\n-----BEGIN CERTIFICATE-----\nMIIFBjCCAu6gAwIBAgIRAIp9PhPWLzDvI4a9KQdrNPgwDQYJKoZIhvcNAQELBQAw\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw\nWhcNMjcwMzEyMjM1OTU5WjAzMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg\nRW5jcnlwdDEMMAoGA1UEAxMDUjExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAuoe8XBsAOcvKCs3UZxD5ATylTqVhyybKUvsVAbe5KPUoHu0nsyQYOWcJ\nDAjs4DqwO3cOvfPlOVRBDE6uQdaZdN5R2+97/1i9qLcT9t4x1fJyyXJqC4N0lZxG\nAGQUmfOx2SLZzaiSqhwmej/+71gFewiVgdtxD4774zEJuwm+UE1fj5F2PVqdnoPy\n6cRms+EGZkNIGIBloDcYmpuEMpexsr3E+BUAnSeI++JjF5ZsmydnS8TbKF5pwnnw\nSVzgJFDhxLyhBax7QG0AtMJBP6dYuC/FXJuluwme8f7rsIU5/agK70XEeOtlKsLP\nXzze41xNG/cLJyuqC0J3U095ah2H2QIDAQABo4H4MIH1MA4GA1UdDwEB/wQEAwIB\nhjAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwEgYDVR0TAQH/BAgwBgEB\n/wIBADAdBgNVHQ4EFgQUxc9GpOr0w8B6bJXELbBeki8m47kwHwYDVR0jBBgwFoAU\nebRZ5nu25eQBc4AIiMgaWPbpm24wMgYIKwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAC\nhhZodHRwOi8veDEuaS5sZW5jci5vcmcvMBMGA1UdIAQMMAowCAYGZ4EMAQIBMCcG\nA1UdHwQgMB4wHKAaoBiGFmh0dHA6Ly94MS5jLmxlbmNyLm9yZy8wDQYJKoZIhvcN\nAQELBQADggIBAE7iiV0KAxyQOND1H/lxXPjDj7I3iHpvsCUf7b632IYGjukJhM1y\nv4Hz/MrPU0jtvfZpQtSlET41yBOykh0FX+ou1Nj4ScOt9ZmWnO8m2OG0JAtIIE38\n01S0qcYhyOE2G/93ZCkXufBL713qzXnQv5C/viOykNpKqUgxdKlEC+Hi9i2DcaR1\ne9KUwQUZRhy5j/PEdEglKg3l9dtD4tuTm7kZtB8v32oOjzHTYw+7KdzdZiw/sBtn\nUfhBPORNuay4pJxmY/WrhSMdzFO2q3Gu3MUBcdo27goYKjL9CTF8j/Zz55yctUoV\naneCWs/ajUX+HypkBTA+c8LGDLnWO2NKq0YD/pnARkAnYGPfUDoHR9gVSp/qRx+Z\nWghiDLZsMwhN1zjtSC0uBWiugF3vTNzYIEFfaPG7Ws3jDrAMMYebQ95JQ+HIBD/R\nPBuHRTBpqKlyDnkSHDHYPiNX3adPoPAcgdF3H2/W0rmoswMWgTlLn1Wu0mrks7/q\npdWfS6PJ1jty80r2VKsM/Dj3YIDfbjXKdaFU5C+8bhfJGqU3taKauuz0wHVGT3eo\n6FlWkWYtbt4pgdamlwVeZEW+LM7qZEJEsMNPrfC03APKmZsJgpWCDWOKZvkZcvjV\nuYkQ4omYCTX5ohy+knMjdOmdH9c7SpqEWBDC86fiNex+O0XOMEZSa8DA\n-----END CERTIFICATE-----\n', '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAsSojjURNeVCxNUM1kEkkEeC5x5ueL6u0oXce00xf0uyjMj4P\nX/Yxr1t/OODisWM9H4VGmAsFXzhbqVR2+R6woWDT4JyJqRul24UCvVo/NnjYkB5Y\nJp+k22DScLzBBFTbGxKMekHtBfb8Zp8b9n3haSYH9B/ZNSxpei1SAyjtAtWNYZjL\n4hob4vWcXqllQ3wpUHY6aLjnatlFgByyDD90BEtrRDwM9WwCSeCHs6vFx1Z1SNHw\niGrPHaLRKTqfFBz0EnmIIEcFDbHS1Iu0VzYU1RUsaPh6BpebKQv10BQNisKLZbgm\nLjVGf7iDqZoSZ8anEnqCtq5sxxNRNU2yUD0BfQIDAQABAoIBAAuGkIgJoW8ARnsO\n1jP+vM498WV7P1wIvr2svHsQx6t28iSxYA+FOn/BmIH93WRJbr9MzMYqR2KbDcQz\nHBytDV7TqA0U2f7VWeGxYkzTy3N9gqdDlhwIfEblDQ5rtTSVLv5F+Lt2a6yyQALk\nHINZdqSVe7nQfZwa5CTK6yrh9Xi0NJT9VMQNW20bi+rfeLcSVls5o+RtpNSF0f6K\n1rHP3M4JctarKV1luI09Gt8SZmjJNWgERwEukJ2LPj1h/egi8EByeM8Za3vSruO0\npelo23hBAzA54i5j9yJhY/dQq/OPQR40iCnX8lqCNufn5UxkLpXpW/iz7kbM27Qc\nO523FJECgYEA6zJw6D+ZR7UEb181WwFx7LaTUnJFpCBqtNquMExp3Z7/+WXFh4D3\nR0g4Y9ex2RtMDTvKzpxuBFJ2Ioe1Cbe1o0aJJ0RchHXqYzmTV4z3ysToCi+EQMdo\nPhFzlMkiMrGBL2NdN7WRMfcVS/j/Crorh//u2IDmqs1KfvTiWVdgGLECgYEAwNWr\n3ocw7WjpTbdiAwKYkut0m/5iQUtX8VG/B+Mi695U+/JeCqcSiZ+Kn0SSEMl2/5Y9\nshak5VOIFMg+8zTBE8JnVsXBm5fDlyaNxLTm2S4QYFmaWMNd4sINSu8PKPnqpB5l\nfz5MEyU6BYqZm1FPd9S6rQX8k75SynAY/ASf6I0CgYAGIEM2mWl5p6pc6ELwguIf\nw0WlERrlQn7lBQkicusS/bA3I8J4XBCUx7nIR/b267IYaiVHKmXnMOqozZYWkbVm\nclDiN/Kc9KFNQlOnJSHTIzvUABbWxRre+v9isbZ1UZBG1rS33b/RaFkiqHKkKeM2\nYKEs/HaFnmFBW0mGADOdkQKBgCwnQKk9T0itAu6WlMyXpZPOce7d+mV0+Pf6fcEy\nkuZrDA58WmgaBTaZ30XEj6VkW+j937aNVMhlrf0ENNW/OI5rrvnBYilcQJ61ZzSp\nL0p/L6TAH4U/hpnLUPvdbR02zDEz/I18GDMyDO0D5VQY9Bc+EFhZzJGPvKk5rZ7W\n+D5pAoGBAKZESi2LFqtF/WyYXCGpml2tdJl/mY2d97mJSFNbzRY7nJ7Mpma8f6Zt\n45PTSoX9hKEZTQTrvyX6wtxBbVHhBKs9+tKvbBFPkAIzGbFstHMivqBzdc3MOPAH\nRlh/ucR9SiaoM6YOtPSTA75TUZZp/CAlygtgsA25P7g2cM7nKbg4\n-----END RSA PRIVATE KEY-----\n', '', '', '{\"issuer\": \"CN=R11,O=Let\'s Encrypt,C=US\", \"subject\": \"CN=*.aleo.xin\", \"version\": 3, \"keyUsage\": \"[true, false, true, false, false, false, false, false, false]\", \"notAfter\": 1758917332000, \"notBefore\": 1751141333000, \"chainLength\": 2, \"serialNumber\": \"6c244249a427fa6e4d517a2c05fa7f5cdae\", \"extendedKeyUsage\": [\"1.3.6.1.5.5.7.3.1\", \"1.3.6.1.5.5.7.3.2\"], \"publicKeyAlgorithm\": \"RSA\", \"signatureAlgorithm\": \"SHA256withRSA\", \"subjectAlternativeNames\": [\"*.aleo.xin\", \"*.fil.asia\"]}', '2025-09-27 04:08:52', '不要在乎太多', '2025-07-24 18:47:35', '2025-07-28 13:49:37', 'test'); +INSERT INTO `iot_certificate` (`id`, `ssl_key`, `name`, `cert_content`, `key_content`, `cert_password`, `key_password`, `cert_info`, `expire_time`, `remark`, `create_time`, `update_time`, `create_user`) VALUES (4, '6882199a5074ee4ca05cd440', '美利坚合众国', '-----BEGIN CERTIFICATE-----\nMIIGOjCCBSKgAwIBAgIQDlcl4pOUpFSPwiO5YEHwvjANBgkqhkiG9w0BAQsFADBg\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMR8wHQYDVQQDExZSYXBpZFNTTCBUTFMgUlNBIENBIEcx\nMB4XDTI1MDcyMDAwMDAwMFoXDTI2MDcxOTIzNTk1OVowHDEaMBgGA1UEAxMRc29j\nY2VybGlmZS5jb20uY24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDH\njLjefUk1EjNeNhMPmHZwZ6OYtAqQI8r7eevNDHC3wTnJYC6RUMHjG+h9QIgCp4cl\nS9DaDZBsPIfVu57KrRWxwKTbZIuoBk9P6xVCJ9112H7rhIAygQOxm8q2FIzH4fE6\nYOhOuTZlmIwCp9QM9DKE+qib2j0VkjpyuPzB244VPxvnvVkxjTHt0LeVxkTWQuOu\niYpVvPa9QHRkg82NpjitLxN7zGCIcUDIeix2O5KTSEAwu+GLYOP9zTByPDHdOOgb\nwQlrH3+oHVnPEvHn0aKyylV6xTfHYHmqsn7Q7NUXZ//RAGqfg1cssjg8lnSb/UcO\nXD86UxELey0Mferdjx0BAgMBAAGjggMyMIIDLjAfBgNVHSMEGDAWgBQM22yCSQ9K\nZwq4FO56xEhSiOtWODAdBgNVHQ4EFgQUmuGKGcJBNKz6vrJEr7WqBo76lqAwMwYD\nVR0RBCwwKoIRc29jY2VybGlmZS5jb20uY26CFXd3dy5zb2NjZXJsaWZlLmNvbS5j\nbjA+BgNVHSAENzA1MDMGBmeBDAECATApMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3\nLmRpZ2ljZXJ0LmNvbS9DUFMwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsG\nAQUFBwMBBggrBgEFBQcDAjA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY2RwLnJh\ncGlkc3NsLmNvbS9SYXBpZFNTTFRMU1JTQUNBRzEuY3JsMHYGCCsGAQUFBwEBBGow\naDAmBggrBgEFBQcwAYYaaHR0cDovL3N0YXR1cy5yYXBpZHNzbC5jb20wPgYIKwYB\nBQUHMAKGMmh0dHA6Ly9jYWNlcnRzLnJhcGlkc3NsLmNvbS9SYXBpZFNTTFRMU1JT\nQUNBRzEuY3J0MAwGA1UdEwEB/wQCMAAwggF/BgorBgEEAdZ5AgQCBIIBbwSCAWsB\naQB3ANdtfRDRp/V3wsfpX9cAv/mCyTNaZeHQswFzF8DIxWl3AAABmCYur30AAAQD\nAEgwRgIhAMDoK0FT2UdrJJcSwMPW2Bz7yz/BntIWow+6gT431EupAiEA49Lik0B4\nwaHXlrxbn1O7L1xE8gZEu/sDI/HiG1oik6MAdgDCMX5XRRmjRe5/ON6ykEHrx8Ih\nWiK/f9W1rXaa2Q5SzQAAAZgmLq+wAAAEAwBHMEUCIQDCrbQCn25x7UXEmc0tCNWH\nmHzei5+96lc2+WwKuiRq8AIgMwkih6aNjsWxHrqKZkXTxxgRGGO5rwuNqnPXB79X\n5egAdgCUTkOH+uzB74HzGSQmqBhlAcfTXzgCAT9yZ31VNy4Z2AAAAZgmLq/CAAAE\nAwBHMEUCIQC4V+nzV1zGRGTdI34tlng7BBtRbAAXMsoVkvkMhUi2zAIgcao+J9mx\nONb8q1fhORf56VL2FHGMtdmA2GM1ioJsY3QwDQYJKoZIhvcNAQELBQADggEBAJ49\nv69wIe81vz/fQXXOkZcUy53Vr6tJJ/BVBAGshEfJOfPG7WbVU6MgXg89txYnS6oL\nemPZefrV2Yj27sYtFZqqfP+zVL0v14uykuAYE2HOs17AfMbN1C3L3cBPxRK0MMOy\newz7W509c+nujCgre22H95p8/6GKGOu035LS541W1q7pFJr+Ut2ghid5GTabMcps\nfjcdrqK+eYIlsIhmFMSly9iH7+aeDLC9L0mggvvH1Vysn3EaWODUv8Ksx9SSUiXS\n9nlu/BTTuYGrAiX25LvWxS4s+eSgCswMdhTy9EtVo5A+ydfuuhoPO/qc3qZvrqJ3\nkIZmNUDZ2g9d/+G1uq4=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEszCCA5ugAwIBAgIQCyWUIs7ZgSoVoE6ZUooO+jANBgkqhkiG9w0BAQsFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\nMjAeFw0xNzExMDIxMjI0MzNaFw0yNzExMDIxMjI0MzNaMGAxCzAJBgNVBAYTAlVT\nMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\nb20xHzAdBgNVBAMTFlJhcGlkU1NMIFRMUyBSU0EgQ0EgRzEwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQC/uVklRBI1FuJdUEkFCuDL/I3aJQiaZ6aibRHj\nap/ap9zy1aYNrphe7YcaNwMoPsZvXDR+hNJOo9gbgOYVTPq8gXc84I75YKOHiVA4\nNrJJQZ6p2sJQyqx60HkEIjzIN+1LQLfXTlpuznToOa1hyTD0yyitFyOYwURM+/CI\n8FNFMpBhw22hpeAQkOOLmsqT5QZJYeik7qlvn8gfD+XdDnk3kkuuu0eG+vuyrSGr\n5uX5LRhFWlv1zFQDch/EKmd163m6z/ycx/qLa9zyvILc7cQpb+k7TLra9WE17YPS\nn9ANjG+ECo9PDW3N9lwhKQCNvw1gGoguyCQu7HE7BnW8eSSFAgMBAAGjggFmMIIB\nYjAdBgNVHQ4EFgQUDNtsgkkPSmcKuBTuesRIUojrVjgwHwYDVR0jBBgwFoAUTiJU\nIBiV5uNu5g/6+rkS7QYXjzkwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsG\nAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMDQGCCsGAQUFBwEB\nBCgwJjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEIGA1Ud\nHwQ7MDkwN6A1oDOGMWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEds\nb2JhbFJvb3RHMi5jcmwwYwYDVR0gBFwwWjA3BglghkgBhv1sAQEwKjAoBggrBgEF\nBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzALBglghkgBhv1sAQIw\nCAYGZ4EMAQIBMAgGBmeBDAECAjANBgkqhkiG9w0BAQsFAAOCAQEAGUSlOb4K3Wtm\nSlbmE50UYBHXM0SKXPqHMzk6XQUpCheF/4qU8aOhajsyRQFDV1ih/uPIg7YHRtFi\nCTq4G+zb43X1T77nJgSOI9pq/TqCwtukZ7u9VLL3JAq3Wdy2moKLvvC8tVmRzkAe\n0xQCkRKIjbBG80MSyDX/R4uYgj6ZiNT/Zg6GI6RofgqgpDdssLc0XIRQEotxIZcK\nzP3pGJ9FCbMHmMLLyuBd+uCWvVcF2ogYAawufChS/PT61D9rqzPRS5I2uqa3tmIT\n44JhJgWhBnFMb7AGQkvNq9KNS9dd3GWc17H/dXa1enoxzWjE0hBdFjxPhUb0W3wi\n8o34/m8Fxw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEfjCCA2agAwIBAgIQD+Ayq4RNAzEGxQyOE8iwaDANBgkqhkiG9w0BAQsFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\nQTAeFw0yNDAxMTgwMDAwMDBaFw0zMTExMDkyMzU5NTlaMGExCzAJBgNVBAYTAlVT\nMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\nb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\nq2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\ntCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\nvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo4IBMDCC\nASwwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUTiJUIBiV5uNu5g/6+rkS7QYX\njzkwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDgYDVR0PAQH/BAQD\nAgGGMHQGCCsGAQUFBwEBBGgwZjAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZGln\naWNlcnQuY24wPwYIKwYBBQUHMAKGM2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNu\nL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNydDBABgNVHR8EOTA3MDWgM6Axhi9odHRw\nOi8vY3JsLmRpZ2ljZXJ0LmNuL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDARBgNV\nHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQELBQADggEBAHRBl3jN7+XHBUK0dZnu\nhMdoNwD1nCROU3BTIh1TNzRI0bQ0m5+C/dCRzzlqoSAFHUlOi+OiDltWkXTzmQn6\nZ8bH5PFBy5sYpc/8cNPoSzhyqcpvvEZvv/Ivc0Up+dzma7vBDJC9WrMRUUlSFSQp\nkdXSmphDNkXJsgARmxzc18IN6LYMRiOWlY7RE2F900pPW60BvJHHNCX0bbSRj/Ql\nbmVq8wuftBD++D+RS8K++ujpMjFBROyWfBX+woQDGsMazkmgulQdnZrdj476elOL\naxRvrSgEorju1kJM7M65z2RUZrfzQYW/1rs8mRUXin6iEtad/Rv1ZI1WGYmWPyBm\npbo=\n-----END CERTIFICATE-----\n', '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAx4y43n1JNRIzXjYTD5h2cGejmLQKkCPK+3nrzQxwt8E5yWAu\nkVDB4xvofUCIAqeHJUvQ2g2QbDyH1bueyq0VscCk22SLqAZPT+sVQifdddh+64SA\nMoEDsZvKthSMx+HxOmDoTrk2ZZiMAqfUDPQyhPqom9o9FZI6crj8wduOFT8b571Z\nMY0x7dC3lcZE1kLjromKVbz2vUB0ZIPNjaY4rS8Te8xgiHFAyHosdjuSk0hAMLvh\ni2Dj/c0wcjwx3TjoG8EJax9/qB1ZzxLx59GisspVesU3x2B5qrJ+0OzVF2f/0QBq\nn4NXLLI4PJZ0m/1HDlw/OlMRC3stDH3q3Y8dAQIDAQABAoIBAALU4Gh7aSJGAMrl\nsNWmd9aTYrSMQd+DobUs6MRqWIaDyQzhntxXg8TNlNOc91q1Fo6xxncnPNHx9iOs\nuG2PHeQ9kLYY28vcBdyWgDqSo73kGLSOI9HAcTVTbPTL+MDUvmF+1gg/ikIDTRTn\nKkDImn3qEghSHrTgNBDQ+kWXvR3YI4onZlj7/KBoQPiUFU07bAauYXtzPhrBKX8R\nx/LzAAy/vftzX56tirv+uoHDWd1FF4oXgtnI9wJHfnbVww4MOMvJnHHblSKOlQ8k\ns+ORVfzzUbRdHEC5KfZ+m6mEc/NX0+UF1si8lG516uSFoE25nEvJ1TOr1jWEASfm\n/nxrw2UCgYEA6G8p5t7CAli3eNQe38Gq5Y8dd1eAn1b3KXK2ylT7Sw/kz4L3hZnn\nyoCqRXu8J1Apu6uAvNtPDtYcoIGaV5sucE2v2vUReYla/DjUYbL8sHiRa/RtqFcP\nOw7wP4hH3SIJ19+/IOSk4EEOgV36qHTkYA3EdJrVXnwYGYuKCfODiF0CgYEA28gK\nG288Gq0fXMfxZYRFD6vUJDejp3Bl/aZkojsJDER42UKiJ/SsLAM4vlXOn9Jjit7E\nUbYip3WmMxs5pZDm/FLqwbuR9KD2sUsoU0RIO77krI3d73nWarTfZduWLlB3EpSL\nRMEIEZPTBVsWLnhNUwYeQaePWT5hjmNxN+naTPUCgYEA43j3XxSedkFwvXWLCZy1\nrZfUVJl0LXykwxGbRoTIsftKCLO5niyzJgGpofqodykCzsHXafyPcIum1IZCEH60\ngf/lnLm+cl00iCbt4veSSPqWBzrdnqQlKb1C7kpnWkkXrQcyO/TITxgcJAtfZkGP\nx1lx7FQjgSadR1LCs154WZkCgYAgx+pyAxpU5EOMRCpGOP9QAhIHQOFBBSvBNINI\nC0QjN0dLKHwlN4JCokxhX00NW4ULXoSrp13t2kdq0gG8Jyu8sOVg9ggkE6oX8ZzX\nJpUbjDJxZkuW/3Yyvj2f5Ku2xrur4soO/Dem34SXBOn/DmSCIKR8qHBiIELOyRhG\n/bNYzQKBgQCeQJBjMlID54OOvqN59+Ai4IJJZOFgxaCHh8p5SA4tgJwMTXo1U0NL\nrw2XMlcBC8B6R03siX8CHcRfSxfdhDXm36PTF/5ID7spdh31REOuRsEApsuYwyOt\n8a176FagV8IYNhTNYioLrvYfB3NgRjnJiAeJarFRSA1LPZdjSjTtag==\n-----END RSA PRIVATE KEY-----\n', '112233', '112233', '{\"issuer\": \"CN=RapidSSL TLS RSA CA G1,OU=www.digicert.com,O=DigiCert Inc,C=US\", \"subject\": \"CN=soccerlife.com.cn\", \"version\": 3, \"keyUsage\": \"[true, false, true, false, false, false, false, false, false]\", \"notAfter\": 1784505599000, \"notBefore\": 1752969600000, \"chainLength\": 3, \"serialNumber\": \"e5725e29394a4548fc223b96041f0be\", \"extendedKeyUsage\": [\"1.3.6.1.5.5.7.3.1\", \"1.3.6.1.5.5.7.3.2\"], \"publicKeyAlgorithm\": \"RSA\", \"signatureAlgorithm\": \"SHA256withRSA\", \"subjectAlternativeNames\": [\"soccerlife.com.cn\", \"www.soccerlife.com.cn\"]}', '2026-07-20 07:59:59', '证书', '2025-07-24 19:31:38', '2025-07-28 13:49:37', 'test'); +INSERT INTO `iot_certificate` (`id`, `ssl_key`, `name`, `cert_content`, `key_content`, `cert_password`, `key_password`, `cert_info`, `expire_time`, `remark`, `create_time`, `update_time`, `create_user`) VALUES (5, '68878a3f38fc3fc98f6ec1c2', '192886.xyz', '-----BEGIN CERTIFICATE-----\nMIIFCTCCA/GgAwIBAgISBh5YBq0fDvHG+QfuSUObDmDqMA0GCSqGSIb3DQEBCwUA\nMDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD\nEwNSMTEwHhcNMjUwNzI4MTEzMzM1WhcNMjUxMDI2MTEzMzM0WjAXMRUwEwYDVQQD\nDAwqLjE5Mjg4Ni54eXowggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCe\nZYpCQA2t+TuKC2rvi0m7OAp97zEy5MLS6/6ZRnho9YdpLw5p1Qo0hfXChQcRaGTR\nmQlhmubv0CEhA1UXqQcam+IbQ0nk6bJ3Fz5VFU2pdfvzOLDXQ9hxOLVcrqKbylE+\n2dkQItXtwT/RDJqwon22jeZtQeBn5+SzzwBWPQQlWRDRMC5QRTfg09BD94ZJdvL7\nkLbWHK72KtROkz3G84yvHnUBxaIX1rp5aC18NqN+J1W81cho8fbmWancM5LT0B9P\nduqEFnWLZg5heT59kcafW0upmk/ifV2LV407p7J0JhlyU8swtBVqUHAdPcm8wyHV\nEStCQyajwxmyehbCDBCrAgMBAAGjggIxMIICLTAOBgNVHQ8BAf8EBAMCBaAwHQYD\nVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0O\nBBYEFOYlP9rNteROmWK4ZVeOIriD7lDXMB8GA1UdIwQYMBaAFMXPRqTq9MPAemyV\nxC2wXpIvJuO5MDMGCCsGAQUFBwEBBCcwJTAjBggrBgEFBQcwAoYXaHR0cDovL3Ix\nMS5pLmxlbmNyLm9yZy8wKQYDVR0RBCIwIIIMKi4xOTI4ODYueHl6ghAqLmlvdC4x\nOTI4ODYueHl6MBMGA1UdIAQMMAowCAYGZ4EMAQIBMC8GA1UdHwQoMCYwJKAioCCG\nHmh0dHA6Ly9yMTEuYy5sZW5jci5vcmcvMTA3LmNybDCCAQYGCisGAQQB1nkCBAIE\ngfcEgfQA8gB3AKRCxQZJYGFUjw/U6pz7ei0mRU2HqX8v30VZ9idPOoRUAAABmFEE\nyrgAAAQDAEgwRgIhAImSDGmzwAF4JrEohgn9zujI/nyREjtxLTQ1jcEnBhWeAiEA\nyyml64Vh1Y9mTlGsj6v2sYFuiXNnUbzb/cE1KU0kI8YAdwDM+w9qhXEJZf6Vm1PO\n6bJ8IumFXA2XjbapflTA/kwNsAAAAZhRBMrUAAAEAwBIMEYCIQCOrCQeHoCnUUE2\nQV/3KzaQs/ig9FqGR/nTjC3agBRzfQIhAILx1LyEQ8jeJ3AP6ZLwnr31LBv2qJ4D\n8c8y2Gz/F1WmMA0GCSqGSIb3DQEBCwUAA4IBAQB//Iv2AevaA97+Glo2WtkeRjdk\n7AWuWsyR2tgQjV4XFD/SfKMH8aHpKYymhJCJV1MjUZXWWfetNQIxnEX7d3rmd8TR\nBMwI2ZMta0m9lwmaWipbkDZn18xwI/aD6vnE8uAr0pyBXipAgNJGY15QG4JYOOAT\npxCHP9j73n1jn8x7wRMrEmvxUdpkvFNwCrlJkyUtvYT3q/F1nWEVh/DhwyPuTdjd\nqOQc1CHGt9FV7jWtVE/dS8qpgAttBBc8bdP0W0u5CJNoB2aN+HsRVvx+6EdtFGIN\nLjnx2fpyCW6XXameewoP8QCdydfVIqkwsghpPSUp2oqil5vqanUnqtrNh97f\n-----END CERTIFICATE-----\n\n-----BEGIN CERTIFICATE-----\nMIIFBjCCAu6gAwIBAgIRAIp9PhPWLzDvI4a9KQdrNPgwDQYJKoZIhvcNAQELBQAw\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw\nWhcNMjcwMzEyMjM1OTU5WjAzMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg\nRW5jcnlwdDEMMAoGA1UEAxMDUjExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAuoe8XBsAOcvKCs3UZxD5ATylTqVhyybKUvsVAbe5KPUoHu0nsyQYOWcJ\nDAjs4DqwO3cOvfPlOVRBDE6uQdaZdN5R2+97/1i9qLcT9t4x1fJyyXJqC4N0lZxG\nAGQUmfOx2SLZzaiSqhwmej/+71gFewiVgdtxD4774zEJuwm+UE1fj5F2PVqdnoPy\n6cRms+EGZkNIGIBloDcYmpuEMpexsr3E+BUAnSeI++JjF5ZsmydnS8TbKF5pwnnw\nSVzgJFDhxLyhBax7QG0AtMJBP6dYuC/FXJuluwme8f7rsIU5/agK70XEeOtlKsLP\nXzze41xNG/cLJyuqC0J3U095ah2H2QIDAQABo4H4MIH1MA4GA1UdDwEB/wQEAwIB\nhjAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwEgYDVR0TAQH/BAgwBgEB\n/wIBADAdBgNVHQ4EFgQUxc9GpOr0w8B6bJXELbBeki8m47kwHwYDVR0jBBgwFoAU\nebRZ5nu25eQBc4AIiMgaWPbpm24wMgYIKwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAC\nhhZodHRwOi8veDEuaS5sZW5jci5vcmcvMBMGA1UdIAQMMAowCAYGZ4EMAQIBMCcG\nA1UdHwQgMB4wHKAaoBiGFmh0dHA6Ly94MS5jLmxlbmNyLm9yZy8wDQYJKoZIhvcN\nAQELBQADggIBAE7iiV0KAxyQOND1H/lxXPjDj7I3iHpvsCUf7b632IYGjukJhM1y\nv4Hz/MrPU0jtvfZpQtSlET41yBOykh0FX+ou1Nj4ScOt9ZmWnO8m2OG0JAtIIE38\n01S0qcYhyOE2G/93ZCkXufBL713qzXnQv5C/viOykNpKqUgxdKlEC+Hi9i2DcaR1\ne9KUwQUZRhy5j/PEdEglKg3l9dtD4tuTm7kZtB8v32oOjzHTYw+7KdzdZiw/sBtn\nUfhBPORNuay4pJxmY/WrhSMdzFO2q3Gu3MUBcdo27goYKjL9CTF8j/Zz55yctUoV\naneCWs/ajUX+HypkBTA+c8LGDLnWO2NKq0YD/pnARkAnYGPfUDoHR9gVSp/qRx+Z\nWghiDLZsMwhN1zjtSC0uBWiugF3vTNzYIEFfaPG7Ws3jDrAMMYebQ95JQ+HIBD/R\nPBuHRTBpqKlyDnkSHDHYPiNX3adPoPAcgdF3H2/W0rmoswMWgTlLn1Wu0mrks7/q\npdWfS6PJ1jty80r2VKsM/Dj3YIDfbjXKdaFU5C+8bhfJGqU3taKauuz0wHVGT3eo\n6FlWkWYtbt4pgdamlwVeZEW+LM7qZEJEsMNPrfC03APKmZsJgpWCDWOKZvkZcvjV\nuYkQ4omYCTX5ohy+knMjdOmdH9c7SpqEWBDC86fiNex+O0XOMEZSa8DA\n-----END CERTIFICATE-----\n', '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAnmWKQkANrfk7igtq74tJuzgKfe8xMuTC0uv+mUZ4aPWHaS8O\nadUKNIX1woUHEWhk0ZkJYZrm79AhIQNVF6kHGpviG0NJ5Omydxc+VRVNqXX78ziw\n10PYcTi1XK6im8pRPtnZECLV7cE/0QyasKJ9to3mbUHgZ+fks88AVj0EJVkQ0TAu\nUEU34NPQQ/eGSXby+5C21hyu9irUTpM9xvOMrx51AcWiF9a6eWgtfDajfidVvNXI\naPH25lmp3DOS09AfT3bqhBZ1i2YOYXk+fZHGn1tLqZpP4n1di1eNO6eydCYZclPL\nMLQValBwHT3JvMMh1RErQkMmo8MZsnoWwgwQqwIDAQABAoIBAEfhkp3ADbUuzps1\n1UCB+GLS6sf9vVj4QYWQfUi3lzsieWH9bdAPUwNE66Nr1SpMMrx/c6dssXKRBAfE\nSGBlX4juxuxSW/1bJJcw4COieEz38u+MgYwfCOFQt9UolziV1srr97p/5I9xzHEl\nBCo2g1BOEwzwAVOGA96piOCZ7gC4NMdBMm8OWqjOUAopqV15dsaIZ9A7YwPf7qmN\n8ntWGlaD/ZTn8F/xJt370/AhfzvMGnvyl0TC02mg91dl4KcCui4jFva2HYZnlH/a\nlMYXPsQJC5Eds7gN8Sd1nnwFX27oT7nSGbSKS3D6E2Yrd8yAxfHzYHQpdnE+YHgK\nR4lPlYECgYEAxylBeilj/mOjJAVVexhHoWCBLvVc1n4NCXSFu2IhtfaraeGI8/Q+\nKWSe1rmAxKONmJpe3RtEU+rwlg8HrtN7TkbpOZhaiTsfkiOwVZpjLrRm4hxFmSMk\nhDIqNp4KCwd1iLtcu8k4wYrBFShjvpXUVBtA+9vpmOmwFgjhOBrshmsCgYEAy5oF\nzy+yInQeL1f3xXXd4TLP2TKOA4fwC/bkSXpGBNonc0VOQS86naxK0wv3Y4eV44Y3\nx7vNvd5SbBqqCQYT9M7WPnvghVGQyY9idMO2WImLK4hYQvoJc0aCUnYXO6vw3hin\nnZ0+v0bzWNqkgRgj1Nr3lDDR0H+3SzA9eAKarsECgYEAt0ehKSHkEpxw6to5wzEh\n4kHkQGmuiPmxV60y0LHLIiChuKvEE/UGK6OchTBrbwqwaOpPOJ06MRv5Ua79PAcE\n55FcWhN55eZR9HQBd5W/hqCgOZ8Y8YekuD9OF7sytNmlcTIzceEI6Rue+rCTsjVC\nFDCMifrkK0OLN4vub2pflocCgYA6avMHw3OlFxQiGWdqLKuBFgc1FqBQuwxRIoHV\n41N7yOxQ0q3lXhu/adP+Qgy1bg2/vp76nBQoSkXz5qKq/Cd1snMecoS1O/YDHzTs\ns8r6FFzuHKzWas+TV8PEzUVVRbxSvVg6jQeX8k+YSwOwVrDRPhKTXn6VCFAbLrg0\nWurvAQKBgH9DEAy0UUdPMwlrbOMd//c5hSlojJU230+U/CH1HofRYLHAlixOr8EP\nwrH1TNowgCFEEv6AaW+ovX9UHeVQxXbg93wZyIgYMikykzwgeCck0K8LT4UYaaen\nWYw6Dd6JqqOrl6VLxjhwiyyeznXcN2DRAC4j8dxRQmFSTFOBb7Xn\n-----END RSA PRIVATE KEY-----\n', '1122', '1122', '{\"issuer\": \"CN=R11,O=Let\'s Encrypt,C=US\", \"subject\": \"CN=*.192886.xyz\", \"version\": 3, \"keyUsage\": \"[true, false, true, false, false, false, false, false, false]\", \"notAfter\": 1761478414000, \"notBefore\": 1753702415000, \"chainLength\": 2, \"serialNumber\": \"61e5806ad1f0ef1c6f907ee49439b0e60ea\", \"extendedKeyUsage\": [\"1.3.6.1.5.5.7.3.1\", \"1.3.6.1.5.5.7.3.2\"], \"publicKeyAlgorithm\": \"RSA\", \"signatureAlgorithm\": \"SHA256withRSA\", \"subjectAlternativeNames\": [\"*.192886.xyz\", \"*.iot.192886.xyz\"]}', '2025-10-26 19:33:34', '*.iot.192886.xyz', '2025-07-28 22:33:35', '2025-07-28 22:33:35', 'iot'); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_dashboard_statistics +-- ---------------------------- +DROP TABLE IF EXISTS `iot_dashboard_statistics`; +CREATE TABLE `iot_dashboard_statistics` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `stat_date` date NOT NULL COMMENT '统计日期', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '产品Key,NULL表示全产品', + `channel` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '推送渠道,NULL表示全渠道', + `metric_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '指标类型(device_total/device_online/message_total/message_success/message_failed)', + `metric_value` bigint NOT NULL DEFAULT '0' COMMENT '指标值', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_date_product_channel_metric` (`stat_date`,`product_key`,`channel`,`metric_type`) +) ENGINE=InnoDB AUTO_INCREMENT=686 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='仪表盘统计表'; + + +-- ---------------------------- +-- Table structure for iot_device +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device`; +CREATE TABLE `iot_device` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '本平台设备唯一标识符', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '第三方平台设备ID唯一标识符', + `derive_metadata` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '派生元数据,有的设备的属性,功能,事件可能会动态的添加', + `configuration` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '其他配置', + `product_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '产品名称', + `nick_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '别名', + `features` bigint DEFAULT NULL, + `gw_product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '网关产品ProductKey', + `device_secret` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '1设备密钥', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '产品实例', + `device_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '设备实例名称', + `creator_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '实例名称', + `application` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '归属应用', + `creator_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `state` tinyint(1) DEFAULT NULL COMMENT '0-离线,1-在线', + `detail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '说明', + `create_time` bigint unsigned DEFAULT NULL COMMENT '创建时间', + `registry_time` bigint unsigned DEFAULT NULL COMMENT '激活时间', + `online_time` bigint unsigned DEFAULT NULL COMMENT '最后通信时间', + `areas_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '区域ID', + `coordinate` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '坐标', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `idx_device_id` (`product_key`,`device_id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_device_name` (`device_name`) USING BTREE, + KEY `idx_creator_id` (`creator_id`) USING BTREE, + KEY `idx_online` (`product_key`,`state`,`online_time`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=57 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='设备表'; + +-- ---------------------------- +-- Records of iot_device +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (1, 'fa1b39fb287a42dd8a0f2cbae0d698d2', '202504150038', NULL, NULL, '{\"meterNo\":\"202504150038\"}', '单相导轨XA-GXSD81(4G)', NULL, NULL, NULL, NULL, 'abcxassxnanahsman', '园区1幢小房间总表', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1750734595, 1750734756, 1752217002, '330113003', '120.23475,31.207561'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (3, '271232e01d17496f86ab925bdc56eda2', 'cz001923', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', '无声飞插', 'test', NULL, '1938504461874020352', NULL, 0, '一个智能模拟操作', 1751109708, 1752313407, 1752314963, '330212105', '120.23475,31.207561'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (6, '9217646384fd46b8816359f03d5802ac', 'ov0000112', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', '小小插座', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1751119477, 1752300564, 1752314319, '320506', '31.207561,120.23475'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (16, '433d68787b2b4fd08dec8fa8ffc60f79', '202505220093', NULL, NULL, '{}', '单相导轨XA-GXSD81(4G)', NULL, NULL, NULL, NULL, 'abcxassxnanahsman', '202505220093', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752038771, 1752038778, 1754630784, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (17, '58479b07927342b6bd12c9dfb988e6c9', '202411190247', NULL, NULL, '{}', '单相导轨XA-GXSD81(4G)', NULL, NULL, NULL, NULL, 'abcxassxnanahsman', '顶楼灯泡照明表', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752040116, 1752040126, 1754630580, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (18, '29727447e38d49d6a9154aa2dafb47b5', '7g0000117', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', '7g0000117', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752315969, 1752315975, 1754134955, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (19, '8b0679d5b2054c3389994a905998ccaa', 'so0000132', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'so0000132', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752315969, 1752315975, 1754134955, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (20, '903071db080c4267900a6ab1e159b5a1', 'xh0000101', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'xh0000101', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752315969, 1752315975, 1754134955, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (21, 'c23296b6e84b489290a33a0f0bf33b17', 'ov0000411', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'ov0000411', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752316154, 1752316157, 1752392225, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (22, 'e8517d0ff4984431b2f0874e5ae8a29e', 'ov0001411', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'ov0001411', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752316175, 1752316181, 1752407597, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (23, '4db2d96130964fc49258e4eef0cc55f1', 'ov00014113', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'ov00014113', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752404352, 1752404366, 1756191802, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (24, 'd2dd545dc6b541e9b277de24a8237d4d', 'ov00014116', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'ov00014116', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752404354, 1752404366, 1756191802, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (25, '05a9960673fd47e587b041ff1018898a', 'ov00014110', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'ov00014110', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752404362, 1752404402, 1756191802, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (26, 'c0638ea17c5a4bcf9ad66a2a80ef0ab4', 'ov00014114', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'ov00014114', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752404364, 1752404402, 1756191802, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (27, '52956f3a0bac430aa3f9ce2fd30f83fa', 'ov00014118', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'ov00014118', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752404366, 1752404390, 1756191802, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (28, '82b178e4cff840a4b3128da81e0718d0', 'ov00014115', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'ov00014115', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752404368, 1752404378, 1756191802, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (29, '962ad84e0d12407ebad3cffbb3137e1e', 'ov00014111', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'ov00014111', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752404370, 1752404390, 1756191802, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (30, '7af18836921a48e9aedbb33551032a8a', 'ov00014117', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', '潜江雅园1号楼底商店铺', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752404372, 1752404390, 1756191802, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (31, 'ab13d2f27d164648adf7d3aa6478bb09', 'ov00014119', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'ov00014119', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752404376, 1752404402, 1756191802, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (32, 'c64f520c8a2e4c9698c6c8f0521db713', 'ov00014112', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', '小导向', 'test', NULL, '1938504461874020352', NULL, 0, NULL, 1752404424, 1752404462, 1756191802, '542521', '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (34, 'e33678b0ba8d48ec995c8ae9a823a6eb', '00010107', NULL, NULL, '{}', '开关面板-文档版', NULL, NULL, NULL, NULL, '38fc80e', '文档设备', 'test', NULL, NULL, NULL, 0, NULL, 1753802579, 1753765134, 1754144297, '542521', '120.291848,30.448023'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (35, 'ff49e3ac84e54b659d6b4d31aca35554', '00010108', NULL, NULL, '{}', '开关面板-文档版', NULL, NULL, NULL, NULL, '38fc80e', '00010108', 'auto_register', NULL, NULL, NULL, 0, NULL, 1754039014, 1754039017, 1754713024, NULL, '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (36, '0847c610465e41a38839daab0fe36af6', '00020107', NULL, NULL, '{}', '开关面板-文档版', NULL, NULL, NULL, NULL, '38fc80e', '00020107', 'auto_register', NULL, NULL, NULL, 0, NULL, 1754039365, 1754039366, 1754787075, NULL, '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (37, '8019b103c9c54ab2a59153b99b301c5b', '00050107', NULL, NULL, '{}', '开关面板-文档版', NULL, NULL, NULL, NULL, '38fc80e', '00050107', 'auto_register', NULL, NULL, NULL, 0, NULL, 1754041958, 1754041966, 1754181219, NULL, '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (38, '77ff448819014018b4566d99cd0b11cc', 'docsmqtt001', NULL, NULL, '{}', '开关面板-MQTT-doc编写', NULL, NULL, NULL, NULL, '688cd92fdb9e0158872f4aa9', 'docsmqtt001', 'auto_register', NULL, NULL, NULL, 0, NULL, 1754064092, 1754064101, 1754094454, NULL, '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (39, '828f2a663ba54f1a970904d966cd6b56', 'docsmqtt002', NULL, NULL, '{}', '开关面板-MQTT-doc编写', NULL, NULL, NULL, NULL, '688cd92fdb9e0158872f4aa9', 'docsmqtt002', 'auto_register', NULL, NULL, NULL, 0, NULL, 1754095004, 1754095009, 1754095751, NULL, '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (40, '0ebb38fd253c4c4b8bf9a3eafc4ddba0', 'docsmqtt003', NULL, NULL, '{}', '开关面板-MQTT-doc编写', NULL, NULL, NULL, NULL, '688cd92fdb9e0158872f4aa9', 'docsmqtt003', 'auto_register', NULL, NULL, NULL, 0, NULL, 1754096022, 1754096034, 1754096094, NULL, '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (41, '1ba1da4813164c0faedd99279dddb670', 'docsmqtt004', NULL, NULL, '{}', '开关面板-MQTT-doc编写', NULL, NULL, NULL, NULL, '688cd92fdb9e0158872f4aa9', '文档编写004', 'test', NULL, NULL, NULL, 0, NULL, 1754208378, 1754096238, 1755011523, NULL, '123.778233,41.899421'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (46, 'd9331ad5d9ef49c6ad0b51ab3c92681d', '202507070079', NULL, NULL, '{}', '单相导轨XA-GXSD81(4G)', NULL, NULL, NULL, NULL, 'abcxassxnanahsman', '202507070079', 'auto_register', NULL, NULL, NULL, 0, NULL, 1754787197, 1754787207, 1755226964, NULL, '81.314141,31.081598'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (47, '95b5117486114b639c0a644d92034042', 'DT2025001', NULL, NULL, '{}', '网关DTU(面板)', NULL, NULL, NULL, NULL, 'lB0Tl1rgf8uz', '1层网关', 'test', NULL, NULL, NULL, 0, NULL, 1754811100, NULL, NULL, NULL, '84.74627,36.478393'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (51, '9b7dfcb1974841b19c31df1dda4e1b5d', '1001', 'DT2025001', NULL, '{}', 'DTU(面板)子设备', NULL, NULL, 'lB0Tl1rgf8uz', NULL, 'lipSSVUj9XLt', '1001', 'test', NULL, NULL, NULL, 0, NULL, 1754815493, NULL, NULL, NULL, '120.291848,30.448023'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (52, '12307328738c4c3ebf34a2da8d27af0b', '1002', 'DT2025001', NULL, '{}', 'DTU(面板)子设备', NULL, NULL, 'lB0Tl1rgf8uz', NULL, 'lipSSVUj9XLt', '1002', 'test', NULL, NULL, NULL, 0, NULL, 1754815525, NULL, NULL, NULL, '104.057932,40.624518'); +INSERT INTO `iot_device` (`id`, `iot_id`, `device_id`, `ext_device_id`, `derive_metadata`, `configuration`, `product_name`, `nick_name`, `features`, `gw_product_key`, `device_secret`, `product_key`, `device_name`, `creator_id`, `instance`, `application`, `creator_name`, `state`, `detail`, `create_time`, `registry_time`, `online_time`, `areas_id`, `coordinate`) VALUES (55, '9cedf37c74ba418c8dca64a2c23a612f', 'ov0000004', NULL, NULL, '{}', '智能插座', NULL, NULL, NULL, NULL, '681c0775c2dc427d0480ab5f', 'ov0000004', 'auto_register', NULL, NULL, NULL, 0, NULL, 1755419988, 1755419990, 1756215123, NULL, '81.314141,31.081598'); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_fence_rel +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_fence_rel`; +CREATE TABLE `iot_device_fence_rel` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `fence_id` bigint NOT NULL COMMENT '围栏id', + `iot_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备唯一标识符', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备序列号', + `creator_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人', + `create_date` datetime DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='设备和围栏中间表'; + +-- ---------------------------- +-- Records of iot_device_fence_rel +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_function_history +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_function_history`; +CREATE TABLE `iot_device_function_history` ( + `id` bigint NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '产品key', + `iot_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备序列号', + `device_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `ext_param` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '{}' COMMENT '额外参数', + `down_state` tinyint DEFAULT NULL COMMENT '指令配置状态 0.待下发;1.下发中;2.已下发', + `down_result` tinyint DEFAULT NULL COMMENT '下发结果 0.失败 1.成功', + `down_error` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '异常信息', + `task_id` bigint DEFAULT NULL COMMENT '所属任务id', + `update_time` datetime DEFAULT NULL COMMENT '下发时间', + `retry` int DEFAULT '0' COMMENT '下发次数', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='设备功能批量下发历史'; + +-- ---------------------------- +-- Records of iot_device_function_history +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_function_history` (`id`, `product_key`, `iot_id`, `device_id`, `device_name`, `ext_param`, `down_state`, `down_result`, `down_error`, `task_id`, `update_time`, `retry`) VALUES (1925489900741828608, '67c69f2a6b0001a9b22a6c9a', 'f7690d50ba1d497caf8b64cf3f4f7d91', '202411190247', '5楼办公室', NULL, 2, 0, 'deviceId不存在', 1925489900725051392, '2025-06-29 18:26:58', 2); +INSERT INTO `iot_device_function_history` (`id`, `product_key`, `iot_id`, `device_id`, `device_name`, `ext_param`, `down_state`, `down_result`, `down_error`, `task_id`, `update_time`, `retry`) VALUES (1939308928588496896, '681c0775c2dc427d0480ab5f', NULL, 'gg0000181', '冰河路35', NULL, 2, 1, 'success', 1939308928533970944, '2025-06-29 21:04:23', 2); +INSERT INTO `iot_device_function_history` (`id`, `product_key`, `iot_id`, `device_id`, `device_name`, `ext_param`, `down_state`, `down_result`, `down_error`, `task_id`, `update_time`, `retry`) VALUES (1948016860616392704, '681c0775c2dc427d0480ab5f', NULL, 'ov00014117', '潜江雅园1号楼底商店铺', NULL, 2, 1, 'success', 1948016860553478144, '2025-07-31 22:27:28', 2); +INSERT INTO `iot_device_function_history` (`id`, `product_key`, `iot_id`, `device_id`, `device_name`, `ext_param`, `down_state`, `down_result`, `down_error`, `task_id`, `update_time`, `retry`) VALUES (1948016860616392705, '681c0775c2dc427d0480ab5f', NULL, 'ov0001411', 'ov0001411', NULL, 2, 1, 'success', 1948016860553478144, '2025-07-31 22:27:33', 2); +INSERT INTO `iot_device_function_history` (`id`, `product_key`, `iot_id`, `device_id`, `device_name`, `ext_param`, `down_state`, `down_result`, `down_error`, `task_id`, `update_time`, `retry`) VALUES (1948016860616392706, '681c0775c2dc427d0480ab5f', NULL, '7g0000117', '7g0000117', NULL, 2, 1, 'success', 1948016860553478144, '2025-07-31 22:27:38', 2); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_function_task +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_function_task`; +CREATE TABLE `iot_device_function_task` ( + `id` bigint NOT NULL, + `task_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务名称', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `begin_time` datetime DEFAULT NULL COMMENT '任务开始时间', + `end_time` datetime DEFAULT NULL COMMENT '任务结束时间', + `creator` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人姓名', + `creator_id` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人', + `command` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '功能指令', + `command_data` varchar(800) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令内容', + `status` tinyint DEFAULT NULL COMMENT '状态 0.待执行;1.已执行;2.正在执行', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `dev_function_task_id_uindex` (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='设备功能批量下发任务表'; + +-- ---------------------------- +-- Records of iot_device_function_task +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_function_task` (`id`, `task_name`, `product_key`, `begin_time`, `end_time`, `creator`, `creator_id`, `command`, `command_data`, `status`) VALUES (1925489900725051392, '鹏辉单相导轨表DDSY1986-4G清表', '67c69f2a6b0001a9b22a6c9a', '2025-05-22 17:52:21', '2025-05-22 17:52:21', 'liulihai', 'liulihai', 'reset', '{\"function\":\"reset\",\"messageType\":\"FUNCTIONS\",\"data\":{\"function\":\"reset\"}}', 1); +INSERT INTO `iot_device_function_task` (`id`, `task_name`, `product_key`, `begin_time`, `end_time`, `creator`, `creator_id`, `command`, `command_data`, `status`) VALUES (1939308928533970944, '智能插座开关控制', '681c0775c2dc427d0480ab5f', '2025-06-29 21:04:14', '2025-06-29 21:04:14', 'iot', 'iot', 'switchControl', '{\"function\":\"switchControl\",\"messageType\":\"FUNCTIONS\",\"data\":{\"function\":\"switchControl\",\"open\":\"0\"}}', 1); +INSERT INTO `iot_device_function_task` (`id`, `task_name`, `product_key`, `begin_time`, `end_time`, `creator`, `creator_id`, `command`, `command_data`, `status`) VALUES (1948016860553478144, '智能插座新开', '681c0775c2dc427d0480ab5f', '2025-07-23 21:46:27', '2025-07-23 21:46:37', 'iot', 'iot', 'newkai', '{\"function\":\"newkai\",\"messageType\":\"FUNCTIONS\",\"data\":{\"function\":\"newkai\",\"docs\":123}}', 1); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_geo_fence +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_geo_fence`; +CREATE TABLE `iot_device_geo_fence` ( + `id` bigint NOT NULL COMMENT 'id', + `name` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '围栏名称', + `status` tinyint DEFAULT NULL COMMENT '围栏状态 0.启用 1.停用', + `touch_way` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '触发模式 in.进入 out.离开 all.进入&离开', + `fence` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '范围', + `type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '类型 circle.圆 polygon.多边形', + `point` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '圆形中心点', + `radius` decimal(16,8) DEFAULT NULL COMMENT '半径', + `creator_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人', + `create_date` datetime DEFAULT NULL COMMENT '创建时间', + `update_date` datetime DEFAULT NULL COMMENT '更新时间', + `week_time` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '每周触发时间(天)', + `begin_time` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '每天开始时间', + `end_time` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '每天结束时间', + `creator_user` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '归属第三方应用', + `no_trigger_time` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '不触发时间段', + `delay_time` int DEFAULT NULL COMMENT '围栏触发延迟时间(分钟)', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='设备围栏表'; + +-- ---------------------------- +-- Records of iot_device_geo_fence +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_group +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_group`; +CREATE TABLE `iot_device_group` ( + `id` bigint NOT NULL COMMENT '分组ID,非自增', + `group_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '分组名称', + `group_code` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '分组标识', + `group_describe` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '群组描述', + `parent_id` bigint DEFAULT '0' COMMENT '父id', + `group_level` tinyint(1) NOT NULL DEFAULT '0' COMMENT '分组级别', + `has_child` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否有子分组', + `active_dev_count` int(10) unsigned zerofill DEFAULT NULL COMMENT '关联设备树', + `relat_dev_count` int(10) unsigned zerofill DEFAULT NULL COMMENT '激活设备数', + `tag` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '标签', + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例编号', + `creator_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL, + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='设备分组表'; + +-- ---------------------------- +-- Records of iot_device_group +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_group` (`id`, `group_name`, `group_code`, `group_describe`, `parent_id`, `group_level`, `has_child`, `active_dev_count`, `relat_dev_count`, `tag`, `instance`, `creator_id`, `create_time`, `update_time`) VALUES (1949364879098044416, '1楼消控室(烟感)', NULL, '消控室烟感~', 0, 1, 1, NULL, NULL, NULL, NULL, 'iot', NULL, '2025-07-31 12:09:07'); +INSERT INTO `iot_device_group` (`id`, `group_name`, `group_code`, `group_describe`, `parent_id`, `group_level`, `has_child`, `active_dev_count`, `relat_dev_count`, `tag`, `instance`, `creator_id`, `create_time`, `update_time`) VALUES (1950447085375692800, '牛逼分组', NULL, '不要问我为什么牛逼', 0, 1, 0, NULL, NULL, NULL, NULL, 'test', NULL, NULL); +INSERT INTO `iot_device_group` (`id`, `group_name`, `group_code`, `group_describe`, `parent_id`, `group_level`, `has_child`, `active_dev_count`, `relat_dev_count`, `tag`, `instance`, `creator_id`, `create_time`, `update_time`) VALUES (1950770676340260864, '508小机房', NULL, '人员杂乱,小心火烛\n', 1949364879098044416, 2, 0, NULL, NULL, NULL, NULL, 'iot', NULL, '2025-07-31 12:09:47'); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_history +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_history`; +CREATE TABLE `iot_device_history` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '设备序列号', + `device_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '产品唯一标识', + `coordinate` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '坐标(经纬度)', + `first_online_time` bigint DEFAULT NULL COMMENT '上线时间', + `creater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备创建者', + `delete_time` bigint DEFAULT NULL COMMENT '设备删除时间', + `create_time` bigint DEFAULT NULL COMMENT '设备创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_history +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log`; +CREATE TABLE `iot_device_log` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `company_no` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `protocol` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '协议', + `device_node` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '节点类型', + `classified_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `command_id` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT NULL COMMENT '指令状态', + `org_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `event` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件名称', + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_device_product_Key` (`device_id`,`product_key`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_0 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_0`; +CREATE TABLE `iot_device_log_0` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_0 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_1 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_1`; +CREATE TABLE `iot_device_log_1` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件名称', + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_1 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_10 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_10`; +CREATE TABLE `iot_device_log_10` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iotid_messagetype_createtime` (`iot_id`,`message_type`,`create_time`) USING BTREE COMMENT 'create by DAS-060eacd1-5b7c-4e1b-af2d-2db322aeada8', + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_10 +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_log_10` (`id`, `iot_id`, `device_id`, `ext_device_id`, `product_key`, `device_name`, `message_type`, `command_id`, `command_status`, `event`, `create_id`, `instance`, `point`, `content`, `create_time`) VALUES (2, '82b178e4cff840a4b3128da81e0718d0', 'ov00014115', '', '681c0775c2dc427d0480ab5f', 'ov00014115', 'EVENT', NULL, 0, 'offline', NULL, '0', '81.314141,31.081598', '{}', 1756305300); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_11 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_11`; +CREATE TABLE `iot_device_log_11` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_11 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_12 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_12`; +CREATE TABLE `iot_device_log_12` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_12 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_13 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_13`; +CREATE TABLE `iot_device_log_13` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_13 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_14 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_14`; +CREATE TABLE `iot_device_log_14` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_14 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_15 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_15`; +CREATE TABLE `iot_device_log_15` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_create_date_product_key` (`create_time`,`product_key`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_15 +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_log_15` (`id`, `iot_id`, `device_id`, `ext_device_id`, `product_key`, `device_name`, `message_type`, `command_id`, `command_status`, `event`, `create_id`, `instance`, `point`, `content`, `create_time`) VALUES (2, '4db2d96130964fc49258e4eef0cc55f1', 'ov00014113', '', '681c0775c2dc427d0480ab5f', 'ov00014113', 'EVENT', NULL, 0, 'offline', NULL, '0', '81.314141,31.081598', '{}', 1756305300); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_16 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_16`; +CREATE TABLE `iot_device_log_16` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件名称', + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_16 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_17 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_17`; +CREATE TABLE `iot_device_log_17` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件名称', + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_17 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_18 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_18`; +CREATE TABLE `iot_device_log_18` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件名称', + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_18 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_19 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_19`; +CREATE TABLE `iot_device_log_19` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件名称', + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_19 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_2 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_2`; +CREATE TABLE `iot_device_log_2` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件名称', + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_2 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_20 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_20`; +CREATE TABLE `iot_device_log_20` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件名称', + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_20 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_21 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_21`; +CREATE TABLE `iot_device_log_21` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件名称', + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_21 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_22 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_22`; +CREATE TABLE `iot_device_log_22` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件名称', + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_22 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_23 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_23`; +CREATE TABLE `iot_device_log_23` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件名称', + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_23 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_3 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_3`; +CREATE TABLE `iot_device_log_3` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件名称', + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_3 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_4 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_4`; +CREATE TABLE `iot_device_log_4` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_4 +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_log_4` (`id`, `iot_id`, `device_id`, `ext_device_id`, `product_key`, `device_name`, `message_type`, `command_id`, `command_status`, `event`, `create_id`, `instance`, `point`, `content`, `create_time`) VALUES (2, 'ab13d2f27d164648adf7d3aa6478bb09', 'ov00014119', '', '681c0775c2dc427d0480ab5f', 'ov00014119', 'EVENT', NULL, 0, 'offline', NULL, '0', '81.314141,31.081598', '{}', 1756305300); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_5 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_5`; +CREATE TABLE `iot_device_log_5` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_5 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_6 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_6`; +CREATE TABLE `iot_device_log_6` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_6 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_7 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_7`; +CREATE TABLE `iot_device_log_7` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `company_no` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `protocol` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '协议', + `device_node` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '节点类型', + `classified_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `command_id` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT NULL COMMENT '指令状态', + `org_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_7 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_8 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_8`; +CREATE TABLE `iot_device_log_8` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_8 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_9 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_9`; +CREATE TABLE `iot_device_log_9` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一编码', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '第三方设备ID唯一标识符', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '产品ID', + `device_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备名称', + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `command_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '指令ID', + `command_status` tinyint DEFAULT '0' COMMENT '指令状态', + `event` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `point` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '经纬度', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '内容', + `create_time` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `idx_create_date` (`create_time`) USING BTREE, + KEY `idx_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_9 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata`; +CREATE TABLE `iot_device_log_metadata` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_device_id` (`device_id`) USING BTREE, + KEY `idx_create_time` (`create_time`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_0 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_0`; +CREATE TABLE `iot_device_log_metadata_0` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_device_id` (`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_0 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_1 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_1`; +CREATE TABLE `iot_device_log_metadata_1` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_1 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_10 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_10`; +CREATE TABLE `iot_device_log_metadata_10` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_10 +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_log_metadata_10` (`id`, `iot_id`, `product_key`, `device_name`, `device_id`, `message_type`, `event`, `property`, `content`, `ext1`, `ext2`, `ext3`, `create_time`) VALUES (2, '82b178e4cff840a4b3128da81e0718d0', '681c0775c2dc427d0480ab5f', 'ov00014115', 'ov00014115', 'EVENT', 'offline', NULL, '设备离线', NULL, NULL, NULL, 1756305300); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_11 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_11`; +CREATE TABLE `iot_device_log_metadata_11` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_11 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_12 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_12`; +CREATE TABLE `iot_device_log_metadata_12` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_12 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_13 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_13`; +CREATE TABLE `iot_device_log_metadata_13` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_13 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_14 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_14`; +CREATE TABLE `iot_device_log_metadata_14` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_14 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_15 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_15`; +CREATE TABLE `iot_device_log_metadata_15` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_15 +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_log_metadata_15` (`id`, `iot_id`, `product_key`, `device_name`, `device_id`, `message_type`, `event`, `property`, `content`, `ext1`, `ext2`, `ext3`, `create_time`) VALUES (2, '4db2d96130964fc49258e4eef0cc55f1', '681c0775c2dc427d0480ab5f', 'ov00014113', 'ov00014113', 'EVENT', 'offline', NULL, '设备离线', NULL, NULL, NULL, 1756305300); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_16 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_16`; +CREATE TABLE `iot_device_log_metadata_16` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_16 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_17 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_17`; +CREATE TABLE `iot_device_log_metadata_17` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_17 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_18 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_18`; +CREATE TABLE `iot_device_log_metadata_18` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_18 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_19 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_19`; +CREATE TABLE `iot_device_log_metadata_19` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_19 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_2 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_2`; +CREATE TABLE `iot_device_log_metadata_2` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_2 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_20 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_20`; +CREATE TABLE `iot_device_log_metadata_20` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_20 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_21 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_21`; +CREATE TABLE `iot_device_log_metadata_21` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_21 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_22 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_22`; +CREATE TABLE `iot_device_log_metadata_22` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_22 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_23 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_23`; +CREATE TABLE `iot_device_log_metadata_23` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_23 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_3 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_3`; +CREATE TABLE `iot_device_log_metadata_3` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_3 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_4 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_4`; +CREATE TABLE `iot_device_log_metadata_4` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_4 +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_log_metadata_4` (`id`, `iot_id`, `product_key`, `device_name`, `device_id`, `message_type`, `event`, `property`, `content`, `ext1`, `ext2`, `ext3`, `create_time`) VALUES (2, 'ab13d2f27d164648adf7d3aa6478bb09', '681c0775c2dc427d0480ab5f', 'ov00014119', 'ov00014119', 'EVENT', 'offline', NULL, '设备离线', NULL, NULL, NULL, 1756305300); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_5 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_5`; +CREATE TABLE `iot_device_log_metadata_5` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_5 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_6 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_6`; +CREATE TABLE `iot_device_log_metadata_6` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_6 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_7 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_7`; +CREATE TABLE `iot_device_log_metadata_7` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_7 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_8 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_8`; +CREATE TABLE `iot_device_log_metadata_8` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_8 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_log_metadata_9 +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_log_metadata_9`; +CREATE TABLE `iot_device_log_metadata_9` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '产品唯一标识', + `device_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备名称', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `message_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '消息类型', + `event` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `property` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '属性', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '其他', + `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `ext2` varchar(655) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `ext3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '', + `create_time` int DEFAULT '0' COMMENT '发生时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_Id` (`iot_id`) USING BTREE, + KEY `idx_meta_product_device` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_log_metadata_9 +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_model +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_model`; +CREATE TABLE `iot_device_model` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', + `product_key` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '产品编号', + `company_no` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '厂家编号', + `company_name` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '厂家名称', + `device_type` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '设备类型', + `device_type_name` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '设备类型名称', + `device_model` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '设备型号', + `device_model_name` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '型号名称', + `icon` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '设备图标', + `protocol` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '网络协议', + `apps` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '' COMMENT '支持应用', + `support_child` tinyint(1) NOT NULL DEFAULT '0' COMMENT '支持子设备', + `link_to` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '小程序跳转webview页面', + `page_ctrl` json DEFAULT NULL COMMENT '页面配置', + `ext_conf` json DEFAULT NULL COMMENT '后台内部扩展字段', + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `idx_product_Key` (`product_key`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='存储设备型号规格定义(关联产品型号、硬件版本) '; + +-- ---------------------------- +-- Records of iot_device_model +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_protocol +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_protocol`; +CREATE TABLE `iot_device_protocol` ( + `id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '产品key,product_key', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `state` tinyint DEFAULT NULL COMMENT '0-未发布,1-已发布', + `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `configuration` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '配置信息', + `example` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '编解码示例', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='设备协议表'; + +-- ---------------------------- +-- Records of iot_device_protocol +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_protocol` (`id`, `name`, `description`, `state`, `type`, `configuration`, `example`) VALUES ('38fc80e', '开关面板', NULL, 1, 'magic', '{\"needBs4Decode\":false,\"location\":\"import log;\\n\\n// 16进制转10进制数字\\nvar hexToInt = hex => {\\n return Integer.parseInt(hex, 16);\\n};\\n// 预解码,解析上报的设备编号 deviceId1\\nvar preDecode = payload => {\\n var result = {}\\n var payloadHex = payload.toUpperCase() // 转为大写\\n // 校验报文长度和帧头\\n if (payloadHex.length() >= 18 && payloadHex.substring(0, 4) === \\\"A5A5\\\") {\\n // 从第5位开始取4字节(8个字符)作为 deviceId\\n result.deviceId = payloadHex.substring(4, 12)\\n }\\n return result\\n}\\n\\nvar decode =(payload,context) => {\\n log.info(\\\"进入编解码,payload={},context={}\\\",payload,context)\\n var result = [];\\n var payloadHex =payload.toUpperCase();\\n var propertiesObj = { \\\"messageType\\\": \\\"PROPERTIES\\\", \\\"properties\\\": {} };\\n var eventObj = { \\\"messageType\\\": \\\"EVENT\\\", \\\"event\\\": \\\"\\\", \\\"data\\\": {} };\\n\\n // 验证帧头帧尾\\n if (payloadHex.substring(0, 4) != \\\"A5A5\\\" || payloadHex.substring(payloadHex.length() - 4) != \\\"5A5A\\\") {\\n return jsonToStr([{ \\\"error\\\": \\\"无效帧格式\\\" }]);\\n }\\n\\n // 提取核心字段\\n var deviceId = payloadHex.substring(4, 8); // 设备ID(4字节)\\n var cmdCode = payloadHex.substring(8, 10); // 命令码(1字节)\\n var dataLen = hexToInt(payloadHex.substring(10, 12)); // 数据长度\\n var dataHex = payloadHex.substring(12, 12 + dataLen * 2); // 数据区\\n var checksum = payloadHex.substring(12 + dataLen * 2, 14 + dataLen * 2); // 校验和\\n\\n // 设备ID存入所有对象\\n propertiesObj.deviceId = deviceId;\\n eventObj.deviceId = deviceId;\\n //验证新内容\\n propertiesObj.context=context;\\n propertiesObj.tcpEncode = \'HEX\';\\n propertiesObj.replyPayload = \'727270632C676574637371\'\\n // 1. 属性上报(命令码01)\\n if (cmdCode == \\\"01\\\" && dataLen == 7) {\\n // 解析开关状态(01=开,00=关)\\n propertiesObj.properties.powerState = dataHex.substring(0, 2) == \\\"01\\\" ? \\\"01\\\" : \\\"00\\\";\\n // 解析电流(2字节,单位0.01A)\\n var currentHex = dataHex.substring(2, 6);\\n propertiesObj.properties.current = (hexToInt(currentHex) * 0.01).toFixed(2);\\n // 解析电压(2字节,单位0.1V)\\n var voltageHex = dataHex.substring(6, 10);\\n propertiesObj.properties.voltage = (hexToInt(voltageHex) * 0.1).toFixed(1);\\n // 解析功率(2字节,单位1W)\\n var powerHex = dataHex.substring(10, 14);\\n propertiesObj.properties.power = hexToInt(powerHex).toString();\\n // 加入结果集\\n result.push(propertiesObj);\\n }\\n\\n // 2. 事件上报(命令码02)\\n if (cmdCode == \\\"02\\\" && dataLen >= 3) {\\n // 事件类型(01=过载,02=过压,03=上线,04=下线)\\n var eventType = dataHex.substring(0, 2);\\n if (eventType == \\\"01\\\") {\\n eventObj.event = \\\"overload\\\";\\n eventObj.data.overload = hexToInt(dataHex.substring(2, 6)) + \\\"W\\\"; // 过载功率\\n }\\n if (eventType == \\\"03\\\") {\\n eventObj.event = \\\"online\\\";\\n eventObj.data.online = \\\"设备上线\\\";\\n }\\n if (eventType == \\\"04\\\") {\\n eventObj.event = \\\"offline\\\";\\n eventObj.data.offline = \\\"设备下线\\\";\\n }\\n result.push(eventObj);\\n }\\n\\n // 3. 响应报文(命令码04)\\n if (cmdCode == \\\"04\\\" && dataLen == 1) {\\n var replyObj = {\\n \\\"messageType\\\": \\\"REPLY\\\",\\n \\\"function\\\": \\\"switch\\\",\\n \\\"data\\\": { \\\"result\\\": dataHex == \\\"00\\\" ? \\\"success\\\" : \\\"fail\\\" }\\n };\\n replyObj.deviceId = deviceId;\\n result.push(replyObj);\\n }\\n\\n return jsonToStr(result); \\n};\\n\\nvar encode = payload => {\\n var obj = toJson(payload);\\n var deviceId = obj.data.deviceId; // 设备ID(4位16进制,如\\\"0001\\\")\\n var func = obj.function;\\n var params = obj.data;\\n\\n // 控制开关(function=switch)\\n if (func == \\\"switch\\\") {\\n var state = params.state; // 目标状态:01=开,00=关\\n if (state != \\\"01\\\" && state != \\\"00\\\") {\\n return \\\"\\\"; // 无效状态,返回空\\n }\\n\\n // 数据区:控制类型(01)+目标状态\\n var dataHex = \\\"01\\\" + state;\\n var dataLen = dataHex.length() / 2; // 数据长度(字节数)\\n var cmdCode = \\\"03\\\"; // 控制指令命令码\\n\\n // 拼接完整报文(无校验和)\\n var frame = \\\"A5A5\\\" + \\n deviceId + \\n cmdCode + \\n Integer.toHexString(dataLen) + // 数据长度(1位16进制)\\n dataHex + \\n \\\"5A5A\\\";\\n\\n return frame.toUpperCase(); // 转为大写16进制\\n }\\n\\n return \\\"\\\"; // 不支持的指令\\n};\\n\\n//预解码,得到deviceId\\n// return preDecode(\\\"A5A5000101070105DC08F004B05A5A\\\")\\n//属性上报报文\\n// return decode(\'A5A5000101070105DC08F004B07A5A5A\');\\n\\n//过载事件报文\\n// return decode(\\\"A5A5000102030104B0F35A5A\\\")\\n\\n\\n// return encode(\'{\\\"deviceId\\\": \\\"0001\\\",\\\"function\\\": \\\"switch\\\",\\\"data\\\": { \\\"state\\\": \\\"01\\\" }}\')\\n\\n\\n// return encode(\'{\\\"messageType\\\":\\\"FUNCTIONS\\\",\\\"function\\\":\\\"switch\\\",\\\"data\\\":{\\\"deviceId\\\":\\\"00010107\\\",\\\"state\\\":\\\"01\\\"}}\')\",\"provider\":\"38fc80e\",\"supportMethods\":[\"encode\",\"preDecode\",\"decode\"]}', '\"{}\"'); +INSERT INTO `iot_device_protocol` (`id`, `name`, `description`, `state`, `type`, `configuration`, `example`) VALUES ('681c0775c2dc427d0480ab5f', '智能插座', NULL, 1, 'magic', '{\"needBs4Decode\":false,\"location\":\"\\nimport log;\\n\\nvar url=\\\"\\\";\\n\\nvar post = (url,body) =>{\\n \\n}\\n\\nvar encode = payload => {\\n var dt=toJson(payload)\\n dt.dxxc=randomSign();\\n return dt;\\n}\\n\\n//解码\\nvar decode = payload => {\\n var result = []\\n log.info(\\\"编解码收到消息={}\\\",payload)\\n var dt=toJson(payload)\\n var propertiesObj = {\\n \\\"messageType\\\": \\\"PROPERTIES\\\",\\n \\\"properties\\\": {}\\n }\\n\\n var repayObj={}\\n repayObj.tips=\\\"我想改啥就改啥\\\"\\n repayObj.randomNo=randomSign()\\n repayObj.wings=\\\"universal-iot-great\\\"\\n repayObj.iot=\\\"iotuniv\\\"\\n //设置属性\\n propertiesObj.properties=dt\\n propertiesObj.properties.sn=randomSign()\\n //MQTT设置回复\\n propertiesObj.replyPayload=repayObj\\n // propertiesObj.replyPayload=\\\"xzzxc\\\"+randomSign()//设置回复的内容\\n propertiesObj.downTopic=\\\"$thing/down/681c0775c2dc427d0480ab5f/cz001923\\\" //设置回复的主题\\n \\n return propertiesObj;\\n}\\n\\n//设备增加\\nvar add = payload => {\\n var da=post(url,payload)\\n var rs={\\n \\\"code\\\":0,\\n \\\"msg\\\":\\\"success\\\",\\n \\\"codec\\\":\\\"add\\\",\\n \\\"codec\\\":da\\n }\\n return rs;\\n}\\n\\n//设备更新\\nvar update = payload => {\\n var da=post(url,payload)\\n var rs={\\n \\\"code\\\":0,\\n \\\"msg\\\":\\\"success\\\",\\n \\\"codec\\\":\\\"update\\\",\\n \\\"codec\\\":da\\n }\\n return rs;\\n}\\n\\n//设备查询\\nvar query = payload => {\\n var da=post(url,payload)\\n var rs={\\n \\\"code\\\":0,\\n \\\"req\\\":0,\\n \\\"msg\\\":\\\"success\\\",\\n \\\"codec\\\":\\\"query\\\",\\n \\\"codec\\\":da\\n }\\n return rs;\\n}\\n\\n//功能调用\\nvar function = payload => {\\n var da=post(url,payload)\\n var rs={\\n \\\"code\\\":0,\\n \\\"msg\\\":\\\"success\\\",\\n \\\"codec\\\":\\\"function\\\",\\n \\\"codec\\\":da\\n }\\n return rs;\\n}\\n\\n\\n\",\"provider\":\"681c0775c2dc427d0480ab5f\",\"supportMethods\":[\"encode\",\"decode\"]}', '\"{}\"'); +INSERT INTO `iot_device_protocol` (`id`, `name`, `description`, `state`, `type`, `configuration`, `example`) VALUES ('68831a1f38fc106a8dac3c13', 'AI生成开关', NULL, 1, 'magic', '{\"needBs4Decode\":false,\"location\":\"import log;\\n\\n// 16进制转10进制数字\\nvar hexToInt = hex => {\\n return Integer.parseInt(hex, 16);\\n};\\n// 预解码,解析上报的设备编号 deviceId\\nvar preDecode = payload => {\\n var result = {}\\n var payloadHex = payload.toUpperCase() // 转为大写\\n // 校验报文长度和帧头\\n if (payloadHex.length() >= 18 && payloadHex.substring(0, 4) === \\\"A5A5\\\") {\\n // 从第5位开始取4字节(8个字符)作为 deviceId\\n result.deviceId = payloadHex.substring(4, 12)\\n }\\n return result\\n}\\n\\nvar decode =(payload,context) => {\\n log.info(\\\"进入编解码,payload={},context={}\\\",payload,context)\\n var result = [];\\n var payloadHex =payload.toUpperCase();\\n var propertiesObj = { \\\"messageType\\\": \\\"PROPERTIES\\\", \\\"properties\\\": {} };\\n var eventObj = { \\\"messageType\\\": \\\"EVENT\\\", \\\"event\\\": \\\"\\\", \\\"data\\\": {} };\\n\\n // 验证帧头帧尾\\n if (payloadHex.substring(0, 4) != \\\"A5A5\\\" || payloadHex.substring(payloadHex.length() - 4) != \\\"5A5A\\\") {\\n return jsonToStr([{ \\\"error\\\": \\\"无效帧格式\\\" }]);\\n }\\n\\n // 提取核心字段\\n var deviceId = payloadHex.substring(4, 8); // 设备ID(4字节)\\n var cmdCode = payloadHex.substring(8, 10); // 命令码(1字节)\\n var dataLen = hexToInt(payloadHex.substring(10, 12)); // 数据长度\\n var dataHex = payloadHex.substring(12, 12 + dataLen * 2); // 数据区\\n var checksum = payloadHex.substring(12 + dataLen * 2, 14 + dataLen * 2); // 校验和\\n\\n // 设备ID存入所有对象\\n propertiesObj.deviceId = deviceId;\\n eventObj.deviceId = deviceId;\\n //验证新内容\\n propertiesObj.context=context;\\n propertiesObj.tcpEncode = \'HEX\';\\n propertiesObj.replyPayload = \'727270632C676574637371\'\\n // 1. 属性上报(命令码01)\\n if (cmdCode == \\\"01\\\" && dataLen == 7) {\\n // 解析开关状态(01=开,00=关)\\n propertiesObj.properties.powerState = dataHex.substring(0, 2) == \\\"01\\\" ? \\\"01\\\" : \\\"00\\\";\\n // 解析电流(2字节,单位0.01A)\\n var currentHex = dataHex.substring(2, 6);\\n propertiesObj.properties.current = (hexToInt(currentHex) * 0.01).toFixed(2);\\n // 解析电压(2字节,单位0.1V)\\n var voltageHex = dataHex.substring(6, 10);\\n propertiesObj.properties.voltage = (hexToInt(voltageHex) * 0.1).toFixed(1);\\n // 解析功率(2字节,单位1W)\\n var powerHex = dataHex.substring(10, 14);\\n propertiesObj.properties.power = hexToInt(powerHex).toString();\\n // 加入结果集\\n result.push(propertiesObj);\\n }\\n\\n // 2. 事件上报(命令码02)\\n if (cmdCode == \\\"02\\\" && dataLen >= 3) {\\n // 事件类型(01=过载,02=过压,03=上线,04=下线)\\n var eventType = dataHex.substring(0, 2);\\n if (eventType == \\\"01\\\") {\\n eventObj.event = \\\"overload\\\";\\n eventObj.data.overload = hexToInt(dataHex.substring(2, 6)) + \\\"W\\\"; // 过载功率\\n }\\n if (eventType == \\\"03\\\") {\\n eventObj.event = \\\"online\\\";\\n eventObj.data.online = \\\"设备上线\\\";\\n }\\n if (eventType == \\\"04\\\") {\\n eventObj.event = \\\"offline\\\";\\n eventObj.data.offline = \\\"设备下线\\\";\\n }\\n result.push(eventObj);\\n }\\n\\n // 3. 响应报文(命令码04)\\n if (cmdCode == \\\"04\\\" && dataLen == 1) {\\n var replyObj = {\\n \\\"messageType\\\": \\\"REPLY\\\",\\n \\\"function\\\": \\\"switch\\\",\\n \\\"data\\\": { \\\"result\\\": dataHex == \\\"00\\\" ? \\\"success\\\" : \\\"fail\\\" }\\n };\\n replyObj.deviceId = deviceId;\\n result.push(replyObj);\\n }\\n\\n return jsonToStr(result);\\n};\\n\\nvar encode = payload => {\\n var obj = toJson(payload);\\n var deviceId = obj.data.deviceId; // 设备ID(4位16进制,如\\\"0001\\\")\\n var func = obj.function;\\n var params = obj.data;\\n\\n // 控制开关(function=switch)\\n if (func == \\\"switch\\\") {\\n var state = params.state; // 目标状态:01=开,00=关\\n if (state != \\\"01\\\" && state != \\\"00\\\") {\\n return \\\"\\\"; // 无效状态,返回空\\n }\\n\\n // 数据区:控制类型(01)+目标状态\\n var dataHex = \\\"01\\\" + state;\\n var dataLen = dataHex.length() / 2; // 数据长度(字节数)\\n var cmdCode = \\\"03\\\"; // 控制指令命令码\\n\\n // 拼接完整报文(无校验和)\\n var frame = \\\"A5A5\\\" + \\n deviceId + \\n cmdCode + \\n Integer.toHexString(dataLen) + // 数据长度(1位16进制)\\n dataHex + \\n \\\"5A5A\\\";\\n\\n return frame.toUpperCase(); // 转为大写16进制\\n }\\n\\n return \\\"\\\"; // 不支持的指令\\n};\",\"provider\":\"68831a1f38fc106a8dac3c13\",\"supportMethods\":[\"encode\",\"preDecode\",\"decode\"]}', '{}'); +INSERT INTO `iot_device_protocol` (`id`, `name`, `description`, `state`, `type`, `configuration`, `example`) VALUES ('688cd92fdb9e0158872f4aa9', '开关面板-MQTT-doc编写', '123', 1, 'magic', '{\"needBs4Decode\":false,\"location\":\"import log\\n\\n\\n/**\\n * 编码:平台指令转设备格式\\n * @param {Object} payload 平台指令\\n * @returns {String} 设备可识别的报文(二进制、JSON、16进制字符串等)\\n */\\nvar encode = payload => {\\n var rs=toJson(payload);\\n rs.iot=\'演示iot\'\\n rs.randomNum=randomSign();\\n // TODO:实现编码逻辑\\n return \\\"A5A50001030201015A5A\\\";\\n};\\n\",\"provider\":\"688cd92fdb9e0158872f4aa9\",\"supportMethods\":[\"encode\"]}', '{}'); +INSERT INTO `iot_device_protocol` (`id`, `name`, `description`, `state`, `type`, `configuration`, `example`) VALUES ('abcxassxnanahsman', '电表', NULL, 1, 'magic', '{\"needBs4Decode\":false,\"location\":\"//16进制ASCII转STRING\\nvar hex2str = stringhex => {\\n\\n var i = 0\\n var asciidata = \\\"\\\"\\n while (i < stringhex.length()) {\\n asciidata += (String.valueOf(Integer.parseInt(stringhex.substring(i, i + 2), 16)) + \\\",\\\")\\n i += 2\\n }\\n asciidata = asciidata.substring(0, asciidata.length() - 1)\\n return asciiToString(asciidata)\\n}\\n//大小端转换\\nvar reverseStr = data => {\\n var rData = \\\"\\\"\\n if (data.length() >= 0) {\\n let seq = data.length()\\n while (seq > 0) {\\n rData += data.substring(seq - 2, seq)\\n seq = seq - 2\\n }\\n }\\n return rData\\n}\\n//和校验计算\\nvar check_CS = strData => {\\n let i = 0\\n let accum = 0\\n while (i < strData.length()) {\\n accum += Integer.parseInt(strData.substring(i, i + 2), 16)\\n i += 2\\n }\\n let l = Integer.toHexString(accum).length()\\n return Integer.toHexString(accum).substring(l - 2, l).toUpperCase()\\n}\\n//数据转换 倒转 -33\\nvar transform = str => {\\n let i = 0\\n let result = \'\'\\n str = reverseTwoDigit(str)\\n while (i < str.length()) {\\n if (str.substring(i, 2 + i) == \'33\') {\\n i += 2\\n result += \'00\'\\n } else {\\n let value = Integer.parseInt(str.substring(i, 2 + i), 16) - 0x33;\\n i += 2\\n result += String.format(\'%02X\', value)\\n }\\n }\\n return Integer.parseInt(result, 10)\\n}\\n//+0x33方法\\nvar dataplus = strData => {\\n let sData = \'\'\\n let i = 0\\n let pData = 0\\n while (i < strData.length()) {\\n pData = (Integer.parseInt(strData.substring(i, i + 2), 16) + 0x33) & 0b11111111\\n sData += String.format(\'%02X\', pData)\\n i += 2\\n }\\n return sData\\n}\\n\\n//预解码,解析上报的设备编号产品deviceId\\nvar preDecode = payload => {\\n var result = {}\\n //注册包\\n if (payload.length() == 12) {\\n result.deviceId = reverseTwoDigit(payload)\\n }\\n \\n //电表电量上报消息预解码\\n if(payload.length() == 98 && payload.substring(0,2).equals(\'68\')&& payload.substring(payload.length()-2,payload.length()).equals(\'16\')){\\n result.deviceId = reverseStr(payload.substring(2, 14))\\n }\\n return result\\n}\\n//上行处理\\nvar decode = (payload,context) => {\\n var result = []\\n var propertiesObj = {\\n \\\"messageType\\\": \\\"PROPERTIES\\\",\\n \\\"properties\\\": {}\\n }\\n propertiesObj.properties.originalMessage = payload\\n\\n\\n //propertiesObj.properties.shadow = pData.shadow\\n result.push(propertiesObj)\\n //心跳包\\n if (payload.length() == 12) {\\n result.push(propertiesObj)\\n propertiesObj.tcpEncode = \'HEX\';\\n propertiesObj.replyPayload = \'7868\'\\n } else {\\n //抄表结果\\n if (payload.length() >= 94 && payload.substring(14, 18) == \'6891\') {\\n //表号\\n propertiesObj.properties.meterNo = reverseStr(payload.substring(2, 14))\\n var length = Integer.parseInt(payload.substring(18, 20), 16)\\n\\n //总用电量\\n let mData = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\\n propertiesObj.properties.meterData = mData.substring(0, mData.length() - 1)\\n //取影子里的上次读数\\n // if (pData!=null &&pData.containsKey(\'shadow\')) {\\n // if (pData.shadow.state.reported.containsKey(\'meterData\')) {\\n // propertiesObj.properties.lastMeterData = pData.shadow.state.reported.meterData\\n // }\\n // }\\n //propertiesObj.properties.meterData = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\\n //电价\\n propertiesObj.properties.meterPrice = (transform(payload.substring(36, 44)) * 0.0001).toFixed(4)\\n //电压\\n propertiesObj.properties.voltage = (transform(payload.substring(44, 52)) * 0.1).toFixed(1)\\n //电流\\n propertiesObj.properties.current = (transform(payload.substring(52, 60)) * 0.001).toFixed(3)\\n //功率\\n propertiesObj.properties.power = (transform(payload.substring(60, 68)) * 0.1).toFixed(1)\\n //购电次数\\n propertiesObj.properties.purchases = transform(payload.substring(68, 76))\\n //预警电量\\n propertiesObj.properties.warningPower = (transform(payload.substring(76, 84)) * 0.01).toFixed(2)\\n //剩余金额\\n propertiesObj.properties.remainingAmount = (transform(payload.substring(84, 92)) * 0.01).toFixed(2)\\n propertiesObj.properties.openState = (Integer.parseInt(payload.substring(92, 94), 16) - 0x33) == 27 ? \'1\' : \'0\'\\n //新增查询信号值,在reply里面下发\\n propertiesObj.tcpEncode = \'HEX\';\\n propertiesObj.replyPayload = \'727270632C676574637371\'\\n }\\n //抄表分包,第一部分\\n if (payload.length() == 64 && payload.substring(14, 18) == \'6891\') {\\n //表号\\n propertiesObj.properties.meterNo = reverseStr(payload.substring(2, 14))\\n var length = Integer.parseInt(payload.substring(18, 20), 16)\\n\\n //总用电量\\n let mData = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\\n propertiesObj.properties.meterData = mData.substring(0, mData.length() - 1)\\n //取影子里的上次读数\\n // if (pData.containsKey(\'shadow\')) {\\n // if (pData.shadow.state.reported.containsKey(\'meterData\')) {\\n // propertiesObj.properties.lastMeterData = pData.shadow.state.reported.meterData\\n // }\\n // }\\n //propertiesObj.properties.meterData = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\\n //电价\\n propertiesObj.properties.meterPrice = (transform(payload.substring(36, 44)) * 0.0001).toFixed(4)\\n //电压\\n propertiesObj.properties.voltage = (transform(payload.substring(44, 52)) * 0.1).toFixed(1)\\n //电流\\n propertiesObj.properties.current = (transform(payload.substring(52, 60)) * 0.001).toFixed(3)\\n //功率\\n propertiesObj.properties.power = (transform(payload.substring(60, 64)) * 0.1).toFixed(1)\\n }\\n //抄表分包,第二部分\\n if (payload.length() == 34) {\\n //\\n //购电次数\\n propertiesObj.properties.purchases = transform(payload.substring(4, 12))\\n //预警电量\\n propertiesObj.properties.warningPower = (transform(payload.substring(12, 20)) * 0.01).toFixed(2)\\n //剩余金额\\n propertiesObj.properties.remainingAmount = (transform(payload.substring(20, 28)) * 0.01).toFixed(2)\\n propertiesObj.properties.openState = (Integer.parseInt(payload.substring(28, 30), 16) - 0x33) == 27 ? \'1\' : \'0\'\\n //新增查询信号值,在reply里面下发\\n propertiesObj.tcpEncode = \'HEX\';\\n propertiesObj.replyPayload = \'727270632C676574637371\'\\n }\\n //控制开合闸回复的丢数据头68兼容处理\\n if (payload.length() == 48) {\\n if (payload.substring(12, 18).equals(\'689C0D\') && ((payload.substring(42, 44).equals(\'4D\')) || (payload.substring(42, 44).equals(\'4E\')))) {\\n payload = \'68\' + payload\\n }\\n }\\n\\n //控制开合闸回复\\n if (payload.substring(14, 18) == \'689C\') {\\n let replyswitchObj = {\\n \\\"messageType\\\": \\\"REPLY\\\",\\n \\\"function\\\": \\\"switchOnOff\\\",\\n \\\"data\\\": {\\n \\\"openState\\\": (Integer.parseInt(payload.substring(44, 46), 16) - 0x33) == 27 ? \'1\' : \'0\'\\n }\\n }\\n\\n result.push(replyswitchObj)\\n //同步上报通电断电属性\\n propertiesObj.properties.openState = replyswitchObj.data.openState\\n //propertiesObj.properties.meterData = (transform(payload.substring(20, 28)) * 0.01).toFixed(2)\\n propertiesObj.properties.remainingAmount = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\\n propertiesObj.properties.purchases = transform(payload.substring(36, 44))\\n //新增查询信号值,在reply里面下发\\n propertiesObj.tcpEncode = \'HEX\';\\n propertiesObj.replyPayload = \'727270632C676574637371\'\\n }\\n //清表回复\\n if (payload.substring(14, 18) == \'689A\') {\\n let replyswitchObj = {\\n \\\"messageType\\\": \\\"REPLY\\\",\\n \\\"function\\\": \\\"reset\\\",\\n \\\"data\\\": {\\n \\\"result\\\": \'设置成功\'\\n }\\n }\\n result.push(replyswitchObj)\\n let mData = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\\n propertiesObj.properties.meterData = mData.substring(0, mData.length() - 1)\\n //propertiesObj.properties.meterData = (transform(payload.substring(20, 28)) * 0.01).toFixed(2)\\n propertiesObj.properties.remainingAmount = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\\n propertiesObj.properties.purchases = transform(payload.substring(36, 44))\\n propertiesObj.properties.openState = (Integer.parseInt(payload.substring(44, 46), 16) - 0x33) == 27 ? \'1\' : \'0\'\\n }\\n //设置限值回复\\n if (payload.substring(14, 20) == \'68841C\') {\\n let replyLimitObj = {\\n \\\"messageType\\\": \\\"REPLY\\\",\\n \\\"function\\\": \\\"setOverloadLimit\\\",\\n \\\"data\\\": {\\n \\\"overVoltageLimit\\\": \\\"\\\",\\n \\\"overCurrrentLimit\\\": \\\"\\\"\\n }\\n }\\n result.push(replyLimitObj)\\n //囤积金额\\n //propertiesObj.properties.hoardAmount = \\n\\n }\\n //售电回复\\n if (payload.substring(14, 20) == \'68840C\') {\\n let replyRechargeObj = {\\n \\\"messageType\\\": \\\"REPLY\\\",\\n \\\"function\\\": \\\"recharge\\\",\\n \\\"data\\\": {\\n \\\"result\\\": \'设置成功\'\\n }\\n }\\n result.push(replyRechargeObj)\\n //囤积金额\\n //propertiesObj.properties.hoardAmount = \\n\\n }\\n //比较上次读数和这次读数的差额\\n if (propertiesObj.properties.containsKey(\'lastMeterData\') && propertiesObj.properties.containsKey(\'meterData\')) {\\n let shortfallData = Float.parseFloat(propertiesObj.properties.meterData) - Float.parseFloat(propertiesObj.properties.lastMeterData)\\n propertiesObj.properties.shortfallData = Float.parseFloat(shortfallData.toFixed(2))\\n if (propertiesObj.properties.shortfallData < 0) {\\n let shortfallAlarmObj = {\\n \\\"messageType\\\": \\\"EVENT\\\",\\n \\\"event\\\": \\\"\\\",\\n \\\"data\\\": {\\n \\\"level\\\": \\\"warn\\\"\\n }\\n }\\n shortfallAlarmObj.event = \'shortfallAlarm\'\\n shortfallAlarmObj.data.shortfallAlarm = \'读数倒流告警\'\\n shortfallAlarmObj.data.shortfallData = propertiesObj.properties.shortfallData\\n result.push(shortfallAlarmObj)\\n }\\n }\\n //CSQ数据,ICCID数据\\n /**\\n * rrpc,getcsq,22\\n * rrpc,geticcid,898604A8102440120152\\n * \\n */\\n if (payload.length() >= 8 && payload.substring(0, 8).equals(\'72727063\')) {\\n let rData = hex2str(payload)\\n let rArray = rData.split(\',\')\\n //CSQ数据\\n if (rArray[0].equals(\'rrpc\') && rArray[1].equals(\'getcsq\')) {\\n propertiesObj.properties.csq = rArray[2]\\n propertiesObj.properties.signalLevel = csq(propertiesObj.properties.csq)\\n }\\n if (rArray[0].equals(\'rrpc\') && rArray[1].equals(\'geticcid\')) {\\n propertiesObj.properties.iccid = rArray[2]\\n }\\n\\n }\\n }\\n /*//开始字节\\n if (payload.substring(0, 2) != \'68\') {\\n \\treturn jsonToStr(result)\\n }*/\\n\\n\\n return jsonToStr(result)\\n}\\n//下行处理\\nvar encode = payload => {\\n var obj = toJson(payload)\\n //读取电表用量\\n if (obj.function == \'meterRead\') {\\n if (obj.data.meterNo.length() == 12) {\\n let str = \'68\' + reverseStr(obj.data.meterNo) + \'6811043335C433\'\\n let check = check_CS(str)\\n str = str + check + \'16\'\\n return str.toLowerCase()\\n }\\n }\\n //开关闸\\n if (obj.function == \'switchOnOff\') {\\n if (obj.data.meterNo.length() == 12) {\\n //1为合闸\\n let switchOn = obj.data.valveOn == \'1\' ? \'4E\' : \'4D\'\\n let str = \'68\' + reverseStr(obj.data.meterNo) + \'681C103533333334896745\' + switchOn + \'3347773B3A443C\'\\n let check = check_CS(str)\\n str = str + check + \'16\'\\n return str.toLowerCase()\\n }\\n }\\n //清表(清除所有,余额、总电量、购电次数、电价全部初始化)\\n if (obj.function == \'reset\') {\\n if (obj.data.meterNo.length() == 12) {\\n let str = \'68\' + reverseStr(obj.data.meterNo) + \'681A09353333333489674535\'\\n let check = check_CS(str)\\n str = str + check + \'16\'\\n return str.toLowerCase()\\n }\\n }\\n //超限值设置\\n if (obj.function == \'setOverloadLimit\') {\\n if (obj.data.meterNo.length() == 12) {\\n let str = \'68\' + reverseStr(obj.data.meterNo) + \'6804203633333335333333734036333333333373403633\'\\n //电压限值处理\\n if (obj.data.containsKey(\'overVoltageLimit\') && obj.data.overVoltageLimit != NULL) {\\n if (Float.valueOf(obj.data.overVoltageLimit) > 220.0 && Float.valueOf(obj.data.overVoltageLimit) <= 260.0) {\\n let hv = String.format(\'%08X\', Integer.valueOf((Float.valueOf(obj.data.overVoltageLimit) * 10).toFixed(0)))\\n str += reverseStr(dataplus(hv))\\n }\\n //如果电压限值不在范围内,补默认限值260.0V\\n else {\\n str += reverseStr(dataplus(String.format(\'%08X\', 2600)))\\n }\\n }\\n //如果不填写电压限值,默认2600\\n else {\\n str += reverseStr(dataplus(String.format(\'%08X\', 2600)))\\n }\\n //电流限值处理\\n if (obj.data.containsKey(\'overCurrrentLimit\') && obj.data.overCurrrentLimit != NULL) {\\n if (Float.valueOf(obj.data.overCurrrentLimit) > 0.000 && Float.valueOf(obj.data.overCurrrentLimit) < 80.000) {\\n let hI = String.format(\'%08X\', Integer.valueOf((Float.valueOf(obj.data.overCurrrentLimit) * 1000).toFixed(0)))\\n str += reverseStr(dataplus(hI))\\n }\\n //如果电流限值不在范围内,补默认限值80A\\n else {\\n str += reverseStr(dataplus(String.format(\'%08X\', 80000)))\\n }\\n }\\n //如果不填写电流限值,默认80000\\n else {\\n str += reverseStr(dataplus(String.format(\'%08X\', 80000)))\\n }\\n //加倍率,默认1\\n str += \'34333333\'\\n let check = check_CS(str)\\n str = str + check + \'16\'\\n return str.toLowerCase()\\n }\\n }\\n //售电\\n if (obj.function == \'recharge\') {\\n if (obj.data.meterNo.length() == 12) {\\n let str = \'68\' + reverseStr(obj.data.meterNo) + \'6804103433333335333333\'\\n str += reverseStr(dataplus(String.format(\'%08X\', (Integer.valueOf(obj.data.rechargeData)) * 100)))\\n\\n if (obj.data.containsKey(\'rechargeCount\')) {\\n str += reverseStr(dataplus(String.format(\'%08X\', obj.data.rechargeCount)))\\n } else {\\n str += \'34333333\'\\n }\\n\\n let check = check_CS(str)\\n str = str + check + \'16\'\\n return str.toLowerCase()\\n }\\n }\\n //测试\\n if (obj.function == \\\"cmd\\\") {\\n let cmdData = obj.data.cmdData\\n return cmdData\\n }\\n //读取信号强度CSQ\\n if (obj.function == \\\"getCSQ\\\") {\\n let cmdData = \'727270632C676574637371\'\\n return cmdData\\n }\\n //读取物联网卡ICCID\\n if (obj.function == \\\"getICCID\\\") {\\n let cmdData = \'727270632C6765746963636964\'\\n return cmdData\\n }\\n}\",\"provider\":\"abcxassxnanahsman\",\"supportMethods\":[\"encode\",\"preDecode\",\"decode\"]}', '{}'); +INSERT INTO `iot_device_protocol` (`id`, `name`, `description`, `state`, `type`, `configuration`, `example`) VALUES ('abcxzxzxzxs', '智能插座', NULL, 1, 'magic', '{\"needBs4Decode\":false,\"location\":\"\\nimport log;\\n\\nvar url=\\\"\\\";\\n\\nvar post = (url,body) =>{\\n \\n}\\n\\nvar encode = payload => {\\n var dt=toJson(payload)\\n dt.dxxc=randomSign();\\n return dt;\\n}\\n\\n//解码\\nvar decode = payload => {\\n var result = []\\n log.info(\\\"编解码收到消息={}\\\",payload)\\n var dt=toJson(payload)\\n var propertiesObj = {\\n \\\"messageType\\\": \\\"PROPERTIES\\\",\\n \\\"properties\\\": {}\\n }\\n //设置属性\\n propertiesObj.properties=dt\\n propertiesObj.properties.sn=randomSign()\\n //设置回复\\n propertiesObj.replyPayload=\\\"xzzxc\\\"+randomSign()\\n propertiesObj.downTopic=\\\"$thing/down/681c0775c2dc427d0480ab5f/cz001923\\\"\\n var event={\\n \\\"messageType\\\": \\\"EVENT\\\",\\n \\\"event\\\":\\\"batteryRestore\\\",\\n \\\"properties\\\": {\\n // \\\"battery\\\":100\\n }\\n }\\n var lowBatteryAlarm={\\n \\\"messageType\\\": \\\"EVENT\\\",\\n \\\"event\\\":\\\"lowBatteryAlarm\\\",\\n \\\"properties\\\": {\\n //\\\"csq\\\":29\\n }\\n }\\n result.push(propertiesObj)\\n result.push(event)\\n result.push(lowBatteryAlarm)\\n return result;\\n}\\n\\n//设备增加\\nvar add = payload => {\\n var da=post(url,payload)\\n var rs={\\n \\\"code\\\":0,\\n \\\"msg\\\":\\\"success\\\",\\n \\\"codec\\\":\\\"add\\\",\\n \\\"codec\\\":da\\n }\\n return rs;\\n}\\n\\n//设备更新\\nvar update = payload => {\\n var da=post(url,payload)\\n var rs={\\n \\\"code\\\":0,\\n \\\"msg\\\":\\\"success\\\",\\n \\\"codec\\\":\\\"update\\\",\\n \\\"codec\\\":da\\n }\\n return rs;\\n}\\n\\n//设备查询\\nvar query = payload => {\\n var da=post(url,payload)\\n var rs={\\n \\\"code\\\":0,\\n \\\"req\\\":0,\\n \\\"msg\\\":\\\"success\\\",\\n \\\"codec\\\":\\\"query\\\",\\n \\\"codec\\\":da\\n }\\n return rs;\\n}\\n\\n//功能调用\\nvar function = payload => {\\n var da=post(url,payload)\\n var rs={\\n \\\"code\\\":0,\\n \\\"msg\\\":\\\"success\\\",\\n \\\"codec\\\":\\\"function\\\",\\n \\\"codec\\\":da\\n }\\n return rs;\\n}\\n\\n\",\"provider\":\"681c0775c2dc427d0480ab5f\",\"supportMethods\":[\"encode\",\"add\",\"function\",\"query\",\"update\",\"decode\"]}', NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_rule_log +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_rule_log`; +CREATE TABLE `iot_device_rule_log` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `c_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '业务ID', + `c_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '业务名称', + `c_status` tinyint unsigned DEFAULT NULL COMMENT '执行状态:0-有失败、1-成功,2-失败', + `c_type` tinyint unsigned DEFAULT NULL COMMENT '1-场景联动,2-数据流转', + `conditions` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '条件', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, + `create_time` datetime DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_cid` (`c_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=347 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of iot_device_rule_log +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (332, '63', '联动', 1, 1, '系统自动', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"receivers\":\"\",\"content\":\"尊敬的用户你好,设备deviceId=ov00014117,设备名称=潜江雅园1号楼底商店铺产生事件【batteryRestore】,所属租户 请注意查看!\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"dxxc\":\"4\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"receivers\":\"\",\"content\":\"{\\\"deviceId\\\":\\\"ov00014117\\\",\\\"success\\\":\\\"ok\\\"}\"},\"success\":true}]', '2025-08-15 16:25:01', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (333, '63', '联动', 1, 1, '系统自动', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"receivers\":\"\",\"content\":\"尊敬的用户你好,设备deviceId=ov00014117,设备名称=潜江雅园1号楼底商店铺产生事件【batteryRestore】,所属租户 请注意查看!\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"dxxc\":\"85\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"receivers\":\"\",\"content\":\"{\\\"deviceId\\\":\\\"ov00014117\\\",\\\"success\\\":\\\"ok\\\"}\"},\"success\":true}]', '2025-08-15 20:39:49', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (334, '63', '联动', 1, 1, '系统自动', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"receivers\":\"\",\"content\":\"尊敬的用户你好,设备deviceId=ov00014117,设备名称=潜江雅园1号楼底商店铺产生事件【batteryRestore】,所属租户 请注意查看!\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"dxxc\":\"17\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"receivers\":\"\",\"content\":\"{\\\"deviceId\\\":\\\"ov00014117\\\",\\\"success\\\":\\\"ok\\\"}\"},\"success\":true}]', '2025-08-15 23:18:29', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (335, '63', '联动', 1, 1, '系统自动', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"receivers\":\"\",\"content\":\"尊敬的用户你好,设备deviceId=ov00014117,设备名称=潜江雅园1号楼底商店铺产生事件【batteryRestore】,所属租户 请注意查看!\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"dxxc\":\"16\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"receivers\":\"\",\"content\":\"{\\\"deviceId\\\":\\\"ov00014117\\\",\\\"success\\\":\\\"ok\\\"}\"},\"success\":true}]', '2025-08-15 23:18:29', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (336, '63', '联动', 1, 1, '系统自动', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"receivers\":\"\",\"content\":\"尊敬的用户你好,设备deviceId=ov00014117,设备名称=潜江雅园1号楼底商店铺产生事件【batteryRestore】,所属租户 请注意查看!\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"dxxc\":\"26\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"receivers\":\"\",\"content\":\"{\\\"deviceId\\\":\\\"ov00014117\\\",\\\"success\\\":\\\"ok\\\"}\"},\"success\":true}]', '2025-08-16 11:53:24', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (337, '63', '联动', 1, 1, '系统自动', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"receivers\":\"\",\"content\":\"尊敬的用户你好,设备deviceId=ov00014117,设备名称=潜江雅园1号楼底商店铺产生事件【batteryRestore】,所属租户 请注意查看!\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"dxxc\":\"10\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"receivers\":\"\",\"content\":\"{\\\"deviceId\\\":\\\"ov00014117\\\",\\\"success\\\":\\\"ok\\\"}\"},\"success\":true}]', '2025-08-16 13:55:56', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (338, '63', '联动', 1, 1, '系统自动', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"content\":\"尊敬的用户你好,设备deviceId=ov00014117,设备名称=潜江雅园1号楼底商店铺产生事件【batteryRestore】,所属租户 请注意查看!\",\"receivers\":\"\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"dxxc\":\"4\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"content\":\"{\\\"deviceId\\\":\\\"ov00014117\\\",\\\"success\\\":\\\"ok\\\"}\",\"receivers\":\"\"},\"success\":true}]', '2025-08-16 16:15:19', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (339, '63', '联动', 1, 1, '系统自动', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"content\":\"尊敬的用户你好,设备deviceId=ov00014117,设备名称=潜江雅园1号楼底商店铺产生事件【batteryRestore】,所属租户 请注意查看!\",\"receivers\":\"\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"dxxc\":\"4\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"content\":\"{\\\"deviceId\\\":\\\"ov00014117\\\",\\\"success\\\":\\\"ok\\\"}\",\"receivers\":\"\"},\"success\":true}]', '2025-08-16 21:14:36', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (340, '63', '联动', 1, 1, '系统自动', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"content\":\"尊敬的用户你好,设备deviceId=ov00014117,设备名称=潜江雅园1号楼底商店铺产生事件【batteryRestore】,所属租户 请注意查看!\",\"receivers\":\"\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"dxxc\":\"4\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"content\":\"{\\\"deviceId\\\":\\\"ov00014117\\\",\\\"success\\\":\\\"ok\\\"}\",\"receivers\":\"\"},\"success\":true}]', '2025-08-17 14:29:35', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (341, '63', '联动', 1, 1, '手动操作', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"content\":\"尊敬的用户你好,设备deviceId=,设备名称=产生事件【】,所属租户 请注意查看!\",\"receivers\":\"\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"msg\":\"设备可能不在线\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"content\":\"{\\\"deviceId\\\":\\\"\\\",\\\"success\\\":\\\"ok\\\"}\",\"receivers\":\"\"},\"success\":true}]', '2025-08-20 09:30:22', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (342, '65', '01', 1, 1, '手动操作', '[{\"trigger\":\"device\",\"target\":\"ov00014117\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"msg\":\"设备可能不在线\"},\"success\":true}]', '2025-08-26 11:16:33', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (343, '65', '01', 1, 1, '手动操作', '[{\"trigger\":\"device\",\"target\":\"ov00014117\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"msg\":\"设备可能不在线\"},\"success\":true}]', '2025-08-26 11:16:35', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (344, '63', '联动', 1, 1, '系统自动', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"content\":\"尊敬的用户你好,设备deviceId=ov00014117,设备名称=潜江雅园1号楼底商店铺产生事件【batteryRestore】,所属租户 请注意查看!\",\"receivers\":\"\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"dxxc\":\"926\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"content\":\"{\\\"deviceId\\\":\\\"ov00014117\\\",\\\"success\\\":\\\"ok\\\"}\",\"receivers\":\"\"},\"success\":true}]', '2025-08-26 13:50:32', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (345, '63', '联动', 1, 1, '手动操作', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"content\":\"尊敬的用户你好,设备deviceId=,设备名称=产生事件【】,所属租户 请注意查看!\",\"receivers\":\"\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"msg\":\"设备可能不在线\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"content\":\"{\\\"deviceId\\\":\\\"\\\",\\\"success\\\":\\\"ok\\\"}\",\"receivers\":\"\"},\"success\":true}]', '2025-08-26 15:17:05', NULL); +INSERT INTO `iot_device_rule_log` (`id`, `c_id`, `c_name`, `c_status`, `c_type`, `conditions`, `content`, `create_time`, `update_time`) VALUES (346, '63', '联动', 1, 1, '手动操作', '[{\"trigger\":\"notice\",\"target\":\"3\",\"targetName\":\"Aleo的验证模版\",\"result\":{\"content\":\"尊敬的用户你好,设备deviceId=,设备名称=产生事件【】,所属租户 请注意查看!\",\"receivers\":\"\"},\"success\":true},{\"trigger\":\"device\",\"target\":\"ov0001411\",\"param\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}],\"result\":{\"msg\":\"设备可能不在线\"},\"success\":true},{\"trigger\":\"notice\",\"target\":\"5\",\"targetName\":\"广丰机房消息推送\",\"result\":{\"content\":\"{\\\"deviceId\\\":\\\"\\\",\\\"success\\\":\\\"ok\\\"}\",\"receivers\":\"\"},\"success\":true}]', '2025-08-26 16:31:07', NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_shadow +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_shadow`; +CREATE TABLE `iot_device_shadow` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '本平台设备唯一标识符', + `product_key` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '产品Key', + `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '设备自身序号', + `ext_device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '第三方平台设备ID唯一标识符', + `active_time` datetime DEFAULT NULL COMMENT '激活时间', + `online_time` datetime DEFAULT NULL COMMENT '在线时间', + `last_time` datetime DEFAULT NULL COMMENT '最后通信时间', + `metadata` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '影子数据', + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例名称', + `update_date` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `dev_iot_id` (`iot_id`) USING BTREE, + KEY `idx_pk_did` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='设备影子表'; + +-- ---------------------------- +-- Records of iot_device_shadow +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (1, '8b0679d5b2054c3389994a905998ccaa', '681c0775c2dc427d0480ab5f', 'so0000132', NULL, '2025-07-26 12:22:54', '2025-07-26 12:22:54', '2025-08-02 19:42:27', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":\"99\",\"ecl\":\"30\",\"switchStatus\":0,\"sn\":\"44\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1754134946},\"ecl\":{\"timestamp\":1754134946},\"switchStatus\":{\"timestamp\":1754134946},\"sn\":{\"timestamp\":1754134946}}},\"timestamp\":1754134946,\"version\":167}', '0', '2025-08-02 19:42:27'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (2, '903071db080c4267900a6ab1e159b5a1', '681c0775c2dc427d0480ab5f', 'xh0000101', NULL, '2025-07-26 12:22:59', '2025-07-26 12:22:59', '2025-08-02 19:42:27', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":\"21\",\"ecl\":\"11\",\"switchStatus\":0,\"sn\":\"46\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1754134946},\"ecl\":{\"timestamp\":1754134946},\"switchStatus\":{\"timestamp\":1754134946},\"sn\":{\"timestamp\":1754134946}}},\"timestamp\":1754134946,\"version\":166}', '0', '2025-08-02 19:42:27'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (4, '82b178e4cff840a4b3128da81e0718d0', '681c0775c2dc427d0480ab5f', 'ov00014115', NULL, '2025-07-26 16:27:57', '2025-07-26 16:27:57', '2025-08-26 15:03:11', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":\"48\",\"ecl\":\"79\",\"switchStatus\":0,\"csq\":16,\"sn\":\"29323\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1756191791},\"ecl\":{\"timestamp\":1756191791},\"switchStatus\":{\"timestamp\":1756191791},\"csq\":{\"timestamp\":1756191791},\"sn\":{\"timestamp\":1756191791}}},\"timestamp\":1756191791,\"version\":416091}', '0', '2025-08-26 15:03:11'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (5, '7af18836921a48e9aedbb33551032a8a', '681c0775c2dc427d0480ab5f', 'ov00014117', NULL, '2025-07-26 16:27:59', '2025-07-26 16:27:59', '2025-08-26 15:03:12', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":\"75\",\"ecl\":\"83\",\"switchStatus\":1,\"csq\":9,\"sn\":\"29049\",\"current\":\"31.00\",\"power\":\"110\",\"powerState\":0,\"voltage\":\"133.8\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1756191791},\"ecl\":{\"timestamp\":1756191791},\"switchStatus\":{\"timestamp\":1756191791},\"csq\":{\"timestamp\":1756191791},\"sn\":{\"timestamp\":1756191791},\"current\":{\"timestamp\":1755272016},\"power\":{\"timestamp\":1755272016},\"powerState\":{\"timestamp\":1755250506},\"voltage\":{\"timestamp\":1755272016}}},\"timestamp\":1756191791,\"version\":414951}', '0', '2025-08-26 15:03:12'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (6, 'c0638ea17c5a4bcf9ad66a2a80ef0ab4', '681c0775c2dc427d0480ab5f', 'ov00014114', NULL, '2025-07-26 16:27:59', '2025-07-26 16:27:59', '2025-08-26 15:03:12', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":\"1\",\"ecl\":\"11\",\"switchStatus\":0,\"csq\":16,\"sn\":\"28983\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1756191791},\"ecl\":{\"timestamp\":1756191791},\"switchStatus\":{\"timestamp\":1756191791},\"csq\":{\"timestamp\":1756191791},\"sn\":{\"timestamp\":1756191791}}},\"timestamp\":1756191791,\"version\":415857}', '0', '2025-08-26 15:03:12'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (7, 'c64f520c8a2e4c9698c6c8f0521db713', '681c0775c2dc427d0480ab5f', 'ov00014112', NULL, '2025-07-26 16:28:01', '2025-07-26 16:28:01', '2025-08-26 15:03:12', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":\"17\",\"ecl\":\"49\",\"switchStatus\":0,\"csq\":8,\"sn\":\"29079\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1756191791},\"ecl\":{\"timestamp\":1756191791},\"switchStatus\":{\"timestamp\":1756191791},\"csq\":{\"timestamp\":1756191791},\"sn\":{\"timestamp\":1756191791}}},\"timestamp\":1756191791,\"version\":415441}', '0', '2025-08-26 15:03:12'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (8, 'ab13d2f27d164648adf7d3aa6478bb09', '681c0775c2dc427d0480ab5f', 'ov00014119', NULL, '2025-07-26 16:28:03', '2025-07-26 16:28:03', '2025-08-26 15:03:11', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":\"33\",\"ecl\":\"70\",\"switchStatus\":1,\"csq\":11,\"sn\":\"29443\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1756191790},\"ecl\":{\"timestamp\":1756191790},\"switchStatus\":{\"timestamp\":1756191790},\"csq\":{\"timestamp\":1756191790},\"sn\":{\"timestamp\":1756191790}}},\"timestamp\":1756191790,\"version\":413033}', '0', '2025-08-26 15:03:11'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (9, '52956f3a0bac430aa3f9ce2fd30f83fa', '681c0775c2dc427d0480ab5f', 'ov00014118', NULL, '2025-07-26 16:28:04', '2025-07-26 16:28:04', '2025-08-26 15:03:12', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":\"65\",\"ecl\":\"94\",\"switchStatus\":1,\"csq\":21,\"sn\":\"29077\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1756191791},\"ecl\":{\"timestamp\":1756191791},\"switchStatus\":{\"timestamp\":1756191791},\"csq\":{\"timestamp\":1756191791},\"sn\":{\"timestamp\":1756191791}}},\"timestamp\":1756191791,\"version\":414834}', '0', '2025-08-26 15:03:12'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (10, 'd2dd545dc6b541e9b277de24a8237d4d', '681c0775c2dc427d0480ab5f', 'ov00014116', NULL, '2025-07-26 16:28:07', '2025-07-26 16:28:07', '2025-08-26 15:03:12', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":\"56\",\"ecl\":\"69\",\"switchStatus\":0,\"csq\":21,\"sn\":\"29335\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1756191790},\"ecl\":{\"timestamp\":1756191790},\"switchStatus\":{\"timestamp\":1756191790},\"csq\":{\"timestamp\":1756191790},\"sn\":{\"timestamp\":1756191790}}},\"timestamp\":1756191790,\"version\":414559}', '0', '2025-08-26 15:03:12'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (11, '4db2d96130964fc49258e4eef0cc55f1', '681c0775c2dc427d0480ab5f', 'ov00014113', NULL, '2025-07-26 16:28:10', '2025-07-26 16:28:10', '2025-08-26 15:03:12', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":\"77\",\"ecl\":\"16\",\"switchStatus\":1,\"csq\":11,\"sn\":\"29445\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1756191790},\"ecl\":{\"timestamp\":1756191790},\"switchStatus\":{\"timestamp\":1756191790},\"csq\":{\"timestamp\":1756191790},\"sn\":{\"timestamp\":1756191790}}},\"timestamp\":1756191790,\"version\":417293}', '0', '2025-08-26 15:03:12'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (12, '962ad84e0d12407ebad3cffbb3137e1e', '681c0775c2dc427d0480ab5f', 'ov00014111', NULL, '2025-07-26 16:28:13', '2025-07-26 16:28:13', '2025-08-26 15:03:12', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":\"62\",\"ecl\":\"38\",\"switchStatus\":0,\"csq\":0,\"sn\":\"29057\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1756191791},\"ecl\":{\"timestamp\":1756191791},\"switchStatus\":{\"timestamp\":1756191791},\"csq\":{\"timestamp\":1756191791},\"sn\":{\"timestamp\":1756191791}}},\"timestamp\":1756191791,\"version\":415128}', '0', '2025-08-26 15:03:12'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (13, '05a9960673fd47e587b041ff1018898a', '681c0775c2dc427d0480ab5f', 'ov00014110', NULL, '2025-07-26 16:28:20', '2025-07-26 16:28:20', '2025-08-26 15:03:12', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":\"57\",\"ecl\":\"79\",\"switchStatus\":1,\"csq\":23,\"sn\":\"29131\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1756191791},\"ecl\":{\"timestamp\":1756191791},\"switchStatus\":{\"timestamp\":1756191791},\"csq\":{\"timestamp\":1756191791},\"sn\":{\"timestamp\":1756191791}}},\"timestamp\":1756191791,\"version\":414100}', '0', '2025-08-26 15:03:12'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (14, '58479b07927342b6bd12c9dfb988e6c9', 'abcxassxnanahsman', '202411190247', NULL, '2025-07-26 23:17:11', '2025-07-26 23:17:11', '2025-07-27 14:57:01', '{\"state\":{\"desired\":{},\"reported\":{\"originalMessage\":\"684702191124206891253335c43333333333333334337a55333333333333333333333433333333333333338333334e0516\",\"meterNo\":\"202411190247\",\"meterData\":\"0.0\",\"meterPrice\":\"1.0000\",\"voltage\":\"224.7\",\"current\":\"0.000\",\"power\":\"0.0\",\"purchases\":1,\"warningPower\":\"0.00\",\"remainingAmount\":\"50.00\",\"openState\":\"1\"}},\"metadata\":{\"desired\":{},\"reported\":{\"originalMessage\":{\"timestamp\":1753599421},\"meterNo\":{\"timestamp\":1753599421},\"meterData\":{\"timestamp\":1753599421},\"meterPrice\":{\"timestamp\":1753599421},\"voltage\":{\"timestamp\":1753599421},\"current\":{\"timestamp\":1753599421},\"power\":{\"timestamp\":1753599421},\"purchases\":{\"timestamp\":1753599421},\"warningPower\":{\"timestamp\":1753599421},\"remainingAmount\":{\"timestamp\":1753599421},\"openState\":{\"timestamp\":1753599421}}},\"timestamp\":1753599421,\"version\":10}', '0', '2025-07-27 14:57:01'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (15, '29727447e38d49d6a9154aa2dafb47b5', '681c0775c2dc427d0480ab5f', '7g0000117', NULL, '2025-07-27 22:32:32', '2025-07-27 22:32:32', '2025-08-02 19:42:26', '{\"state\":{\"desired\":{},\"reported\":{\"messageType\":\"PROPERTIES\",\"properties\":{\"battery\":\"99\",\"ecl\":\"30\",\"switchStatus\":0},\"sn\":\"3\",\"battery\":\"99\",\"ecl\":\"30\",\"switchStatus\":0}},\"metadata\":{\"desired\":{},\"reported\":{\"messageType\":{\"timestamp\":1754134946},\"properties\":{\"timestamp\":1754134946},\"sn\":{\"timestamp\":1754134946},\"battery\":{\"timestamp\":1754134946},\"ecl\":{\"timestamp\":1754134946},\"switchStatus\":{\"timestamp\":1754134946}}},\"timestamp\":1754134946,\"version\":433}', '0', '2025-08-02 19:42:26'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (16, 'e33678b0ba8d48ec995c8ae9a823a6eb', '38fc80e', '00010107', NULL, '2025-07-29 12:58:47', '2025-07-29 12:58:47', '2025-08-01 17:04:29', '{\"state\":{\"desired\":{},\"reported\":{\"powerState\":\"01\",\"current\":\"15.00\",\"voltage\":\"228.8\",\"power\":\"1200\"}},\"metadata\":{\"desired\":{},\"reported\":{\"powerState\":{\"timestamp\":1754039069},\"current\":{\"timestamp\":1754039069},\"voltage\":{\"timestamp\":1754039069},\"power\":{\"timestamp\":1754039069}}},\"timestamp\":1754039069,\"version\":21}', '0', '2025-08-01 17:04:29'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (17, 'ff49e3ac84e54b659d6b4d31aca35554', '38fc80e', '00010108', NULL, '2025-08-01 17:03:35', '2025-08-01 17:03:35', '2025-08-01 17:03:55', '{\"state\":{\"desired\":{},\"reported\":{}},\"metadata\":{\"desired\":{},\"reported\":{}},\"timestamp\":1754039014,\"version\":1}', '0', '2025-08-01 17:03:55'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (18, '0847c610465e41a38839daab0fe36af6', '38fc80e', '00020107', NULL, '2025-08-01 17:09:25', '2025-08-01 17:09:25', '2025-08-10 08:51:12', '{\"state\":{\"desired\":{},\"reported\":{\"powerState\":\"00\",\"current\":\"0.00\",\"voltage\":\"0.0\",\"power\":\"0\"}},\"metadata\":{\"desired\":{},\"reported\":{\"powerState\":{\"timestamp\":1754787071},\"current\":{\"timestamp\":1754787071},\"voltage\":{\"timestamp\":1754787071},\"power\":{\"timestamp\":1754787071}}},\"timestamp\":1754787071,\"version\":58}', '0', '2025-08-10 08:51:12'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (19, '8019b103c9c54ab2a59153b99b301c5b', '38fc80e', '00050107', NULL, '2025-08-01 17:52:39', '2025-08-01 17:52:39', '2025-08-03 08:33:39', '{\"state\":{\"desired\":{},\"reported\":{\"powerState\":\"01\",\"current\":\"40.00\",\"voltage\":\"228.8\",\"power\":\"3904\"}},\"metadata\":{\"desired\":{},\"reported\":{\"powerState\":{\"timestamp\":1754181219},\"current\":{\"timestamp\":1754181219},\"voltage\":{\"timestamp\":1754181219},\"power\":{\"timestamp\":1754181219}}},\"timestamp\":1754181219,\"version\":13758}', '0', '2025-08-03 08:33:39'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (20, '77ff448819014018b4566d99cd0b11cc', '688cd92fdb9e0158872f4aa9', 'docsmqtt001', NULL, '2025-08-02 00:01:33', '2025-08-02 00:01:33', '2025-08-02 08:27:23', '{\"state\":{\"desired\":{},\"reported\":{\"powerState\":0,\"current\":\"30.00\",\"voltage\":\"221.8\",\"power\":\"600\"}},\"metadata\":{\"desired\":{},\"reported\":{\"powerState\":{\"timestamp\":1754094443},\"current\":{\"timestamp\":1754094443},\"voltage\":{\"timestamp\":1754094443},\"power\":{\"timestamp\":1754094443}}},\"timestamp\":1754094443,\"version\":3}', '0', '2025-08-02 08:27:23'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (21, '828f2a663ba54f1a970904d966cd6b56', '688cd92fdb9e0158872f4aa9', 'docsmqtt002', NULL, '2025-08-02 08:36:44', '2025-08-02 08:36:44', '2025-08-02 08:49:11', '{\"state\":{\"desired\":{},\"reported\":{\"powerState\":1,\"current\":\"31.00\",\"voltage\":\"122.8\",\"power\":\"129\",\"messageType\":\"EVENT\",\"event\":\"overload\"}},\"metadata\":{\"desired\":{},\"reported\":{\"powerState\":{\"timestamp\":1754095033},\"current\":{\"timestamp\":1754095033},\"voltage\":{\"timestamp\":1754095033},\"power\":{\"timestamp\":1754095033},\"messageType\":{\"timestamp\":1754095751},\"event\":{\"timestamp\":1754095751}}},\"timestamp\":1754095751,\"version\":8}', '0', '2025-08-02 08:49:11'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (22, '0ebb38fd253c4c4b8bf9a3eafc4ddba0', '688cd92fdb9e0158872f4aa9', 'docsmqtt003', NULL, '2025-08-02 08:53:42', '2025-08-02 08:53:42', '2025-08-02 08:54:48', '{\"state\":{\"desired\":{},\"reported\":{\"powerState\":1,\"current\":\"31.00\",\"voltage\":\"122.8\",\"power\":\"129\"}},\"metadata\":{\"desired\":{},\"reported\":{\"powerState\":{\"timestamp\":1754096060},\"current\":{\"timestamp\":1754096060},\"voltage\":{\"timestamp\":1754096060},\"power\":{\"timestamp\":1754096060}}},\"timestamp\":1754096060,\"version\":2}', '0', '2025-08-02 08:54:48'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (23, '1ba1da4813164c0faedd99279dddb670', '688cd92fdb9e0158872f4aa9', 'docsmqtt004', NULL, '2025-08-02 08:57:14', '2025-08-02 08:57:14', '2025-08-12 23:11:59', '{\"state\":{\"desired\":{},\"reported\":{\"current\":\"36.00\",\"power\":\"129\",\"powerState\":1,\"voltage\":\"122.8\"}},\"metadata\":{\"desired\":{},\"reported\":{\"current\":{\"timestamp\":1755011518},\"power\":{\"timestamp\":1755011518},\"powerState\":{\"timestamp\":1755011518},\"voltage\":{\"timestamp\":1755011518}}},\"timestamp\":1755011518,\"version\":62}', '0', '2025-08-12 23:11:59'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (24, 'd9331ad5d9ef49c6ad0b51ab3c92681d', 'abcxassxnanahsman', '202507070079', NULL, '2025-08-10 08:53:18', '2025-08-10 08:53:18', '2025-08-15 11:02:44', '{\"state\":{\"desired\":{},\"reported\":{\"originalMessage\":\"687900070725206891253335c43339333333333333337555333333333333333333333333333333333333333333334ec916\",\"meterNo\":\"202507070079\",\"meterData\":\"0.0\",\"meterPrice\":\"0.0000\",\"voltage\":\"224.2\",\"current\":\"0.000\",\"power\":\"0.0\",\"purchases\":0,\"warningPower\":\"0.00\",\"remainingAmount\":\"0.00\",\"openState\":\"1\"}},\"metadata\":{\"desired\":{},\"reported\":{\"originalMessage\":{\"timestamp\":1755226963},\"meterNo\":{\"timestamp\":1755226963},\"meterData\":{\"timestamp\":1755226963},\"meterPrice\":{\"timestamp\":1755226963},\"voltage\":{\"timestamp\":1755226963},\"current\":{\"timestamp\":1755226963},\"power\":{\"timestamp\":1755226963},\"purchases\":{\"timestamp\":1755226963},\"warningPower\":{\"timestamp\":1755226963},\"remainingAmount\":{\"timestamp\":1755226963},\"openState\":{\"timestamp\":1755226963}}},\"timestamp\":1755226963,\"version\":24}', '0', '2025-08-15 11:02:44'); +INSERT INTO `iot_device_shadow` (`id`, `iot_id`, `product_key`, `device_id`, `ext_device_id`, `active_time`, `online_time`, `last_time`, `metadata`, `instance`, `update_date`) VALUES (27, '9cedf37c74ba418c8dca64a2c23a612f', '681c0775c2dc427d0480ab5f', 'ov0000004', NULL, '2025-08-17 16:39:49', '2025-08-17 16:39:49', '2025-08-26 21:31:56', '{\"state\":{\"desired\":{},\"reported\":{\"battery\":86,\"csq\":32,\"ecl\":\"12\",\"switchStatus\":0,\"sn\":\"18\"}},\"metadata\":{\"desired\":{},\"reported\":{\"battery\":{\"timestamp\":1756215116},\"csq\":{\"timestamp\":1756215116},\"ecl\":{\"timestamp\":1756215116},\"switchStatus\":{\"timestamp\":1756215116},\"sn\":{\"timestamp\":1756215116}}},\"timestamp\":1756215116,\"version\":23}', '0', '2025-08-26 21:31:56'); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_statistics +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_statistics`; +CREATE TABLE `iot_device_statistics` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `stat_date` date NOT NULL COMMENT '统计日期', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '产品Key', + `total_devices` bigint NOT NULL DEFAULT '0' COMMENT '设备总数', + `online_devices` bigint NOT NULL DEFAULT '0' COMMENT '在线设备数', + `offline_devices` bigint NOT NULL DEFAULT '0' COMMENT '离线设备数', + `new_devices` bigint NOT NULL DEFAULT '0' COMMENT '新增设备数', + `active_devices` bigint NOT NULL DEFAULT '0' COMMENT '活跃设备数(当天有消息的设备)', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_date_product` (`stat_date`,`product_key`) USING BTREE, + KEY `idx_stat_date` (`stat_date`) USING BTREE, + KEY `idx_product_key` (`product_key`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='设备统计表'; + +-- ---------------------------- +-- Records of iot_device_statistics +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_subscribe +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_subscribe`; +CREATE TABLE `iot_device_subscribe` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `iot_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '设备唯一标识', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '产品唯一标识', + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `msg_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '消息类别:属性(PROPERTIES),指令(REPLY),事件(EVENT),上下线(EVENT:online,offline),所有(ALL)', + `sub_type` enum('DEVICE','PRODUCT') CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '订阅级别:设备级(DEVICE),产品级(PRODUCT)', + `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '订阅地址', + `topic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '主题', + `create_date` datetime DEFAULT NULL COMMENT '创建时间', + `creater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人', + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '0' COMMENT '实例编号', + `enabled` tinyint(1) DEFAULT '1' COMMENT '是否启用', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_iot_id` (`iot_id`) USING BTREE, + KEY `iot_pd` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='设备订阅'; + +-- ---------------------------- +-- Records of iot_device_subscribe +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_tags +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_tags`; +CREATE TABLE `iot_device_tags` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `product_key` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `device_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `iot_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `create_time` bigint DEFAULT NULL, + `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `value` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0', + PRIMARY KEY (`id`) USING BTREE, + KEY `dev_iot_id_idx` (`iot_id`) USING BTREE, + KEY `dev_tag_idx` (`key`,`value`) USING BTREE, + KEY `idx_pk_did` (`product_key`,`device_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=53 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='设备标签表'; + +-- ---------------------------- +-- Records of iot_device_tags +-- ---------------------------- +BEGIN; +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (2, '681c0775c2dc427d0480ab5f', '7g0000117', '271232e01d17496f86ab925bdc56eda2', NULL, '1楼消控室(烟感)', NULL, 'devGroup', '1949364879098044416', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (3, '681c0775c2dc427d0480ab5f', '7g0000117', '9217646384fd46b8816359f03d5802ac', NULL, '1楼消控室(烟感)', NULL, 'devGroup', '1949364879098044416', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (4, '681c0775c2dc427d0480ab5f', '7g0000117', '433d68787b2b4fd08dec8fa8ffc60f79', NULL, '1楼消控室(烟感)', NULL, 'devGroup', '1949364879098044416', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (5, '681c0775c2dc427d0480ab5f', '7g0000117', '58479b07927342b6bd12c9dfb988e6c9', NULL, '1楼消控室(烟感)', NULL, 'devGroup', '1949364879098044416', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (6, '681c0775c2dc427d0480ab5f', '7g0000117', '29727447e38d49d6a9154aa2dafb47b5', NULL, '1楼消控室(烟感)', NULL, 'devGroup', '1949364879098044416', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (7, '681c0775c2dc427d0480ab5f', '7g0000117', '8b0679d5b2054c3389994a905998ccaa', NULL, '1楼消控室(烟感)', NULL, 'devGroup', '1949364879098044416', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (8, '681c0775c2dc427d0480ab5f', '7g0000117', '903071db080c4267900a6ab1e159b5a1', NULL, '1楼消控室(烟感)', NULL, 'devGroup', '1949364879098044416', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (9, '681c0775c2dc427d0480ab5f', '7g0000117', 'c23296b6e84b489290a33a0f0bf33b17', NULL, '1楼消控室(烟感)', NULL, 'devGroup', '1949364879098044416', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (11, '681c0775c2dc427d0480ab5f', '7g0000117', 'fa1b39fb287a42dd8a0f2cbae0d698d2', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (13, '681c0775c2dc427d0480ab5f', '7g0000117', '9217646384fd46b8816359f03d5802ac', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (14, '681c0775c2dc427d0480ab5f', '7g0000117', '433d68787b2b4fd08dec8fa8ffc60f79', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (15, '681c0775c2dc427d0480ab5f', '7g0000117', '58479b07927342b6bd12c9dfb988e6c9', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (16, '681c0775c2dc427d0480ab5f', '7g0000117', '29727447e38d49d6a9154aa2dafb47b5', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (17, '681c0775c2dc427d0480ab5f', '7g0000117', '8b0679d5b2054c3389994a905998ccaa', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (18, '681c0775c2dc427d0480ab5f', '7g0000117', '903071db080c4267900a6ab1e159b5a1', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (19, '681c0775c2dc427d0480ab5f', '7g0000117', 'c23296b6e84b489290a33a0f0bf33b17', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (20, '681c0775c2dc427d0480ab5f', '7g0000117', 'e8517d0ff4984431b2f0874e5ae8a29e', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (21, '681c0775c2dc427d0480ab5f', '7g0000117', '4db2d96130964fc49258e4eef0cc55f1', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (22, '681c0775c2dc427d0480ab5f', '7g0000117', 'd2dd545dc6b541e9b277de24a8237d4d', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (23, '681c0775c2dc427d0480ab5f', '7g0000117', '05a9960673fd47e587b041ff1018898a', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (24, '681c0775c2dc427d0480ab5f', '7g0000117', 'c0638ea17c5a4bcf9ad66a2a80ef0ab4', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (25, '681c0775c2dc427d0480ab5f', '7g0000117', '52956f3a0bac430aa3f9ce2fd30f83fa', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (26, '681c0775c2dc427d0480ab5f', '7g0000117', '82b178e4cff840a4b3128da81e0718d0', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (27, '681c0775c2dc427d0480ab5f', '7g0000117', '962ad84e0d12407ebad3cffbb3137e1e', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (28, '681c0775c2dc427d0480ab5f', '7g0000117', '7af18836921a48e9aedbb33551032a8a', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (29, '681c0775c2dc427d0480ab5f', '7g0000117', 'ab13d2f27d164648adf7d3aa6478bb09', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (30, '681c0775c2dc427d0480ab5f', '7g0000117', 'c64f520c8a2e4c9698c6c8f0521db713', NULL, '牛逼分组', NULL, 'devGroup', '1950447085375692800', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (31, '681c0775c2dc427d0480ab5f', '7g0000117', 'fa1b39fb287a42dd8a0f2cbae0d698d2', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (32, '681c0775c2dc427d0480ab5f', '7g0000117', '271232e01d17496f86ab925bdc56eda2', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (33, '681c0775c2dc427d0480ab5f', '7g0000117', '9217646384fd46b8816359f03d5802ac', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (34, '681c0775c2dc427d0480ab5f', '7g0000117', '433d68787b2b4fd08dec8fa8ffc60f79', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (35, '681c0775c2dc427d0480ab5f', '7g0000117', '58479b07927342b6bd12c9dfb988e6c9', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (36, '681c0775c2dc427d0480ab5f', '7g0000117', '29727447e38d49d6a9154aa2dafb47b5', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (37, '681c0775c2dc427d0480ab5f', '7g0000117', '8b0679d5b2054c3389994a905998ccaa', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (38, '681c0775c2dc427d0480ab5f', '7g0000117', '903071db080c4267900a6ab1e159b5a1', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (39, '681c0775c2dc427d0480ab5f', '7g0000117', 'c23296b6e84b489290a33a0f0bf33b17', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (40, '681c0775c2dc427d0480ab5f', '7g0000117', 'e8517d0ff4984431b2f0874e5ae8a29e', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (41, '681c0775c2dc427d0480ab5f', '7g0000117', '4db2d96130964fc49258e4eef0cc55f1', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (42, '681c0775c2dc427d0480ab5f', '7g0000117', 'd2dd545dc6b541e9b277de24a8237d4d', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (43, '681c0775c2dc427d0480ab5f', '7g0000117', '05a9960673fd47e587b041ff1018898a', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (44, '681c0775c2dc427d0480ab5f', '7g0000117', 'c0638ea17c5a4bcf9ad66a2a80ef0ab4', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (45, '681c0775c2dc427d0480ab5f', '7g0000117', '52956f3a0bac430aa3f9ce2fd30f83fa', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (46, '681c0775c2dc427d0480ab5f', '7g0000117', '82b178e4cff840a4b3128da81e0718d0', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (47, '681c0775c2dc427d0480ab5f', '7g0000117', '962ad84e0d12407ebad3cffbb3137e1e', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (48, '681c0775c2dc427d0480ab5f', '7g0000117', '7af18836921a48e9aedbb33551032a8a', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (49, '681c0775c2dc427d0480ab5f', '7g0000117', 'ab13d2f27d164648adf7d3aa6478bb09', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (50, '681c0775c2dc427d0480ab5f', '7g0000117', 'c64f520c8a2e4c9698c6c8f0521db713', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (51, '681c0775c2dc427d0480ab5f', '7g0000117', 'e33678b0ba8d48ec995c8ae9a823a6eb', NULL, '508小机房', NULL, 'devGroup', '1950770676340260864', 'devGroup', NULL); +INSERT INTO `iot_device_tags` (`id`, `product_key`, `device_id`, `iot_id`, `create_time`, `name`, `description`, `type`, `value`, `key`, `instance`) VALUES (52, '681c0775c2dc427d0480ab5f', '7g0000117', 'd2dd545dc6b541e9b277de24a8237d4d', NULL, '1楼消控室(烟感)', NULL, 'devGroup', '1949364879098044416', 'devGroup', NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_device_up_format +-- ---------------------------- +DROP TABLE IF EXISTS `iot_device_up_format`; +CREATE TABLE `iot_device_up_format` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用名称', + `apply` tinyint NOT NULL COMMENT '应用级别(1-应用 2-产品 3-所有)', + `target_id` bigint NOT NULL COMMENT '目标ID(1-应用主键ID,2-ProductKey,3-0)', + `cfg` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '模板配置', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `unionId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='消息格式化表'; + +-- ---------------------------- +-- Records of iot_device_up_format +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for iot_network +-- ---------------------------- +DROP TABLE IF EXISTS `iot_network`; +CREATE TABLE `iot_network` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'TCP_CLIENT,TCP_SERVER,MQTT_CLIENT,MQTT_SERVER', + `union_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '网络唯一标识:MQTT为自定义惟一标识', + `product_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'ProductKey', + `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '网络组件名称', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '详细描述', + `configuration` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '配置内容', + `create_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建用户', + `create_date` datetime DEFAULT NULL COMMENT '创建时间', + `state` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0-关闭,1-启动', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uniq_network_pk_id` (`union_id`,`type`) USING BTREE COMMENT '网络组件唯一标识' +) ENGINE=InnoDB AUTO_INCREMENT=88 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='网络组件表'; + +-- ---------------------------- +-- Records of iot_network +-- ---------------------------- +BEGIN; +INSERT INTO `iot_network` (`id`, `type`, `union_id`, `product_key`, `name`, `description`, `configuration`, `create_user`, `create_date`, `state`) VALUES (81, 'TCP_SERVER', 'abcxassxnanahsman', 'abcxassxnanahsman', '单相轨道电表1', '电表监听端口', '{\n \"preStore\": false,\n \"enabled\": true,\n \"alwaysPreDecode\": true,\n \"port\": 12345,\n \"host\": \"0.0.0.0\",\n \"ssl\": false,\n \"onlyCache\": false,\n \"cert\": \"\",\n \"key\": \"\",\n \"rootCa\": \"\",\n \"decodeType\": \"HEX\",\n \"parserType\": \"DIRECT\",\n \"allIdleTime\": \"\",\n \"readerIdleTime\": \"\",\n \"writerIdleTime\": \"\",\n \"readTimeout\": \"\",\n \"sendTimeout\": \"\",\n \"idleInterval\": \"\",\n \"delimited\": \"\",\n \"lineMaxLength\": \"\",\n \"delimitedMaxlength\": \"\",\n \"fixedLength\": \"\",\n \"byteOrderLittle\": \"true\",\n \"maxFrameLength\": \"\",\n \"lengthFieldOffset\": \"\",\n \"lengthFieldLength\": \"\",\n \"lengthAdjustment\": \"\",\n \"initialBytesToStrip\": \"\",\n \"failFast\": \"true\"\n}', 'test', '2025-07-09 11:49:24', 1); +INSERT INTO `iot_network` (`id`, `type`, `union_id`, `product_key`, `name`, `description`, `configuration`, `create_user`, `create_date`, `state`) VALUES (85, 'TCP_SERVER', '38fc80e', '38fc80e', '文档版小插座', NULL, '{\"host\":\"0.0.0.0\",\"port\":12344}', 'test', '2025-07-28 21:57:40', 1); +INSERT INTO `iot_network` (`id`, `type`, `union_id`, `product_key`, `name`, `description`, `configuration`, `create_user`, `create_date`, `state`) VALUES (87, 'MQTT_CLIENT', '68ada9c78c22c1573cb70d55', '', '火山引擎99服务器emqx', NULL, '{}', 'iot', '2025-08-26 20:34:15', 0); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_product +-- ---------------------------- +DROP TABLE IF EXISTS `iot_product`; +CREATE TABLE `iot_product` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `product_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '产品编号,当前等同设备型号', + `company_no` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '厂商编号', + `name` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '名称', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '产品KEY', + `third_platform` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '第三方平台:lvzhou,ctwing,onenet,alibaba,baidu', + `classified_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '分类名称', + `product_secret` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '产品密钥(1型1密)', + `third_configuration` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '第三方平台配置信息', + `classified_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '分类ID', + `configuration` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '协议配置', + `network_union_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '网络组件:TCP,MQTT', + `device_node` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '设备节点: gateway(网关),childrenDevice(网关子设备),device(设备)', + `gw_product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '所属项目', + `tags` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '标签字段', + `message_protocol` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '消息协议: ', + `creator_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建者id', + `describe` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '说明', + `store_policy` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '数据存储策略', + `store_policy_configuration` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '数据存储策略配置', + `transport_protocol` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '传输协议: MQTT,COAP,UDP', + `photo_url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '图片地址', + `metadata` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '物模型', + `create_time` bigint DEFAULT NULL COMMENT '创建时间', + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0', + `state` tinyint unsigned DEFAULT '0' COMMENT '产品状态(0-正常)', + `third_down_request` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '第三方产品下发信息', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `is_deleted` tinyint(1) DEFAULT '0', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `idx_product_key` (`product_key`) USING BTREE COMMENT '产品唯一密钥', + KEY `idx_prod_class_id` (`classified_id`) USING BTREE, + KEY `idx_product_id` (`product_id`) USING BTREE COMMENT '产品ID(设备型号)' +) ENGINE=InnoDB AUTO_INCREMENT=542 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='物联网产品基本信息(产品Key、协议类型、制造商)'; + +-- ---------------------------- +-- Records of iot_product +-- ---------------------------- +BEGIN; +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (502, '单相导轨表DDSY1986-4G', '理想东风', '单相导轨XA-GXSD81(4G)', 'abcxassxnanahsman', 'tcp', '验证(beta)', NULL, '{\"customField\":[]}', '100100', '{\"requirePayload\":true,\"requireUpShadow\":true,\"allowInsert\":true,\"alwaysPreDecode\":true,\"productKey\":\"67c69f2a6b0001a9b22a6c9a\",\"ssl\":false,\"onlyCache\":false,\"parserConfiguration\":{\"byteOrderLittle\":true,\"failFast\":true,\"fixedLength\":6},\"parserType\":\"DIRECT\",\"port\":12345,\"preStore\":false,\"host\":\"0.0.0.0\",\"decodeType\":\"HEX\",\"byteOrderLittle\":\"true\",\"failFast\":\"true\",\"fixedLength\":\"\",\"enabled\":true,\"offlineThreshold\":\"48\",\"isGps\":false,\"saveFunctionParam\":true,\"logMaxStorage\":\"300\",\"cert\":\"\",\"key\":\"\",\"rootCa\":\"\",\"allIdleTime\":\"\",\"readerIdleTime\":\"\",\"writerIdleTime\":\"\",\"readTimeout\":\"\",\"sendTimeout\":\"\",\"idleInterval\":\"\",\"delimited\":\"\",\"lineMaxLength\":\"\",\"delimitedMaxlength\":\"\",\"maxFrameLength\":\"\",\"lengthFieldOffset\":\"\",\"lengthFieldLength\":\"\",\"lengthAdjustment\":\"\",\"initialBytesToStrip\":\"\"}', 'abcxassxnanahsman', 'DEVICE', NULL, 'huanbao,yiliao,yanglao', NULL, 'test', NULL, 'mysql', '{\"properties\":[{\"id\":\"openState\",\"maxStorage\":\"200\"},{\"id\":\"meterData\",\"maxStorage\":\"500\"}],\"event\":[]}', '4G LTE Cat.1', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/dianbiao.jpg\",\"detail\":\"\"}', '{\"tags\": [], \"events\": [{\"id\": \"online\", \"name\": \"上线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"offline\", \"name\": \"下线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"shortfallAlarm\", \"name\": \"读数倒流告警\", \"config\": false, \"expands\": {\"level\": \"warn\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}], \"functions\": [{\"id\": \"meterRead\", \"name\": \"获取用电量\", \"config\": false, \"inputs\": [{\"id\": \"meterNo\", \"name\": \"电表表号\", \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"12\"}}}], \"output\": {}}, {\"id\": \"switchOnOff\", \"name\": \"开关阀\", \"config\": false, \"inputs\": [{\"id\": \"meterNo\", \"name\": \"表号\", \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"12\"}}}, {\"id\": \"valveOn\", \"name\": \"开关阀操作\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"分闸(断电)\", \"value\": \"0\"}, {\"text\": \"合闸(来电)\", \"value\": \"1\"}]}}], \"output\": {}}], \"properties\": [{\"id\": \"openState\", \"mode\": \"rw\", \"name\": \"当前的通/断电状态\", \"config\": false, \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"断电\", \"value\": \"0\"}, {\"text\": \"通电\", \"value\": \"1\"}]}}, {\"id\": \"meterData\", \"mode\": \"r\", \"name\": \"电表读数\", \"config\": false, \"valueType\": {\"type\": \"float\", \"unit\": \"kW·h\"}}, {\"id\": \"meterPrice\", \"mode\": \"r\", \"name\": \"电价\", \"config\": false, \"valueType\": {\"type\": \"string\"}, \"description\": \"单位 元/千瓦时\"}, {\"id\": \"voltage\", \"mode\": \"r\", \"name\": \"电压\", \"config\": false, \"valueType\": {\"type\": \"string\", \"unit\": \"V\"}}, {\"id\": \"current\", \"mode\": \"r\", \"name\": \"电流\", \"config\": false, \"valueType\": {\"type\": \"string\", \"unit\": \"A\"}}, {\"id\": \"power\", \"mode\": \"r\", \"name\": \"功率\", \"config\": false, \"valueType\": {\"type\": \"string\", \"unit\": \"W\"}}, {\"id\": \"purchases\", \"mode\": \"r\", \"name\": \"购电次数\", \"config\": false, \"valueType\": {\"type\": \"string\"}}, {\"id\": \"warningPower\", \"mode\": \"r\", \"name\": \"预警电量\", \"config\": false, \"valueType\": {\"type\": \"string\", \"unit\": \"kW·h\"}}, {\"id\": \"remainingAmount\", \"mode\": \"r\", \"name\": \"剩余金额\", \"config\": false, \"valueType\": {\"type\": \"string\"}, \"description\": \"单位 元\"}, {\"id\": \"overVoltageLimit\", \"mode\": \"rw\", \"name\": \"电压过载限值\", \"config\": false, \"valueType\": {\"type\": \"float\", \"unit\": \"V\"}, \"description\": \"超过此电压,电表会立即分闸,精确到1位小数点\"}, {\"id\": \"overCurrrentLimit\", \"mode\": \"rw\", \"name\": \"电流过载限值\", \"config\": false, \"valueType\": {\"type\": \"float\", \"unit\": \"A\"}, \"description\": \"精确到毫安,3个小数点。超过此限值,立即分闸\"}, {\"id\": \"meterNo\", \"mode\": \"r\", \"name\": \"电表表号\", \"config\": false, \"valueType\": {\"type\": \"string\"}}, {\"id\": \"lastMeterData\", \"mode\": \"r\", \"name\": \"上次电表读数\", \"config\": false, \"valueType\": {\"type\": \"float\", \"unit\": \"kW·h\"}}, {\"id\": \"shortfallData\", \"mode\": \"r\", \"name\": \"读数差额\", \"config\": false, \"valueType\": {\"type\": \"float\"}}, {\"id\": \"csq\", \"mode\": \"r\", \"name\": \"信号强度CSQ\", \"config\": false, \"valueType\": {\"type\": \"int\"}}, {\"id\": \"iccid\", \"mode\": \"r\", \"name\": \"物联网卡ICCID\", \"config\": false, \"valueType\": {\"type\": \"string\"}}, {\"id\": \"signalLevel\", \"mode\": \"r\", \"name\": \"信号强度等级\", \"config\": false, \"valueType\": {\"type\": \"string\"}}, {}]}', 1741070122575, '0', 0, NULL, '2025-08-15 13:42:56', 0); +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (526, 'HTTP_WS_CZ001', '格林物联', '智能插座', '681c0775c2dc427d0480ab5f', 'mqtt', '验证(beta)', '8bccf5291e5b4120b645863c328eafe1', '{\"customField\":[]}', NULL, '{\"isGps\":false,\"saveFunctionParam\":true,\"allowInsert\":true}', 'univ_mqtt_server', 'DEVICE', NULL, NULL, NULL, 'test', NULL, 'mysql', '{\"properties\":[{\"id\":\"ecl\",\"maxStorage\":\"999\"},{\"id\":\"switchStatus\",\"maxStorage\":\"999\"},{\"id\":\"battery\",\"maxStorage\":\"999\"},{\"id\":\"csq\",\"maxStorage\":\"999\"}],\"event\":[{\"id\":\"powerOffAlarm\",\"maxStorage\":\"999\"},{\"id\":\"powerOnRestore\",\"maxStorage\":\"999\"},{\"id\":\"lowBatteryAlarm\",\"maxStorage\":\"999\"},{\"id\":\"batteryRestore\",\"maxStorage\":\"999\"}]}', 'NB-IoT', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/liuliang.png\",\"detail\":\"\"}', '{\"tags\": [], \"events\": [{\"id\": \"online\", \"name\": \"上线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"offline\", \"name\": \"下线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"onOffline\", \"name\": \"上下线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"powerOffAlarm\", \"name\": \"断电报警\", \"expands\": {\"level\": \"warn\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}, \"description\": \"你猜\"}, {\"id\": \"powerOnRestore\", \"name\": \"恢复通电\", \"expands\": {\"level\": \"ordinary\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}, {\"id\": \"lowBatteryAlarm\", \"name\": \"电池低电量报警\", \"expands\": {\"level\": \"ordinary\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}, {\"id\": \"batteryRestore\", \"name\": \"电池恢复正常\", \"expands\": {\"level\": \"ordinary\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}], \"functions\": [{\"id\": \"switchControl\", \"name\": \"开关控制\", \"inputs\": [{\"id\": \"open\", \"name\": \"开关控制\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"开\", \"value\": \"0\"}, {\"text\": \"关\", \"value\": \"1\"}]}, \"description\": \"0-开,1-关\"}], \"output\": {}, \"description\": \"你猜\"}, {\"id\": \"newkai\", \"name\": \"新开\", \"config\": false, \"inputs\": [{\"id\": \"docs\", \"name\": \"新开\", \"valueType\": {\"max\": \"99\", \"min\": \"0\", \"type\": \"int\", \"unit\": \"N.m\"}}], \"output\": {}}], \"properties\": [{\"id\": \"ecl\", \"mode\": \"r\", \"name\": \"无线信号覆盖等级\", \"config\": false, \"source\": \"device\", \"valueType\": {\"type\": \"int\", \"unit\": \"Hz\"}, \"description\": \"你猜\"}, {\"id\": \"switchStatus\", \"mode\": \"r\", \"name\": \"开关状态\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"开\", \"value\": \"0\"}, {\"text\": \"关\", \"value\": \"1\"}]}, \"description\": \"开关状态\"}, {\"id\": \"battery\", \"mode\": \"r\", \"name\": \"电池电压\", \"config\": false, \"source\": \"device\", \"valueType\": {\"type\": \"float\", \"unit\": \"kV\"}}, {\"id\": \"csq\", \"mode\": \"r\", \"name\": \"信号\", \"config\": false, \"source\": \"device\", \"valueType\": {\"type\": \"int\", \"unit\": \"KΩ\"}, \"description\": \"信号值20-31,超出无效\"}]}', 1750928224958, '0', 0, '', '2025-08-26 20:19:14', 0); +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (527, 'V587', '绿信科技', '全能监测面板', '685f795c753809d9355d2212', 'http', '验证(beta)', NULL, '{\"customField\":[]}', '100100', '{\"thirdDownSupport\":true,\"thirdDownUrl\":\"http://nas.fil.asia:81/test/echo\"}', NULL, 'DEVICE', NULL, NULL, NULL, 'test', NULL, 'mysql', NULL, 'NB-IoT', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/hujiao.jpg\",\"detail\":\"\"}', '{\"tags\": [], \"events\": [{\"id\": \"online\", \"name\": \"上线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"offline\", \"name\": \"下线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"fire\", \"name\": \"着火了\", \"config\": false, \"expands\": {\"level\": \"urgent\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}, \"description\": \"一级紧急\"}], \"functions\": [{\"id\": \"fireInNull\", \"name\": \"灭火\", \"config\": false, \"inputs\": [{\"id\": \"water\", \"name\": \"水\", \"valueType\": {\"max\": \"50\", \"min\": \"1\", \"type\": \"int\", \"unit\": \"t\"}}, {\"id\": \"feng\", \"name\": \"吹风\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"中级\", \"value\": \"middle\"}, {\"text\": \"轻\", \"value\": \"low\"}, {\"text\": \"疯狂\", \"value\": \"crazy\"}]}}], \"output\": {}, \"description\": \"灭火的描述\"}], \"properties\": [{\"id\": \"csq\", \"mode\": \"r\", \"name\": \"信号值\", \"config\": false, \"valueType\": {\"type\": \"int\"}, \"description\": \"取值范围0-31\"}, {\"id\": \"battery\", \"mode\": \"r\", \"name\": \"🔋电量\", \"config\": false, \"valueType\": {\"type\": \"double\", \"unit\": \"%\"}}]}', 1751087452437, '0', 0, NULL, '2025-08-15 13:44:01', 0); +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (528, 'DP_31', '大豆物联', '小灯泡', '6863896d75386b5c1e7e908c', 'tcp', '验证(beta)', NULL, '{\"customField\":[]}', '100100', '{\"offlineThreshold\":300,\"requirePayload\":\"false\",\"isGps\":\"false\",\"saveFunctionParam\":\"true\",\"requireUpShadow\":\"true\",\"allowInsert\":\"true\"}', 'univ_mqtt_client', 'DEVICE', NULL, NULL, NULL, 'test', NULL, 'mysql', NULL, 'MQTT', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/dianbiao.jpg\",\"detail\":\"\"}', '{\"tags\": [], \"events\": [{\"id\": \"online\", \"name\": \"上线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"offline\", \"name\": \"下线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"onOffline\", \"name\": \"上下线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"powerOffAlarm\", \"name\": \"断电报警\", \"expands\": {\"level\": \"warn\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}, \"description\": \"你猜\"}, {\"id\": \"powerOnRestore\", \"name\": \"恢复通电\", \"expands\": {\"level\": \"ordinary\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}, {\"id\": \"lowBatteryAlarm\", \"name\": \"电池低电量报警\", \"expands\": {\"level\": \"ordinary\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}, {\"id\": \"batteryRestore\", \"name\": \"电池恢复正常\", \"expands\": {\"level\": \"ordinary\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}], \"functions\": [{\"id\": \"switchControl\", \"name\": \"开关控制\", \"inputs\": [{\"id\": \"open\", \"name\": \"开关控制\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"开\", \"value\": \"0\"}, {\"text\": \"关\", \"value\": \"1\"}]}, \"description\": \"0-开,1-关\"}], \"output\": {}, \"description\": \"你猜\"}, {\"id\": \"newkai\", \"name\": \"新开\", \"config\": false, \"inputs\": [{\"id\": \"docs\", \"name\": \"新开\", \"valueType\": {\"type\": \"int\"}}], \"output\": {}}], \"properties\": [{\"id\": \"ecl\", \"mode\": \"r\", \"name\": \"无线信号覆盖等级\", \"valueType\": {\"type\": \"int\"}, \"description\": \"你猜\"}, {\"id\": \"switchStatus\", \"mode\": \"r\", \"name\": \"开关状态\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"开\", \"value\": \"0\"}, {\"text\": \"关\", \"value\": \"1\"}]}, \"description\": \"开关状态\"}, {\"id\": \"battery\", \"mode\": \"r\", \"name\": \"电池电压\", \"valueType\": {\"type\": \"string\", \"unit\": \"V\"}}, {\"id\": \"csq\", \"mode\": \"r\", \"name\": \"信号\", \"config\": false, \"valueType\": {\"type\": \"int\"}, \"description\": \"信号值20-31\"}]}', 1751353709983, '0', 0, NULL, '2025-08-15 13:43:22', 0); +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (530, '三相导轨表XA2005-4G', '人行', '三相导轨表XA2005(4G)', 'xsdsdasds', 'sniTcp', '验证(beta)', NULL, '{\"customField\":[]}', '100100', '{\"requirePayload\":true,\"requireUpShadow\":true,\"allowInsert\":true,\"alwaysPreDecode\":true,\"productKey\":\"67c69f2a6b0001a9b22a6c9a\",\"ssl\":true,\"onlyCache\":false,\"parserConfiguration\":{\"byteOrderLittle\":true,\"failFast\":true,\"fixedLength\":6},\"parserType\":\"DIRECT\",\"port\":55598,\"preStore\":false,\"host\":\"0.0.0.0\",\"decodeType\":\"HEX\",\"byteOrderLittle\":\"true\",\"failFast\":\"true\",\"fixedLength\":\"\",\"enabled\":true,\"offlineThreshold\":\"48\",\"isGps\":false,\"saveFunctionParam\":true,\"logMaxStorage\":\"300\",\"cert\":\"\",\"key\":\"\",\"rootCa\":\"\",\"allIdleTime\":\"\",\"readerIdleTime\":\"\",\"writerIdleTime\":\"\",\"readTimeout\":\"\",\"sendTimeout\":\"\",\"idleInterval\":\"\",\"delimited\":\"\",\"lineMaxLength\":\"\",\"delimitedMaxlength\":\"\",\"maxFrameLength\":\"\",\"lengthFieldOffset\":\"\",\"lengthFieldLength\":\"\",\"lengthAdjustment\":\"\",\"initialBytesToStrip\":\"\",\"sslKey\":\"6882287f38fc33174b2d3869\"}', 'abcxassxnanahsman', 'DEVICE', NULL, 'huanbao,yiliao,yanglao', NULL, 'test', NULL, 'mysql', '{\"properties\":[{\"id\":\"openState\",\"maxStorage\":\"200\"},{\"id\":\"meterData\",\"maxStorage\":\"500\"}],\"event\":[]}', '4G LTE Cat.1', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/liuliang.png\",\"detail\":\"\"}', '{\"tags\": [], \"events\": [{\"id\": \"online\", \"name\": \"上线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"offline\", \"name\": \"下线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"shortfallAlarm\", \"name\": \"读数倒流告警\", \"config\": false, \"expands\": {\"level\": \"warn\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}], \"functions\": [{\"id\": \"meterRead\", \"name\": \"获取用电量\", \"config\": false, \"inputs\": [{\"id\": \"meterNo\", \"name\": \"电表表号\", \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"12\"}}}], \"output\": {}}, {\"id\": \"switchOnOff\", \"name\": \"开关阀\", \"config\": false, \"inputs\": [{\"id\": \"meterNo\", \"name\": \"表号\", \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"12\"}}}, {\"id\": \"valveOn\", \"name\": \"开关阀操作\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"分闸(断电)\", \"value\": \"0\"}, {\"text\": \"合闸(来电)\", \"value\": \"1\"}]}}], \"output\": {}}], \"properties\": [{\"id\": \"openState\", \"mode\": \"rw\", \"name\": \"当前的通/断电状态\", \"config\": false, \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"断电\", \"value\": \"0\"}, {\"text\": \"通电\", \"value\": \"1\"}]}}, {\"id\": \"meterData\", \"mode\": \"r\", \"name\": \"电表读数\", \"config\": false, \"valueType\": {\"type\": \"float\", \"unit\": \"kW·h\"}}, {\"id\": \"meterPrice\", \"mode\": \"r\", \"name\": \"电价\", \"config\": false, \"valueType\": {\"type\": \"string\"}, \"description\": \"单位 元/千瓦时\"}, {\"id\": \"voltage\", \"mode\": \"r\", \"name\": \"电压\", \"config\": false, \"valueType\": {\"type\": \"string\", \"unit\": \"V\"}}, {\"id\": \"current\", \"mode\": \"r\", \"name\": \"电流\", \"config\": false, \"valueType\": {\"type\": \"string\", \"unit\": \"A\"}}, {\"id\": \"power\", \"mode\": \"r\", \"name\": \"功率\", \"config\": false, \"valueType\": {\"type\": \"string\", \"unit\": \"W\"}}, {\"id\": \"purchases\", \"mode\": \"r\", \"name\": \"购电次数\", \"config\": false, \"valueType\": {\"type\": \"string\"}}, {\"id\": \"warningPower\", \"mode\": \"r\", \"name\": \"预警电量\", \"config\": false, \"valueType\": {\"type\": \"string\", \"unit\": \"kW·h\"}}, {\"id\": \"remainingAmount\", \"mode\": \"r\", \"name\": \"剩余金额\", \"config\": false, \"valueType\": {\"type\": \"string\"}, \"description\": \"单位 元\"}, {\"id\": \"overVoltageLimit\", \"mode\": \"rw\", \"name\": \"电压过载限值\", \"config\": false, \"valueType\": {\"type\": \"float\", \"unit\": \"V\"}, \"description\": \"超过此电压,电表会立即分闸,精确到1位小数点\"}, {\"id\": \"overCurrrentLimit\", \"mode\": \"rw\", \"name\": \"电流过载限值\", \"config\": false, \"valueType\": {\"type\": \"float\", \"unit\": \"A\"}, \"description\": \"精确到毫安,3个小数点。超过此限值,立即分闸\"}, {\"id\": \"meterNo\", \"mode\": \"r\", \"name\": \"电表表号\", \"config\": false, \"valueType\": {\"type\": \"string\"}}, {\"id\": \"lastMeterData\", \"mode\": \"r\", \"name\": \"上次电表读数\", \"config\": false, \"valueType\": {\"type\": \"float\", \"unit\": \"kW·h\"}}, {\"id\": \"shortfallData\", \"mode\": \"r\", \"name\": \"读数差额\", \"config\": false, \"valueType\": {\"type\": \"float\"}}, {\"id\": \"csq\", \"mode\": \"r\", \"name\": \"信号强度CSQ\", \"config\": false, \"valueType\": {\"type\": \"int\"}}, {\"id\": \"iccid\", \"mode\": \"r\", \"name\": \"物联网卡ICCID\", \"config\": false, \"valueType\": {\"type\": \"string\"}}, {\"id\": \"signalLevel\", \"mode\": \"r\", \"name\": \"信号强度等级\", \"config\": false, \"valueType\": {\"type\": \"string\"}}, {}]}', 1741070122575, '0', 0, NULL, '2025-08-15 13:46:56', 0); +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (531, 'YG_2025', 'universal', '小蘑菇插座', '68831a1f38fc106a8dac3c13', 'tcp', '验证(beta)', '8bccf5291e5b4120b645863c328eafe1', '{\"customField\":[]}', '100100', '{\"allIdleTime\":0,\"allowInsert\":false,\"alwaysPreDecode\":true,\"byteOrderLittle\":true,\"cert\":\"\",\"decoderType\":\"HEX\",\"delimited\":\"\",\"delimitedMaxlength\":\"\",\"enabled\":true,\"failFast\":true,\"fixedLength\":\"\",\"host\":\"0.0.0.0\",\"idleInterval\":0,\"initialBytesToStrip\":\"\",\"isGps\":false,\"key\":\"\",\"lengthAdjustment\":\"\",\"lengthFieldLength\":\"\",\"lengthFieldOffset\":\"\",\"lineMaxLength\":\"\",\"logMaxStorage\":\"300\",\"maxFrameLength\":\"\",\"offlineThreshold\":\"0.5\",\"onlyCache\":false,\"parserConfiguration\":{\"byteOrderLittle\":true,\"failFast\":true,\"fixedLength\":6},\"parserType\":\"DIRECT\",\"preStore\":false,\"readerIdleTime\":360,\"readTimeout\":0,\"requirePayload\":true,\"requireUpShadow\":true,\"rootCa\":\"\",\"saveFunctionParam\":true,\"sendTimeout\":0,\"port\":11990,\"ssl\":false,\"writerIdleTime\":0,\"decodeType\":\"HEX\",\"encoderType\":\"STRING\"}', '68831a1f38fc106a8dac3c13', 'DEVICE', NULL, NULL, NULL, 'iot', NULL, 'mysql', NULL, '4G LTE Cat.1', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/dianbiao.jpg\",\"detail\":\"\"}', '{\"tags\": [], \"events\": [{\"id\": \"overload\", \"name\": \"过载报警\", \"valueType\": {\"type\": \"int\", \"unit\": \"W\"}}, {\"id\": \"online\", \"name\": \"上线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"offline\", \"name\": \"下线\", \"valueType\": {\"type\": \"string\"}}], \"functions\": [{\"id\": \"switch\", \"name\": \"开关控制\", \"config\": false, \"inputs\": [{\"id\": \"state\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"开\", \"value\": \"01\"}, {\"text\": \"关\", \"value\": \"00\"}]}}, {\"id\": \"deviceId\", \"name\": \"设备编号\", \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}], \"output\": {}}], \"properties\": [{\"id\": \"powerState\", \"mode\": \"rw\", \"name\": \"开关状态\", \"config\": false, \"source\": \"device\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"开\", \"value\": \"01\"}, {\"text\": \"关\", \"value\": \"00\"}]}}, {\"id\": \"current\", \"mode\": \"r\", \"name\": \"电流\", \"valueType\": {\"type\": \"float\", \"unit\": \"A\"}}, {\"id\": \"voltage\", \"mode\": \"r\", \"name\": \"电压\", \"valueType\": {\"type\": \"float\", \"unit\": \"V\"}}, {\"id\": \"power\", \"mode\": \"r\", \"name\": \"功率\", \"valueType\": {\"type\": \"int\", \"unit\": \"W\"}}]}', 1753422367137, '0', 0, NULL, '2025-08-15 13:43:22', 0); +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (532, 'KG_2021', '文档', '开关面板-TCP', '38fc80e', 'tcp', '验证(beta)', '4b683f8bd7c44653a45e9de792251f63', '{\"customField\":[]}', '100100', '{\"preStore\":\"true\",\"enabled\":true,\"alwaysPreDecode\":\"true\",\"ssl\":false,\"onlyCache\":\"false\",\"byteOrderLittle\":\"true\",\"failFast\":\"true\",\"encoderType\":\"HEX\",\"parserType\":\"DIRECT\",\"decodeType\":\"STRING\",\"isGps\":\"false\",\"saveFunctionParam\":\"true\",\"allowInsert\":\"true\",\"offlineThreshold\":12,\"readTimeout\":\"30\",\"decoderType\":\"HEX\",\"requirePayload\":\"\",\"requireUpShadow\":\"\",\"host\":\"0.0.0.0\",\"port\":12344}', '38fc80e', 'DEVICE', NULL, NULL, NULL, 'test', NULL, 'mysql', NULL, '4G LTE Cat.1', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/shuibiao.jpeg\",\"detail\":\"\"}', '{\"tags\": [], \"events\": [{\"id\": \"overload\", \"name\": \"过载报警\", \"valueType\": {\"type\": \"int\", \"unit\": \"W\"}}, {\"id\": \"online\", \"name\": \"上线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"offline\", \"name\": \"下线\", \"valueType\": {\"type\": \"string\"}}], \"functions\": [{\"id\": \"switch\", \"name\": \"开关控制\", \"config\": false, \"inputs\": [{\"id\": \"state\", \"name\": \"控制状态\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"开\", \"value\": \"01\"}, {\"text\": \"关\", \"value\": \"00\"}]}}, {\"id\": \"deviceId\", \"name\": \"设备编号\", \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}], \"output\": {}}], \"properties\": [{\"id\": \"powerState\", \"mode\": \"rw\", \"name\": \"开关状态\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"开\", \"value\": \"01\"}, {\"text\": \"关\", \"value\": \"00\"}]}}, {\"id\": \"current\", \"mode\": \"r\", \"name\": \"电流\", \"valueType\": {\"type\": \"float\", \"unit\": \"A\"}}, {\"id\": \"voltage\", \"mode\": \"r\", \"name\": \"电压\", \"valueType\": {\"type\": \"float\", \"unit\": \"V\"}}, {\"id\": \"power\", \"mode\": \"r\", \"name\": \"功率\", \"valueType\": {\"type\": \"int\", \"unit\": \"W\"}}]}', 1753708542587, '0', 0, NULL, '2025-08-15 13:47:05', 0); +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (533, 'vite_mqtt_2025', 'vite', '开关面板-MQTT', '688cd92fdb9e0158872f4aa9', 'mqtt', '验证(beta)', 'b2e71bfb1bf443eb88878c0827652a55', '{\"customField\":[]}', '100100', '{\"requirePayload\":\"\",\"isGps\":\"\",\"saveFunctionParam\":\"\",\"requireUpShadow\":\"\",\"allowInsert\":\"true\",\"logMaxStorage\":120,\"downTopic\":\"$thing/688cd92fdb9e0158872f4aa9/etc\"}', NULL, 'DEVICE', NULL, NULL, NULL, 'test', NULL, 'mysql', '{\"properties\":[{\"id\":\"voltage\",\"maxStorage\":\"9999\"},{\"id\":\"powerState\",\"maxStorage\":\"9999\"},{\"id\":\"current\",\"maxStorage\":\"9999\"},{\"id\":\"power\",\"maxStorage\":\"9999\"}],\"event\":[{\"id\":\"overload\",\"maxStorage\":\"999\"}]}', 'MQTT', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/dianbiao.jpg\",\"detail\":\"\"}', '{\"tags\": [], \"events\": [{\"id\": \"overload\", \"name\": \"过载报警\", \"valueType\": {\"type\": \"int\", \"unit\": \"W\"}}, {\"id\": \"online\", \"name\": \"上线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"offline\", \"name\": \"下线\", \"valueType\": {\"type\": \"string\"}}], \"functions\": [{\"id\": \"switch\", \"name\": \"开关控制\", \"config\": false, \"inputs\": [{\"id\": \"state\", \"name\": \"控制状态\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"开\", \"value\": \"1\"}, {\"text\": \"关\", \"value\": \"0\"}]}}, {\"id\": \"deviceId\", \"name\": \"设备编号\", \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}], \"output\": {}}], \"properties\": [{\"id\": \"powerState\", \"mode\": \"rw\", \"name\": \"开关状态\", \"config\": false, \"source\": \"device\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"开\", \"value\": \"1\"}, {\"text\": \"关\", \"value\": \"0\"}]}}, {\"id\": \"current\", \"mode\": \"r\", \"name\": \"电流\", \"valueType\": {\"type\": \"float\", \"unit\": \"A\"}}, {\"id\": \"voltage\", \"mode\": \"r\", \"name\": \"电压\", \"valueType\": {\"type\": \"float\", \"unit\": \"V\"}}, {\"id\": \"power\", \"mode\": \"r\", \"name\": \"功率\", \"valueType\": {\"type\": \"int\", \"unit\": \"W\"}}]}', 1754061103336, '0', 0, NULL, '2025-08-15 13:43:22', 0); +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (534, 'HTTP_WS_CZ001', '格林物联', '智能插座', 'abcxzxzxzxs', 'mqtt', NULL, '4e3d75bd43054ef2a9b7fb0b29bf6550', '{\"customField\":[]}', NULL, '{\"isGps\":false,\"saveFunctionParam\":true,\"allowInsert\":true}', 'univ_mqtt_server', 'DEVICE', NULL, NULL, NULL, 'test', NULL, 'mysql', '{\"properties\":[{\"id\":\"ecl\",\"maxStorage\":\"999\"},{\"id\":\"switchStatus\",\"maxStorage\":\"999\"},{\"id\":\"battery\",\"maxStorage\":\"999\"},{\"id\":\"csq\",\"maxStorage\":\"999\"}],\"event\":[{\"id\":\"powerOffAlarm\",\"maxStorage\":\"999\"},{\"id\":\"powerOnRestore\",\"maxStorage\":\"999\"},{\"id\":\"lowBatteryAlarm\",\"maxStorage\":\"999\"},{\"id\":\"batteryRestore\",\"maxStorage\":\"999\"}]}', 'NB-IoT', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/shuibiao.jpeg\",\"detail\":\"\"}', '{\"tags\": [], \"events\": [{\"id\": \"online\", \"name\": \"上线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"offline\", \"name\": \"下线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"onOffline\", \"name\": \"上下线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"powerOffAlarm\", \"name\": \"断电报警\", \"expands\": {\"level\": \"warn\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}, \"description\": \"你猜\"}, {\"id\": \"powerOnRestore\", \"name\": \"恢复通电\", \"expands\": {\"level\": \"ordinary\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}, {\"id\": \"lowBatteryAlarm\", \"name\": \"电池低电量报警\", \"expands\": {\"level\": \"ordinary\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}, {\"id\": \"batteryRestore\", \"name\": \"电池恢复正常\", \"expands\": {\"level\": \"ordinary\"}, \"valueType\": {\"type\": \"string\", \"expands\": {\"maxLength\": \"50\"}}}], \"functions\": [{\"id\": \"switchControl\", \"name\": \"开关控制\", \"inputs\": [{\"id\": \"open\", \"name\": \"开关控制\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"开\", \"value\": \"0\"}, {\"text\": \"关\", \"value\": \"1\"}]}, \"description\": \"0-开,1-关\"}], \"output\": {}, \"description\": \"你猜\"}, {\"id\": \"newkai\", \"name\": \"新开\", \"config\": false, \"inputs\": [{\"id\": \"docs\", \"name\": \"新开\", \"valueType\": {\"max\": \"99\", \"min\": \"0\", \"type\": \"int\", \"unit\": \"N.m\"}}], \"output\": {}}], \"properties\": [{\"id\": \"ecl\", \"mode\": \"r\", \"name\": \"无线信号覆盖等级\", \"config\": false, \"source\": \"device\", \"valueType\": {\"type\": \"int\", \"unit\": \"Hz\"}, \"description\": \"你猜\"}, {\"id\": \"switchStatus\", \"mode\": \"r\", \"name\": \"开关状态\", \"valueType\": {\"type\": \"enum\", \"elements\": [{\"text\": \"开\", \"value\": \"0\"}, {\"text\": \"关\", \"value\": \"1\"}]}, \"description\": \"开关状态\"}, {\"id\": \"battery\", \"mode\": \"r\", \"name\": \"电池电压\", \"config\": false, \"source\": \"device\", \"valueType\": {\"type\": \"float\", \"unit\": \"kV\"}}, {\"id\": \"csq\", \"mode\": \"r\", \"name\": \"信号\", \"config\": false, \"source\": \"device\", \"valueType\": {\"type\": \"int\", \"unit\": \"KΩ\"}, \"description\": \"信号值20-31,超出无效\"}]}', 1754405586290, NULL, 0, '', '2025-08-15 13:47:03', NULL); +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (535, 'XYX-001-L', '深圳市星韵鑫科技有限公司', '拍照抄表器', '68975d7e6b9414067b209199', 'ctaiot', '拍照抄表器', NULL, NULL, '100100', '{\"masterKey\":\"1550a607ab394785b36117cbca608b8e\",\"productId\":17242608,\"payloadFormat\":0,\"ttl\":86400,\"operator\":\"univ\",\"dataType\":2,\"downParaType\":0,\"requirePayload\":true,\"powerModel\":0,\"lwm2mEdrxTime\":null}', NULL, 'DEVICE', NULL, NULL, NULL, 'iot', NULL, 'mysql', NULL, 'NB-iot', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/yangan.png\",\"detail\":\"\"}', '{\"tags\": [], \"events\": [{\"id\": \"online\", \"name\": \"上线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"offline\", \"name\": \"下线\", \"valueType\": {\"type\": \"string\"}}], \"functions\": [], \"properties\": [{\"id\": \"iccid\", \"mode\": \"r\", \"name\": \"ICCID\", \"valueType\": {\"type\": \"string\"}, \"description\": \"SIM卡ICCID\"}]}', 1754750334032, '0', 0, '{\"ctwingRequestData\":{\"ttl\":0,\"dataType\":0,\"nodeType\":0,\"accessType\":0,\"networkType\":0,\"productProtocol\":0,\"dataEncryption\":0,\"authType\":0,\"endpointFormat\":0,\"tupIsThrough\":0,\"payloadFormat\":0,\"powerModel\":0,\"productId\":\"XYX-001-L\",\"pageNow\":0,\"pageSize\":0,\"publicProductId\":99015579},\"creatorId\":\"iot\",\"companyNo\":\"深圳市星韵鑫科技有限公司\",\"classifiedId\":\"100100\",\"classifiedName\":\"拍照抄表器\",\"data\":{\"productId\":\"XYX-001-L\",\"publicProductId\":99015579},\"cmd\":\"PUBPRO_ADD\",\"deviceReuse\":false,\"isAdmin\":false,\"allowInsert\":false}', '2025-08-15 13:47:08', 0); +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (536, 'DTU_2025_univ', '四喜', '网关DTU(面板)', 'lB0Tl1rgf8uz', 'mqtt', '验证(beta)', 'd239fdd5660f4e4893655182fc20c11d', '{\"customField\":[]}', '100100', NULL, NULL, 'GATEWAY', NULL, NULL, NULL, 'test', NULL, 'mysql', NULL, 'Wi-Fi', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/dianbiao.jpg\",\"detail\":\"\"}', '{\"tags\":[],\"events\":[{\"id\":\"online\",\"name\":\"上线\",\"valueType\":{\"type\":\"string\"}},{\"id\":\"offline\",\"name\":\"下线\",\"valueType\":{\"type\":\"string\"}}],\"functions\":[],\"properties\":[]}', 1754800276327, '0', 0, NULL, '2025-08-15 13:43:22', 0); +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (538, 'SX_Blue', '四喜', 'DTU(面板)子设备', 'lipSSVUj9XLt', 'mqtt', '验证(beta)', '0a0a1c19446749dc91d19c1c44aa405e', '{\"customField\":[]}', '100100', NULL, NULL, 'GATEWAY_SUB_DEVICE', 'lB0Tl1rgf8uz', 'yanglao,yiliao', NULL, 'test', NULL, 'mysql', NULL, '有线', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/shuibiao.jpeg\",\"detail\":\"\"}', '{\"tags\": [], \"events\": [{\"id\": \"online\", \"name\": \"上线\", \"valueType\": {\"type\": \"string\"}}, {\"id\": \"offline\", \"name\": \"下线\", \"valueType\": {\"type\": \"string\"}}], \"functions\": [], \"properties\": []}', 1754800824069, '0', 0, NULL, '2025-08-15 13:46:43', 0); +INSERT INTO `iot_product` (`id`, `product_id`, `company_no`, `name`, `product_key`, `third_platform`, `classified_name`, `product_secret`, `third_configuration`, `classified_id`, `configuration`, `network_union_id`, `device_node`, `gw_product_key`, `tags`, `message_protocol`, `creator_id`, `describe`, `store_policy`, `store_policy_configuration`, `transport_protocol`, `photo_url`, `metadata`, `create_time`, `instance`, `state`, `third_down_request`, `update_time`, `is_deleted`) VALUES (539, 'DTU_Blue', '东信', 'DTU蓝牙电表', '8qlWaAd8Rz2B', 'mqtt', '验证(beta)', 'a613a63b62834d29b00b7c8f7b806c6e', '{\"customField\":[]}', '100100', NULL, NULL, 'GATEWAY', NULL, NULL, NULL, 'test', NULL, 'mysql', NULL, '有线', '{\"img\":\"http://qiniu.oss.filecoin.ren/device/logo/yangan.png\",\"detail\":\"\"}', '{\"tags\":[],\"events\":[{\"id\":\"online\",\"name\":\"上线\",\"valueType\":{\"type\":\"string\"}},{\"id\":\"offline\",\"name\":\"下线\",\"valueType\":{\"type\":\"string\"}}],\"functions\":[],\"properties\":[]}', 1754812722452, '0', 0, NULL, '2025-08-15 13:46:34', 0); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_product_sort +-- ---------------------------- +DROP TABLE IF EXISTS `iot_product_sort`; +CREATE TABLE `iot_product_sort` ( + `id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '分类id', + `parent_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '父id', + `has_child` tinyint(1) NOT NULL COMMENT '是否有子节点', + `identification` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '标识', + `classified_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '分类名称', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '说明', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '创建者', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='产品分类'; + +-- ---------------------------- +-- Records of iot_product_sort +-- ---------------------------- +BEGIN; +INSERT INTO `iot_product_sort` (`id`, `parent_id`, `has_child`, `identification`, `classified_name`, `description`, `create_time`, `create_by`) VALUES ('100', '0', 1, 'IoT-Universal ', 'IoT-Universal ', '首个分类', '2025-06-24 14:21:08', 'test'); +INSERT INTO `iot_product_sort` (`id`, `parent_id`, `has_child`, `identification`, `classified_name`, `description`, `create_time`, `create_by`) VALUES ('100100', '100', 0, 'beta', '验证(beta)', NULL, '2025-06-24 14:46:20', 'test'); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_user +-- ---------------------------- +DROP TABLE IF EXISTS `iot_user`; +CREATE TABLE `iot_user` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `org_id` bigint DEFAULT NULL COMMENT '归属组织机构', + `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名', + `password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '别名', + `union_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户唯一标识', + `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '邮箱', + `salt` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码加盐', + `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '手机号', + `status` tinyint unsigned DEFAULT NULL COMMENT '账号状态(0正常,1停用)', + `avatar` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '头像', + `parent_union_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '上级用户唯一id', + `identity` tinyint NOT NULL DEFAULT '1' COMMENT '账号身份 0.超级管理员 1.普通用户 2.子用户', + `login_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '登录IP', + `login_date` datetime DEFAULT NULL COMMENT '登录时间', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建者', + `create_date` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `update_date` datetime DEFAULT NULL, + `cfg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '自定义配置', + `remark` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注', + `deleted` tinyint unsigned DEFAULT NULL COMMENT '0-正常,1-删除', + `license` int unsigned DEFAULT '0' COMMENT '022租户接入数限制', + `license_total_amount` int unsigned DEFAULT '1000' COMMENT 'license总额度', + `register_from` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '注册来源', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=1944353342247555073 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户表'; + +-- ---------------------------- +-- Records of iot_user +-- ---------------------------- +BEGIN; +INSERT INTO `iot_user` (`id`, `org_id`, `username`, `password`, `alias`, `union_id`, `email`, `salt`, `mobile`, `status`, `avatar`, `parent_union_id`, `identity`, `login_ip`, `login_date`, `create_by`, `create_date`, `update_by`, `update_date`, `cfg`, `remark`, `deleted`, `license`, `license_total_amount`, `register_from`) VALUES (1, NULL, 'iot', '$2a$10$opb7qtNzvAaQBbbXFuW1R.HJSOXPQjd0GtxdRNnuo.gfcfSZyYpci', 'iot', 'iot', 'wo8335224@gmail.com', NULL, '19219288111', 0, NULL, NULL, 0, NULL, '2025-08-28 09:22:24', '', '2025-06-23 16:18:01', 'iot', '2025-08-12 18:46:23', '{\"exclusiveFirstLogin\":false}', NULL, 0, 4998, 5000, NULL); +INSERT INTO `iot_user` (`id`, `org_id`, `username`, `password`, `alias`, `union_id`, `email`, `salt`, `mobile`, `status`, `avatar`, `parent_union_id`, `identity`, `login_ip`, `login_date`, `create_by`, `create_date`, `update_by`, `update_date`, `cfg`, `remark`, `deleted`, `license`, `license_total_amount`, `register_from`) VALUES (1937479897454936064, NULL, 'test', '$2a$10$lo9j6knUiLMqsMgpajRNV.geNetYI/ryeQFxlSDAJXpd/.QRmiOpG', '请误故意删数据!否则IP拉黑', 'test', NULL, NULL, '13800138001', 0, NULL, NULL, 1, NULL, '2025-08-28 10:26:39', 'iot', '2025-06-24 19:56:19', 'iot', '2025-08-12 18:09:34', NULL, NULL, 0, 5001, 5000, 'admin'); +INSERT INTO `iot_user` (`id`, `org_id`, `username`, `password`, `alias`, `union_id`, `email`, `salt`, `mobile`, `status`, `avatar`, `parent_union_id`, `identity`, `login_ip`, `login_date`, `create_by`, `create_date`, `update_by`, `update_date`, `cfg`, `remark`, `deleted`, `license`, `license_total_amount`, `register_from`) VALUES (1944353342247555072, NULL, 'view', '$2a$10$B03AP27l8sinWJ0d5POQ/..8585fKpZhgndFG2QM0CjRtxbJacvZ.', '只读', 'view', NULL, NULL, '13900139000', 0, NULL, NULL, 1, NULL, NULL, 'iot', '2025-07-13 19:08:56', NULL, NULL, NULL, NULL, 0, 5000, 5000, 'admin'); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_user_application +-- ---------------------------- +DROP TABLE IF EXISTS `iot_user_application`; +CREATE TABLE `iot_user_application` ( + `app_unique_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '应用标识', + `union_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `app_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '应用名称', + `app_id` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '调用凭证APPID,必须16位', + `up_topic` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'MQ上行主题,英文逗号分隔', + `down_topic` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'MQ下行主题,英文逗号分隔', + `notify_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '回调地址', + `app_secret` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '调用密钥,必须32位', + `valid_end_date` datetime DEFAULT NULL COMMENT '授权结束时间', + `scope` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '授权范围', + `app_status` tinyint unsigned DEFAULT '0' COMMENT '0-正常,1-停用', + `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT 'public' COMMENT '实例名称', + `remark` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '描述', + `logo` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'logo', + `create_date` datetime DEFAULT NULL COMMENT '创建时间', + `cfg` json DEFAULT NULL COMMENT '额外配置', + `deleted` tinyint unsigned DEFAULT '0' COMMENT '0-正常,1-删除', + `http_enable` tinyint(1) DEFAULT '1' COMMENT 'http推送是否启用,0:不启用 1:启用', + `mqtt_enable` tinyint(1) DEFAULT '1' COMMENT 'mqtt推送是否启用,0:不启用 1:启用', + `tcp_enable` tinyint(1) DEFAULT '0' COMMENT 'tcp 809协议第三方推送是否启用 0不启用 1启用', + PRIMARY KEY (`app_unique_id`) USING BTREE, + UNIQUE KEY `idx_app_id` (`app_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户应用信息表'; + +-- ---------------------------- +-- Records of iot_user_application +-- ---------------------------- +BEGIN; +INSERT INTO `iot_user_application` (`app_unique_id`, `union_id`, `app_name`, `app_id`, `up_topic`, `down_topic`, `notify_url`, `app_secret`, `valid_end_date`, `scope`, `app_status`, `instance`, `remark`, `logo`, `create_date`, `cfg`, `deleted`, `http_enable`, `mqtt_enable`, `tcp_enable`) VALUES ('1938504461874020352', 'test', '笑盈盈1', '9PSfpC7Yp53fih5H', 'test/9PSfpC7Yp53fih5H', NULL, 'http://127.0.0.1:9092/test/echo', '5Lr9r7t53rU3AGOYEDA4vIVZmMMChjLJ', NULL, NULL, 0, 'public', '笑死钱1238', NULL, '2025-06-27 15:47:34', '{\"http\": {\"url\": \"http://127.0.0.1:9092/test/echo\"}, \"jdbc\": {}, \"mqtt\": {\"url\": \"tcp:127.0.0.1:1883\", \"topic\": \"$universal/appid/#\", \"enable\": false, \"support\": false, \"password\": \"xajsba123\", \"username\": \"xxxx\"}, \"kafka\": {\"enable\": false, \"support\": false}, \"rockerMQ\": {\"enable\": false, \"support\": false}, \"rocketMQ\": {\"enable\": false, \"support\": false}}', 0, 1, 1, 0); +COMMIT; + +-- ---------------------------- +-- Table structure for iot_user_sub +-- ---------------------------- +DROP TABLE IF EXISTS `iot_user_sub`; +CREATE TABLE `iot_user_sub` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `org_id` bigint DEFAULT NULL COMMENT '归属组织机构', + `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名', + `password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '别名', + `union_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户唯一标识', + `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '邮箱', + `salt` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码加盐', + `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '手机号', + `status` tinyint unsigned DEFAULT NULL COMMENT '账号状态(0正常,1停用)', + `avatar` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '头像', + `parent_union_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '上级用户唯一id', + `identity` tinyint NOT NULL DEFAULT '2' COMMENT '账号身份 0.超级管理员 1.普通用户 2.子用户', + `login_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '登录IP', + `login_date` datetime DEFAULT NULL COMMENT '登录时间', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建者', + `create_date` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `update_date` datetime DEFAULT NULL, + `cfg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '自定义配置', + `remark` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注', + `deleted` tinyint unsigned DEFAULT NULL COMMENT '0-正常,1-删除', + `license` int unsigned DEFAULT '0' COMMENT '022租户接入数限制', + `license_total_amount` int unsigned DEFAULT '1000' COMMENT 'license总额度', + `authorized_application_id` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '该账号有权限操作的应用id列表', + `app_id` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '子账号用于oauth2认证的客户端id', + `app_secret` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '子账号用于oauth2认证的客户端密钥', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='子账号表'; + +-- ---------------------------- +-- Records of iot_user_sub +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for notice_channel +-- ---------------------------- +DROP TABLE IF EXISTS `notice_channel`; +CREATE TABLE `notice_channel` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '配置名称', + `channel_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '渠道类型(如dingTalk、sms_ali、sms_tencent、feishu、wecom、email等)', + `config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '渠道参数(JSON字符串,如webhook、appKey、smtp等)', + `status` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='通知notice_channel表'; + +-- ---------------------------- +-- Records of notice_channel +-- ---------------------------- +BEGIN; +INSERT INTO `notice_channel` (`id`, `name`, `channel_type`, `config`, `status`, `remark`, `creator`, `create_time`, `update_time`) VALUES (1, 'xzxzx', 'email', '{}', '1', '', 'test', '2025-07-19 09:14:25', '2025-07-28 13:51:10'); +INSERT INTO `notice_channel` (`id`, `name`, `channel_type`, `config`, `status`, `remark`, `creator`, `create_time`, `update_time`) VALUES (2, 'xzxzx', 'email', '{}', '1', '', 'test', '2025-07-19 09:14:28', '2025-07-28 13:51:10'); +INSERT INTO `notice_channel` (`id`, `name`, `channel_type`, `config`, `status`, `remark`, `creator`, `create_time`, `update_time`) VALUES (3, 'AleoXin个人群', 'dingTalk', '{\"webhook\":\"https://oapi.dingtalk.com/robot/send?access_token=f731085dd9ef094114794a0f5ff2cc09e017d18acea205415cc03039af472661\",\"secret\":\"SECee225435f2e30b7218a2e7486bc7671d7ec0725e6e7d15142783f7b488161ddf\"}', '0', '', 'test', '2025-07-19 21:40:08', '2025-07-28 13:51:10'); +INSERT INTO `notice_channel` (`id`, `name`, `channel_type`, `config`, `status`, `remark`, `creator`, `create_time`, `update_time`) VALUES (4, '阿里呀', 'sms_ali', '{\"accessKey\":\"xzxz\",\"secretKey\":\"xzxz\",\"signName\":\"xzxz\",\"templateCode\":\"xzxzx\"}', '0', NULL, 'test', '2025-07-20 10:01:20', '2025-07-28 13:51:10'); +INSERT INTO `notice_channel` (`id`, `name`, `channel_type`, `config`, `status`, `remark`, `creator`, `create_time`, `update_time`) VALUES (5, '腾讯云语音', 'sms_tencent', '{\"secretId\":\"kcssds\",\"secretKey\":\"dsdscxcxc\",\"signName\":\"ccx\",\"templateCode\":\"xzxzxz\"}', '0', NULL, 'test', '2025-07-20 10:01:44', '2025-07-28 13:51:10'); +INSERT INTO `notice_channel` (`id`, `name`, `channel_type`, `config`, `status`, `remark`, `creator`, `create_time`, `update_time`) VALUES (6, 'cdcndjc', 'feishu', '{\"webhook\":\"kdsdsndsd\"}', '0', NULL, 'test', '2025-07-20 10:02:03', '2025-07-28 13:51:10'); +INSERT INTO `notice_channel` (`id`, `name`, `channel_type`, `config`, `status`, `remark`, `creator`, `create_time`, `update_time`) VALUES (7, '三楼消息', 'webhook', '{\"webhook\":\"http://192.168.31.194:9092/test/echo\",\"AuthTokenKey\":\"Universal_web\",\"TokenValue\":\"xsxscdscd\"}', '0', NULL, 'iot', '2025-07-23 20:35:07', '2025-08-10 22:02:08'); +COMMIT; + +-- ---------------------------- +-- Table structure for notice_send_record +-- ---------------------------- +DROP TABLE IF EXISTS `notice_send_record`; +CREATE TABLE `notice_send_record` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `template_id` bigint DEFAULT NULL COMMENT '模板ID', + `config_id` bigint DEFAULT NULL COMMENT '通知配置ID', + `receivers` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '实际收件人', + `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '实际参数(JSON字符串)', + `status` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发送状态(如SUCCESS、FAIL)', + `result` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '返回结果', + `send_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '发送时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_template_id` (`template_id`) USING BTREE, + KEY `idx_config_id` (`config_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='通知发送记录表'; + +-- ---------------------------- +-- Records of notice_send_record +-- ---------------------------- +BEGIN; +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (1, 3, 3, NULL, '{sceneName=联动, execTime=2025-08-15T14:18:02.877686094, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), time=1755238682672, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-15 14:18:03'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (2, 5, 7, NULL, '{sceneName=联动, execTime=2025-08-15T14:18:03.173005886, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), time=1755238682672, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-15 14:18:03'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (3, 3, 3, NULL, '{sceneName=联动, execTime=2025-08-15T16:25:00.980141149, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), time=1755246300935, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-15 16:25:01'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (4, 5, 7, NULL, '{sceneName=联动, execTime=2025-08-15T16:25:01.410751050, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), time=1755246300935, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-15 16:25:01'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (5, 3, 3, NULL, '{sceneName=联动, execTime=2025-08-15T20:39:49.057189411, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), time=1755261588961, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-15 20:39:50'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (6, 5, 7, NULL, '{sceneName=联动, execTime=2025-08-15T20:39:50.071993579, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), time=1755261588961, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-15 20:39:50'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (7, 3, 3, NULL, '{sceneName=联动, execTime=2025-08-15T23:18:28.783384155, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), eventName=电池恢复正常, time=1755271108374, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-15 23:18:29'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (8, 3, 3, NULL, '{sceneName=联动, execTime=2025-08-15T23:18:28.786797391, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), eventName=电池恢复正常, time=1755271108338, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-15 23:18:29'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (9, 5, 7, NULL, '{sceneName=联动, execTime=2025-08-15T23:18:29.081467901, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), eventName=电池恢复正常, time=1755271108374, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-15 23:18:29'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (10, 5, 7, NULL, '{sceneName=联动, execTime=2025-08-15T23:18:29.081467901, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), eventName=电池恢复正常, time=1755271108338, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-15 23:18:29'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (11, 3, 3, NULL, '{sceneName=联动, execTime=2025-08-16T11:53:24.445527, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), time=1755316404345, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-16 11:53:25'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (12, 5, 7, NULL, '{sceneName=联动, execTime=2025-08-16T11:53:36.419875, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), time=1755316404345, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-16 11:53:36'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (13, 3, 3, NULL, '{sceneName=联动, execTime=2025-08-16T13:55:55.926409797, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), time=1755323755908, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-16 13:55:56'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (14, 5, 7, NULL, '{sceneName=联动, execTime=2025-08-16T13:55:56.266825124, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), time=1755323755908, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-16 13:55:56'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (15, 3, 3, NULL, '{sceneName=联动, execTime=2025-08-16T16:15:19.217470131, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), time=1755332119170, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-16 16:15:20'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (16, 5, 7, NULL, '{sceneName=联动, execTime=2025-08-16T16:15:19.634643340, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), time=1755332119170, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-16 16:15:20'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (17, 3, 3, NULL, '{sceneName=联动, execTime=2025-08-16T21:14:36.422883894, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), time=1755350076311, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-16 21:14:37'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (18, 5, 7, NULL, '{sceneName=联动, execTime=2025-08-16T21:14:36.806093456, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), time=1755350076311, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-16 21:14:37'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (19, 3, 3, NULL, '{sceneName=联动, execTime=2025-08-17T14:29:34.595719677, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), time=1755412174562, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-17 14:29:35'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (20, 5, 7, NULL, '{sceneName=联动, execTime=2025-08-17T14:29:34.962613774, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), time=1755412174562, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-17 14:29:35'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (21, 3, 3, NULL, '{sceneName=联动, sceneId=63, execTime=2025-08-20T09:30:21.288363321, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}]}', 'SUCCESS', 'OK', '2025-08-20 09:30:22'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (22, 5, 7, NULL, '{sceneName=联动, sceneId=63, execTime=2025-08-20T09:30:21.656242463, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}]}', 'SUCCESS', 'OK', '2025-08-20 09:30:22'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (23, 3, 3, NULL, '{sceneName=联动, execTime=2025-08-26T13:50:31.860793421, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), time=1756187430545, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-26 13:50:39'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (24, 5, 7, NULL, '{sceneName=联动, execTime=2025-08-26T13:50:39.344427857, trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}], productKey=681c0775c2dc427d0480ab5f, deviceId=ov00014117, deviceName=潜江雅园1号楼底商店铺, iotId=7af18836921a48e9aedbb33551032a8a, userUnionId=test, messageType=EVENT, deviceNode=DEVICE, sceneId=63, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), time=1756187430545, event=batteryRestore, properties={}}', 'SUCCESS', 'OK', '2025-08-26 13:50:40'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (25, 3, 3, NULL, '{sceneName=联动, sceneId=63, execTime=2025-08-26T15:17:04.707173421, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}]}', 'SUCCESS', 'OK', '2025-08-26 15:17:05'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (26, 5, 7, NULL, '{sceneName=联动, sceneId=63, execTime=2025-08-26T15:17:04.900569804, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}]}', 'SUCCESS', 'OK', '2025-08-26 15:17:05'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (27, 3, 3, NULL, '{sceneName=联动, sceneId=63, execTime=2025-08-26T16:31:06.471396634, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=3, noticeTemplateName=Aleo的验证模版, execTriggerType=null), trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}]}', 'SUCCESS', 'OK', '2025-08-26 16:31:07'); +INSERT INTO `notice_send_record` (`id`, `template_id`, `config_id`, `receivers`, `params`, `status`, `result`, `send_time`) VALUES (28, 5, 7, NULL, '{sceneName=联动, sceneId=63, execTime=2025-08-26T16:31:06.645486175, action=TriggerBO(trigger=notice, cron=null, type=null, deviceId=null, deviceName=null, modelId=null, filters=[], execData=null, noticeTemplateId=5, noticeTemplateName=广丰机房消息推送, execTriggerType=null), trigger=[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}]}', 'SUCCESS', 'OK', '2025-08-26 16:31:07'); +COMMIT; + +-- ---------------------------- +-- Table structure for notice_template +-- ---------------------------- +DROP TABLE IF EXISTS `notice_template`; +CREATE TABLE `notice_template` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '模板名称', + `channel_id` bigint NOT NULL COMMENT '关联的通知配置ID', + `channel_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '渠道类型(如dingTalk、sms_ali、sms_tencent、feishu、wecom、email等)', + `status` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '状态', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '模板内容(支持变量占位符)', + `receivers` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '收件人(手机号、邮箱、群ID等,逗号分隔或JSON)', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_config_id` (`channel_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='通知模板表'; + +-- ---------------------------- +-- Records of notice_template +-- ---------------------------- +BEGIN; +INSERT INTO `notice_template` (`id`, `name`, `channel_id`, `channel_type`, `status`, `content`, `receivers`, `remark`, `creator`, `create_time`, `update_time`) VALUES (2, '行啊叫啊是', 4, 'feishu', '1', '#{productKey}#{deviceName}#{messageId}#{location}#{alertLevel}#{payload}#{topic}#{timestamp}#{eventType}', NULL, '飒飒', 'test', '2025-07-20 10:10:40', '2025-07-28 13:50:59'); +INSERT INTO `notice_template` (`id`, `name`, `channel_id`, `channel_type`, `status`, `content`, `receivers`, `remark`, `creator`, `create_time`, `update_time`) VALUES (3, 'Aleo的验证模版', 3, 'dingTalk', '1', '尊敬的用户你好,设备deviceId=#{deviceId},设备名称=#{deviceName}产生事件【#{event}】,所属租户 #appUnionId 请注意查看!', NULL, 'NASA\'s', 'test', '2025-07-20 10:57:39', '2025-07-30 15:54:03'); +INSERT INTO `notice_template` (`id`, `name`, `channel_id`, `channel_type`, `status`, `content`, `receivers`, `remark`, `creator`, `create_time`, `update_time`) VALUES (4, '3点提醒', 3, 'dingTalk', '1', '你好,现在是15点了哦~记得喝水~', NULL, '', 'test', '2025-07-23 14:59:43', '2025-07-28 13:50:59'); +INSERT INTO `notice_template` (`id`, `name`, `channel_id`, `channel_type`, `status`, `content`, `receivers`, `remark`, `creator`, `create_time`, `update_time`) VALUES (5, '三楼消息推送', 7, 'webhook', '1', '{\"deviceId\":\"#{deviceId}\",\"success\":\"ok\"}', NULL, 'JSON', 'iot', '2025-07-23 20:47:31', '2025-08-10 22:01:42'); +COMMIT; + +-- ---------------------------- +-- Table structure for oauth_client_details +-- ---------------------------- +DROP TABLE IF EXISTS `oauth_client_details`; +CREATE TABLE `oauth_client_details` ( + `client_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '客户端标识', + `resource_ids` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '资源标识', + `client_secret` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '客户端秘钥', + `scope` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `authorized_grant_types` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '授权类型\r\n authorization_code:授权码模式\r\nrefresh_token:刷新token\r\npassword:密码模式\r\nimplicit:隐私授权模式\r\nclient_credentials:客户端模式', + `web_server_redirect_uri` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '授权成功后回调URL', + `authorities` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `access_token_validity` int DEFAULT NULL COMMENT '访问令牌的有效时间', + `refresh_token_validity` int DEFAULT NULL COMMENT '刷新令牌的有效时间', + `additional_information` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '额外的信息,备用', + `autoapprove` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '自动批准标识:0代表不显示授权页面 1显示授权页面', + `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码', + `iot_union_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一标识', + PRIMARY KEY (`client_id`) USING BTREE, + UNIQUE KEY `idx_unq_client_id` (`client_id`) USING BTREE COMMENT 'client_id唯一校验' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='oauth2鉴权密钥表'; + +-- ---------------------------- +-- Records of oauth_client_details +-- ---------------------------- +BEGIN; +INSERT INTO `oauth_client_details` (`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`, `username`, `password`, `iot_union_id`) VALUES ('9PSfpC7Yp53fih5H', NULL, '$2a$10$L.cmEPEr/jc3wnoAsjjzO.txPa4voKe.ONO3fMnzKW39yI5Nq53Si', 'all', 'client_credentials,refresh_token,password', 'http://localhost:9091/client-authcode/login', NULL, 350, 36000, NULL, '1', NULL, NULL, 'iot'); +COMMIT; + +-- ---------------------------- +-- Table structure for protocol_file +-- ---------------------------- +DROP TABLE IF EXISTS `protocol_file`; +CREATE TABLE `protocol_file` ( + `file_path` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `file_content` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, + PRIMARY KEY (`file_path`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- ---------------------------- +-- Records of protocol_file +-- ---------------------------- +BEGIN; +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('/api/', 'this is directory'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('/api/123/', 'this is directory'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('/api/123/group.json', '{\n \"properties\" : { },\n \"id\" : \"7a98d2939f3344ab8641d4d667c2c262\",\n \"name\" : \"123\",\n \"type\" : \"api\",\n \"parentId\" : \"0\",\n \"path\" : \"12\",\n \"createTime\" : 1755090411512,\n \"updateTime\" : null,\n \"createBy\" : \"test\",\n \"updateBy\" : null,\n \"paths\" : [ ],\n \"options\" : [ ]\n}'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('/datasource/', 'this is directory'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('/function/', 'this is directory'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/', 'this is directory'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/Ctwing(电信AEP)/', 'this is directory'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/Ctwing(电信AEP)/group.json', '{\n \"properties\" : { },\n \"id\" : \"69eeb7ac5e72480d97f16064fe01a938\",\n \"name\" : \"Ctwing(电信AEP)\",\n \"type\" : \"api\",\n \"parentId\" : \"0\",\n \"path\" : \"ctwing\",\n \"createTime\" : 1752800093705,\n \"updateTime\" : null,\n \"createBy\" : \"test\",\n \"updateBy\" : null,\n \"paths\" : [ ],\n \"options\" : [ ]\n}'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/TCP/', 'this is directory'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/TCP/group.json', '{\n \"properties\" : { },\n \"id\" : \"f4beb08dc7fe4157b9f63a22a0e1a430\",\n \"name\" : \"TCP\",\n \"type\" : \"api\",\n \"parentId\" : \"0\",\n \"path\" : \"tcp\",\n \"createTime\" : 1752800005714,\n \"updateTime\" : null,\n \"createBy\" : \"test\",\n \"updateBy\" : null,\n \"paths\" : [ ],\n \"options\" : [ ]\n}'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/TCP/单相导轨XA-GXSD8-4G.ms', '{\n \"properties\" : { },\n \"id\" : \"5393bfe75eb64c2182f4308458eadb5e\",\n \"script\" : null,\n \"groupId\" : \"f4beb08dc7fe4157b9f63a22a0e1a430\",\n \"name\" : \"单相导轨XA-GXSD8-4G\",\n \"createTime\" : 1752805008549,\n \"updateTime\" : 1754787309090,\n \"lock\" : null,\n \"createBy\" : \"test\",\n \"updateBy\" : \"test\",\n \"path\" : \"abcxassxnanahsman\",\n \"method\" : \"POST\",\n \"parameters\" : [ ],\n \"options\" : [ ],\n \"requestBody\" : null,\n \"headers\" : [ ],\n \"paths\" : [ ],\n \"responseBody\" : null,\n \"description\" : \"这个是一个电表\",\n \"requestBodyDefinition\" : null,\n \"responseBodyDefinition\" : null\n}\n================================\n//16进制ASCII转STRING\nvar hex2str = stringhex => {\n\n var i = 0\n var asciidata = \"\"\n while (i < stringhex.length()) {\n asciidata += (String.valueOf(Integer.parseInt(stringhex.substring(i, i + 2), 16)) + \",\")\n i += 2\n }\n asciidata = asciidata.substring(0, asciidata.length() - 1)\n return asciiToString(asciidata)\n}\n//大小端转换\nvar reverseStr = data => {\n var rData = \"\"\n if (data.length() >= 0) {\n let seq = data.length()\n while (seq > 0) {\n rData += data.substring(seq - 2, seq)\n seq = seq - 2\n }\n }\n return rData\n}\n//和校验计算\nvar check_CS = strData => {\n let i = 0\n let accum = 0\n while (i < strData.length()) {\n accum += Integer.parseInt(strData.substring(i, i + 2), 16)\n i += 2\n }\n let l = Integer.toHexString(accum).length()\n return Integer.toHexString(accum).substring(l - 2, l).toUpperCase()\n}\n//数据转换 倒转 -33\nvar transform = str => {\n let i = 0\n let result = \'\'\n str = reverseTwoDigit(str)\n while (i < str.length()) {\n if (str.substring(i, 2 + i) == \'33\') {\n i += 2\n result += \'00\'\n } else {\n let value = Integer.parseInt(str.substring(i, 2 + i), 16) - 0x33;\n i += 2\n result += String.format(\'%02X\', value)\n }\n }\n return Integer.parseInt(result, 10)\n}\n//+0x33方法\nvar dataplus = strData => {\n let sData = \'\'\n let i = 0\n let pData = 0\n while (i < strData.length()) {\n pData = (Integer.parseInt(strData.substring(i, i + 2), 16) + 0x33) & 0b11111111\n sData += String.format(\'%02X\', pData)\n i += 2\n }\n return sData\n}\n\n//预解码,解析上报的设备编号产品deviceId\nvar preDecode = payload => {\n var result = {}\n //注册包\n if (payload.length() == 12) {\n result.deviceId = reverseTwoDigit(payload)\n }\n \n //电表电量上报消息预解码\n if(payload.length() == 98 && payload.substring(0,2).equals(\'68\')&& payload.substring(payload.length()-2,payload.length()).equals(\'16\')){\n result.deviceId = reverseStr(payload.substring(2, 14))\n }\n return result\n}\n//上行处理\nvar decode = (payload,context) => {\n var result = []\n var propertiesObj = {\n \"messageType\": \"PROPERTIES\",\n \"properties\": {}\n }\n propertiesObj.properties.originalMessage = payload\n\n\n //propertiesObj.properties.shadow = pData.shadow\n result.push(propertiesObj)\n //心跳包\n if (payload.length() == 12) {\n result.push(propertiesObj)\n propertiesObj.tcpEncode = \'HEX\';\n propertiesObj.replyPayload = \'7868\'\n } else {\n //抄表结果\n if (payload.length() >= 94 && payload.substring(14, 18) == \'6891\') {\n //表号\n propertiesObj.properties.meterNo = reverseStr(payload.substring(2, 14))\n var length = Integer.parseInt(payload.substring(18, 20), 16)\n\n //总用电量\n let mData = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\n propertiesObj.properties.meterData = mData.substring(0, mData.length() - 1)\n //取影子里的上次读数\n // if (pData!=null &&pData.containsKey(\'shadow\')) {\n // if (pData.shadow.state.reported.containsKey(\'meterData\')) {\n // propertiesObj.properties.lastMeterData = pData.shadow.state.reported.meterData\n // }\n // }\n //propertiesObj.properties.meterData = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\n //电价\n propertiesObj.properties.meterPrice = (transform(payload.substring(36, 44)) * 0.0001).toFixed(4)\n //电压\n propertiesObj.properties.voltage = (transform(payload.substring(44, 52)) * 0.1).toFixed(1)\n //电流\n propertiesObj.properties.current = (transform(payload.substring(52, 60)) * 0.001).toFixed(3)\n //功率\n propertiesObj.properties.power = (transform(payload.substring(60, 68)) * 0.1).toFixed(1)\n //购电次数\n propertiesObj.properties.purchases = transform(payload.substring(68, 76))\n //预警电量\n propertiesObj.properties.warningPower = (transform(payload.substring(76, 84)) * 0.01).toFixed(2)\n //剩余金额\n propertiesObj.properties.remainingAmount = (transform(payload.substring(84, 92)) * 0.01).toFixed(2)\n propertiesObj.properties.openState = (Integer.parseInt(payload.substring(92, 94), 16) - 0x33) == 27 ? \'1\' : \'0\'\n //新增查询信号值,在reply里面下发\n propertiesObj.tcpEncode = \'HEX\';\n propertiesObj.replyPayload = \'727270632C676574637371\'\n }\n //抄表分包,第一部分\n if (payload.length() == 64 && payload.substring(14, 18) == \'6891\') {\n //表号\n propertiesObj.properties.meterNo = reverseStr(payload.substring(2, 14))\n var length = Integer.parseInt(payload.substring(18, 20), 16)\n\n //总用电量\n let mData = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\n propertiesObj.properties.meterData = mData.substring(0, mData.length() - 1)\n //取影子里的上次读数\n // if (pData.containsKey(\'shadow\')) {\n // if (pData.shadow.state.reported.containsKey(\'meterData\')) {\n // propertiesObj.properties.lastMeterData = pData.shadow.state.reported.meterData\n // }\n // }\n //propertiesObj.properties.meterData = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\n //电价\n propertiesObj.properties.meterPrice = (transform(payload.substring(36, 44)) * 0.0001).toFixed(4)\n //电压\n propertiesObj.properties.voltage = (transform(payload.substring(44, 52)) * 0.1).toFixed(1)\n //电流\n propertiesObj.properties.current = (transform(payload.substring(52, 60)) * 0.001).toFixed(3)\n //功率\n propertiesObj.properties.power = (transform(payload.substring(60, 64)) * 0.1).toFixed(1)\n }\n //抄表分包,第二部分\n if (payload.length() == 34) {\n //\n //购电次数\n propertiesObj.properties.purchases = transform(payload.substring(4, 12))\n //预警电量\n propertiesObj.properties.warningPower = (transform(payload.substring(12, 20)) * 0.01).toFixed(2)\n //剩余金额\n propertiesObj.properties.remainingAmount = (transform(payload.substring(20, 28)) * 0.01).toFixed(2)\n propertiesObj.properties.openState = (Integer.parseInt(payload.substring(28, 30), 16) - 0x33) == 27 ? \'1\' : \'0\'\n //新增查询信号值,在reply里面下发\n propertiesObj.tcpEncode = \'HEX\';\n propertiesObj.replyPayload = \'727270632C676574637371\'\n }\n //控制开合闸回复的丢数据头68兼容处理\n if (payload.length() == 48) {\n if (payload.substring(12, 18).equals(\'689C0D\') && ((payload.substring(42, 44).equals(\'4D\')) || (payload.substring(42, 44).equals(\'4E\')))) {\n payload = \'68\' + payload\n }\n }\n\n //控制开合闸回复\n if (payload.substring(14, 18) == \'689C\') {\n let replyswitchObj = {\n \"messageType\": \"REPLY\",\n \"function\": \"switchOnOff\",\n \"data\": {\n \"openState\": (Integer.parseInt(payload.substring(44, 46), 16) - 0x33) == 27 ? \'1\' : \'0\'\n }\n }\n\n result.push(replyswitchObj)\n //同步上报通电断电属性\n propertiesObj.properties.openState = replyswitchObj.data.openState\n //propertiesObj.properties.meterData = (transform(payload.substring(20, 28)) * 0.01).toFixed(2)\n propertiesObj.properties.remainingAmount = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\n propertiesObj.properties.purchases = transform(payload.substring(36, 44))\n //新增查询信号值,在reply里面下发\n propertiesObj.tcpEncode = \'HEX\';\n propertiesObj.replyPayload = \'727270632C676574637371\'\n }\n //清表回复\n if (payload.substring(14, 18) == \'689A\') {\n let replyswitchObj = {\n \"messageType\": \"REPLY\",\n \"function\": \"reset\",\n \"data\": {\n \"result\": \'设置成功\'\n }\n }\n result.push(replyswitchObj)\n let mData = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\n propertiesObj.properties.meterData = mData.substring(0, mData.length() - 1)\n //propertiesObj.properties.meterData = (transform(payload.substring(20, 28)) * 0.01).toFixed(2)\n propertiesObj.properties.remainingAmount = (transform(payload.substring(28, 36)) * 0.01).toFixed(2)\n propertiesObj.properties.purchases = transform(payload.substring(36, 44))\n propertiesObj.properties.openState = (Integer.parseInt(payload.substring(44, 46), 16) - 0x33) == 27 ? \'1\' : \'0\'\n }\n //设置限值回复\n if (payload.substring(14, 20) == \'68841C\') {\n let replyLimitObj = {\n \"messageType\": \"REPLY\",\n \"function\": \"setOverloadLimit\",\n \"data\": {\n \"overVoltageLimit\": \"\",\n \"overCurrrentLimit\": \"\"\n }\n }\n result.push(replyLimitObj)\n //囤积金额\n //propertiesObj.properties.hoardAmount = \n\n }\n //售电回复\n if (payload.substring(14, 20) == \'68840C\') {\n let replyRechargeObj = {\n \"messageType\": \"REPLY\",\n \"function\": \"recharge\",\n \"data\": {\n \"result\": \'设置成功\'\n }\n }\n result.push(replyRechargeObj)\n //囤积金额\n //propertiesObj.properties.hoardAmount = \n\n }\n //比较上次读数和这次读数的差额\n if (propertiesObj.properties.containsKey(\'lastMeterData\') && propertiesObj.properties.containsKey(\'meterData\')) {\n let shortfallData = Float.parseFloat(propertiesObj.properties.meterData) - Float.parseFloat(propertiesObj.properties.lastMeterData)\n propertiesObj.properties.shortfallData = Float.parseFloat(shortfallData.toFixed(2))\n if (propertiesObj.properties.shortfallData < 0) {\n let shortfallAlarmObj = {\n \"messageType\": \"EVENT\",\n \"event\": \"\",\n \"data\": {\n \"level\": \"warn\"\n }\n }\n shortfallAlarmObj.event = \'shortfallAlarm\'\n shortfallAlarmObj.data.shortfallAlarm = \'读数倒流告警\'\n shortfallAlarmObj.data.shortfallData = propertiesObj.properties.shortfallData\n result.push(shortfallAlarmObj)\n }\n }\n //CSQ数据,ICCID数据\n /**\n * rrpc,getcsq,22\n * rrpc,geticcid,898604A8102440120152\n * \n */\n if (payload.length() >= 8 && payload.substring(0, 8).equals(\'72727063\')) {\n let rData = hex2str(payload)\n let rArray = rData.split(\',\')\n //CSQ数据\n if (rArray[0].equals(\'rrpc\') && rArray[1].equals(\'getcsq\')) {\n propertiesObj.properties.csq = rArray[2]\n propertiesObj.properties.signalLevel = csq(propertiesObj.properties.csq)\n }\n if (rArray[0].equals(\'rrpc\') && rArray[1].equals(\'geticcid\')) {\n propertiesObj.properties.iccid = rArray[2]\n }\n\n }\n }\n /*//开始字节\n if (payload.substring(0, 2) != \'68\') {\n return jsonToStr(result)\n }*/\n\n\n return jsonToStr(result)\n}\n//下行处理\nvar encode = payload => {\n var obj = toJson(payload)\n //读取电表用量\n if (obj.function == \'meterRead\') {\n if (obj.data.meterNo.length() == 12) {\n let str = \'68\' + reverseStr(obj.data.meterNo) + \'6811043335C433\'\n let check = check_CS(str)\n str = str + check + \'16\'\n return str.toLowerCase()\n }\n }\n //开关闸\n if (obj.function == \'switchOnOff\') {\n if (obj.data.meterNo.length() == 12) {\n //1为合闸\n let switchOn = obj.data.valveOn == \'1\' ? \'4E\' : \'4D\'\n let str = \'68\' + reverseStr(obj.data.meterNo) + \'681C103533333334896745\' + switchOn + \'3347773B3A443C\'\n let check = check_CS(str)\n str = str + check + \'16\'\n return str.toLowerCase()\n }\n }\n //清表(清除所有,余额、总电量、购电次数、电价全部初始化)\n if (obj.function == \'reset\') {\n if (obj.data.meterNo.length() == 12) {\n let str = \'68\' + reverseStr(obj.data.meterNo) + \'681A09353333333489674535\'\n let check = check_CS(str)\n str = str + check + \'16\'\n return str.toLowerCase()\n }\n }\n //超限值设置\n if (obj.function == \'setOverloadLimit\') {\n if (obj.data.meterNo.length() == 12) {\n let str = \'68\' + reverseStr(obj.data.meterNo) + \'6804203633333335333333734036333333333373403633\'\n //电压限值处理\n if (obj.data.containsKey(\'overVoltageLimit\') && obj.data.overVoltageLimit != NULL) {\n if (Float.valueOf(obj.data.overVoltageLimit) > 220.0 && Float.valueOf(obj.data.overVoltageLimit) <= 260.0) {\n let hv = String.format(\'%08X\', Integer.valueOf((Float.valueOf(obj.data.overVoltageLimit) * 10).toFixed(0)))\n str += reverseStr(dataplus(hv))\n }\n //如果电压限值不在范围内,补默认限值260.0V\n else {\n str += reverseStr(dataplus(String.format(\'%08X\', 2600)))\n }\n }\n //如果不填写电压限值,默认2600\n else {\n str += reverseStr(dataplus(String.format(\'%08X\', 2600)))\n }\n //电流限值处理\n if (obj.data.containsKey(\'overCurrrentLimit\') && obj.data.overCurrrentLimit != NULL) {\n if (Float.valueOf(obj.data.overCurrrentLimit) > 0.000 && Float.valueOf(obj.data.overCurrrentLimit) < 80.000) {\n let hI = String.format(\'%08X\', Integer.valueOf((Float.valueOf(obj.data.overCurrrentLimit) * 1000).toFixed(0)))\n str += reverseStr(dataplus(hI))\n }\n //如果电流限值不在范围内,补默认限值80A\n else {\n str += reverseStr(dataplus(String.format(\'%08X\', 80000)))\n }\n }\n //如果不填写电流限值,默认80000\n else {\n str += reverseStr(dataplus(String.format(\'%08X\', 80000)))\n }\n //加倍率,默认1\n str += \'34333333\'\n let check = check_CS(str)\n str = str + check + \'16\'\n return str.toLowerCase()\n }\n }\n //售电\n if (obj.function == \'recharge\') {\n if (obj.data.meterNo.length() == 12) {\n let str = \'68\' + reverseStr(obj.data.meterNo) + \'6804103433333335333333\'\n str += reverseStr(dataplus(String.format(\'%08X\', (Integer.valueOf(obj.data.rechargeData)) * 100)))\n\n if (obj.data.containsKey(\'rechargeCount\')) {\n str += reverseStr(dataplus(String.format(\'%08X\', obj.data.rechargeCount)))\n } else {\n str += \'34333333\'\n }\n\n let check = check_CS(str)\n str = str + check + \'16\'\n return str.toLowerCase()\n }\n }\n //测试\n if (obj.function == \"cmd\") {\n let cmdData = obj.data.cmdData\n return cmdData\n }\n //读取信号强度CSQ\n if (obj.function == \"getCSQ\") {\n let cmdData = \'727270632C676574637371\'\n return cmdData\n }\n //读取物联网卡ICCID\n if (obj.function == \"getICCID\") {\n let cmdData = \'727270632C6765746963636964\'\n return cmdData\n }\n}\n\n// return preDecode(\'689300220525206891253335c43368333333333333334b563333c7673333b7b533333333333333333333333333334ed016\')\n// return decode(\'68121111111111689C0D33343333333c3333343333334ecs16\')\n//return encode(\'{\"messageType\":\"FUNCTIONS\",\"function\":\"recharge\",\"data\":{\"meterNo\":\"202411190247\",\"rechargeData\":10000,\"rechargeCount\":2}}\')\nreturn decode(\'684702191124206891253335c43333333333333334337a55333333333333333333333433333333333333338333334e0516\')\n//return decode(\'{\"payload\":\"202411190261\"}\')\n\n//带影子数据\n//return decode(\'{\"shadow\":{\"metadata\":{\"desired\":{},\"reported\":{\"purchases\":{\"timestamp\":1747726475},\"openState\":{\"timestamp\":1747726475},\"meterPrice\":{\"timestamp\":1747726475},\"voltage\":{\"timestamp\":1747726475},\"remainingAmount\":{\"timestamp\":1747726475},\"current\":{\"timestamp\":1747726475},\"warningPower\":{\"timestamp\":1747726475},\"meterNo\":{\"timestamp\":1747726475},\"power\":{\"timestamp\":1747726475},\"originalMessage\":{\"timestamp\":1747726723},\"meterData\":{\"timestamp\":1747726475}}},\"version\":315,\"state\":{\"desired\":{},\"reported\":{\"purchases\":15,\"openState\":\"1\",\"meterPrice\":\"1.0000\",\"voltage\":\"227.9\",\"remainingAmount\":\"199999.04\",\"current\":\"0.000\",\"warningPower\":\"0.00\",\"meterNo\":\"202411190247\",\"power\":\"0.0\",\"originalMessage\":\"727270632C676574637371\",\"meterData\":\"1.0\"}},\"timestamp\":1747726723},\"productKey\":\"67c69f2a6b0001a9b22a6c9a\",\"deviceId\":\"202411190247\",\"iotId\":\"63337253d93644bcad1513ca318a520a\",\"payload\":\"684702191124206891253335c43333333333333334337a55333333333333333333333433333333333333338333334e0516\"}\')\n\n//CSQ数据\nreturn decode(\'727270632C6765746373712C3232\')\n\n//ICCID数据\n// return decode(\'{\"payload\":\"727270632C67657469636369642C3839383630344138313032343430313230313532\"}\')'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/TCP/小蘑菇插座.ms', '{\n \"properties\" : { },\n \"id\" : \"b5723ec5a8e24bdaa647e60a1dc82638\",\n \"script\" : null,\n \"groupId\" : \"f4beb08dc7fe4157b9f63a22a0e1a430\",\n \"name\" : \"小蘑菇插座\",\n \"createTime\" : 1753423344428,\n \"updateTime\" : 1754039332333,\n \"lock\" : null,\n \"createBy\" : \"test\",\n \"updateBy\" : \"test\",\n \"path\" : \"68831a1f38fc106a8dac3c13\",\n \"method\" : \"POST\",\n \"parameters\" : [ ],\n \"options\" : [ ],\n \"requestBody\" : null,\n \"headers\" : [ ],\n \"paths\" : [ ],\n \"responseBody\" : \"[]\",\n \"description\" : \"AI自动生成的解析和协议\",\n \"requestBodyDefinition\" : null,\n \"responseBodyDefinition\" : null\n}\r\n================================\r\nimport log;\n\n// 16进制转10进制数字\nvar hexToInt = hex => {\n return Integer.parseInt(hex, 16);\n};\n// 预解码,解析上报的设备编号 deviceId\nvar preDecode = payload => {\n var result = {}\n var payloadHex = payload.toUpperCase() // 转为大写\n // 校验报文长度和帧头\n if (payloadHex.length() >= 18 && payloadHex.substring(0, 4) === \"A5A5\") {\n // 从第5位开始取4字节(8个字符)作为 deviceId\n result.deviceId = payloadHex.substring(4, 12)\n }\n return result\n}\n\nvar decode =(payload,context) => {\n log.info(\"进入编解码,payload={},context={}\",payload,context)\n var result = [];\n var payloadHex =payload.toUpperCase();\n var propertiesObj = { \"messageType\": \"PROPERTIES\", \"properties\": {} };\n var eventObj = { \"messageType\": \"EVENT\", \"event\": \"\", \"data\": {} };\n\n // 验证帧头帧尾\n if (payloadHex.substring(0, 4) != \"A5A5\" || payloadHex.substring(payloadHex.length() - 4) != \"5A5A\") {\n return jsonToStr([{ \"error\": \"无效帧格式\" }]);\n }\n\n // 提取核心字段\n var deviceId = payloadHex.substring(4, 8); // 设备ID(4字节)\n var cmdCode = payloadHex.substring(8, 10); // 命令码(1字节)\n var dataLen = hexToInt(payloadHex.substring(10, 12)); // 数据长度\n var dataHex = payloadHex.substring(12, 12 + dataLen * 2); // 数据区\n var checksum = payloadHex.substring(12 + dataLen * 2, 14 + dataLen * 2); // 校验和\n\n // 设备ID存入所有对象\n propertiesObj.deviceId = deviceId;\n eventObj.deviceId = deviceId;\n //验证新内容\n propertiesObj.context=context;\n propertiesObj.tcpEncode = \'HEX\';\n propertiesObj.replyPayload = \'727270632C676574637371\'\n // 1. 属性上报(命令码01)\n if (cmdCode == \"01\" && dataLen == 7) {\n // 解析开关状态(01=开,00=关)\n propertiesObj.properties.powerState = dataHex.substring(0, 2) == \"01\" ? \"01\" : \"00\";\n // 解析电流(2字节,单位0.01A)\n var currentHex = dataHex.substring(2, 6);\n propertiesObj.properties.current = (hexToInt(currentHex) * 0.01).toFixed(2);\n // 解析电压(2字节,单位0.1V)\n var voltageHex = dataHex.substring(6, 10);\n propertiesObj.properties.voltage = (hexToInt(voltageHex) * 0.1).toFixed(1);\n // 解析功率(2字节,单位1W)\n var powerHex = dataHex.substring(10, 14);\n propertiesObj.properties.power = hexToInt(powerHex).toString();\n // 加入结果集\n result.push(propertiesObj);\n }\n\n // 2. 事件上报(命令码02)\n if (cmdCode == \"02\" && dataLen >= 3) {\n // 事件类型(01=过载,02=过压,03=上线,04=下线)\n var eventType = dataHex.substring(0, 2);\n if (eventType == \"01\") {\n eventObj.event = \"overload\";\n eventObj.data.overload = hexToInt(dataHex.substring(2, 6)) + \"W\"; // 过载功率\n }\n if (eventType == \"03\") {\n eventObj.event = \"online\";\n eventObj.data.online = \"设备上线\";\n }\n if (eventType == \"04\") {\n eventObj.event = \"offline\";\n eventObj.data.offline = \"设备下线\";\n }\n result.push(eventObj);\n }\n\n // 3. 响应报文(命令码04)\n if (cmdCode == \"04\" && dataLen == 1) {\n var replyObj = {\n \"messageType\": \"REPLY\",\n \"function\": \"switch\",\n \"data\": { \"result\": dataHex == \"00\" ? \"success\" : \"fail\" }\n };\n replyObj.deviceId = deviceId;\n result.push(replyObj);\n }\n\n return jsonToStr(result);\n};\n\nvar encode = payload => {\n var obj = toJson(payload);\n var deviceId = obj.data.deviceId; // 设备ID(4位16进制,如\"0001\")\n var func = obj.function;\n var params = obj.data;\n\n // 控制开关(function=switch)\n if (func == \"switch\") {\n var state = params.state; // 目标状态:01=开,00=关\n if (state != \"01\" && state != \"00\") {\n return \"\"; // 无效状态,返回空\n }\n\n // 数据区:控制类型(01)+目标状态\n var dataHex = \"01\" + state;\n var dataLen = dataHex.length() / 2; // 数据长度(字节数)\n var cmdCode = \"03\"; // 控制指令命令码\n\n // 拼接完整报文(无校验和)\n var frame = \"A5A5\" + \n deviceId + \n cmdCode + \n Integer.toHexString(dataLen) + // 数据长度(1位16进制)\n dataHex + \n \"5A5A\";\n\n return frame.toUpperCase(); // 转为大写16进制\n }\n\n return \"\"; // 不支持的指令\n};\n\n// return preDecode(\"A5A5000101070105DC08F004B05A5A\")\n//属性上报报文\nreturn decode(\'A5A5000201070103E809C405DC5A5A\');\n\n//过载事件报文\n// return decode(\"A5A5000102030104B0F35A5A\")\n\n\n// return encode(\'{\"deviceId\": \"0001\",\"function\": \"switch\",\"data\": { \"state\": \"01\" }}\')\n\n\n// return encode(\'{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"00010107\",\"state\":\"01\"}}\')'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/test/', 'this is directory'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/test/group.json', '{\n \"properties\" : { },\n \"id\" : \"77b59996549d4638a85888f7edd8131d\",\n \"name\" : \"test\",\n \"type\" : \"api\",\n \"parentId\" : \"0\",\n \"path\" : \"test\",\n \"createTime\" : 1753682463817,\n \"updateTime\" : null,\n \"createBy\" : \"test\",\n \"updateBy\" : null,\n \"paths\" : [ ],\n \"options\" : [ ]\n}'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/test/小蘑菇插座.ms', '{\n \"properties\" : { },\n \"id\" : \"6ead3277d72e4740bbe5e2a29c2672f4\",\n \"script\" : null,\n \"groupId\" : \"77b59996549d4638a85888f7edd8131d\",\n \"name\" : \"小蘑菇插座\",\n \"createTime\" : 1753682493898,\n \"updateTime\" : 1755239952178,\n \"lock\" : \"0\",\n \"createBy\" : \"test\",\n \"updateBy\" : \"test\",\n \"path\" : \"68831a1f38fc106a8dac3c13\",\n \"method\" : \"POST\",\n \"parameters\" : [ ],\n \"options\" : [ ],\n \"requestBody\" : null,\n \"headers\" : [ ],\n \"paths\" : [ ],\n \"responseBody\" : \"{\\n \\\"deviceId\\\": \\\"00010107\\\"\\n}\",\n \"description\" : \"AI自动生成的解析和协议\",\n \"requestBodyDefinition\" : null,\n \"responseBodyDefinition\" : null\n}\r\n================================\r\nimport log;\n\n// 16进制转10进制数字\nvar hexToInt = hex => {\n return Integer.parseInt(hex, 16);\n};\n// 预解码,解析上报的设备编号 deviceId\nvar preDecode = payload => {\n var result = {}\n var payloadHex = payload.toUpperCase() // 转为大写\n // 校验报文长度和帧头\n if (payloadHex.length() >= 18 && payloadHex.substring(0, 4) === \"A5A5\") {\n // 从第5位开始取4字节(8个字符)作为 deviceId\n result.deviceId = payloadHex.substring(4, 12)\n }\n return result\n}\n\nvar decode =(payload,context) => {\n log.info(\"进入编解码,payload={},context={}\",payload,context)\n var result = [];\n var payloadHex =payload.toUpperCase();\n var propertiesObj = { \"messageType\": \"PROPERTIES\", \"properties\": {} };\n var eventObj = { \"messageType\": \"EVENT\", \"event\": \"\", \"data\": {} };\n\n // 验证帧头帧尾\n if (payloadHex.substring(0, 4) != \"A5A5\" || payloadHex.substring(payloadHex.length() - 4) != \"5A5A\") {\n return jsonToStr([{ \"error\": \"无效帧格式\" }]);\n }\n\n // 提取核心字段\n var deviceId = payloadHex.substring(4, 8); // 设备ID(4字节)\n var cmdCode = payloadHex.substring(8, 10); // 命令码(1字节)\n var dataLen = hexToInt(payloadHex.substring(10, 12)); // 数据长度\n var dataHex = payloadHex.substring(12, 12 + dataLen * 2); // 数据区\n var checksum = payloadHex.substring(12 + dataLen * 2, 14 + dataLen * 2); // 校验和\n\n // 设备ID存入所有对象\n propertiesObj.deviceId = deviceId;\n eventObj.deviceId = deviceId;\n //验证新内容\n propertiesObj.context=context;\n propertiesObj.tcpEncode = \'HEX\';\n propertiesObj.replyPayload = \'727270632C676574637371\'\n // 1. 属性上报(命令码01)\n if (cmdCode == \"01\" && dataLen == 7) {\n // 解析开关状态(01=开,00=关)\n propertiesObj.properties.powerState = dataHex.substring(0, 2) == \"01\" ? \"01\" : \"00\";\n // 解析电流(2字节,单位0.01A)\n var currentHex = dataHex.substring(2, 6);\n propertiesObj.properties.current = (hexToInt(currentHex) * 0.01).toFixed(2);\n // 解析电压(2字节,单位0.1V)\n var voltageHex = dataHex.substring(6, 10);\n propertiesObj.properties.voltage = (hexToInt(voltageHex) * 0.1).toFixed(1);\n // 解析功率(2字节,单位1W)\n var powerHex = dataHex.substring(10, 14);\n propertiesObj.properties.power = hexToInt(powerHex).toString();\n // 加入结果集\n result.push(propertiesObj);\n }\n\n // 2. 事件上报(命令码02)\n if (cmdCode == \"02\" && dataLen >= 3) {\n // 事件类型(01=过载,02=过压,03=上线,04=下线)\n var eventType = dataHex.substring(0, 2);\n if (eventType == \"01\") {\n eventObj.event = \"overload\";\n eventObj.data.overload = hexToInt(dataHex.substring(2, 6)) + \"W\"; // 过载功率\n }\n if (eventType == \"03\") {\n eventObj.event = \"online\";\n eventObj.data.online = \"设备上线\";\n }\n if (eventType == \"04\") {\n eventObj.event = \"offline\";\n eventObj.data.offline = \"设备下线\";\n }\n result.push(eventObj);\n }\n\n // 3. 响应报文(命令码04)\n if (cmdCode == \"04\" && dataLen == 1) {\n var replyObj = {\n \"messageType\": \"REPLY\",\n \"function\": \"switch\",\n \"data\": { \"result\": dataHex == \"00\" ? \"success\" : \"fail\" }\n };\n replyObj.deviceId = deviceId;\n result.push(replyObj);\n }\n\n return jsonToStr(result);\n};\n\nvar encode = payload => {\n var obj = toJson(payload);\n var deviceId = obj.data.deviceId; // 设备ID(4位16进制,如\"0001\")\n var func = obj.function;\n var params = obj.data;\n\n // 控制开关(function=switch)\n if (func == \"switch\") {\n var state = params.state; // 目标状态:01=开,00=关\n if (state != \"01\" && state != \"00\") {\n return \"\"; // 无效状态,返回空\n }\n\n // 数据区:控制类型(01)+目标状态\n var dataHex = \"01\" + state;\n var dataLen = dataHex.length() / 2; // 数据长度(字节数)\n var cmdCode = \"03\"; // 控制指令命令码\n\n // 拼接完整报文(无校验和)\n var frame = \"A5A5\" + \n deviceId + \n cmdCode + \n Integer.toHexString(dataLen) + // 数据长度(1位16进制)\n dataHex + \n \"5A5A\";\n\n return frame.toUpperCase(); // 转为大写16进制\n }\n\n return \"\"; // 不支持的指令\n};\n\n// return preDecode(\"A5A5000101070105DC08F004B05A5A\")\n//属性上报报文\nreturn decode(\'A5A5000101070105DC08F004B07A5A5A\');\n\n//过载事件报文\n// return decode(\"A5A5000102030104B0F35A5A\")\n\n\n// return encode(\'{\"deviceId\": \"0001\",\"function\": \"switch\",\"data\": { \"state\": \"01\" }}\')\n\n\n// return encode(\'{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"00010107\",\"state\":\"01\"}}\')'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/test/开关面板-MQTT-doc编写.ms', '{\n \"properties\" : { },\n \"id\" : \"e3857faf66c04d9499c4d1e6a4406036\",\n \"script\" : null,\n \"groupId\" : \"77b59996549d4638a85888f7edd8131d\",\n \"name\" : \"开关面板-MQTT-doc编写\",\n \"createTime\" : 1754144167814,\n \"updateTime\" : 1754209859636,\n \"lock\" : null,\n \"createBy\" : \"test\",\n \"updateBy\" : \"test\",\n \"path\" : \"688cd92fdb9e0158872f4aa9\",\n \"method\" : \"POST\",\n \"parameters\" : [ ],\n \"options\" : [ ],\n \"requestBody\" : null,\n \"headers\" : [ ],\n \"paths\" : [ ],\n \"responseBody\" : null,\n \"description\" : \"开关面板-MQTT-doc编写\",\n \"requestBodyDefinition\" : null,\n \"responseBodyDefinition\" : null\n}\r\n================================\r\nimport log\n\n\n/**\n * 编码:平台指令转设备格式\n * @param {Object} payload 平台指令\n * @returns {String} 设备可识别的报文(二进制、JSON、16进制字符串等)\n */\nvar encode = payload => {\n var rs=toJson(payload);\n rs.iot=\'演示iot\'\n rs.randomNum=randomSign();\n // TODO:实现编码逻辑\n return rs;\n};\n'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/test/开关面板-文档版-TCP.ms', '{\n \"properties\" : { },\n \"id\" : \"44e371fdbede4e44a0b3ddf7620c8b54\",\n \"script\" : null,\n \"groupId\" : \"77b59996549d4638a85888f7edd8131d\",\n \"name\" : \"开关面板-文档版-TCP\",\n \"createTime\" : 1753764305597,\n \"updateTime\" : 1754144117243,\n \"lock\" : \"0\",\n \"createBy\" : \"test\",\n \"updateBy\" : \"test\",\n \"path\" : \"38fc80e\",\n \"method\" : \"POST\",\n \"parameters\" : [ ],\n \"options\" : [ ],\n \"requestBody\" : null,\n \"headers\" : [ ],\n \"paths\" : [ ],\n \"responseBody\" : null,\n \"description\" : \"文档演示协议\",\n \"requestBodyDefinition\" : null,\n \"responseBodyDefinition\" : null\n}\r\n================================\r\nimport log;\n\n// 16进制转10进制数字\nvar hexToInt = hex => {\n return Integer.parseInt(hex, 16);\n};\n// 预解码,解析上报的设备编号 deviceId\nvar preDecode = payload => {\n var result = {}\n var payloadHex = payload.toUpperCase() // 转为大写\n // 校验报文长度和帧头\n if (payloadHex.length() >= 18 && payloadHex.substring(0, 4) === \"A5A5\") {\n // 从第5位开始取4字节(8个字符)作为 deviceId\n result.deviceId = payloadHex.substring(4, 12)\n }\n return result\n}\n\nvar decode =(payload,context) => {\n log.info(\"进入编解码,payload={},context={}\",payload,context)\n var result = [];\n var payloadHex =payload.toUpperCase();\n var propertiesObj = { \"messageType\": \"PROPERTIES\", \"properties\": {} };\n var eventObj = { \"messageType\": \"EVENT\", \"event\": \"\", \"data\": {} };\n\n // 验证帧头帧尾\n if (payloadHex.substring(0, 4) != \"A5A5\" || payloadHex.substring(payloadHex.length() - 4) != \"5A5A\") {\n return jsonToStr([{ \"error\": \"无效帧格式\" }]);\n }\n\n // 提取核心字段\n var deviceId = payloadHex.substring(4, 8); // 设备ID(4字节)\n var cmdCode = payloadHex.substring(8, 10); // 命令码(1字节)\n var dataLen = hexToInt(payloadHex.substring(10, 12)); // 数据长度\n var dataHex = payloadHex.substring(12, 12 + dataLen * 2); // 数据区\n var checksum = payloadHex.substring(12 + dataLen * 2, 14 + dataLen * 2); // 校验和\n\n // 设备ID存入所有对象\n propertiesObj.deviceId = deviceId;\n eventObj.deviceId = deviceId;\n //验证新内容\n propertiesObj.context=context;\n propertiesObj.tcpEncode = \'HEX\';\n propertiesObj.replyPayload = \'727270632C676574637371\'\n // 1. 属性上报(命令码01)\n if (cmdCode == \"01\" && dataLen == 7) {\n // 解析开关状态(01=开,00=关)\n propertiesObj.properties.powerState = dataHex.substring(0, 2) == \"01\" ? \"01\" : \"00\";\n // 解析电流(2字节,单位0.01A)\n var currentHex = dataHex.substring(2, 6);\n propertiesObj.properties.current = (hexToInt(currentHex) * 0.01).toFixed(2);\n // 解析电压(2字节,单位0.1V)\n var voltageHex = dataHex.substring(6, 10);\n propertiesObj.properties.voltage = (hexToInt(voltageHex) * 0.1).toFixed(1);\n // 解析功率(2字节,单位1W)\n var powerHex = dataHex.substring(10, 14);\n propertiesObj.properties.power = hexToInt(powerHex).toString();\n // 加入结果集\n result.push(propertiesObj);\n }\n\n // 2. 事件上报(命令码02)\n if (cmdCode == \"02\" && dataLen >= 3) {\n // 事件类型(01=过载,02=过压,03=上线,04=下线)\n var eventType = dataHex.substring(0, 2);\n if (eventType == \"01\") {\n eventObj.event = \"overload\";\n eventObj.data.overload = hexToInt(dataHex.substring(2, 6)) + \"W\"; // 过载功率\n }\n if (eventType == \"03\") {\n eventObj.event = \"online\";\n eventObj.data.online = \"设备上线\";\n }\n if (eventType == \"04\") {\n eventObj.event = \"offline\";\n eventObj.data.offline = \"设备下线\";\n }\n result.push(eventObj);\n }\n\n // 3. 响应报文(命令码04)\n if (cmdCode == \"04\" && dataLen == 1) {\n var replyObj = {\n \"messageType\": \"REPLY\",\n \"function\": \"switch\",\n \"data\": { \"result\": dataHex == \"00\" ? \"success\" : \"fail\" }\n };\n replyObj.deviceId = deviceId;\n result.push(replyObj);\n }\n\n return jsonToStr(result);\n};\n\nvar encode = payload => {\n var obj = toJson(payload);\n var deviceId = obj.data.deviceId; // 设备ID(4位16进制,如\"0001\")\n var func = obj.function;\n var params = obj.data;\n\n // 控制开关(function=switch)\n if (func == \"switch\") {\n var state = params.state; // 目标状态:01=开,00=关\n if (state != \"01\" && state != \"00\") {\n return \"\"; // 无效状态,返回空\n }\n\n // 数据区:控制类型(01)+目标状态\n var dataHex = \"01\" + state;\n var dataLen = dataHex.length() / 2; // 数据长度(字节数)\n var cmdCode = \"03\"; // 控制指令命令码\n\n // 拼接完整报文(无校验和)\n var frame = \"A5A5\" + \n deviceId + \n cmdCode + \n Integer.toHexString(dataLen) + // 数据长度(1位16进制)\n dataHex + \n \"5A5A\";\n\n return frame.toUpperCase(); // 转为大写16进制\n }\n\n return \"\"; // 不支持的指令\n};\n\n//预解码,得到deviceId\n// return preDecode(\"A5A5000101070105DC08F004B05A5A\")\n//属性上报报文\n// return decode(\'A5A5000101070105DC08F004B07A5A5A\');\n\n//过载事件报文\n// return decode(\"A5A5000102030104B0F35A5A\")\n\n\n// return encode(\'{\"deviceId\": \"0001\",\"function\": \"switch\",\"data\": { \"state\": \"01\" }}\')\n\n\n// return encode(\'{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"00010107\",\"state\":\"01\"}}\')'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/test/智能插座(格林物联).ms', '{\n \"properties\" : { },\n \"id\" : \"1975956f8f474cf1a914373daaa0709d\",\n \"script\" : null,\n \"groupId\" : \"77b59996549d4638a85888f7edd8131d\",\n \"name\" : \"智能插座(格林物联)\",\n \"createTime\" : 1754285636730,\n \"updateTime\" : 1754286430946,\n \"lock\" : \"1\",\n \"createBy\" : \"test\",\n \"updateBy\" : \"test\",\n \"path\" : \"681c0775c2dc427d0480ab5f\",\n \"method\" : \"POST\",\n \"parameters\" : [ ],\n \"options\" : [ ],\n \"requestBody\" : null,\n \"headers\" : [ ],\n \"paths\" : [ ],\n \"responseBody\" : \"{\\n \\\"messageType\\\": \\\"PROPERTIES\\\",\\n \\\"properties\\\": {\\n \\\"temp\\\": 1,\\n \\\"sn\\\": \\\"23176\\\"\\n },\\n \\\"replyPayload\\\": \\\"xzzxc23177\\\",\\n \\\"downTopic\\\": \\\"$thing/down/681c0775c2dc427d0480ab5f/cz001923\\\"\\n}\",\n \"description\" : null,\n \"requestBodyDefinition\" : null,\n \"responseBodyDefinition\" : null\n}\r\n================================\r\n\nimport log;\n\nvar url=\"\";\n\nvar post = (url,body) =>{\n \n}\n\nvar encode = payload => {\n var dt=toJson(payload)\n dt.dxxc=randomSign();\n return dt;\n}\n\n//解码\nvar decode = payload => {\n var result = []\n log.info(\"编解码收到消息={}\",payload)\n var dt=toJson(payload)\n var propertiesObj = {\n \"messageType\": \"PROPERTIES\",\n \"properties\": {}\n }\n //设置属性\n propertiesObj.properties=dt\n propertiesObj.properties.sn=randomSign()\n //MQTT设置回复\n propertiesObj.replyPayload=\"xzzxc\"+randomSign()//设置回复的内容\n propertiesObj.downTopic=\"$thing/down/681c0775c2dc427d0480ab5f/cz001923\" //设置回复的主题\n \n return propertiesObj;\n}\n\n//设备增加\nvar add = payload => {\n var da=post(url,payload)\n var rs={\n \"code\":0,\n \"msg\":\"success\",\n \"codec\":\"add\",\n \"codec\":da\n }\n return rs;\n}\n\n//设备更新\nvar update = payload => {\n var da=post(url,payload)\n var rs={\n \"code\":0,\n \"msg\":\"success\",\n \"codec\":\"update\",\n \"codec\":da\n }\n return rs;\n}\n\n//设备查询\nvar query = payload => {\n var da=post(url,payload)\n var rs={\n \"code\":0,\n \"req\":0,\n \"msg\":\"success\",\n \"codec\":\"query\",\n \"codec\":da\n }\n return rs;\n}\n\n//功能调用\nvar function = payload => {\n var da=post(url,payload)\n var rs={\n \"code\":0,\n \"msg\":\"success\",\n \"codec\":\"function\",\n \"codec\":da\n }\n return rs;\n}\n\n\nreturn decode(\"{\'temp\':1}\")'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/test/演示教学一下.ms', '{\n \"properties\" : { },\n \"id\" : \"3b003406803b42d3b00ea0bde1892a50\",\n \"script\" : null,\n \"groupId\" : \"77b59996549d4638a85888f7edd8131d\",\n \"name\" : \"演示教学一下\",\n \"createTime\" : 1754210880647,\n \"updateTime\" : 1754212263177,\n \"lock\" : null,\n \"createBy\" : \"test\",\n \"updateBy\" : \"test\",\n \"path\" : \"DEMO\",\n \"method\" : \"POST\",\n \"parameters\" : [ ],\n \"options\" : [ ],\n \"requestBody\" : null,\n \"headers\" : [ ],\n \"paths\" : [ ],\n \"responseBody\" : \"{\\n \\\"messageType\\\": \\\"PROPERTIES\\\",\\n \\\"properties\\\": {\\n \\\"payload\\\": \\\"A1B2C3\\\",\\n \\\"randomStr\\\": \\\"38678\\\"\\n }\\n}\",\n \"description\" : null,\n \"requestBodyDefinition\" : null,\n \"responseBodyDefinition\" : null\n}\r\n================================\r\n/**\n * 预解码:提取设备身份标识\n * @param {String} payload 设备原始报文(可能是二进制、JSON、16进制字符串等)\n * @returns {Object} {deviceId, productKey}\n */\nvar preDecode = payload => {\n var rs = {\n \"deviceId\": \"\",\n \"productKey\": \"\"\n };\n // TODO:解析设备标识\n return rs;\n};\n\n/**\n * 编码:平台指令转设备格式\n * @param {Object} payload 平台指令\n * @returns {String} 设备可识别的报文(二进制、JSON、16进制字符串等)\n */\nvar encode = payload => {\n var result = \"\";\n // TODO:实现编码逻辑\n return result;\n};\n\n/**\n * 解码:设备数据转平台格式\n * @param {String} payload 设备原始报文(二进制、JSON、16进制字符串等)\n * @returns {Array} 平台标准消息数组,每个元素结构:\n * [\n * {\n * messageType: \"PROPERTIES\", // 属性类型\n * properties: {} // 解析后的属性键值对(根据物模型动态调整)\n * },\n * {\n * messageType: \"EVENT\", // 事件类型\n * event: \"\", // 事件名称\n * data: {} // 事件参数\n * }\n * ]\n */\nvar decode = payload => {\n var rs = [];\n let isJsonParam=false;\n let newPayload={}\n if(isJson(payload)){\n newPayload=toJson(payload)\n isJsonParam=true\n }\n // 属性\n var propertiesObj = {\n \"messageType\": \"PROPERTIES\",\n \"properties\": {}\n };\n propertiesObj.properties=newPayload\n propertiesObj.isJsonParam=isJsonParam\n\n rs.push(propertiesObj);\n // 事件\n var event = {\n \"messageType\": \"EVENT\",\n \"event\": \"\",\n \"data\": {}\n };\n // rs.push(event);\n \n return propertiesObj;\n};\n\n// 调试示例(正式使用请删除或注释)\n//return preDecode(\"\");\n// return encode({});\nreturn decode(\'{\"current\": \"199.00\",\"power\": \"199\",\"powerState\": 0,\"voltage\": \"199.8\"}\');'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/测试分组/', 'this is directory'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/api/测试分组/group.json', '{\n \"properties\" : { },\n \"id\" : \"bea746cb74484886b5ea0d1efc678636\",\n \"name\" : \"测试分组\",\n \"type\" : \"api\",\n \"parentId\" : \"0\",\n \"path\" : \"/testprotocol\",\n \"createTime\" : 1755154048548,\n \"updateTime\" : null,\n \"createBy\" : \"test\",\n \"updateBy\" : null,\n \"paths\" : [ ],\n \"options\" : [ ]\n}'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/datasource/', 'this is directory'); +INSERT INTO `protocol_file` (`file_path`, `file_content`) VALUES ('universal/function/', 'this is directory'); +COMMIT; + +-- ---------------------------- +-- Table structure for protocol_file_backup +-- ---------------------------- +DROP TABLE IF EXISTS `protocol_file_backup`; +CREATE TABLE `protocol_file_backup` ( + `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '原对象ID', + `create_date` bigint NOT NULL COMMENT '备份时间', + `tag` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '标签', + `type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '类型', + `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '原名称', + `content` blob COMMENT '备份内容', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作人', + PRIMARY KEY (`id`,`create_date`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Records of protocol_file_backup +-- ---------------------------- +BEGIN; +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('1975956f8f474cf1a914373daaa0709d', 1754285636766, NULL, 'api', '智能插座(格林物联)', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223139373539353666386634373463663161393134333733646161613037303964222C0A20202273637269707422203A20225C6E696D706F7274206C6F673B5C6E5C6E7661722075726C3D5C225C223B5C6E5C6E76617220706F7374203D202875726C2C626F647929203D3E7B5C6E2020205C6E7D5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E202020207661722064743D746F4A736F6E287061796C6F6164295C6E2020202064742E647878633D72616E646F6D5369676E28293B5C6E2020202072657475726E2064743B5C6E7D5C6E5C6E2F2FE8A7A3E7A0815C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205B5D5C6E202020206C6F672E696E666F285C22E7BC96E8A7A3E7A081E694B6E588B0E6B688E681AF3D7B7D5C222C7061796C6F6164295C6E202020207661722064743D746F4A736F6E287061796C6F6164295C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D5C6E202020202F2FE8AEBEE7BDAEE5B19EE680A75C6E2020202070726F706572746965734F626A2E70726F706572746965733D64745C6E2020202070726F706572746965734F626A2E70726F706572746965732E736E3D72616E646F6D5369676E28295C6E202020202F2FE8AEBEE7BDAEE59B9EE5A48D5C6E2020202070726F706572746965734F626A2E7265706C795061796C6F61643D5C22787A7A78635C222B72616E646F6D5369676E28295C6E2020202070726F706572746965734F626A2E646F776E546F7069633D5C22247468696E672F646F776E2F3638316330373735633264633432376430343830616235662F637A3030313932335C225C6E20202020766172206576656E743D7B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A5C2262617474657279526573746F72655C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E20202020202020202020202F2F205C22626174746572795C223A3130305C6E20202020202020207D5C6E202020207D5C6E2020202020766172206C6F7742617474657279416C61726D3D7B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A5C226C6F7742617474657279416C61726D5C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020202F2F5C226373715C223A32395C6E20202020202020207D5C6E202020207D5C6E20202020726573756C742E707573682870726F706572746965734F626A295C6E20202020726573756C742E70757368286576656E74295C6E20202020726573756C742E70757368286C6F7742617474657279416C61726D295C6E2020202072657475726E20726573756C743B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E5A29EE58AA05C6E76617220616464203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C226164645C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E69BB4E696B05C6E76617220757064617465203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C227570646174655C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E69FA5E8AFA25C6E766172207175657279203D207061796C6F6164203D3E207B5C6E2020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C227265715C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C2271756572795C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE58A9FE883BDE8B083E794A85C6E7661722066756E6374696F6E203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C2266756E6374696F6E5C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E699BAE883BDE68F92E5BAA728E6A0BCE69E97E789A9E8819429222C0A20202263726561746554696D6522203A20313735343238353633363733302C0A20202275706461746554696D6522203A206E756C6C2C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A206E756C6C2C0A2020227061746822203A2022363831633037373563326463343237643034383061623566222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('1975956f8f474cf1a914373daaa0709d', 1754285700984, NULL, 'api', '智能插座(格林物联)', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223139373539353666386634373463663161393134333733646161613037303964222C0A20202273637269707422203A20225C6E696D706F7274206C6F673B5C6E5C6E7661722075726C3D5C225C223B5C6E5C6E76617220706F7374203D202875726C2C626F647929203D3E7B5C6E2020205C6E7D5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E202020207661722064743D746F4A736F6E287061796C6F6164295C6E2020202064742E647878633D72616E646F6D5369676E28293B5C6E2020202072657475726E2064743B5C6E7D5C6E5C6E2F2FE8A7A3E7A0815C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205B5D5C6E202020206C6F672E696E666F285C22E7BC96E8A7A3E7A081E694B6E588B0E6B688E681AF3D7B7D5C222C7061796C6F6164295C6E202020207661722064743D746F4A736F6E287061796C6F6164295C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D5C6E202020202F2FE8AEBEE7BDAEE5B19EE680A75C6E2020202070726F706572746965734F626A2E70726F706572746965733D64745C6E2020202070726F706572746965734F626A2E70726F706572746965732E736E3D72616E646F6D5369676E28295C6E202020202F2FE8AEBEE7BDAEE59B9EE5A48D5C6E2020202070726F706572746965734F626A2E7265706C795061796C6F61643D5C22787A7A78635C222B72616E646F6D5369676E28295C6E2020202070726F706572746965734F626A2E646F776E546F7069633D5C22247468696E672F646F776E2F3638316330373735633264633432376430343830616235662F637A3030313932335C225C6E20202020766172206576656E743D7B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A5C2262617474657279526573746F72655C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E20202020202020202020202F2F205C22626174746572795C223A3130305C6E20202020202020207D5C6E202020207D5C6E2020202020766172206C6F7742617474657279416C61726D3D7B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A5C226C6F7742617474657279416C61726D5C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020202F2F5C226373715C223A32395C6E20202020202020207D5C6E202020207D5C6E20202020726573756C742E707573682870726F706572746965734F626A295C6E20202020726573756C742E70757368286576656E74295C6E20202020726573756C742E70757368286C6F7742617474657279416C61726D295C6E2020202072657475726E20726573756C743B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E5A29EE58AA05C6E76617220616464203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C226164645C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E69BB4E696B05C6E76617220757064617465203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C227570646174655C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E69FA5E8AFA25C6E766172207175657279203D207061796C6F6164203D3E207B5C6E2020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C227265715C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C2271756572795C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE58A9FE883BDE8B083E794A85C6E7661722066756E6374696F6E203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C2266756E6374696F6E5C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E699BAE883BDE68F92E5BAA728E6A0BCE69E97E789A9E8819429222C0A20202263726561746554696D6522203A20313735343238353633363733302C0A20202275706461746554696D6522203A20313735343238353730303936382C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363831633037373563326463343237643034383061623566222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('1975956f8f474cf1a914373daaa0709d', 1754285863924, NULL, 'api', '智能插座(格林物联)', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223139373539353666386634373463663161393134333733646161613037303964222C0A20202273637269707422203A20225C6E696D706F7274206C6F673B5C6E5C6E7661722075726C3D5C225C223B5C6E5C6E76617220706F7374203D202875726C2C626F647929203D3E7B5C6E2020205C6E7D5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E202020207661722064743D746F4A736F6E287061796C6F6164295C6E2020202064742E647878633D72616E646F6D5369676E28293B5C6E2020202072657475726E2064743B5C6E7D5C6E5C6E2F2FE8A7A3E7A0815C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205B5D5C6E202020206C6F672E696E666F285C22E7BC96E8A7A3E7A081E694B6E588B0E6B688E681AF3D7B7D5C222C7061796C6F6164295C6E202020207661722064743D746F4A736F6E287061796C6F6164295C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D5C6E202020202F2FE8AEBEE7BDAEE5B19EE680A75C6E2020202070726F706572746965734F626A2E70726F706572746965733D64745C6E2020202070726F706572746965734F626A2E70726F706572746965732E736E3D72616E646F6D5369676E28295C6E202020202F2F4D515454E8AEBEE7BDAEE59B9EE5A48D5C6E2020202070726F706572746965734F626A2E7265706C795061796C6F61643D5C22787A7A78635C222B72616E646F6D5369676E28292F2FE8AEBEE7BDAEE59B9EE5A48DE79A84E58685E5AEB95C6E2020202070726F706572746965734F626A2E646F776E546F7069633D5C22247468696E672F646F776E2F3638316330373735633264633432376430343830616235662F637A3030313932335C22202F2FE8AEBEE7BDAEE59B9EE5A48DE79A84E4B8BBE9A2985C6E20202020766172206576656E743D7B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A5C2262617474657279526573746F72655C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E20202020202020202020202F2F205C22626174746572795C223A3130305C6E20202020202020207D5C6E202020207D5C6E2020202020766172206C6F7742617474657279416C61726D3D7B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A5C226C6F7742617474657279416C61726D5C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020202F2F5C226373715C223A32395C6E20202020202020207D5C6E202020207D5C6E20202020726573756C742E707573682870726F706572746965734F626A295C6E20202020726573756C742E70757368286576656E74295C6E20202020726573756C742E70757368286C6F7742617474657279416C61726D295C6E2020202072657475726E20726573756C743B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E5A29EE58AA05C6E76617220616464203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C226164645C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E69BB4E696B05C6E76617220757064617465203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C227570646174655C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E69FA5E8AFA25C6E766172207175657279203D207061796C6F6164203D3E207B5C6E2020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C227265715C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C2271756572795C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE58A9FE883BDE8B083E794A85C6E7661722066756E6374696F6E203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C2266756E6374696F6E5C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E699BAE883BDE68F92E5BAA728E6A0BCE69E97E789A9E8819429222C0A20202263726561746554696D6522203A20313735343238353633363733302C0A20202275706461746554696D6522203A20313735343238353836333838342C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363831633037373563326463343237643034383061623566222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('1975956f8f474cf1a914373daaa0709d', 1754285901501, NULL, 'api', '智能插座(格林物联)', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223139373539353666386634373463663161393134333733646161613037303964222C0A20202273637269707422203A20225C6E696D706F7274206C6F673B5C6E5C6E7661722075726C3D5C225C223B5C6E5C6E76617220706F7374203D202875726C2C626F647929203D3E7B5C6E2020205C6E7D5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E202020207661722064743D746F4A736F6E287061796C6F6164295C6E2020202064742E647878633D72616E646F6D5369676E28293B5C6E2020202072657475726E2064743B5C6E7D5C6E5C6E2F2FE8A7A3E7A0815C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205B5D5C6E202020206C6F672E696E666F285C22E7BC96E8A7A3E7A081E694B6E588B0E6B688E681AF3D7B7D5C222C7061796C6F6164295C6E202020207661722064743D746F4A736F6E287061796C6F6164295C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D5C6E202020202F2FE8AEBEE7BDAEE5B19EE680A75C6E2020202070726F706572746965734F626A2E70726F706572746965733D64745C6E2020202070726F706572746965734F626A2E70726F706572746965732E736E3D72616E646F6D5369676E28295C6E202020202F2F4D515454E8AEBEE7BDAEE59B9EE5A48D5C6E2020202070726F706572746965734F626A2E7265706C795061796C6F61643D5C22787A7A78635C222B72616E646F6D5369676E28292F2FE8AEBEE7BDAEE59B9EE5A48DE79A84E58685E5AEB95C6E2020202070726F706572746965734F626A2E646F776E546F7069633D5C22247468696E672F646F776E2F3638316330373735633264633432376430343830616235662F637A3030313932335C22202F2FE8AEBEE7BDAEE59B9EE5A48DE79A84E4B8BBE9A2985C6E20202020766172206576656E743D7B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A5C2262617474657279526573746F72655C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E20202020202020202020202F2F205C22626174746572795C223A3130305C6E20202020202020207D5C6E202020207D5C6E2020202020766172206C6F7742617474657279416C61726D3D7B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A5C226C6F7742617474657279416C61726D5C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020202F2F5C226373715C223A32395C6E20202020202020207D5C6E202020207D5C6E20202020726573756C742E707573682870726F706572746965734F626A295C6E20202020726573756C742E70757368286576656E74295C6E20202020726573756C742E70757368286C6F7742617474657279416C61726D295C6E2020202072657475726E20726573756C743B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E5A29EE58AA05C6E76617220616464203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C226164645C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E69BB4E696B05C6E76617220757064617465203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C227570646174655C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E69FA5E8AFA25C6E766172207175657279203D207061796C6F6164203D3E207B5C6E2020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C227265715C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C2271756572795C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE58A9FE883BDE8B083E794A85C6E7661722066756E6374696F6E203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C2266756E6374696F6E5C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E5C6E72657475726E206465636F6465285C227B2774656D70273A317D5C2229222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E699BAE883BDE68F92E5BAA728E6A0BCE69E97E789A9E8819429222C0A20202263726561746554696D6522203A20313735343238353633363733302C0A20202275706461746554696D6522203A20313735343238353930313438302C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363831633037373563326463343237643034383061623566222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('1975956f8f474cf1a914373daaa0709d', 1754285928764, NULL, 'api', '智能插座(格林物联)', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223139373539353666386634373463663161393134333733646161613037303964222C0A20202273637269707422203A20225C6E696D706F7274206C6F673B5C6E5C6E7661722075726C3D5C225C223B5C6E5C6E76617220706F7374203D202875726C2C626F647929203D3E7B5C6E2020205C6E7D5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E202020207661722064743D746F4A736F6E287061796C6F6164295C6E2020202064742E647878633D72616E646F6D5369676E28293B5C6E2020202072657475726E2064743B5C6E7D5C6E5C6E2F2FE8A7A3E7A0815C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205B5D5C6E202020206C6F672E696E666F285C22E7BC96E8A7A3E7A081E694B6E588B0E6B688E681AF3D7B7D5C222C7061796C6F6164295C6E202020207661722064743D746F4A736F6E287061796C6F6164295C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D5C6E202020202F2FE8AEBEE7BDAEE5B19EE680A75C6E2020202070726F706572746965734F626A2E70726F706572746965733D64745C6E2020202070726F706572746965734F626A2E70726F706572746965732E736E3D72616E646F6D5369676E28295C6E202020202F2F4D515454E8AEBEE7BDAEE59B9EE5A48D5C6E2020202070726F706572746965734F626A2E7265706C795061796C6F61643D5C22787A7A78635C222B72616E646F6D5369676E28292F2FE8AEBEE7BDAEE59B9EE5A48DE79A84E58685E5AEB95C6E2020202070726F706572746965734F626A2E646F776E546F7069633D5C22247468696E672F646F776E2F3638316330373735633264633432376430343830616235662F637A3030313932335C22202F2FE8AEBEE7BDAEE59B9EE5A48DE79A84E4B8BBE9A2985C6E20202020766172206576656E743D7B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A5C2262617474657279526573746F72655C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E20202020202020202020202F2F205C22626174746572795C223A3130305C6E20202020202020207D5C6E202020207D5C6E2020202020766172206C6F7742617474657279416C61726D3D7B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A5C226C6F7742617474657279416C61726D5C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020202F2F5C226373715C223A32395C6E20202020202020207D5C6E202020207D5C6E20202020726573756C742E707573682870726F706572746965734F626A295C6E20202020726573756C742E70757368286576656E74295C6E20202020726573756C742E70757368286C6F7742617474657279416C61726D295C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E5A29EE58AA05C6E76617220616464203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C226164645C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E69BB4E696B05C6E76617220757064617465203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C227570646174655C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E69FA5E8AFA25C6E766172207175657279203D207061796C6F6164203D3E207B5C6E2020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C227265715C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C2271756572795C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE58A9FE883BDE8B083E794A85C6E7661722066756E6374696F6E203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C2266756E6374696F6E5C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E5C6E72657475726E206465636F6465285C227B2774656D70273A317D5C2229222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E699BAE883BDE68F92E5BAA728E6A0BCE69E97E789A9E8819429222C0A20202263726561746554696D6522203A20313735343238353633363733302C0A20202275706461746554696D6522203A20313735343238353932383733392C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363831633037373563326463343237643034383061623566222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20225B5C6E202020207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020205C2274656D705C223A20312C5C6E2020202020202020202020205C22736E5C223A205C2232323631345C225C6E20202020202020207D2C5C6E20202020202020205C227265706C795061796C6F61645C223A205C22787A7A786332323631355C222C5C6E20202020202020205C22646F776E546F7069635C223A205C22247468696E672F646F776E2F3638316330373735633264633432376430343830616235662F637A3030313932335C225C6E202020207D2C5C6E202020207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C2262617474657279526573746F72655C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D2C5C6E202020207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C226C6F7742617474657279416C61726D5C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D5C6E5D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('1975956f8f474cf1a914373daaa0709d', 1754286426458, NULL, 'api', '智能插座(格林物联)', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223139373539353666386634373463663161393134333733646161613037303964222C0A20202273637269707422203A20225C6E696D706F7274206C6F673B5C6E5C6E7661722075726C3D5C225C223B5C6E5C6E76617220706F7374203D202875726C2C626F647929203D3E7B5C6E2020205C6E7D5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E202020207661722064743D746F4A736F6E287061796C6F6164295C6E2020202064742E647878633D72616E646F6D5369676E28293B5C6E2020202072657475726E2064743B5C6E7D5C6E5C6E2F2FE8A7A3E7A0815C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205B5D5C6E202020206C6F672E696E666F285C22E7BC96E8A7A3E7A081E694B6E588B0E6B688E681AF3D7B7D5C222C7061796C6F6164295C6E202020207661722064743D746F4A736F6E287061796C6F6164295C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D5C6E202020202F2FE8AEBEE7BDAEE5B19EE680A75C6E2020202070726F706572746965734F626A2E70726F706572746965733D64745C6E2020202070726F706572746965734F626A2E70726F706572746965732E736E3D72616E646F6D5369676E28295C6E202020202F2F4D515454E8AEBEE7BDAEE59B9EE5A48D5C6E2020202070726F706572746965734F626A2E7265706C795061796C6F61643D5C22787A7A78635C222B72616E646F6D5369676E28292F2FE8AEBEE7BDAEE59B9EE5A48DE79A84E58685E5AEB95C6E2020202070726F706572746965734F626A2E646F776E546F7069633D5C22247468696E672F646F776E2F3638316330373735633264633432376430343830616235662F637A3030313932335C22202F2FE8AEBEE7BDAEE59B9EE5A48DE79A84E4B8BBE9A2985C6E2020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E5A29EE58AA05C6E76617220616464203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C226164645C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E69BB4E696B05C6E76617220757064617465203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C227570646174655C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE8AEBEE5A487E69FA5E8AFA25C6E766172207175657279203D207061796C6F6164203D3E207B5C6E2020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C227265715C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C2271756572795C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E2F2FE58A9FE883BDE8B083E794A85C6E7661722066756E6374696F6E203D207061796C6F6164203D3E207B5C6E202020207661722064613D706F73742875726C2C7061796C6F6164295C6E202020207661722072733D7B5C6E20202020202020205C22636F64655C223A302C5C6E20202020202020205C226D73675C223A5C22737563636573735C222C5C6E20202020202020205C22636F6465635C223A5C2266756E6374696F6E5C222C5C6E20202020202020205C22636F6465635C223A64615C6E202020207D5C6E2020202072657475726E2072733B5C6E7D5C6E5C6E5C6E72657475726E206465636F6465285C227B2774656D70273A317D5C2229222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E699BAE883BDE68F92E5BAA728E6A0BCE69E97E789A9E8819429222C0A20202263726561746554696D6522203A20313735343238353633363733302C0A20202275706461746554696D6522203A20313735343238363432363433382C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363831633037373563326463343237643034383061623566222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B5C6E20202020202020205C2274656D705C223A20312C5C6E20202020202020205C22736E5C223A205C2232333137365C225C6E202020207D2C5C6E202020205C227265706C795061796C6F61645C223A205C22787A7A786332333137375C222C5C6E202020205C22646F776E546F7069635C223A205C22247468696E672F646F776E2F3638316330373735633264633432376430343830616235662F637A3030313932335C225C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754210880678, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E707573682870726F70657274696573293B5C6E202020205C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E2F2F2072657475726E206465636F6465285C225C22293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A206E756C6C2C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A206E756C6C2C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211239567, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E707573682870726F70657274696573293B5C6E202020205C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F6465285C224131413241335C22293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313233393533372C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211294357, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965732E7470733D7061796C6F61645C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F6465285C224131413241335C22293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313239343333372C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211363153, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965732E70726F706572746965732E7470733D7061796C6F61645C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F6465285C224131413241335C22293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313336333131322C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B7D2C5C6E202020205C227470735C223A205C224131413241335C225C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211472389, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020207661722074703D7B7D5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D746F4A736F6E287061796C6F6164295C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965732E70726F706572746965733D74705C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F6465287B5C6E20205C2263757272656E745C223A205C223139392E30305C222C5C6E20205C22706F7765725C223A205C223139395C222C5C6E20205C22706F77657253746174655C223A20302C5C6E20205C22766F6C746167655C223A205C223139392E385C225C6E7D293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313437323237332C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B5C6E20202020202020205C227470735C223A205C224131413241335C225C6E202020207D5C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211490070, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020207661722074703D7B7D5C6E202020202F2F2069662869734A736F6E287061796C6F616429297B5C6E202020202F2F202020202074703D746F4A736F6E287061796C6F6164295C6E202020202F2F207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965732E70726F706572746965733D7061796C6F61645C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F6465287B5C6E20205C2263757272656E745C223A205C223139392E30305C222C5C6E20205C22706F7765725C223A205C223139395C222C5C6E20205C22706F77657253746174655C223A20302C5C6E20205C22766F6C746167655C223A205C223139392E385C225C6E7D293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313439303034392C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B7D5C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211534738, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020207661722074703D7B7D5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D747275653B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965732E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965732E74703D74705C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F6465287B5C6E20205C2263757272656E745C223A205C223139392E30305C222C5C6E20205C22706F7765725C223A205C223139395C222C5C6E20205C22706F77657253746174655C223A20302C5C6E20205C22766F6C746167655C223A205C223139392E385C225C6E7D293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313533343730362C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B5C6E20202020202020205C2263757272656E745C223A205C223139392E30305C222C5C6E20202020202020205C22706F7765725C223A205C223139395C222C5C6E20202020202020205C22706F77657253746174655C223A20302C5C6E20202020202020205C22766F6C746167655C223A205C223139392E385C225C6E202020207D5C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211546925, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020207661722074703B5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D747275653B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965732E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965732E74703D74705C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F6465287B5C6E20205C2263757272656E745C223A205C223139392E30305C222C5C6E20205C22706F7765725C223A205C223139395C222C5C6E20205C22706F77657253746174655C223A20302C5C6E20205C22766F6C746167655C223A205C223139392E385C225C6E7D293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313534363931302C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B5C6E20202020202020205C2263757272656E745C223A205C223139392E30305C222C5C6E20202020202020205C22706F7765725C223A205C223139395C222C5C6E20202020202020205C22706F77657253746174655C223A20302C5C6E20202020202020205C22766F6C746167655C223A205C223139392E385C225C6E202020207D2C5C6E202020205C2274705C223A207B7D5C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211611429, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020207661722074703B5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D747275653B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965732E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965732E74703D74705C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313631313430332C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B5C6E20202020202020205C2263757272656E745C223A205C223139392E30305C222C5C6E20202020202020205C22706F7765725C223A205C223139395C222C5C6E20202020202020205C22706F77657253746174655C223A20302C5C6E20202020202020205C22766F6C746167655C223A205C223139392E385C225C6E202020207D2C5C6E202020205C2274705C223A206E756C6C5C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211618306, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020207661722074703B5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D747275653B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965732E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965732E70726F706572746965732E74703D74705C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313631383238342C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A205C227B5C5C5C2263757272656E745C5C5C223A205C5C5C223139392E30305C5C5C222C5C5C5C22706F7765725C5C5C223A205C5C5C223139395C5C5C222C5C5C5C22706F77657253746174655C5C5C223A20302C5C5C5C22766F6C746167655C5C5C223A205C5C5C223139392E385C5C5C227D5C222C5C6E202020205C2274705C223A20747275655C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211627995, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020207661722074703B5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D747275653B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965732E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965732E70726F706572746965732E6A736F6E3D74705C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313632373935332C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211642677, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020207661722074703B5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D5C2269734A736F6E5C223B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965732E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965732E70726F706572746965732E6A736F6E3D74705C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313634323635332C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211655476, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020207661722074703B5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D5C2269734A736F6E5C223B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965732E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965732E70726F706572746965732E787A3D74705C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313635353433352C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211685608, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020207661722074703B5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D5C2269734A736F6E5C223B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965732E787A3D74705C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313638353537392C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211693641, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020207661722074703D5C225C225C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D5C2269734A736F6E5C223B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965732E787A3D74705C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313639333535302C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211710950, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742074703D5C225C225C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D5C2269734A736F6E5C223B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965732E787A3D74705C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313731303930382C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211737079, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742074703D5C225C225C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D5C2269734A736F6E5C223B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965732E74703D74705C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313733373036342C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211752970, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742074703D5C225C225C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D5C2269734A736F6E5C223B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E202020205C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313735323934362C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211757718, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742074703D5C225C225C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D5C2269734A736F6E5C223B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965732E74703D74705C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313735373635372C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211763875, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742074703D5C225C225C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D5C2269734A736F6E5C223B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965732E74703D72735C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313736333833332C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211777727, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742074703D5C225C225C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D5C2269734A736F6E5C223B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965732E74703D72735C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313737373638392C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211780761, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742074703D5C225C225C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D5C2269734A736F6E5C223B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965732E74703D72735C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313738303732352C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211786972, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742074703D5C225C225C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D5C2269734A736F6E5C223B5C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313738363933312C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211832878, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742074703D7B7D5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E202020202020202074703D746F4A736F6E287061796C6F6164295C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313833323834302C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A205C227B5C5C5C2263757272656E745C5C5C223A205C5C5C223139392E30305C5C5C222C5C5C5C22706F7765725C5C5C223A205C5C5C223139395C5C5C222C5C5C5C22706F77657253746174655C5C5C223A20302C5C5C5C22766F6C746167655C5C5C223A205C5C5C223139392E385C5C5C227D5C225C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211840337, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742074703D7B7D5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E20202020202020207061796C6F61643D746F4A736F6E287061796C6F6164295C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313834303330342C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A205C227B5C5C5C2263757272656E745C5C5C223A205C5C5C223139392E30305C5C5C222C5C5C5C22706F7765725C5C5C223A205C5C5C223139395C5C5C222C5C5C5C22706F77657253746174655C5C5C223A20302C5C5C5C22766F6C746167655C5C5C223A205C5C5C223139392E385C5C5C227D5C225C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211853577, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742069734A736F6E506172616D3D66616C73653B5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E20202020202020207061796C6F61643D746F4A736F6E287061796C6F6164295C6E202020202020202069734A736F6E506172616D3D747275655C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313835333536332C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A205C227B5C5C5C2263757272656E745C5C5C223A205C5C5C223139392E30305C5C5C222C5C5C5C22706F7765725C5C5C223A205C5C5C223139395C5C5C222C5C5C5C22706F77657253746174655C5C5C223A20302C5C5C5C22766F6C746167655C5C5C223A205C5C5C223139392E385C5C5C227D5C225C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211859625, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742069734A736F6E506172616D3D66616C73653B5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E20202020202020207061796C6F61643D746F4A736F6E287061796C6F6164295C6E202020202020202069734A736F6E506172616D3D747275655C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965733D69734A736F6E506172616D5C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313835393630352C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A205C227B5C5C5C2263757272656E745C5C5C223A205C5C5C223139392E30305C5C5C222C5C5C5C22706F7765725C5C5C223A205C5C5C223139395C5C5C222C5C5C5C22706F77657253746174655C5C5C223A20302C5C5C5C22766F6C746167655C5C5C223A205C5C5C223139392E385C5C5C227D5C225C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211871687, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742069734A736F6E506172616D3D66616C73653B5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E20202020202020207061796C6F61643D746F4A736F6E287061796C6F6164295C6E202020202020202069734A736F6E506172616D3D747275655C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965733D69734A736F6E506172616D5C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313837313637312C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A20747275655C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211889575, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742069734A736F6E506172616D3D66616C73653B5C6E202020206C6574206E65775061796C6F61643D7B7D5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E20202020202020206E65775061796C6F61643D746F4A736F6E287061796C6F6164295C6E202020202020202069734A736F6E506172616D3D747275655C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D7061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965733D69734A736F6E506172616D5C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313838393533302C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A20747275655C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211890923, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742069734A736F6E506172616D3D66616C73653B5C6E202020206C6574206E65775061796C6F61643D7B7D5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E20202020202020206E65775061796C6F61643D746F4A736F6E287061796C6F6164295C6E202020202020202069734A736F6E506172616D3D747275655C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D6E65775061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965733D69734A736F6E506172616D5C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313839303839382C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A20747275655C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211902387, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742069734A736F6E506172616D3D66616C73653B5C6E202020206C6574206E65775061796C6F61643D7B7D5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E20202020202020206E65775061796C6F61643D746F4A736F6E287061796C6F6164295C6E202020202020202069734A736F6E506172616D3D747275655C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D6E65775061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965732E69734A736F6E506172616D3D69734A736F6E506172616D5C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313930323335392C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A20747275655C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754211918430, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742069734A736F6E506172616D3D66616C73653B5C6E202020206C6574206E65775061796C6F61643D7B7D5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E20202020202020206E65775061796C6F61643D746F4A736F6E287061796C6F6164295C6E202020202020202069734A736F6E506172616D3D747275655C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D6E65775061796C6F61645C6E2020202070726F706572746965734F626A2E69734A736F6E506172616D3D69734A736F6E506172616D5C6E5C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231313931383339312C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B5C6E20202020202020205C2263757272656E745C223A205C223139392E30305C222C5C6E20202020202020205C22706F7765725C223A205C223139395C222C5C6E20202020202020205C22706F77657253746174655C223A20302C5C6E20202020202020205C22766F6C746167655C223A205C223139392E385C222C5C6E20202020202020205C2269734A736F6E506172616D5C223A20747275655C6E202020207D5C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754212136922, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E707573682870726F70657274696573293B5C6E202020205C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F6465285C224131423243335C22293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231323133363838352C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B5C6E20202020202020205C2263757272656E745C223A205C223139392E30305C222C5C6E20202020202020205C22706F7765725C223A205C223139395C222C5C6E20202020202020205C22706F77657253746174655C223A20302C5C6E20202020202020205C22766F6C746167655C223A205C223139392E385C222C5C6E20202020202020205C2269734A736F6E506172616D5C223A20747275655C6E202020207D5C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754212172739, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E7061796C6F61643D7061796C6F61645C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F6465285C224131423243335C22293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231323137323732342C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B5C6E20202020202020205C2263757272656E745C223A205C223139392E30305C222C5C6E20202020202020205C22706F7765725C223A205C223139395C222C5C6E20202020202020205C22706F77657253746174655C223A20302C5C6E20202020202020205C22766F6C746167655C223A205C223139392E385C222C5C6E20202020202020205C2269734A736F6E506172616D5C223A20747275655C6E202020207D5C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754212175337, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E7061796C6F61643D7061796C6F61645C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F6465285C224131423243335C22293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231323137353239362C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B5C6E20202020202020205C2263757272656E745C223A205C223139392E30305C222C5C6E20202020202020205C22706F7765725C223A205C223139395C222C5C6E20202020202020205C22706F77657253746174655C223A20302C5C6E20202020202020205C22766F6C746167655C223A205C223139392E385C222C5C6E20202020202020205C2269734A736F6E506172616D5C223A20747275655C6E202020207D5C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754212243404, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E707573682870726F70657274696573293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E7061796C6F61643D7061796C6F61645C6E2020202070726F706572746965734F626A2E70726F706572746965732E72616E646F6D5374723D72616E646F6D5369676E28295C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F6465285C224131423243335C22293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231323234333336322C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B5C6E20202020202020205C227061796C6F61645C223A205C224131423243335C225C6E202020207D5C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('3b003406803b42d3b00ea0bde1892a50', 1754212263209, NULL, 'api', '演示教学一下', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223362303033343036383033623432643362303065613062646531383932613530222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020206C65742069734A736F6E506172616D3D66616C73653B5C6E202020206C6574206E65775061796C6F61643D7B7D5C6E2020202069662869734A736F6E287061796C6F616429297B5C6E20202020202020206E65775061796C6F61643D746F4A736F6E287061796C6F6164295C6E202020202020202069734A736F6E506172616D3D747275655C6E202020207D5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E2020202070726F706572746965734F626A2E70726F706572746965733D6E65775061796C6F61645C6E2020202070726F706572746965734F626A2E69734A736F6E506172616D3D69734A736F6E506172616D5C6E5C6E2020202072732E707573682870726F706572746965734F626A293B5C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2070726F706572746965734F626A3B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E72657475726E206465636F646528277B5C2263757272656E745C223A205C223139392E30305C222C5C22706F7765725C223A205C223139395C222C5C22706F77657253746174655C223A20302C5C22766F6C746167655C223A205C223139392E385C227D27293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E6BC94E7A4BAE69599E5ADA6E4B880E4B88B222C0A20202263726561746554696D6522203A20313735343231303838303634372C0A20202275706461746554696D6522203A20313735343231323236333137372C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202244454D4F222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E202020205C2270726F706572746965735C223A207B5C6E20202020202020205C227061796C6F61645C223A205C224131423243335C222C5C6E20202020202020205C2272616E646F6D5374725C223A205C2233383637385C225C6E202020207D5C6E7D222C0A2020226465736372697074696F6E22203A206E756C6C2C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('44e371fdbede4e44a0b3ddf7620c8b54', 1753764305611, NULL, 'api', '开关面板-文档版', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223434653337316664626564653465343461306233646466373632306338623534222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E707573682870726F70657274696573293B5C6E202020205C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E2F2F2072657475726E206465636F6465285C225C22293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2DE69687E6A1A3E78988222C0A20202263726561746554696D6522203A20313735333736343330353539372C0A20202275706461746554696D6522203A206E756C6C2C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A206E756C6C2C0A2020227061746822203A202233386663383065222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E69687E6A1A3E6BC94E7A4BAE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('44e371fdbede4e44a0b3ddf7620c8b54', 1753764307282, NULL, 'api', '开关面板-文档版', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223434653337316664626564653465343461306233646466373632306338623534222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E707573682870726F70657274696573293B5C6E202020205C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E2F2F2072657475726E206465636F6465285C225C22293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2DE69687E6A1A3E78988222C0A20202263726561746554696D6522203A20313735333736343330353539372C0A20202275706461746554696D6522203A20313735333736343330373237322C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202233386663383065222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E69687E6A1A3E6BC94E7A4BAE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('44e371fdbede4e44a0b3ddf7620c8b54', 1753764312138, NULL, 'api', '开关面板-文档版', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223434653337316664626564653465343461306233646466373632306338623534222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E72657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2DE69687E6A1A3E78988222C0A20202263726561746554696D6522203A20313735333736343330353539372C0A20202275706461746554696D6522203A20313735333736343331323132372C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202233386663383065222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E69687E6A1A3E6BC94E7A4BAE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('44e371fdbede4e44a0b3ddf7620c8b54', 1753764337243, NULL, 'api', '开关面板-文档版', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223434653337316664626564653465343461306233646466373632306338623534222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2FE9A284E8A7A3E7A081EFBC8CE5BE97E588B064657669636549645C6E72657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E2F2F2072657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2DE69687E6A1A3E78988222C0A20202263726561746554696D6522203A20313735333736343330353539372C0A20202275706461746554696D6522203A20313735333736343333373233322C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202233386663383065222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20225B5C6E202020207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020205C22706F77657253746174655C223A205C2230315C222C5C6E2020202020202020202020205C2263757272656E745C223A205C2231352E30305C222C5C6E2020202020202020202020205C22766F6C746167655C223A205C223232382E385C222C5C6E2020202020202020202020205C22706F7765725C223A205C22313230305C225C6E20202020202020207D2C5C6E20202020202020205C2264657669636549645C223A205C22303030315C222C5C6E20202020202020205C22746370456E636F64655C223A205C224845585C222C5C6E20202020202020205C227265706C795061796C6F61645C223A205C22373237323730363332433637363537343633373337315C225C6E202020207D5C6E5D222C0A2020226465736372697074696F6E22203A2022E69687E6A1A3E6BC94E7A4BAE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('44e371fdbede4e44a0b3ddf7620c8b54', 1753764902384, NULL, 'api', '开关面板-文档版', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223434653337316664626564653465343461306233646466373632306338623534222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2FE9A284E8A7A3E7A081EFBC8CE5BE97E588B064657669636549645C6E72657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E2F2F2072657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2DE69687E6A1A3E78988222C0A20202263726561746554696D6522203A20313735333736343330353539372C0A20202275706461746554696D6522203A20313735333736343930323337352C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202233386663383065222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E69687E6A1A3E6BC94E7A4BAE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('44e371fdbede4e44a0b3ddf7620c8b54', 1753764914652, NULL, 'api', '开关面板-文档版', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223434653337316664626564653465343461306233646466373632306338623534222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2FE9A284E8A7A3E7A081EFBC8CE5BE97E588B064657669636549645C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E2F2F2072657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2DE69687E6A1A3E78988222C0A20202263726561746554696D6522203A20313735333736343330353539372C0A20202275706461746554696D6522203A20313735333736343931343633382C0A2020226C6F636B22203A202230222C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202233386663383065222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E69687E6A1A3E6BC94E7A4BAE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('44e371fdbede4e44a0b3ddf7620c8b54', 1754144095867, NULL, 'api', '开关面板-文档版-TCP', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223434653337316664626564653465343461306233646466373632306338623534222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2FE9A284E8A7A3E7A081EFBC8CE5BE97E588B064657669636549645C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E2F2F2072657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2DE69687E6A1A3E789882D544350222C0A20202263726561746554696D6522203A20313735333736343330353539372C0A20202275706461746554696D6522203A20313735343134343039353737372C0A2020226C6F636B22203A202230222C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202233386663383065222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E69687E6A1A3E6BC94E7A4BAE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('44e371fdbede4e44a0b3ddf7620c8b54', 1754144097001, NULL, 'api', '开关面板-文档版-TCP', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223434653337316664626564653465343461306233646466373632306338623534222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2FE9A284E8A7A3E7A081EFBC8CE5BE97E588B064657669636549645C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E2F2F2072657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2DE69687E6A1A3E789882D544350222C0A20202263726561746554696D6522203A20313735333736343330353539372C0A20202275706461746554696D6522203A20313735343134343039363937392C0A2020226C6F636B22203A202230222C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202233386663383065222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E69687E6A1A3E6BC94E7A4BAE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('44e371fdbede4e44a0b3ddf7620c8b54', 1754144117347, NULL, 'api', '开关面板-文档版-TCP', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223434653337316664626564653465343461306233646466373632306338623534222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2FE9A284E8A7A3E7A081EFBC8CE5BE97E588B064657669636549645C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E2F2F2072657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2DE69687E6A1A3E789882D544350222C0A20202263726561746554696D6522203A20313735333736343330353539372C0A20202275706461746554696D6522203A20313735343134343131373234332C0A2020226C6F636B22203A202230222C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A202233386663383065222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E69687E6A1A3E6BC94E7A4BAE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('5393bfe75eb64c2182f4308458eadb5e', 1754787304079, NULL, 'api', '单相导轨XA-GXSD8-4G', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223533393362666537356562363463323138326634333038343538656164623565222C0A20202273637269707422203A20222F2F3136E8BF9BE588B64153434949E8BDAC535452494E475C6E7661722068657832737472203D20737472696E67686578203D3E207B5C6E5C6E202020207661722069203D20305C6E2020202076617220617363696964617461203D205C225C225C6E202020207768696C65202869203C20737472696E676865782E6C656E677468282929207B5C6E2020202020202020617363696964617461202B3D2028537472696E672E76616C75654F6628496E74656765722E7061727365496E7428737472696E676865782E737562737472696E6728692C2069202B2032292C2031362929202B205C222C5C22295C6E202020202020202069202B3D20325C6E202020207D5C6E20202020617363696964617461203D206173636969646174612E737562737472696E6728302C206173636969646174612E6C656E6774682829202D2031295C6E2020202072657475726E206173636969546F537472696E6728617363696964617461295C6E7D5C6E2F2FE5A4A7E5B08FE7ABAFE8BDACE68DA25C6E7661722072657665727365537472203D2064617461203D3E207B5C6E20202020766172207244617461203D205C225C225C6E2020202069662028646174612E6C656E6774682829203E3D203029207B5C6E20202020202020206C657420736571203D20646174612E6C656E67746828295C6E20202020202020207768696C652028736571203E203029207B5C6E2020202020202020202020207244617461202B3D20646174612E737562737472696E6728736571202D20322C20736571295C6E202020202020202020202020736571203D20736571202D20325C6E20202020202020207D5C6E202020207D5C6E2020202072657475726E2072446174615C6E7D5C6E2F2FE5928CE6A0A1E9AA8CE8AEA1E7AE975C6E76617220636865636B5F4353203D2073747244617461203D3E207B5C6E202020206C65742069203D20305C6E202020206C657420616363756D203D20305C6E202020207768696C65202869203C20737472446174612E6C656E677468282929207B5C6E2020202020202020616363756D202B3D20496E74656765722E7061727365496E7428737472446174612E737562737472696E6728692C2069202B2032292C203136295C6E202020202020202069202B3D20325C6E202020207D5C6E202020206C6574206C203D20496E74656765722E746F486578537472696E6728616363756D292E6C656E67746828295C6E2020202072657475726E20496E74656765722E746F486578537472696E6728616363756D292E737562737472696E67286C202D20322C206C292E746F55707065724361736528295C6E7D5C6E2F2FE695B0E68DAEE8BDACE68DA220E58092E8BDAC202D33335C6E766172207472616E73666F726D203D20737472203D3E207B5C6E202020206C65742069203D20305C6E202020206C657420726573756C74203D2027275C6E20202020737472203D207265766572736554776F446967697428737472295C6E202020207768696C65202869203C207374722E6C656E677468282929207B5C6E2020202020202020696620287374722E737562737472696E6728692C2032202B206929203D3D202733332729207B5C6E20202020202020202020202069202B3D20325C6E202020202020202020202020726573756C74202B3D20273030275C6E20202020202020207D20656C7365207B5C6E2020202020202020202020206C65742076616C7565203D20496E74656765722E7061727365496E74287374722E737562737472696E6728692C2032202B2069292C20313629202D20307833333B5C6E20202020202020202020202069202B3D20325C6E202020202020202020202020726573756C74202B3D20537472696E672E666F726D6174282725303258272C2076616C7565295C6E20202020202020207D5C6E202020207D5C6E2020202072657475726E20496E74656765722E7061727365496E7428726573756C742C203130295C6E7D5C6E2F2F2B30783333E696B9E6B3955C6E7661722064617461706C7573203D2073747244617461203D3E207B5C6E202020206C6574207344617461203D2027275C6E202020206C65742069203D20305C6E202020206C6574207044617461203D20305C6E202020207768696C65202869203C20737472446174612E6C656E677468282929207B5C6E20202020202020207044617461203D2028496E74656765722E7061727365496E7428737472446174612E737562737472696E6728692C2069202B2032292C20313629202B203078333329202620306231313131313131315C6E20202020202020207344617461202B3D20537472696E672E666F726D6174282725303258272C207044617461295C6E202020202020202069202B3D20325C6E202020207D5C6E2020202072657475726E2073446174615C6E7D5C6E5C6E2F2FE9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB7E4BAA7E5938164657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E202020202F2FE6B3A8E5868CE58C855C6E20202020696620287061796C6F61642E6C656E6774682829203D3D20313229207B5C6E2020202020202020726573756C742E6465766963654964203D207265766572736554776F4469676974287061796C6F6164295C6E202020207D5C6E202020205C6E202020202F2FE794B5E8A1A8E794B5E9878FE4B88AE68AA5E6B688E681AFE9A284E8A7A3E7A0815C6E202020206966287061796C6F61642E6C656E6774682829203D3D203938202626207061796C6F61642E737562737472696E6728302C32292E657175616C732827363827292626207061796C6F61642E737562737472696E67287061796C6F61642E6C656E67746828292D322C7061796C6F61642E6C656E6774682829292E657175616C73282731362729297B5C6E2020202020202020726573756C742E6465766963654964203D2072657665727365537472287061796C6F61642E737562737472696E6728322C20313429295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E2F2FE4B88AE8A18CE5A484E790865C6E766172206465636F6465203D20287061796C6F61642C636F6E7465787429203D3E207B5C6E2020202076617220726573756C74203D205B5D5C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D5C6E2020202070726F706572746965734F626A2E70726F706572746965732E6F726967696E616C4D657373616765203D207061796C6F61645C6E5C6E5C6E202020202F2F70726F706572746965734F626A2E70726F706572746965732E736861646F77203D2070446174612E736861646F775C6E20202020726573756C742E707573682870726F706572746965734F626A295C6E202020202F2FE5BF83E8B7B3E58C855C6E20202020696620287061796C6F61642E6C656E6774682829203D3D20313229207B5C6E2020202020202020726573756C742E707573682870726F706572746965734F626A295C6E202020202020202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202020202020202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737383638275C6E202020207D20656C7365207B5C6E20202020202020202F2FE68A84E8A1A8E7BB93E69E9C5C6E2020202020202020696620287061796C6F61642E6C656E6774682829203E3D203934202626207061796C6F61642E737562737472696E672831342C20313829203D3D2027363839312729207B5C6E2020202020202020202020202F2FE8A1A8E58FB75C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D657465724E6F203D2072657665727365537472287061796C6F61642E737562737472696E6728322C20313429295C6E202020202020202020202020766172206C656E677468203D20496E74656765722E7061727365496E74287061796C6F61642E737562737472696E672831382C203230292C203136295C6E5C6E2020202020202020202020202F2FE680BBE794A8E794B5E9878F5C6E2020202020202020202020206C6574206D44617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D206D446174612E737562737472696E6728302C206D446174612E6C656E6774682829202D2031295C6E2020202020202020202020202F2FE58F96E5BDB1E5AD90E9878CE79A84E4B88AE6ACA1E8AFBBE695B05C6E2020202020202020202020202F2F20696620287044617461213D6E756C6C20262670446174612E636F6E7461696E734B65792827736861646F77272929207B5C6E2020202020202020202020202F2F20202020206966202870446174612E736861646F772E73746174652E7265706F727465642E636F6E7461696E734B657928276D6574657244617461272929207B5C6E2020202020202020202020202F2F20202020202020202070726F706572746965734F626A2E70726F706572746965732E6C6173744D6574657244617461203D2070446174612E736861646F772E73746174652E7265706F727465642E6D65746572446174615C6E2020202020202020202020202F2F20202020207D5C6E2020202020202020202020202F2F207D5C6E2020202020202020202020202F2F70726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E2020202020202020202020202F2FE794B5E4BBB75C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D657465725072696365203D20287472616E73666F726D287061796C6F61642E737562737472696E672833362C2034342929202A20302E30303031292E746F46697865642834295C6E2020202020202020202020202F2FE794B5E58E8B5C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D20287472616E73666F726D287061796C6F61642E737562737472696E672834342C2035322929202A20302E31292E746F46697865642831295C6E2020202020202020202020202F2FE794B5E6B5815C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D20287472616E73666F726D287061796C6F61642E737562737472696E672835322C2036302929202A20302E303031292E746F46697865642833295C6E2020202020202020202020202F2FE58A9FE78E875C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20287472616E73666F726D287061796C6F61642E737562737472696E672836302C2036382929202A20302E31292E746F46697865642831295C6E2020202020202020202020202F2FE8B4ADE794B5E6ACA1E695B05C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E707572636861736573203D207472616E73666F726D287061796C6F61642E737562737472696E672836382C20373629295C6E2020202020202020202020202F2FE9A284E8ADA6E794B5E9878F5C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E7761726E696E67506F776572203D20287472616E73666F726D287061796C6F61642E737562737472696E672837362C2038342929202A20302E3031292E746F46697865642832295C6E2020202020202020202020202F2FE589A9E4BD99E98791E9A29D5C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E72656D61696E696E67416D6F756E74203D20287472616E73666F726D287061796C6F61642E737562737472696E672838342C2039322929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6F70656E5374617465203D2028496E74656765722E7061727365496E74287061796C6F61642E737562737472696E672839322C203934292C20313629202D203078333329203D3D203237203F20273127203A202730275C6E2020202020202020202020202F2FE696B0E5A29EE69FA5E8AFA2E4BFA1E58FB7E580BCEFBC8CE59CA87265706C79E9878CE99DA2E4B88BE58F915C6E20202020202020202020202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E20202020202020202020202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202020202020207D5C6E20202020202020202F2FE68A84E8A1A8E58886E58C85EFBC8CE7ACACE4B880E983A8E588865C6E2020202020202020696620287061796C6F61642E6C656E6774682829203D3D203634202626207061796C6F61642E737562737472696E672831342C20313829203D3D2027363839312729207B5C6E2020202020202020202020202F2FE8A1A8E58FB75C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D657465724E6F203D2072657665727365537472287061796C6F61642E737562737472696E6728322C20313429295C6E202020202020202020202020766172206C656E677468203D20496E74656765722E7061727365496E74287061796C6F61642E737562737472696E672831382C203230292C203136295C6E5C6E2020202020202020202020202F2FE680BBE794A8E794B5E9878F5C6E2020202020202020202020206C6574206D44617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D206D446174612E737562737472696E6728302C206D446174612E6C656E6774682829202D2031295C6E2020202020202020202020202F2FE58F96E5BDB1E5AD90E9878CE79A84E4B88AE6ACA1E8AFBBE695B05C6E2020202020202020202020202F2F206966202870446174612E636F6E7461696E734B65792827736861646F77272929207B5C6E2020202020202020202020202F2F20202020206966202870446174612E736861646F772E73746174652E7265706F727465642E636F6E7461696E734B657928276D6574657244617461272929207B5C6E2020202020202020202020202F2F20202020202020202070726F706572746965734F626A2E70726F706572746965732E6C6173744D6574657244617461203D2070446174612E736861646F772E73746174652E7265706F727465642E6D65746572446174615C6E2020202020202020202020202F2F20202020207D5C6E2020202020202020202020202F2F207D5C6E2020202020202020202020202F2F70726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E2020202020202020202020202F2FE794B5E4BBB75C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D657465725072696365203D20287472616E73666F726D287061796C6F61642E737562737472696E672833362C2034342929202A20302E30303031292E746F46697865642834295C6E2020202020202020202020202F2FE794B5E58E8B5C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D20287472616E73666F726D287061796C6F61642E737562737472696E672834342C2035322929202A20302E31292E746F46697865642831295C6E2020202020202020202020202F2FE794B5E6B5815C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D20287472616E73666F726D287061796C6F61642E737562737472696E672835322C2036302929202A20302E303031292E746F46697865642833295C6E2020202020202020202020202F2FE58A9FE78E875C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20287472616E73666F726D287061796C6F61642E737562737472696E672836302C2036342929202A20302E31292E746F46697865642831295C6E20202020202020207D5C6E20202020202020202F2FE68A84E8A1A8E58886E58C85EFBC8CE7ACACE4BA8CE983A8E588865C6E2020202020202020696620287061796C6F61642E6C656E6774682829203D3D20333429207B5C6E2020202020202020202020202F2F5C6E2020202020202020202020202F2FE8B4ADE794B5E6ACA1E695B05C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E707572636861736573203D207472616E73666F726D287061796C6F61642E737562737472696E6728342C20313229295C6E2020202020202020202020202F2FE9A284E8ADA6E794B5E9878F5C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E7761726E696E67506F776572203D20287472616E73666F726D287061796C6F61642E737562737472696E672831322C2032302929202A20302E3031292E746F46697865642832295C6E2020202020202020202020202F2FE589A9E4BD99E98791E9A29D5C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E72656D61696E696E67416D6F756E74203D20287472616E73666F726D287061796C6F61642E737562737472696E672832302C2032382929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6F70656E5374617465203D2028496E74656765722E7061727365496E74287061796C6F61642E737562737472696E672832382C203330292C20313629202D203078333329203D3D203237203F20273127203A202730275C6E2020202020202020202020202F2FE696B0E5A29EE69FA5E8AFA2E4BFA1E58FB7E580BCEFBC8CE59CA87265706C79E9878CE99DA2E4B88BE58F915C6E20202020202020202020202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E20202020202020202020202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202020202020207D5C6E20202020202020202F2FE68EA7E588B6E5BC80E59088E997B8E59B9EE5A48DE79A84E4B8A2E695B0E68DAEE5A4B43638E585BCE5AEB9E5A484E790865C6E2020202020202020696620287061796C6F61642E6C656E6774682829203D3D20343829207B5C6E202020202020202020202020696620287061796C6F61642E737562737472696E672831322C203138292E657175616C73282736383943304427292026262028287061796C6F61642E737562737472696E672834322C203434292E657175616C7328273444272929207C7C20287061796C6F61642E737562737472696E672834322C203434292E657175616C73282734452729292929207B5C6E202020202020202020202020202020207061796C6F6164203D2027363827202B207061796C6F61645C6E2020202020202020202020207D5C6E20202020202020207D5C6E5C6E20202020202020202F2FE68EA7E588B6E5BC80E59088E997B8E59B9EE5A48D5C6E2020202020202020696620287061796C6F61642E737562737472696E672831342C20313829203D3D2027363839432729207B5C6E2020202020202020202020206C6574207265706C797377697463684F626A203D207B5C6E202020202020202020202020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E202020202020202020202020202020205C2266756E6374696F6E5C223A205C227377697463684F6E4F66665C222C5C6E202020202020202020202020202020205C22646174615C223A207B5C6E20202020202020202020202020202020202020205C226F70656E53746174655C223A2028496E74656765722E7061727365496E74287061796C6F61642E737562737472696E672834342C203436292C20313629202D203078333329203D3D203237203F20273127203A202730275C6E202020202020202020202020202020207D5C6E2020202020202020202020207D5C6E5C6E202020202020202020202020726573756C742E70757368287265706C797377697463684F626A295C6E2020202020202020202020202F2FE5908CE6ADA5E4B88AE68AA5E9809AE794B5E696ADE794B5E5B19EE680A75C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6F70656E5374617465203D207265706C797377697463684F626A2E646174612E6F70656E53746174655C6E2020202020202020202020202F2F70726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832302C2032382929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E72656D61696E696E67416D6F756E74203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E707572636861736573203D207472616E73666F726D287061796C6F61642E737562737472696E672833362C20343429295C6E2020202020202020202020202F2FE696B0E5A29EE69FA5E8AFA2E4BFA1E58FB7E580BCEFBC8CE59CA87265706C79E9878CE99DA2E4B88BE58F915C6E20202020202020202020202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E20202020202020202020202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202020202020207D5C6E20202020202020202F2FE6B885E8A1A8E59B9EE5A48D5C6E2020202020202020696620287061796C6F61642E737562737472696E672831342C20313829203D3D2027363839412729207B5C6E2020202020202020202020206C6574207265706C797377697463684F626A203D207B5C6E202020202020202020202020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E202020202020202020202020202020205C2266756E6374696F6E5C223A205C2272657365745C222C5C6E202020202020202020202020202020205C22646174615C223A207B5C6E20202020202020202020202020202020202020205C22726573756C745C223A2027E8AEBEE7BDAEE68890E58A9F275C6E202020202020202020202020202020207D5C6E2020202020202020202020207D5C6E202020202020202020202020726573756C742E70757368287265706C797377697463684F626A295C6E2020202020202020202020206C6574206D44617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D206D446174612E737562737472696E6728302C206D446174612E6C656E6774682829202D2031295C6E2020202020202020202020202F2F70726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832302C2032382929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E72656D61696E696E67416D6F756E74203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E707572636861736573203D207472616E73666F726D287061796C6F61642E737562737472696E672833362C20343429295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6F70656E5374617465203D2028496E74656765722E7061727365496E74287061796C6F61642E737562737472696E672834342C203436292C20313629202D203078333329203D3D203237203F20273127203A202730275C6E20202020202020207D5C6E20202020202020202F2FE8AEBEE7BDAEE99990E580BCE59B9EE5A48D5C6E2020202020202020696620287061796C6F61642E737562737472696E672831342C20323029203D3D20273638383431432729207B5C6E2020202020202020202020206C6574207265706C794C696D69744F626A203D207B5C6E202020202020202020202020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E202020202020202020202020202020205C2266756E6374696F6E5C223A205C227365744F7665726C6F61644C696D69745C222C5C6E202020202020202020202020202020205C22646174615C223A207B5C6E20202020202020202020202020202020202020205C226F766572566F6C746167654C696D69745C223A205C225C222C5C6E20202020202020202020202020202020202020205C226F7665724375727272656E744C696D69745C223A205C225C225C6E202020202020202020202020202020207D5C6E2020202020202020202020207D5C6E202020202020202020202020726573756C742E70757368287265706C794C696D69744F626A295C6E2020202020202020202020202F2FE59BA4E7A7AFE98791E9A29D5C6E2020202020202020202020202F2F70726F706572746965734F626A2E70726F706572746965732E686F617264416D6F756E74203D205C6E5C6E20202020202020207D5C6E20202020202020202F2FE594AEE794B5E59B9EE5A48D5C6E2020202020202020696620287061796C6F61642E737562737472696E672831342C20323029203D3D20273638383430432729207B5C6E2020202020202020202020206C6574207265706C7952656368617267654F626A203D207B5C6E202020202020202020202020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E202020202020202020202020202020205C2266756E6374696F6E5C223A205C2272656368617267655C222C5C6E202020202020202020202020202020205C22646174615C223A207B5C6E20202020202020202020202020202020202020205C22726573756C745C223A2027E8AEBEE7BDAEE68890E58A9F275C6E202020202020202020202020202020207D5C6E2020202020202020202020207D5C6E202020202020202020202020726573756C742E70757368287265706C7952656368617267654F626A295C6E2020202020202020202020202F2FE59BA4E7A7AFE98791E9A29D5C6E2020202020202020202020202F2F70726F706572746965734F626A2E70726F706572746965732E686F617264416D6F756E74203D205C6E5C6E20202020202020207D5C6E20202020202020202F2FE6AF94E8BE83E4B88AE6ACA1E8AFBBE695B0E5928CE8BF99E6ACA1E8AFBBE695B0E79A84E5B7AEE9A29D5C6E20202020202020206966202870726F706572746965734F626A2E70726F706572746965732E636F6E7461696E734B657928276C6173744D657465724461746127292026262070726F706572746965734F626A2E70726F706572746965732E636F6E7461696E734B657928276D6574657244617461272929207B5C6E2020202020202020202020206C65742073686F727466616C6C44617461203D20466C6F61742E7061727365466C6F61742870726F706572746965734F626A2E70726F706572746965732E6D657465724461746129202D20466C6F61742E7061727365466C6F61742870726F706572746965734F626A2E70726F706572746965732E6C6173744D6574657244617461295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E73686F727466616C6C44617461203D20466C6F61742E7061727365466C6F61742873686F727466616C6C446174612E746F4669786564283229295C6E2020202020202020202020206966202870726F706572746965734F626A2E70726F706572746965732E73686F727466616C6C44617461203C203029207B5C6E202020202020202020202020202020206C65742073686F727466616C6C416C61726D4F626A203D207B5C6E20202020202020202020202020202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020202020202020202020202020205C226576656E745C223A205C225C222C5C6E20202020202020202020202020202020202020205C22646174615C223A207B5C6E2020202020202020202020202020202020202020202020205C226C6576656C5C223A205C227761726E5C225C6E20202020202020202020202020202020202020207D5C6E202020202020202020202020202020207D5C6E2020202020202020202020202020202073686F727466616C6C416C61726D4F626A2E6576656E74203D202773686F727466616C6C416C61726D275C6E2020202020202020202020202020202073686F727466616C6C416C61726D4F626A2E646174612E73686F727466616C6C416C61726D203D2027E8AFBBE695B0E58092E6B581E5918AE8ADA6275C6E2020202020202020202020202020202073686F727466616C6C416C61726D4F626A2E646174612E73686F727466616C6C44617461203D2070726F706572746965734F626A2E70726F706572746965732E73686F727466616C6C446174615C6E20202020202020202020202020202020726573756C742E707573682873686F727466616C6C416C61726D4F626A295C6E2020202020202020202020207D5C6E20202020202020207D5C6E20202020202020202F2F435351E695B0E68DAEEFBC8C4943434944E695B0E68DAE5C6E20202020202020202F2A2A5C6E2020202020202020202A20727270632C6765746373712C32325C6E2020202020202020202A20727270632C67657469636369642C38393836303441383130323434303132303135325C6E2020202020202020202A205C6E2020202020202020202A2F5C6E2020202020202020696620287061796C6F61642E6C656E6774682829203E3D2038202626207061796C6F61642E737562737472696E6728302C2038292E657175616C7328273732373237303633272929207B5C6E2020202020202020202020206C6574207244617461203D2068657832737472287061796C6F6164295C6E2020202020202020202020206C657420724172726179203D2072446174612E73706C697428272C27295C6E2020202020202020202020202F2F435351E695B0E68DAE5C6E202020202020202020202020696620287241727261795B305D2E657175616C732827727270632729202626207241727261795B315D2E657175616C732827676574637371272929207B5C6E2020202020202020202020202020202070726F706572746965734F626A2E70726F706572746965732E637371203D207241727261795B325D5C6E2020202020202020202020202020202070726F706572746965734F626A2E70726F706572746965732E7369676E616C4C6576656C203D206373712870726F706572746965734F626A2E70726F706572746965732E637371295C6E2020202020202020202020207D5C6E202020202020202020202020696620287241727261795B305D2E657175616C732827727270632729202626207241727261795B315D2E657175616C7328276765746963636964272929207B5C6E2020202020202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6963636964203D207241727261795B325D5C6E2020202020202020202020207D5C6E5C6E20202020202020207D5C6E202020207D5C6E202020202F2A2F2FE5BC80E5A78BE5AD97E88A825C6E20202020696620287061796C6F61642E737562737472696E6728302C20322920213D202736382729207B5C6E202020205C7472657475726E206A736F6E546F53747228726573756C74295C6E202020207D2A2F5C6E5C6E5C6E2020202072657475726E206A736F6E546F53747228726573756C74295C6E7D5C6E2F2FE4B88BE8A18CE5A484E790865C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E20202020766172206F626A203D20746F4A736F6E287061796C6F6164295C6E202020202F2FE8AFBBE58F96E794B5E8A1A8E794A8E9878F5C6E20202020696620286F626A2E66756E6374696F6E203D3D20276D65746572526561642729207B5C6E2020202020202020696620286F626A2E646174612E6D657465724E6F2E6C656E6774682829203D3D20313229207B5C6E2020202020202020202020206C657420737472203D2027363827202B2072657665727365537472286F626A2E646174612E6D657465724E6F29202B20273638313130343333333543343333275C6E2020202020202020202020206C657420636865636B203D20636865636B5F435328737472295C6E202020202020202020202020737472203D20737472202B20636865636B202B20273136275C6E20202020202020202020202072657475726E207374722E746F4C6F7765724361736528295C6E20202020202020207D5C6E202020207D5C6E202020202F2FE5BC80E585B3E997B85C6E20202020696620286F626A2E66756E6374696F6E203D3D20277377697463684F6E4F66662729207B5C6E2020202020202020696620286F626A2E646174612E6D657465724E6F2E6C656E6774682829203D3D20313229207B5C6E2020202020202020202020202F2F31E4B8BAE59088E997B85C6E2020202020202020202020206C6574207377697463684F6E203D206F626A2E646174612E76616C76654F6E203D3D20273127203F2027344527203A20273444275C6E2020202020202020202020206C657420737472203D2027363827202B2072657665727365537472286F626A2E646174612E6D657465724E6F29202B20273638314331303335333333333333333438393637343527202B207377697463684F6E202B20273333343737373342334134343343275C6E2020202020202020202020206C657420636865636B203D20636865636B5F435328737472295C6E202020202020202020202020737472203D20737472202B20636865636B202B20273136275C6E20202020202020202020202072657475726E207374722E746F4C6F7765724361736528295C6E20202020202020207D5C6E202020207D5C6E202020202F2FE6B885E8A1A828E6B885E999A4E68980E69C89EFBC8CE4BD99E9A29DE38081E680BBE794B5E9878FE38081E8B4ADE794B5E6ACA1E695B0E38081E794B5E4BBB7E585A8E983A8E5889DE5A78BE58C96295C6E20202020696620286F626A2E66756E6374696F6E203D3D202772657365742729207B5C6E2020202020202020696620286F626A2E646174612E6D657465724E6F2E6C656E6774682829203D3D20313229207B5C6E2020202020202020202020206C657420737472203D2027363827202B2072657665727365537472286F626A2E646174612E6D657465724E6F29202B2027363831413039333533333333333333343839363734353335275C6E2020202020202020202020206C657420636865636B203D20636865636B5F435328737472295C6E202020202020202020202020737472203D20737472202B20636865636B202B20273136275C6E20202020202020202020202072657475726E207374722E746F4C6F7765724361736528295C6E20202020202020207D5C6E202020207D5C6E202020202F2FE8B685E99990E580BCE8AEBEE7BDAE5C6E20202020696620286F626A2E66756E6374696F6E203D3D20277365744F7665726C6F61644C696D69742729207B5C6E2020202020202020696620286F626A2E646174612E6D657465724E6F2E6C656E6774682829203D3D20313229207B5C6E2020202020202020202020206C657420737472203D2027363827202B2072657665727365537472286F626A2E646174612E6D657465724E6F29202B202736383034323033363333333333333335333333333333373334303336333333333333333333333733343033363333275C6E2020202020202020202020202F2FE794B5E58E8BE99990E580BCE5A484E790865C6E202020202020202020202020696620286F626A2E646174612E636F6E7461696E734B657928276F766572566F6C746167654C696D69742729202626206F626A2E646174612E6F766572566F6C746167654C696D697420213D204E554C4C29207B5C6E2020202020202020202020202020202069662028466C6F61742E76616C75654F66286F626A2E646174612E6F766572566F6C746167654C696D697429203E203232302E3020262620466C6F61742E76616C75654F66286F626A2E646174612E6F766572566F6C746167654C696D697429203C3D203236302E3029207B5C6E20202020202020202020202020202020202020206C6574206876203D20537472696E672E666F726D6174282725303858272C20496E74656765722E76616C75654F662828466C6F61742E76616C75654F66286F626A2E646174612E6F766572566F6C746167654C696D697429202A203130292E746F466978656428302929295C6E2020202020202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328687629295C6E202020202020202020202020202020207D5C6E202020202020202020202020202020202F2FE5A682E69E9CE794B5E58E8BE99990E580BCE4B88DE59CA8E88C83E59BB4E58685EFBC8CE8A1A5E9BB98E8AEA4E99990E580BC3236302E30565C6E20202020202020202020202020202020656C7365207B5C6E2020202020202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328537472696E672E666F726D6174282725303858272C20323630302929295C6E202020202020202020202020202020207D5C6E2020202020202020202020207D5C6E2020202020202020202020202F2FE5A682E69E9CE4B88DE5A1ABE58699E794B5E58E8BE99990E580BCEFBC8CE9BB98E8AEA4323630305C6E202020202020202020202020656C7365207B5C6E20202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328537472696E672E666F726D6174282725303858272C20323630302929295C6E2020202020202020202020207D5C6E2020202020202020202020202F2FE794B5E6B581E99990E580BCE5A484E790865C6E202020202020202020202020696620286F626A2E646174612E636F6E7461696E734B657928276F7665724375727272656E744C696D69742729202626206F626A2E646174612E6F7665724375727272656E744C696D697420213D204E554C4C29207B5C6E2020202020202020202020202020202069662028466C6F61742E76616C75654F66286F626A2E646174612E6F7665724375727272656E744C696D697429203E20302E30303020262620466C6F61742E76616C75654F66286F626A2E646174612E6F7665724375727272656E744C696D697429203C2038302E30303029207B5C6E20202020202020202020202020202020202020206C6574206849203D20537472696E672E666F726D6174282725303858272C20496E74656765722E76616C75654F662828466C6F61742E76616C75654F66286F626A2E646174612E6F7665724375727272656E744C696D697429202A2031303030292E746F466978656428302929295C6E2020202020202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328684929295C6E202020202020202020202020202020207D5C6E202020202020202020202020202020202F2FE5A682E69E9CE794B5E6B581E99990E580BCE4B88DE59CA8E88C83E59BB4E58685EFBC8CE8A1A5E9BB98E8AEA4E99990E580BC3830415C6E20202020202020202020202020202020656C7365207B5C6E2020202020202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328537472696E672E666F726D6174282725303858272C2038303030302929295C6E202020202020202020202020202020207D5C6E2020202020202020202020207D5C6E2020202020202020202020202F2FE5A682E69E9CE4B88DE5A1ABE58699E794B5E6B581E99990E580BCEFBC8CE9BB98E8AEA438303030305C6E202020202020202020202020656C7365207B5C6E20202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328537472696E672E666F726D6174282725303858272C2038303030302929295C6E2020202020202020202020207D5C6E2020202020202020202020202F2FE58AA0E5808DE78E87EFBC8CE9BB98E8AEA4315C6E202020202020202020202020737472202B3D20273334333333333333275C6E2020202020202020202020206C657420636865636B203D20636865636B5F435328737472295C6E202020202020202020202020737472203D20737472202B20636865636B202B20273136275C6E20202020202020202020202072657475726E207374722E746F4C6F7765724361736528295C6E20202020202020207D5C6E202020207D5C6E202020202F2FE594AEE794B55C6E20202020696620286F626A2E66756E6374696F6E203D3D202772656368617267652729207B5C6E2020202020202020696620286F626A2E646174612E6D657465724E6F2E6C656E6774682829203D3D20313229207B5C6E2020202020202020202020206C657420737472203D2027363827202B2072657665727365537472286F626A2E646174612E6D657465724E6F29202B202736383034313033343333333333333335333333333333275C6E202020202020202020202020737472202B3D20726576657273655374722864617461706C757328537472696E672E666F726D6174282725303858272C2028496E74656765722E76616C75654F66286F626A2E646174612E7265636861726765446174612929202A203130302929295C6E5C6E202020202020202020202020696620286F626A2E646174612E636F6E7461696E734B657928277265636861726765436F756E74272929207B5C6E20202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328537472696E672E666F726D6174282725303858272C206F626A2E646174612E7265636861726765436F756E742929295C6E2020202020202020202020207D20656C7365207B5C6E20202020202020202020202020202020737472202B3D20273334333333333333275C6E2020202020202020202020207D5C6E5C6E2020202020202020202020206C657420636865636B203D20636865636B5F435328737472295C6E202020202020202020202020737472203D20737472202B20636865636B202B20273136275C6E20202020202020202020202072657475726E207374722E746F4C6F7765724361736528295C6E20202020202020207D5C6E202020207D5C6E202020202F2FE6B58BE8AF955C6E20202020696620286F626A2E66756E6374696F6E203D3D205C22636D645C2229207B5C6E20202020202020206C657420636D6444617461203D206F626A2E646174612E636D64446174615C6E202020202020202072657475726E20636D64446174615C6E202020207D5C6E202020202F2FE8AFBBE58F96E4BFA1E58FB7E5BCBAE5BAA64353515C6E20202020696620286F626A2E66756E6374696F6E203D3D205C226765744353515C2229207B5C6E20202020202020206C657420636D6444617461203D202737323732373036333243363736353734363337333731275C6E202020202020202072657475726E20636D64446174615C6E202020207D5C6E202020202F2FE8AFBBE58F96E789A9E88194E7BD91E58DA149434349445C6E20202020696620286F626A2E66756E6374696F6E203D3D205C2267657449434349445C2229207B5C6E20202020202020206C657420636D6444617461203D20273732373237303633324336373635373436393633363336393634275C6E202020202020202072657475726E20636D64446174615C6E202020207D5C6E7D5C6E5C6E2F2F2072657475726E207072654465636F64652827363839333030323230353235323036383931323533333335633433333638333333333333333333333333333334623536333333336337363733333333623762353333333333333333333333333333333333333333333333333333333334656430313627295C6E2F2F2072657475726E206465636F64652827363831323131313131313131313136383943304433333334333333333333336333333333333433333333333334656373313627295C6E2F2F72657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C2272656368617267655C222C5C22646174615C223A7B5C226D657465724E6F5C223A5C223230323431313139303234375C222C5C227265636861726765446174615C223A31303030302C5C227265636861726765436F756E745C223A327D7D27295C6E72657475726E206465636F64652827363834373032313931313234323036383931323533333335633433333333333333333333333333333334333337613535333333333333333333333333333333333333333333343333333333333333333333333333333338333333333334653035313627295C6E2F2F72657475726E206465636F646528277B5C227061796C6F61645C223A5C223230323431313139303236315C227D27295C6E2F2F72657475726E2027E58D8EE695B0E789A9E88194E7BD91E59FBAE59CB0EFBC88496F742057617375EFBC89275C6E5C6E2F2FE5B8A6E5BDB1E5AD90E695B0E68DAE5C6E2F2F72657475726E206465636F646528277B5C22736861646F775C223A7B5C226D657461646174615C223A7B5C22646573697265645C223A7B7D2C5C227265706F727465645C223A7B5C227075726368617365735C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C226F70656E53746174655C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C226D6574657250726963655C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C22766F6C746167655C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C2272656D61696E696E67416D6F756E745C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C2263757272656E745C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C227761726E696E67506F7765725C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C226D657465724E6F5C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C22706F7765725C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C226F726967696E616C4D6573736167655C223A7B5C2274696D657374616D705C223A313734373732363732337D2C5C226D65746572446174615C223A7B5C2274696D657374616D705C223A313734373732363437357D7D7D2C5C2276657273696F6E5C223A3331352C5C2273746174655C223A7B5C22646573697265645C223A7B7D2C5C227265706F727465645C223A7B5C227075726368617365735C223A31352C5C226F70656E53746174655C223A5C22315C222C5C226D6574657250726963655C223A5C22312E303030305C222C5C22766F6C746167655C223A5C223232372E395C222C5C2272656D61696E696E67416D6F756E745C223A5C223139393939392E30345C222C5C2263757272656E745C223A5C22302E3030305C222C5C227761726E696E67506F7765725C223A5C22302E30305C222C5C226D657465724E6F5C223A5C223230323431313139303234375C222C5C22706F7765725C223A5C22302E305C222C5C226F726967696E616C4D6573736167655C223A5C22373237323730363332433637363537343633373337315C222C5C226D65746572446174615C223A5C22312E305C227D7D2C5C2274696D657374616D705C223A313734373732363732337D2C5C2270726F647563744B65795C223A5C223637633639663261366230303031613962323261366339615C222C5C2264657669636549645C223A5C223230323431313139303234375C222C5C22696F7449645C223A5C2236333333373235336439333634346263616431353133636133313861353230615C222C5C227061796C6F61645C223A5C2236383437303231393131323432303638393132353333333563343333333333333333333333333333333433333761353533333333333333333333333333333333333333333334333333333333333333333333333333333833333333333465303531365C227D27295C6E5C6E2F2F435351E695B0E68DAE5C6E72657475726E206465636F646528273732373237303633324336373635373436333733373132433332333227295C6E5C6E2F2F4943434944E695B0E68DAE5C6E2F2F2072657475726E206465636F646528277B5C227061796C6F61645C223A5C2237323732373036333243363736353734363936333633363936343243333833393338333633303334343133383331333033323334333433303331333233303331333533325C227D2729222C0A20202267726F7570496422203A20226634626562303864633766653431353762396636336132326130653161343330222C0A2020226E616D6522203A2022E58D95E79BB8E5AFBCE8BDA858412D47585344382D3447222C0A20202263726561746554696D6522203A20313735323830353030383534392C0A20202275706461746554696D6522203A20313735343738373330343035332C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A2022696F74222C0A202022757064617465427922203A2022696F74222C0A2020227061746822203A202261626378617373786E616E6168736D616E222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20225B5C6E202020207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020205C226F726967696E616C4D6573736167655C223A205C2236383132313131313131313131313638394330443333333433333333333333633333333333343333333333333465637331365C222C5C6E2020202020202020202020205C226F70656E53746174655C223A205C22315C222C5C6E2020202020202020202020205C2272656D61696E696E67416D6F756E745C223A205C22392E30305C222C5C6E2020202020202020202020205C227075726368617365735C223A20315C6E20202020202020207D2C5C6E20202020202020205C22746370456E636F64655C223A205C224845585C222C5C6E20202020202020205C227265706C795061796C6F61645C223A205C22373237323730363332433637363537343633373337315C225C6E202020207D2C5C6E202020207B5C6E20202020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E20202020202020205C2266756E6374696F6E5C223A205C227377697463684F6E4F66665C222C5C6E20202020202020205C22646174615C223A207B5C6E2020202020202020202020205C226F70656E53746174655C223A205C22315C225C6E20202020202020207D5C6E202020207D5C6E5D222C0A2020226465736372697074696F6E22203A2022E8BF99E4B8AAE698AFE4B880E4B8AAE794B5E8A1A8222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A207B0A20202020226E616D6522203A2022222C0A202020202276616C756522203A2022222C0A20202020226465736372697074696F6E22203A2022222C0A2020202022726571756972656422203A2066616C73652C0A2020202022646174615479706522203A20224F626A656374222C0A20202020227479706522203A206E756C6C2C0A202020202264656661756C7456616C756522203A206E756C6C2C0A202020202276616C69646174655479706522203A2022222C0A20202020226572726F7222203A2022222C0A202020202265787072657373696F6E22203A2022222C0A20202020226368696C6472656E22203A205B207B0A202020202020226E616D6522203A20226465766963654964222C0A2020202020202276616C756522203A2022323032353035323230303933222C0A202020202020226465736372697074696F6E22203A2022222C0A20202020202022726571756972656422203A2066616C73652C0A20202020202022646174615479706522203A2022537472696E67222C0A202020202020227479706522203A206E756C6C2C0A2020202020202264656661756C7456616C756522203A206E756C6C2C0A2020202020202276616C69646174655479706522203A2022222C0A202020202020226572726F7222203A2022222C0A2020202020202265787072657373696F6E22203A2022222C0A202020202020226368696C6472656E22203A205B205D0A202020207D205D0A20207D0A7D, 'iot'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('5393bfe75eb64c2182f4308458eadb5e', 1754787309114, NULL, 'api', '单相导轨XA-GXSD8-4G', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223533393362666537356562363463323138326634333038343538656164623565222C0A20202273637269707422203A20222F2F3136E8BF9BE588B64153434949E8BDAC535452494E475C6E7661722068657832737472203D20737472696E67686578203D3E207B5C6E5C6E202020207661722069203D20305C6E2020202076617220617363696964617461203D205C225C225C6E202020207768696C65202869203C20737472696E676865782E6C656E677468282929207B5C6E2020202020202020617363696964617461202B3D2028537472696E672E76616C75654F6628496E74656765722E7061727365496E7428737472696E676865782E737562737472696E6728692C2069202B2032292C2031362929202B205C222C5C22295C6E202020202020202069202B3D20325C6E202020207D5C6E20202020617363696964617461203D206173636969646174612E737562737472696E6728302C206173636969646174612E6C656E6774682829202D2031295C6E2020202072657475726E206173636969546F537472696E6728617363696964617461295C6E7D5C6E2F2FE5A4A7E5B08FE7ABAFE8BDACE68DA25C6E7661722072657665727365537472203D2064617461203D3E207B5C6E20202020766172207244617461203D205C225C225C6E2020202069662028646174612E6C656E6774682829203E3D203029207B5C6E20202020202020206C657420736571203D20646174612E6C656E67746828295C6E20202020202020207768696C652028736571203E203029207B5C6E2020202020202020202020207244617461202B3D20646174612E737562737472696E6728736571202D20322C20736571295C6E202020202020202020202020736571203D20736571202D20325C6E20202020202020207D5C6E202020207D5C6E2020202072657475726E2072446174615C6E7D5C6E2F2FE5928CE6A0A1E9AA8CE8AEA1E7AE975C6E76617220636865636B5F4353203D2073747244617461203D3E207B5C6E202020206C65742069203D20305C6E202020206C657420616363756D203D20305C6E202020207768696C65202869203C20737472446174612E6C656E677468282929207B5C6E2020202020202020616363756D202B3D20496E74656765722E7061727365496E7428737472446174612E737562737472696E6728692C2069202B2032292C203136295C6E202020202020202069202B3D20325C6E202020207D5C6E202020206C6574206C203D20496E74656765722E746F486578537472696E6728616363756D292E6C656E67746828295C6E2020202072657475726E20496E74656765722E746F486578537472696E6728616363756D292E737562737472696E67286C202D20322C206C292E746F55707065724361736528295C6E7D5C6E2F2FE695B0E68DAEE8BDACE68DA220E58092E8BDAC202D33335C6E766172207472616E73666F726D203D20737472203D3E207B5C6E202020206C65742069203D20305C6E202020206C657420726573756C74203D2027275C6E20202020737472203D207265766572736554776F446967697428737472295C6E202020207768696C65202869203C207374722E6C656E677468282929207B5C6E2020202020202020696620287374722E737562737472696E6728692C2032202B206929203D3D202733332729207B5C6E20202020202020202020202069202B3D20325C6E202020202020202020202020726573756C74202B3D20273030275C6E20202020202020207D20656C7365207B5C6E2020202020202020202020206C65742076616C7565203D20496E74656765722E7061727365496E74287374722E737562737472696E6728692C2032202B2069292C20313629202D20307833333B5C6E20202020202020202020202069202B3D20325C6E202020202020202020202020726573756C74202B3D20537472696E672E666F726D6174282725303258272C2076616C7565295C6E20202020202020207D5C6E202020207D5C6E2020202072657475726E20496E74656765722E7061727365496E7428726573756C742C203130295C6E7D5C6E2F2F2B30783333E696B9E6B3955C6E7661722064617461706C7573203D2073747244617461203D3E207B5C6E202020206C6574207344617461203D2027275C6E202020206C65742069203D20305C6E202020206C6574207044617461203D20305C6E202020207768696C65202869203C20737472446174612E6C656E677468282929207B5C6E20202020202020207044617461203D2028496E74656765722E7061727365496E7428737472446174612E737562737472696E6728692C2069202B2032292C20313629202B203078333329202620306231313131313131315C6E20202020202020207344617461202B3D20537472696E672E666F726D6174282725303258272C207044617461295C6E202020202020202069202B3D20325C6E202020207D5C6E2020202072657475726E2073446174615C6E7D5C6E5C6E2F2FE9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB7E4BAA7E5938164657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E202020202F2FE6B3A8E5868CE58C855C6E20202020696620287061796C6F61642E6C656E6774682829203D3D20313229207B5C6E2020202020202020726573756C742E6465766963654964203D207265766572736554776F4469676974287061796C6F6164295C6E202020207D5C6E202020205C6E202020202F2FE794B5E8A1A8E794B5E9878FE4B88AE68AA5E6B688E681AFE9A284E8A7A3E7A0815C6E202020206966287061796C6F61642E6C656E6774682829203D3D203938202626207061796C6F61642E737562737472696E6728302C32292E657175616C732827363827292626207061796C6F61642E737562737472696E67287061796C6F61642E6C656E67746828292D322C7061796C6F61642E6C656E6774682829292E657175616C73282731362729297B5C6E2020202020202020726573756C742E6465766963654964203D2072657665727365537472287061796C6F61642E737562737472696E6728322C20313429295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E2F2FE4B88AE8A18CE5A484E790865C6E766172206465636F6465203D20287061796C6F61642C636F6E7465787429203D3E207B5C6E2020202076617220726573756C74203D205B5D5C6E202020207661722070726F706572746965734F626A203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D5C6E2020202070726F706572746965734F626A2E70726F706572746965732E6F726967696E616C4D657373616765203D207061796C6F61645C6E5C6E5C6E202020202F2F70726F706572746965734F626A2E70726F706572746965732E736861646F77203D2070446174612E736861646F775C6E20202020726573756C742E707573682870726F706572746965734F626A295C6E202020202F2FE5BF83E8B7B3E58C855C6E20202020696620287061796C6F61642E6C656E6774682829203D3D20313229207B5C6E2020202020202020726573756C742E707573682870726F706572746965734F626A295C6E202020202020202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202020202020202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737383638275C6E202020207D20656C7365207B5C6E20202020202020202F2FE68A84E8A1A8E7BB93E69E9C5C6E2020202020202020696620287061796C6F61642E6C656E6774682829203E3D203934202626207061796C6F61642E737562737472696E672831342C20313829203D3D2027363839312729207B5C6E2020202020202020202020202F2FE8A1A8E58FB75C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D657465724E6F203D2072657665727365537472287061796C6F61642E737562737472696E6728322C20313429295C6E202020202020202020202020766172206C656E677468203D20496E74656765722E7061727365496E74287061796C6F61642E737562737472696E672831382C203230292C203136295C6E5C6E2020202020202020202020202F2FE680BBE794A8E794B5E9878F5C6E2020202020202020202020206C6574206D44617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D206D446174612E737562737472696E6728302C206D446174612E6C656E6774682829202D2031295C6E2020202020202020202020202F2FE58F96E5BDB1E5AD90E9878CE79A84E4B88AE6ACA1E8AFBBE695B05C6E2020202020202020202020202F2F20696620287044617461213D6E756C6C20262670446174612E636F6E7461696E734B65792827736861646F77272929207B5C6E2020202020202020202020202F2F20202020206966202870446174612E736861646F772E73746174652E7265706F727465642E636F6E7461696E734B657928276D6574657244617461272929207B5C6E2020202020202020202020202F2F20202020202020202070726F706572746965734F626A2E70726F706572746965732E6C6173744D6574657244617461203D2070446174612E736861646F772E73746174652E7265706F727465642E6D65746572446174615C6E2020202020202020202020202F2F20202020207D5C6E2020202020202020202020202F2F207D5C6E2020202020202020202020202F2F70726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E2020202020202020202020202F2FE794B5E4BBB75C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D657465725072696365203D20287472616E73666F726D287061796C6F61642E737562737472696E672833362C2034342929202A20302E30303031292E746F46697865642834295C6E2020202020202020202020202F2FE794B5E58E8B5C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D20287472616E73666F726D287061796C6F61642E737562737472696E672834342C2035322929202A20302E31292E746F46697865642831295C6E2020202020202020202020202F2FE794B5E6B5815C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D20287472616E73666F726D287061796C6F61642E737562737472696E672835322C2036302929202A20302E303031292E746F46697865642833295C6E2020202020202020202020202F2FE58A9FE78E875C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20287472616E73666F726D287061796C6F61642E737562737472696E672836302C2036382929202A20302E31292E746F46697865642831295C6E2020202020202020202020202F2FE8B4ADE794B5E6ACA1E695B05C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E707572636861736573203D207472616E73666F726D287061796C6F61642E737562737472696E672836382C20373629295C6E2020202020202020202020202F2FE9A284E8ADA6E794B5E9878F5C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E7761726E696E67506F776572203D20287472616E73666F726D287061796C6F61642E737562737472696E672837362C2038342929202A20302E3031292E746F46697865642832295C6E2020202020202020202020202F2FE589A9E4BD99E98791E9A29D5C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E72656D61696E696E67416D6F756E74203D20287472616E73666F726D287061796C6F61642E737562737472696E672838342C2039322929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6F70656E5374617465203D2028496E74656765722E7061727365496E74287061796C6F61642E737562737472696E672839322C203934292C20313629202D203078333329203D3D203237203F20273127203A202730275C6E2020202020202020202020202F2FE696B0E5A29EE69FA5E8AFA2E4BFA1E58FB7E580BCEFBC8CE59CA87265706C79E9878CE99DA2E4B88BE58F915C6E20202020202020202020202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E20202020202020202020202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202020202020207D5C6E20202020202020202F2FE68A84E8A1A8E58886E58C85EFBC8CE7ACACE4B880E983A8E588865C6E2020202020202020696620287061796C6F61642E6C656E6774682829203D3D203634202626207061796C6F61642E737562737472696E672831342C20313829203D3D2027363839312729207B5C6E2020202020202020202020202F2FE8A1A8E58FB75C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D657465724E6F203D2072657665727365537472287061796C6F61642E737562737472696E6728322C20313429295C6E202020202020202020202020766172206C656E677468203D20496E74656765722E7061727365496E74287061796C6F61642E737562737472696E672831382C203230292C203136295C6E5C6E2020202020202020202020202F2FE680BBE794A8E794B5E9878F5C6E2020202020202020202020206C6574206D44617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D206D446174612E737562737472696E6728302C206D446174612E6C656E6774682829202D2031295C6E2020202020202020202020202F2FE58F96E5BDB1E5AD90E9878CE79A84E4B88AE6ACA1E8AFBBE695B05C6E2020202020202020202020202F2F206966202870446174612E636F6E7461696E734B65792827736861646F77272929207B5C6E2020202020202020202020202F2F20202020206966202870446174612E736861646F772E73746174652E7265706F727465642E636F6E7461696E734B657928276D6574657244617461272929207B5C6E2020202020202020202020202F2F20202020202020202070726F706572746965734F626A2E70726F706572746965732E6C6173744D6574657244617461203D2070446174612E736861646F772E73746174652E7265706F727465642E6D65746572446174615C6E2020202020202020202020202F2F20202020207D5C6E2020202020202020202020202F2F207D5C6E2020202020202020202020202F2F70726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E2020202020202020202020202F2FE794B5E4BBB75C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D657465725072696365203D20287472616E73666F726D287061796C6F61642E737562737472696E672833362C2034342929202A20302E30303031292E746F46697865642834295C6E2020202020202020202020202F2FE794B5E58E8B5C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D20287472616E73666F726D287061796C6F61642E737562737472696E672834342C2035322929202A20302E31292E746F46697865642831295C6E2020202020202020202020202F2FE794B5E6B5815C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D20287472616E73666F726D287061796C6F61642E737562737472696E672835322C2036302929202A20302E303031292E746F46697865642833295C6E2020202020202020202020202F2FE58A9FE78E875C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20287472616E73666F726D287061796C6F61642E737562737472696E672836302C2036342929202A20302E31292E746F46697865642831295C6E20202020202020207D5C6E20202020202020202F2FE68A84E8A1A8E58886E58C85EFBC8CE7ACACE4BA8CE983A8E588865C6E2020202020202020696620287061796C6F61642E6C656E6774682829203D3D20333429207B5C6E2020202020202020202020202F2F5C6E2020202020202020202020202F2FE8B4ADE794B5E6ACA1E695B05C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E707572636861736573203D207472616E73666F726D287061796C6F61642E737562737472696E6728342C20313229295C6E2020202020202020202020202F2FE9A284E8ADA6E794B5E9878F5C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E7761726E696E67506F776572203D20287472616E73666F726D287061796C6F61642E737562737472696E672831322C2032302929202A20302E3031292E746F46697865642832295C6E2020202020202020202020202F2FE589A9E4BD99E98791E9A29D5C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E72656D61696E696E67416D6F756E74203D20287472616E73666F726D287061796C6F61642E737562737472696E672832302C2032382929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6F70656E5374617465203D2028496E74656765722E7061727365496E74287061796C6F61642E737562737472696E672832382C203330292C20313629202D203078333329203D3D203237203F20273127203A202730275C6E2020202020202020202020202F2FE696B0E5A29EE69FA5E8AFA2E4BFA1E58FB7E580BCEFBC8CE59CA87265706C79E9878CE99DA2E4B88BE58F915C6E20202020202020202020202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E20202020202020202020202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202020202020207D5C6E20202020202020202F2FE68EA7E588B6E5BC80E59088E997B8E59B9EE5A48DE79A84E4B8A2E695B0E68DAEE5A4B43638E585BCE5AEB9E5A484E790865C6E2020202020202020696620287061796C6F61642E6C656E6774682829203D3D20343829207B5C6E202020202020202020202020696620287061796C6F61642E737562737472696E672831322C203138292E657175616C73282736383943304427292026262028287061796C6F61642E737562737472696E672834322C203434292E657175616C7328273444272929207C7C20287061796C6F61642E737562737472696E672834322C203434292E657175616C73282734452729292929207B5C6E202020202020202020202020202020207061796C6F6164203D2027363827202B207061796C6F61645C6E2020202020202020202020207D5C6E20202020202020207D5C6E5C6E20202020202020202F2FE68EA7E588B6E5BC80E59088E997B8E59B9EE5A48D5C6E2020202020202020696620287061796C6F61642E737562737472696E672831342C20313829203D3D2027363839432729207B5C6E2020202020202020202020206C6574207265706C797377697463684F626A203D207B5C6E202020202020202020202020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E202020202020202020202020202020205C2266756E6374696F6E5C223A205C227377697463684F6E4F66665C222C5C6E202020202020202020202020202020205C22646174615C223A207B5C6E20202020202020202020202020202020202020205C226F70656E53746174655C223A2028496E74656765722E7061727365496E74287061796C6F61642E737562737472696E672834342C203436292C20313629202D203078333329203D3D203237203F20273127203A202730275C6E202020202020202020202020202020207D5C6E2020202020202020202020207D5C6E5C6E202020202020202020202020726573756C742E70757368287265706C797377697463684F626A295C6E2020202020202020202020202F2FE5908CE6ADA5E4B88AE68AA5E9809AE794B5E696ADE794B5E5B19EE680A75C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6F70656E5374617465203D207265706C797377697463684F626A2E646174612E6F70656E53746174655C6E2020202020202020202020202F2F70726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832302C2032382929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E72656D61696E696E67416D6F756E74203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E707572636861736573203D207472616E73666F726D287061796C6F61642E737562737472696E672833362C20343429295C6E2020202020202020202020202F2FE696B0E5A29EE69FA5E8AFA2E4BFA1E58FB7E580BCEFBC8CE59CA87265706C79E9878CE99DA2E4B88BE58F915C6E20202020202020202020202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E20202020202020202020202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202020202020207D5C6E20202020202020202F2FE6B885E8A1A8E59B9EE5A48D5C6E2020202020202020696620287061796C6F61642E737562737472696E672831342C20313829203D3D2027363839412729207B5C6E2020202020202020202020206C6574207265706C797377697463684F626A203D207B5C6E202020202020202020202020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E202020202020202020202020202020205C2266756E6374696F6E5C223A205C2272657365745C222C5C6E202020202020202020202020202020205C22646174615C223A207B5C6E20202020202020202020202020202020202020205C22726573756C745C223A2027E8AEBEE7BDAEE68890E58A9F275C6E202020202020202020202020202020207D5C6E2020202020202020202020207D5C6E202020202020202020202020726573756C742E70757368287265706C797377697463684F626A295C6E2020202020202020202020206C6574206D44617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D206D446174612E737562737472696E6728302C206D446174612E6C656E6774682829202D2031295C6E2020202020202020202020202F2F70726F706572746965734F626A2E70726F706572746965732E6D6574657244617461203D20287472616E73666F726D287061796C6F61642E737562737472696E672832302C2032382929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E72656D61696E696E67416D6F756E74203D20287472616E73666F726D287061796C6F61642E737562737472696E672832382C2033362929202A20302E3031292E746F46697865642832295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E707572636861736573203D207472616E73666F726D287061796C6F61642E737562737472696E672833362C20343429295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6F70656E5374617465203D2028496E74656765722E7061727365496E74287061796C6F61642E737562737472696E672834342C203436292C20313629202D203078333329203D3D203237203F20273127203A202730275C6E20202020202020207D5C6E20202020202020202F2FE8AEBEE7BDAEE99990E580BCE59B9EE5A48D5C6E2020202020202020696620287061796C6F61642E737562737472696E672831342C20323029203D3D20273638383431432729207B5C6E2020202020202020202020206C6574207265706C794C696D69744F626A203D207B5C6E202020202020202020202020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E202020202020202020202020202020205C2266756E6374696F6E5C223A205C227365744F7665726C6F61644C696D69745C222C5C6E202020202020202020202020202020205C22646174615C223A207B5C6E20202020202020202020202020202020202020205C226F766572566F6C746167654C696D69745C223A205C225C222C5C6E20202020202020202020202020202020202020205C226F7665724375727272656E744C696D69745C223A205C225C225C6E202020202020202020202020202020207D5C6E2020202020202020202020207D5C6E202020202020202020202020726573756C742E70757368287265706C794C696D69744F626A295C6E2020202020202020202020202F2FE59BA4E7A7AFE98791E9A29D5C6E2020202020202020202020202F2F70726F706572746965734F626A2E70726F706572746965732E686F617264416D6F756E74203D205C6E5C6E20202020202020207D5C6E20202020202020202F2FE594AEE794B5E59B9EE5A48D5C6E2020202020202020696620287061796C6F61642E737562737472696E672831342C20323029203D3D20273638383430432729207B5C6E2020202020202020202020206C6574207265706C7952656368617267654F626A203D207B5C6E202020202020202020202020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E202020202020202020202020202020205C2266756E6374696F6E5C223A205C2272656368617267655C222C5C6E202020202020202020202020202020205C22646174615C223A207B5C6E20202020202020202020202020202020202020205C22726573756C745C223A2027E8AEBEE7BDAEE68890E58A9F275C6E202020202020202020202020202020207D5C6E2020202020202020202020207D5C6E202020202020202020202020726573756C742E70757368287265706C7952656368617267654F626A295C6E2020202020202020202020202F2FE59BA4E7A7AFE98791E9A29D5C6E2020202020202020202020202F2F70726F706572746965734F626A2E70726F706572746965732E686F617264416D6F756E74203D205C6E5C6E20202020202020207D5C6E20202020202020202F2FE6AF94E8BE83E4B88AE6ACA1E8AFBBE695B0E5928CE8BF99E6ACA1E8AFBBE695B0E79A84E5B7AEE9A29D5C6E20202020202020206966202870726F706572746965734F626A2E70726F706572746965732E636F6E7461696E734B657928276C6173744D657465724461746127292026262070726F706572746965734F626A2E70726F706572746965732E636F6E7461696E734B657928276D6574657244617461272929207B5C6E2020202020202020202020206C65742073686F727466616C6C44617461203D20466C6F61742E7061727365466C6F61742870726F706572746965734F626A2E70726F706572746965732E6D657465724461746129202D20466C6F61742E7061727365466C6F61742870726F706572746965734F626A2E70726F706572746965732E6C6173744D6574657244617461295C6E20202020202020202020202070726F706572746965734F626A2E70726F706572746965732E73686F727466616C6C44617461203D20466C6F61742E7061727365466C6F61742873686F727466616C6C446174612E746F4669786564283229295C6E2020202020202020202020206966202870726F706572746965734F626A2E70726F706572746965732E73686F727466616C6C44617461203C203029207B5C6E202020202020202020202020202020206C65742073686F727466616C6C416C61726D4F626A203D207B5C6E20202020202020202020202020202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020202020202020202020202020205C226576656E745C223A205C225C222C5C6E20202020202020202020202020202020202020205C22646174615C223A207B5C6E2020202020202020202020202020202020202020202020205C226C6576656C5C223A205C227761726E5C225C6E20202020202020202020202020202020202020207D5C6E202020202020202020202020202020207D5C6E2020202020202020202020202020202073686F727466616C6C416C61726D4F626A2E6576656E74203D202773686F727466616C6C416C61726D275C6E2020202020202020202020202020202073686F727466616C6C416C61726D4F626A2E646174612E73686F727466616C6C416C61726D203D2027E8AFBBE695B0E58092E6B581E5918AE8ADA6275C6E2020202020202020202020202020202073686F727466616C6C416C61726D4F626A2E646174612E73686F727466616C6C44617461203D2070726F706572746965734F626A2E70726F706572746965732E73686F727466616C6C446174615C6E20202020202020202020202020202020726573756C742E707573682873686F727466616C6C416C61726D4F626A295C6E2020202020202020202020207D5C6E20202020202020207D5C6E20202020202020202F2F435351E695B0E68DAEEFBC8C4943434944E695B0E68DAE5C6E20202020202020202F2A2A5C6E2020202020202020202A20727270632C6765746373712C32325C6E2020202020202020202A20727270632C67657469636369642C38393836303441383130323434303132303135325C6E2020202020202020202A205C6E2020202020202020202A2F5C6E2020202020202020696620287061796C6F61642E6C656E6774682829203E3D2038202626207061796C6F61642E737562737472696E6728302C2038292E657175616C7328273732373237303633272929207B5C6E2020202020202020202020206C6574207244617461203D2068657832737472287061796C6F6164295C6E2020202020202020202020206C657420724172726179203D2072446174612E73706C697428272C27295C6E2020202020202020202020202F2F435351E695B0E68DAE5C6E202020202020202020202020696620287241727261795B305D2E657175616C732827727270632729202626207241727261795B315D2E657175616C732827676574637371272929207B5C6E2020202020202020202020202020202070726F706572746965734F626A2E70726F706572746965732E637371203D207241727261795B325D5C6E2020202020202020202020202020202070726F706572746965734F626A2E70726F706572746965732E7369676E616C4C6576656C203D206373712870726F706572746965734F626A2E70726F706572746965732E637371295C6E2020202020202020202020207D5C6E202020202020202020202020696620287241727261795B305D2E657175616C732827727270632729202626207241727261795B315D2E657175616C7328276765746963636964272929207B5C6E2020202020202020202020202020202070726F706572746965734F626A2E70726F706572746965732E6963636964203D207241727261795B325D5C6E2020202020202020202020207D5C6E5C6E20202020202020207D5C6E202020207D5C6E202020202F2A2F2FE5BC80E5A78BE5AD97E88A825C6E20202020696620287061796C6F61642E737562737472696E6728302C20322920213D202736382729207B5C6E202020205C7472657475726E206A736F6E546F53747228726573756C74295C6E202020207D2A2F5C6E5C6E5C6E2020202072657475726E206A736F6E546F53747228726573756C74295C6E7D5C6E2F2FE4B88BE8A18CE5A484E790865C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E20202020766172206F626A203D20746F4A736F6E287061796C6F6164295C6E202020202F2FE8AFBBE58F96E794B5E8A1A8E794A8E9878F5C6E20202020696620286F626A2E66756E6374696F6E203D3D20276D65746572526561642729207B5C6E2020202020202020696620286F626A2E646174612E6D657465724E6F2E6C656E6774682829203D3D20313229207B5C6E2020202020202020202020206C657420737472203D2027363827202B2072657665727365537472286F626A2E646174612E6D657465724E6F29202B20273638313130343333333543343333275C6E2020202020202020202020206C657420636865636B203D20636865636B5F435328737472295C6E202020202020202020202020737472203D20737472202B20636865636B202B20273136275C6E20202020202020202020202072657475726E207374722E746F4C6F7765724361736528295C6E20202020202020207D5C6E202020207D5C6E202020202F2FE5BC80E585B3E997B85C6E20202020696620286F626A2E66756E6374696F6E203D3D20277377697463684F6E4F66662729207B5C6E2020202020202020696620286F626A2E646174612E6D657465724E6F2E6C656E6774682829203D3D20313229207B5C6E2020202020202020202020202F2F31E4B8BAE59088E997B85C6E2020202020202020202020206C6574207377697463684F6E203D206F626A2E646174612E76616C76654F6E203D3D20273127203F2027344527203A20273444275C6E2020202020202020202020206C657420737472203D2027363827202B2072657665727365537472286F626A2E646174612E6D657465724E6F29202B20273638314331303335333333333333333438393637343527202B207377697463684F6E202B20273333343737373342334134343343275C6E2020202020202020202020206C657420636865636B203D20636865636B5F435328737472295C6E202020202020202020202020737472203D20737472202B20636865636B202B20273136275C6E20202020202020202020202072657475726E207374722E746F4C6F7765724361736528295C6E20202020202020207D5C6E202020207D5C6E202020202F2FE6B885E8A1A828E6B885E999A4E68980E69C89EFBC8CE4BD99E9A29DE38081E680BBE794B5E9878FE38081E8B4ADE794B5E6ACA1E695B0E38081E794B5E4BBB7E585A8E983A8E5889DE5A78BE58C96295C6E20202020696620286F626A2E66756E6374696F6E203D3D202772657365742729207B5C6E2020202020202020696620286F626A2E646174612E6D657465724E6F2E6C656E6774682829203D3D20313229207B5C6E2020202020202020202020206C657420737472203D2027363827202B2072657665727365537472286F626A2E646174612E6D657465724E6F29202B2027363831413039333533333333333333343839363734353335275C6E2020202020202020202020206C657420636865636B203D20636865636B5F435328737472295C6E202020202020202020202020737472203D20737472202B20636865636B202B20273136275C6E20202020202020202020202072657475726E207374722E746F4C6F7765724361736528295C6E20202020202020207D5C6E202020207D5C6E202020202F2FE8B685E99990E580BCE8AEBEE7BDAE5C6E20202020696620286F626A2E66756E6374696F6E203D3D20277365744F7665726C6F61644C696D69742729207B5C6E2020202020202020696620286F626A2E646174612E6D657465724E6F2E6C656E6774682829203D3D20313229207B5C6E2020202020202020202020206C657420737472203D2027363827202B2072657665727365537472286F626A2E646174612E6D657465724E6F29202B202736383034323033363333333333333335333333333333373334303336333333333333333333333733343033363333275C6E2020202020202020202020202F2FE794B5E58E8BE99990E580BCE5A484E790865C6E202020202020202020202020696620286F626A2E646174612E636F6E7461696E734B657928276F766572566F6C746167654C696D69742729202626206F626A2E646174612E6F766572566F6C746167654C696D697420213D204E554C4C29207B5C6E2020202020202020202020202020202069662028466C6F61742E76616C75654F66286F626A2E646174612E6F766572566F6C746167654C696D697429203E203232302E3020262620466C6F61742E76616C75654F66286F626A2E646174612E6F766572566F6C746167654C696D697429203C3D203236302E3029207B5C6E20202020202020202020202020202020202020206C6574206876203D20537472696E672E666F726D6174282725303858272C20496E74656765722E76616C75654F662828466C6F61742E76616C75654F66286F626A2E646174612E6F766572566F6C746167654C696D697429202A203130292E746F466978656428302929295C6E2020202020202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328687629295C6E202020202020202020202020202020207D5C6E202020202020202020202020202020202F2FE5A682E69E9CE794B5E58E8BE99990E580BCE4B88DE59CA8E88C83E59BB4E58685EFBC8CE8A1A5E9BB98E8AEA4E99990E580BC3236302E30565C6E20202020202020202020202020202020656C7365207B5C6E2020202020202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328537472696E672E666F726D6174282725303858272C20323630302929295C6E202020202020202020202020202020207D5C6E2020202020202020202020207D5C6E2020202020202020202020202F2FE5A682E69E9CE4B88DE5A1ABE58699E794B5E58E8BE99990E580BCEFBC8CE9BB98E8AEA4323630305C6E202020202020202020202020656C7365207B5C6E20202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328537472696E672E666F726D6174282725303858272C20323630302929295C6E2020202020202020202020207D5C6E2020202020202020202020202F2FE794B5E6B581E99990E580BCE5A484E790865C6E202020202020202020202020696620286F626A2E646174612E636F6E7461696E734B657928276F7665724375727272656E744C696D69742729202626206F626A2E646174612E6F7665724375727272656E744C696D697420213D204E554C4C29207B5C6E2020202020202020202020202020202069662028466C6F61742E76616C75654F66286F626A2E646174612E6F7665724375727272656E744C696D697429203E20302E30303020262620466C6F61742E76616C75654F66286F626A2E646174612E6F7665724375727272656E744C696D697429203C2038302E30303029207B5C6E20202020202020202020202020202020202020206C6574206849203D20537472696E672E666F726D6174282725303858272C20496E74656765722E76616C75654F662828466C6F61742E76616C75654F66286F626A2E646174612E6F7665724375727272656E744C696D697429202A2031303030292E746F466978656428302929295C6E2020202020202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328684929295C6E202020202020202020202020202020207D5C6E202020202020202020202020202020202F2FE5A682E69E9CE794B5E6B581E99990E580BCE4B88DE59CA8E88C83E59BB4E58685EFBC8CE8A1A5E9BB98E8AEA4E99990E580BC3830415C6E20202020202020202020202020202020656C7365207B5C6E2020202020202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328537472696E672E666F726D6174282725303858272C2038303030302929295C6E202020202020202020202020202020207D5C6E2020202020202020202020207D5C6E2020202020202020202020202F2FE5A682E69E9CE4B88DE5A1ABE58699E794B5E6B581E99990E580BCEFBC8CE9BB98E8AEA438303030305C6E202020202020202020202020656C7365207B5C6E20202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328537472696E672E666F726D6174282725303858272C2038303030302929295C6E2020202020202020202020207D5C6E2020202020202020202020202F2FE58AA0E5808DE78E87EFBC8CE9BB98E8AEA4315C6E202020202020202020202020737472202B3D20273334333333333333275C6E2020202020202020202020206C657420636865636B203D20636865636B5F435328737472295C6E202020202020202020202020737472203D20737472202B20636865636B202B20273136275C6E20202020202020202020202072657475726E207374722E746F4C6F7765724361736528295C6E20202020202020207D5C6E202020207D5C6E202020202F2FE594AEE794B55C6E20202020696620286F626A2E66756E6374696F6E203D3D202772656368617267652729207B5C6E2020202020202020696620286F626A2E646174612E6D657465724E6F2E6C656E6774682829203D3D20313229207B5C6E2020202020202020202020206C657420737472203D2027363827202B2072657665727365537472286F626A2E646174612E6D657465724E6F29202B202736383034313033343333333333333335333333333333275C6E202020202020202020202020737472202B3D20726576657273655374722864617461706C757328537472696E672E666F726D6174282725303858272C2028496E74656765722E76616C75654F66286F626A2E646174612E7265636861726765446174612929202A203130302929295C6E5C6E202020202020202020202020696620286F626A2E646174612E636F6E7461696E734B657928277265636861726765436F756E74272929207B5C6E20202020202020202020202020202020737472202B3D20726576657273655374722864617461706C757328537472696E672E666F726D6174282725303858272C206F626A2E646174612E7265636861726765436F756E742929295C6E2020202020202020202020207D20656C7365207B5C6E20202020202020202020202020202020737472202B3D20273334333333333333275C6E2020202020202020202020207D5C6E5C6E2020202020202020202020206C657420636865636B203D20636865636B5F435328737472295C6E202020202020202020202020737472203D20737472202B20636865636B202B20273136275C6E20202020202020202020202072657475726E207374722E746F4C6F7765724361736528295C6E20202020202020207D5C6E202020207D5C6E202020202F2FE6B58BE8AF955C6E20202020696620286F626A2E66756E6374696F6E203D3D205C22636D645C2229207B5C6E20202020202020206C657420636D6444617461203D206F626A2E646174612E636D64446174615C6E202020202020202072657475726E20636D64446174615C6E202020207D5C6E202020202F2FE8AFBBE58F96E4BFA1E58FB7E5BCBAE5BAA64353515C6E20202020696620286F626A2E66756E6374696F6E203D3D205C226765744353515C2229207B5C6E20202020202020206C657420636D6444617461203D202737323732373036333243363736353734363337333731275C6E202020202020202072657475726E20636D64446174615C6E202020207D5C6E202020202F2FE8AFBBE58F96E789A9E88194E7BD91E58DA149434349445C6E20202020696620286F626A2E66756E6374696F6E203D3D205C2267657449434349445C2229207B5C6E20202020202020206C657420636D6444617461203D20273732373237303633324336373635373436393633363336393634275C6E202020202020202072657475726E20636D64446174615C6E202020207D5C6E7D5C6E5C6E2F2F2072657475726E207072654465636F64652827363839333030323230353235323036383931323533333335633433333638333333333333333333333333333334623536333333336337363733333333623762353333333333333333333333333333333333333333333333333333333334656430313627295C6E2F2F2072657475726E206465636F64652827363831323131313131313131313136383943304433333334333333333333336333333333333433333333333334656373313627295C6E2F2F72657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C2272656368617267655C222C5C22646174615C223A7B5C226D657465724E6F5C223A5C223230323431313139303234375C222C5C227265636861726765446174615C223A31303030302C5C227265636861726765436F756E745C223A327D7D27295C6E72657475726E206465636F64652827363834373032313931313234323036383931323533333335633433333333333333333333333333333334333337613535333333333333333333333333333333333333333333343333333333333333333333333333333338333333333334653035313627295C6E2F2F72657475726E206465636F646528277B5C227061796C6F61645C223A5C223230323431313139303236315C227D27295C6E2F2F72657475726E2027E58D8EE695B0E789A9E88194E7BD91E59FBAE59CB0EFBC88496F742057617375EFBC89275C6E5C6E2F2FE5B8A6E5BDB1E5AD90E695B0E68DAE5C6E2F2F72657475726E206465636F646528277B5C22736861646F775C223A7B5C226D657461646174615C223A7B5C22646573697265645C223A7B7D2C5C227265706F727465645C223A7B5C227075726368617365735C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C226F70656E53746174655C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C226D6574657250726963655C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C22766F6C746167655C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C2272656D61696E696E67416D6F756E745C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C2263757272656E745C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C227761726E696E67506F7765725C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C226D657465724E6F5C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C22706F7765725C223A7B5C2274696D657374616D705C223A313734373732363437357D2C5C226F726967696E616C4D6573736167655C223A7B5C2274696D657374616D705C223A313734373732363732337D2C5C226D65746572446174615C223A7B5C2274696D657374616D705C223A313734373732363437357D7D7D2C5C2276657273696F6E5C223A3331352C5C2273746174655C223A7B5C22646573697265645C223A7B7D2C5C227265706F727465645C223A7B5C227075726368617365735C223A31352C5C226F70656E53746174655C223A5C22315C222C5C226D6574657250726963655C223A5C22312E303030305C222C5C22766F6C746167655C223A5C223232372E395C222C5C2272656D61696E696E67416D6F756E745C223A5C223139393939392E30345C222C5C2263757272656E745C223A5C22302E3030305C222C5C227761726E696E67506F7765725C223A5C22302E30305C222C5C226D657465724E6F5C223A5C223230323431313139303234375C222C5C22706F7765725C223A5C22302E305C222C5C226F726967696E616C4D6573736167655C223A5C22373237323730363332433637363537343633373337315C222C5C226D65746572446174615C223A5C22312E305C227D7D2C5C2274696D657374616D705C223A313734373732363732337D2C5C2270726F647563744B65795C223A5C223637633639663261366230303031613962323261366339615C222C5C2264657669636549645C223A5C223230323431313139303234375C222C5C22696F7449645C223A5C2236333333373235336439333634346263616431353133636133313861353230615C222C5C227061796C6F61645C223A5C2236383437303231393131323432303638393132353333333563343333333333333333333333333333333433333761353533333333333333333333333333333333333333333334333333333333333333333333333333333833333333333465303531365C227D27295C6E5C6E2F2F435351E695B0E68DAE5C6E72657475726E206465636F646528273732373237303633324336373635373436333733373132433332333227295C6E5C6E2F2F4943434944E695B0E68DAE5C6E2F2F2072657475726E206465636F646528277B5C227061796C6F61645C223A5C2237323732373036333243363736353734363936333633363936343243333833393338333633303334343133383331333033323334333433303331333233303331333533325C227D2729222C0A20202267726F7570496422203A20226634626562303864633766653431353762396636336132326130653161343330222C0A2020226E616D6522203A2022E58D95E79BB8E5AFBCE8BDA858412D47585344382D3447222C0A20202263726561746554696D6522203A20313735323830353030383534392C0A20202275706461746554696D6522203A20313735343738373330393039302C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A2022696F74222C0A202022757064617465427922203A2022696F74222C0A2020227061746822203A202261626378617373786E616E6168736D616E222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E8BF99E4B8AAE698AFE4B880E4B8AAE794B5E8A1A8222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'iot'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('6ead3277d72e4740bbe5e2a29c2672f4', 1753764243959, NULL, 'api', '小蘑菇插座', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223665616433323737643732653437343062626535653261323963323637326634222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E72657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E2F2F2072657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5B08FE89891E88F87E68F92E5BAA7222C0A20202263726561746554696D6522203A20313735333638323439333839382C0A20202275706461746554696D6522203A20313735333736343234333934382C0A2020226C6F636B22203A202230222C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363838333161316633386663313036613864616333633133222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20225B5C6E202020207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020205C22706F77657253746174655C223A205C2230315C222C5C6E2020202020202020202020205C2263757272656E745C223A205C2231352E30305C222C5C6E2020202020202020202020205C22766F6C746167655C223A205C223232382E385C222C5C6E2020202020202020202020205C22706F7765725C223A205C22313230305C225C6E20202020202020207D2C5C6E20202020202020205C2264657669636549645C223A205C22303030315C222C5C6E20202020202020205C22746370456E636F64655C223A205C224845585C222C5C6E20202020202020205C227265706C795061796C6F61645C223A205C22373237323730363332433637363537343633373337315C225C6E202020207D5C6E5D222C0A2020226465736372697074696F6E22203A20224149E887AAE58AA8E7949FE68890E79A84E8A7A3E69E90E5928CE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('6ead3277d72e4740bbe5e2a29c2672f4', 1753764264195, NULL, 'api', '小蘑菇插座', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223665616433323737643732653437343062626535653261323963323637326634222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E72657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5B08FE89891E88F87E68F92E5BAA7222C0A20202263726561746554696D6522203A20313735333638323439333839382C0A20202275706461746554696D6522203A20313735333736343236343138302C0A2020226C6F636B22203A202230222C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363838333161316633386663313036613864616333633133222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C2264657669636549645C223A205C2230303031303130375C225C6E7D222C0A2020226465736372697074696F6E22203A20224149E887AAE58AA8E7949FE68890E79A84E8A7A3E69E90E5928CE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('6ead3277d72e4740bbe5e2a29c2672f4', 1755239952196, NULL, 'api', '小蘑菇插座', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223665616433323737643732653437343062626535653261323963323637326634222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E72657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5B08FE89891E88F87E68F92E5BAA7222C0A20202263726561746554696D6522203A20313735333638323439333839382C0A20202275706461746554696D6522203A20313735353233393935323137382C0A2020226C6F636B22203A202230222C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363838333161316633386663313036613864616333633133222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20227B5C6E202020205C2264657669636549645C223A205C2230303031303130375C225C6E7D222C0A2020226465736372697074696F6E22203A20224149E887AAE58AA8E7949FE68890E79A84E8A7A3E69E90E5928CE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('7a98d2939f3344ab8641d4d667c2c262', 1755090411542, NULL, 'api-group', '123', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20223761393864323933396633333434616238363431643464363637633263323632222C0A2020226E616D6522203A2022313233222C0A2020227479706522203A2022617069222C0A202022706172656E74496422203A202230222C0A2020227061746822203A20223132222C0A20202263726561746554696D6522203A20313735353039303431313531322C0A20202275706461746554696D6522203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A206E756C6C2C0A202022706174687322203A205B205D2C0A2020226F7074696F6E7322203A205B205D0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('b5723ec5a8e24bdaa647e60a1dc82638', 1754039119888, NULL, 'api', '小蘑菇插座', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226235373233656335613865323462646161363437653630613164633832363338222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E72657475726E206465636F64652827413541353030303130313038303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20226634626562303864633766653431353762396636336132326130653161343330222C0A2020226E616D6522203A2022E5B08FE89891E88F87E68F92E5BAA7222C0A20202263726561746554696D6522203A20313735333432333334343432382C0A20202275706461746554696D6522203A20313735343033393131393836342C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A2022696F74222C0A202022757064617465427922203A2022696F74222C0A2020227061746822203A2022363838333161316633386663313036613864616333633133222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20225B5C6E202020207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020205C22706F77657253746174655C223A205C2230315C222C5C6E2020202020202020202020205C2263757272656E745C223A205C2231352E30305C222C5C6E2020202020202020202020205C22766F6C746167655C223A205C223232382E385C222C5C6E2020202020202020202020205C22706F7765725C223A205C22313230305C225C6E20202020202020207D2C5C6E20202020202020205C2264657669636549645C223A205C22303030315C222C5C6E20202020202020205C22746370456E636F64655C223A205C224845585C222C5C6E20202020202020205C227265706C795061796C6F61645C223A205C22373237323730363332433637363537343633373337315C225C6E202020207D5C6E5D222C0A2020226465736372697074696F6E22203A20224149E887AAE58AA8E7949FE68890E79A84E8A7A3E69E90E5928CE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'iot'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('b5723ec5a8e24bdaa647e60a1dc82638', 1754039125577, NULL, 'api', '小蘑菇插座', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226235373233656335613865323462646161363437653630613164633832363338222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E72657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20226634626562303864633766653431353762396636336132326130653161343330222C0A2020226E616D6522203A2022E5B08FE89891E88F87E68F92E5BAA7222C0A20202263726561746554696D6522203A20313735333432333334343432382C0A20202275706461746554696D6522203A20313735343033393132353535302C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A2022696F74222C0A202022757064617465427922203A2022696F74222C0A2020227061746822203A2022363838333161316633386663313036613864616333633133222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20225B5D222C0A2020226465736372697074696F6E22203A20224149E887AAE58AA8E7949FE68890E79A84E8A7A3E69E90E5928CE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'iot'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('b5723ec5a8e24bdaa647e60a1dc82638', 1754039132600, NULL, 'api', '小蘑菇插座', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226235373233656335613865323462646161363437653630613164633832363338222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E72657475726E206465636F64652827413541353030303130313038303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20226634626562303864633766653431353762396636336132326130653161343330222C0A2020226E616D6522203A2022E5B08FE89891E88F87E68F92E5BAA7222C0A20202263726561746554696D6522203A20313735333432333334343432382C0A20202275706461746554696D6522203A20313735343033393133323537352C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A2022696F74222C0A202022757064617465427922203A2022696F74222C0A2020227061746822203A2022363838333161316633386663313036613864616333633133222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20225B5C6E202020207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020205C22706F77657253746174655C223A205C2230315C222C5C6E2020202020202020202020205C2263757272656E745C223A205C2231352E30305C222C5C6E2020202020202020202020205C22766F6C746167655C223A205C223232382E385C222C5C6E2020202020202020202020205C22706F7765725C223A205C22313230305C225C6E20202020202020207D2C5C6E20202020202020205C2264657669636549645C223A205C22303030315C222C5C6E20202020202020205C22746370456E636F64655C223A205C224845585C222C5C6E20202020202020205C227265706C795061796C6F61645C223A205C22373237323730363332433637363537343633373337315C225C6E202020207D5C6E5D222C0A2020226465736372697074696F6E22203A20224149E887AAE58AA8E7949FE68890E79A84E8A7A3E69E90E5928CE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'iot'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('b5723ec5a8e24bdaa647e60a1dc82638', 1754039136271, NULL, 'api', '小蘑菇插座', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226235373233656335613865323462646161363437653630613164633832363338222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E72657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20226634626562303864633766653431353762396636336132326130653161343330222C0A2020226E616D6522203A2022E5B08FE89891E88F87E68F92E5BAA7222C0A20202263726561746554696D6522203A20313735333432333334343432382C0A20202275706461746554696D6522203A20313735343033393133363235302C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A2022696F74222C0A202022757064617465427922203A2022696F74222C0A2020227061746822203A2022363838333161316633386663313036613864616333633133222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20225B5D222C0A2020226465736372697074696F6E22203A20224149E887AAE58AA8E7949FE68890E79A84E8A7A3E69E90E5928CE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'iot'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('b5723ec5a8e24bdaa647e60a1dc82638', 1754039158106, NULL, 'api', '小蘑菇插座', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226235373233656335613865323462646161363437653630613164633832363338222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E72657475726E206465636F6465282741354135303030313031303837303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20226634626562303864633766653431353762396636336132326130653161343330222C0A2020226E616D6522203A2022E5B08FE89891E88F87E68F92E5BAA7222C0A20202263726561746554696D6522203A20313735333432333334343432382C0A20202275706461746554696D6522203A20313735343033393135383038312C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A2022696F74222C0A202022757064617465427922203A2022696F74222C0A2020227061746822203A2022363838333161316633386663313036613864616333633133222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20225B5C6E202020207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020205C22706F77657253746174655C223A205C2230315C222C5C6E2020202020202020202020205C2263757272656E745C223A205C2231352E30305C222C5C6E2020202020202020202020205C22766F6C746167655C223A205C223232382E385C222C5C6E2020202020202020202020205C22706F7765725C223A205C22313230305C225C6E20202020202020207D2C5C6E20202020202020205C2264657669636549645C223A205C22303030315C222C5C6E20202020202020205C22746370456E636F64655C223A205C224845585C222C5C6E20202020202020205C227265706C795061796C6F61645C223A205C22373237323730363332433637363537343633373337315C225C6E202020207D5C6E5D222C0A2020226465736372697074696F6E22203A20224149E887AAE58AA8E7949FE68890E79A84E8A7A3E69E90E5928CE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'iot'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('b5723ec5a8e24bdaa647e60a1dc82638', 1754039162333, NULL, 'api', '小蘑菇插座', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226235373233656335613865323462646161363437653630613164633832363338222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E72657475726E206465636F64652827413541353030303130313037303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20226634626562303864633766653431353762396636336132326130653161343330222C0A2020226E616D6522203A2022E5B08FE89891E88F87E68F92E5BAA7222C0A20202263726561746554696D6522203A20313735333432333334343432382C0A20202275706461746554696D6522203A20313735343033393136323330372C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A2022696F74222C0A202022757064617465427922203A2022696F74222C0A2020227061746822203A2022363838333161316633386663313036613864616333633133222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20225B5D222C0A2020226465736372697074696F6E22203A20224149E887AAE58AA8E7949FE68890E79A84E8A7A3E69E90E5928CE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'iot'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('b5723ec5a8e24bdaa647e60a1dc82638', 1754039182069, NULL, 'api', '小蘑菇插座', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226235373233656335613865323462646161363437653630613164633832363338222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E72657475726E206465636F64652827413541353030303130313038303130354443303846303034423037413541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20226634626562303864633766653431353762396636336132326130653161343330222C0A2020226E616D6522203A2022E5B08FE89891E88F87E68F92E5BAA7222C0A20202263726561746554696D6522203A20313735333432333334343432382C0A20202275706461746554696D6522203A20313735343033393138323034362C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A2022696F74222C0A202022757064617465427922203A2022696F74222C0A2020227061746822203A2022363838333161316633386663313036613864616333633133222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20225B5C6E202020207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B5C6E2020202020202020202020205C22706F77657253746174655C223A205C2230315C222C5C6E2020202020202020202020205C2263757272656E745C223A205C2231352E30305C222C5C6E2020202020202020202020205C22766F6C746167655C223A205C223232382E385C222C5C6E2020202020202020202020205C22706F7765725C223A205C22313230305C225C6E20202020202020207D2C5C6E20202020202020205C2264657669636549645C223A205C22303030315C222C5C6E20202020202020205C22746370456E636F64655C223A205C224845585C222C5C6E20202020202020205C227265706C795061796C6F61645C223A205C22373237323730363332433637363537343633373337315C225C6E202020207D5C6E5D222C0A2020226465736372697074696F6E22203A20224149E887AAE58AA8E7949FE68890E79A84E8A7A3E69E90E5928CE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'iot'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('b5723ec5a8e24bdaa647e60a1dc82638', 1754039332364, NULL, 'api', '小蘑菇插座', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226235373233656335613865323462646161363437653630613164633832363338222C0A20202273637269707422203A2022696D706F7274206C6F673B5C6E5C6E2F2F203136E8BF9BE588B6E8BDAC3130E8BF9BE588B6E695B0E5AD975C6E76617220686578546F496E74203D20686578203D3E207B5C6E202072657475726E20496E74656765722E7061727365496E74286865782C203136293B5C6E7D3B5C6E2F2F20E9A284E8A7A3E7A081EFBC8CE8A7A3E69E90E4B88AE68AA5E79A84E8AEBEE5A487E7BC96E58FB72064657669636549645C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D207B7D5C6E20202020766172207061796C6F6164486578203D207061796C6F61642E746F5570706572436173652829202F2F20E8BDACE4B8BAE5A4A7E586995C6E202020202F2F20E6A0A1E9AA8CE68AA5E69687E995BFE5BAA6E5928CE5B8A7E5A4B45C6E20202020696620287061796C6F61644865782E6C656E6774682829203E3D203138202626207061796C6F61644865782E737562737472696E6728302C203429203D3D3D205C22413541355C2229207B5C6E20202020202020202F2F20E4BB8EE7ACAC35E4BD8DE5BC80E5A78BE58F9634E5AD97E88A82EFBC8838E4B8AAE5AD97E7ACA6EFBC89E4BD9CE4B8BA2064657669636549645C6E2020202020202020726573756C742E6465766963654964203D207061796C6F61644865782E737562737472696E6728342C203132295C6E202020207D5C6E2020202072657475726E20726573756C745C6E7D5C6E5C6E766172206465636F6465203D287061796C6F61642C636F6E7465787429203D3E207B5C6E20206C6F672E696E666F285C22E8BF9BE585A5E7BC96E8A7A3E7A0812C7061796C6F61643D7B7D2C636F6E746578743D7B7D5C222C7061796C6F61642C636F6E74657874295C6E202076617220726573756C74203D205B5D3B5C6E2020766172207061796C6F6164486578203D7061796C6F61642E746F55707065724361736528293B5C6E20207661722070726F706572746965734F626A203D207B205C226D657373616765547970655C223A205C2250524F504552544945535C222C205C2270726F706572746965735C223A207B7D207D3B5C6E2020766172206576656E744F626A203D207B205C226D657373616765547970655C223A205C224556454E545C222C205C226576656E745C223A205C225C222C205C22646174615C223A207B7D207D3B5C6E5C6E20202F2F20E9AA8CE8AF81E5B8A7E5A4B4E5B8A7E5B0BE5C6E2020696620287061796C6F61644865782E737562737472696E6728302C20342920213D205C22413541355C22207C7C207061796C6F61644865782E737562737472696E67287061796C6F61644865782E6C656E6774682829202D20342920213D205C22354135415C2229207B5C6E2020202072657475726E206A736F6E546F537472285B7B205C226572726F725C223A205C22E697A0E69588E5B8A7E6A0BCE5BC8F5C22207D5D293B5C6E20207D5C6E5C6E20202F2F20E68F90E58F96E6A0B8E5BF83E5AD97E6AEB55C6E2020766172206465766963654964203D207061796C6F61644865782E737562737472696E6728342C2038293B202F2F20E8AEBEE5A4874944EFBC8834E5AD97E88A82EFBC895C6E202076617220636D64436F6465203D207061796C6F61644865782E737562737472696E6728382C203130293B202F2F20E591BDE4BBA4E7A081EFBC8831E5AD97E88A82EFBC895C6E202076617220646174614C656E203D20686578546F496E74287061796C6F61644865782E737562737472696E672831302C20313229293B202F2F20E695B0E68DAEE995BFE5BAA65C6E20207661722064617461486578203D207061796C6F61644865782E737562737472696E672831322C203132202B20646174614C656E202A2032293B202F2F20E695B0E68DAEE58CBA5C6E202076617220636865636B73756D203D207061796C6F61644865782E737562737472696E67283132202B20646174614C656E202A20322C203134202B20646174614C656E202A2032293B202F2F20E6A0A1E9AA8CE5928C5C6E5C6E20202F2F20E8AEBEE5A4874944E5AD98E585A5E68980E69C89E5AFB9E8B1A15C6E202070726F706572746965734F626A2E6465766963654964203D2064657669636549643B5C6E20206576656E744F626A2E6465766963654964203D2064657669636549643B5C6E20202F2FE9AA8CE8AF81E696B0E58685E5AEB95C6E202070726F706572746965734F626A2E636F6E746578743D636F6E746578743B5C6E202070726F706572746965734F626A2E746370456E636F6465203D2027484558273B5C6E202070726F706572746965734F626A2E7265706C795061796C6F6164203D202737323732373036333243363736353734363337333731275C6E20202F2F20312E20E5B19EE680A7E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813031EFBC895C6E202069662028636D64436F6465203D3D205C2230315C2220262620646174614C656E203D3D203729207B5C6E202020202F2F20E8A7A3E69E90E5BC80E585B3E78AB6E68081EFBC8830313DE5BC80EFBC8C30303DE585B3EFBC895C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F7765725374617465203D20646174614865782E737562737472696E6728302C203229203D3D205C2230315C22203F205C2230315C22203A205C2230305C223B5C6E202020202F2F20E8A7A3E69E90E794B5E6B581EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E303141EFBC895C6E202020207661722063757272656E74486578203D20646174614865782E737562737472696E6728322C2036293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E63757272656E74203D2028686578546F496E742863757272656E7448657829202A20302E3031292E746F46697865642832293B5C6E202020202F2F20E8A7A3E69E90E794B5E58E8BEFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D302E3156EFBC895C6E2020202076617220766F6C74616765486578203D20646174614865782E737562737472696E6728362C203130293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E766F6C74616765203D2028686578546F496E7428766F6C7461676548657829202A20302E31292E746F46697865642831293B5C6E202020202F2F20E8A7A3E69E90E58A9FE78E87EFBC8832E5AD97E88A82EFBC8CE58D95E4BD8D3157EFBC895C6E2020202076617220706F776572486578203D20646174614865782E737562737472696E672831302C203134293B5C6E2020202070726F706572746965734F626A2E70726F706572746965732E706F776572203D20686578546F496E7428706F776572486578292E746F537472696E6728293B5C6E202020202F2F20E58AA0E585A5E7BB93E69E9CE99B865C6E20202020726573756C742E707573682870726F706572746965734F626A293B5C6E20207D5C6E5C6E20202F2F20322E20E4BA8BE4BBB6E4B88AE68AA5EFBC88E591BDE4BBA4E7A0813032EFBC895C6E202069662028636D64436F6465203D3D205C2230325C2220262620646174614C656E203E3D203329207B5C6E202020202F2F20E4BA8BE4BBB6E7B1BBE59E8BEFBC8830313DE8BF87E8BDBDEFBC8C30323DE8BF87E58E8BEFBC8C30333DE4B88AE7BABFEFBC8C30343DE4B88BE7BABFEFBC895C6E20202020766172206576656E7454797065203D20646174614865782E737562737472696E6728302C2032293B5C6E20202020696620286576656E7454797065203D3D205C2230315C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F7665726C6F61645C223B5C6E2020202020206576656E744F626A2E646174612E6F7665726C6F6164203D20686578546F496E7428646174614865782E737562737472696E6728322C20362929202B205C22575C223B202F2F20E8BF87E8BDBDE58A9FE78E875C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230335C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F6E6C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F6E6C696E65203D205C22E8AEBEE5A487E4B88AE7BABF5C223B5C6E202020207D5C6E20202020696620286576656E7454797065203D3D205C2230345C2229207B5C6E2020202020206576656E744F626A2E6576656E74203D205C226F66666C696E655C223B5C6E2020202020206576656E744F626A2E646174612E6F66666C696E65203D205C22E8AEBEE5A487E4B88BE7BABF5C223B5C6E202020207D5C6E20202020726573756C742E70757368286576656E744F626A293B5C6E20207D5C6E5C6E20202F2F20332E20E5938DE5BA94E68AA5E69687EFBC88E591BDE4BBA4E7A0813034EFBC895C6E202069662028636D64436F6465203D3D205C2230345C2220262620646174614C656E203D3D203129207B5C6E20202020766172207265706C794F626A203D207B5C6E2020202020205C226D657373616765547970655C223A205C225245504C595C222C5C6E2020202020205C2266756E6374696F6E5C223A205C227377697463685C222C5C6E2020202020205C22646174615C223A207B205C22726573756C745C223A2064617461486578203D3D205C2230305C22203F205C22737563636573735C22203A205C226661696C5C22207D5C6E202020207D3B5C6E202020207265706C794F626A2E6465766963654964203D2064657669636549643B5C6E20202020726573756C742E70757368287265706C794F626A293B5C6E20207D5C6E5C6E202072657475726E206A736F6E546F53747228726573756C74293B5C6E7D3B5C6E5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020766172206F626A203D20746F4A736F6E287061796C6F6164293B5C6E2020766172206465766963654964203D206F626A2E646174612E64657669636549643B202F2F20E8AEBEE5A4874944EFBC8834E4BD8D3136E8BF9BE588B6EFBC8CE5A6825C22303030315C22EFBC895C6E20207661722066756E63203D206F626A2E66756E6374696F6E3B5C6E202076617220706172616D73203D206F626A2E646174613B5C6E5C6E20202F2F20E68EA7E588B6E5BC80E585B3EFBC8866756E6374696F6E3D737769746368EFBC895C6E20206966202866756E63203D3D205C227377697463685C2229207B5C6E20202020766172207374617465203D20706172616D732E73746174653B202F2F20E79BAEE6A087E78AB6E68081EFBC9A30313DE5BC80EFBC8C30303DE585B35C6E2020202069662028737461746520213D205C2230315C2220262620737461746520213D205C2230305C2229207B5C6E20202020202072657475726E205C225C223B202F2F20E697A0E69588E78AB6E68081EFBC8CE8BF94E59B9EE7A9BA5C6E202020207D5C6E5C6E202020202F2F20E695B0E68DAEE58CBAEFBC9AE68EA7E588B6E7B1BBE59E8B283031292BE79BAEE6A087E78AB6E680815C6E202020207661722064617461486578203D205C2230315C22202B2073746174653B5C6E2020202076617220646174614C656E203D20646174614865782E6C656E6774682829202F20323B202F2F20E695B0E68DAEE995BFE5BAA6EFBC88E5AD97E88A82E695B0EFBC895C6E2020202076617220636D64436F6465203D205C2230335C223B202F2F20E68EA7E588B6E68C87E4BBA4E591BDE4BBA4E7A0815C6E5C6E202020202F2F20E68BBCE68EA5E5AE8CE695B4E68AA5E69687EFBC88E697A0E6A0A1E9AA8CE5928CEFBC895C6E20202020766172206672616D65203D205C22413541355C22202B205C6E202020202020202020202020202020206465766963654964202B205C6E20202020202020202020202020202020636D64436F6465202B205C6E20202020202020202020202020202020496E74656765722E746F486578537472696E6728646174614C656E29202B202F2F20E695B0E68DAEE995BFE5BAA6EFBC8831E4BD8D3136E8BF9BE588B6EFBC895C6E2020202020202020202020202020202064617461486578202B205C6E202020202020202020202020202020205C22354135415C223B5C6E5C6E2020202072657475726E206672616D652E746F55707065724361736528293B202F2F20E8BDACE4B8BAE5A4A7E586993136E8BF9BE588B65C6E20207D5C6E5C6E202072657475726E205C225C223B202F2F20E4B88DE694AFE68C81E79A84E68C87E4BBA45C6E7D3B5C6E5C6E2F2F2072657475726E207072654465636F6465285C224135413530303031303130373031303544433038463030344230354135415C22295C6E2F2FE5B19EE680A7E4B88AE68AA5E68AA5E696875C6E72657475726E206465636F6465282741354135303030323031303730313033453830394334303544433541354127293B5C6E5C6E2F2FE8BF87E8BDBDE4BA8BE4BBB6E68AA5E696875C6E2F2F2072657475726E206465636F6465285C224135413530303031303230333031303442304633354135415C22295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C2264657669636549645C223A205C22303030315C222C5C2266756E6374696F6E5C223A205C227377697463685C222C5C22646174615C223A207B205C2273746174655C223A205C2230315C22207D7D27295C6E5C6E5C6E2F2F2072657475726E20656E636F646528277B5C226D657373616765547970655C223A5C2246554E4354494F4E535C222C5C2266756E6374696F6E5C223A5C227377697463685C222C5C22646174615C223A7B5C2264657669636549645C223A5C2230303031303130375C222C5C2273746174655C223A5C2230315C227D7D2729222C0A20202267726F7570496422203A20226634626562303864633766653431353762396636336132326130653161343330222C0A2020226E616D6522203A2022E5B08FE89891E88F87E68F92E5BAA7222C0A20202263726561746554696D6522203A20313735333432333334343432382C0A20202275706461746554696D6522203A20313735343033393333323333332C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A2022696F74222C0A202022757064617465427922203A2022696F74222C0A2020227061746822203A2022363838333161316633386663313036613864616333633133222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A20225B5D222C0A2020226465736372697074696F6E22203A20224149E887AAE58AA8E7949FE68890E79A84E8A7A3E69E90E5928CE58D8FE8AEAE222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'iot'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('bea746cb74484886b5ea0d1efc678636', 1755154048575, NULL, 'api-group', '测试分组', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226265613734366362373434383438383662356561306431656663363738363336222C0A2020226E616D6522203A2022E6B58BE8AF95E58886E7BB84222C0A2020227479706522203A2022617069222C0A202022706172656E74496422203A202230222C0A2020227061746822203A20222F7465737470726F746F636F6C222C0A20202263726561746554696D6522203A20313735353135343034383534382C0A20202275706461746554696D6522203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A206E756C6C2C0A202022706174687322203A205B205D2C0A2020226F7074696F6E7322203A205B205D0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('e3857faf66c04d9499c4d1e6a4406036', 1754144167840, NULL, 'api', '开关面板-MQTT-doc编写', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226533383537666166363663303464393439396334643165366134343036303336222C0A20202273637269707422203A20222F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E707573682870726F70657274696573293B5C6E202020205C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E2F2F2072657475726E206465636F6465285C225C22293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E58699222C0A20202263726561746554696D6522203A20313735343134343136373831342C0A20202275706461746554696D6522203A206E756C6C2C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A206E756C6C2C0A2020227061746822203A2022363838636439326664623965303135383837326634616139222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E58699222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('e3857faf66c04d9499c4d1e6a4406036', 1754144175957, NULL, 'api', '开关面板-MQTT-doc编写', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226533383537666166363663303464393439396334643165366134343036303336222C0A20202273637269707422203A2022696D706F7274206C6F675C6E2F2A2A5C6E202A20E9A284E8A7A3E7A081EFBC9AE68F90E58F96E8AEBEE5A487E8BAABE4BBBDE6A087E8AF865C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E58FAFE883BDE698AFE4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B4F626A6563747D207B64657669636549642C2070726F647563744B65797D5C6E202A2F5C6E766172207072654465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D207B5C6E20202020202020205C2264657669636549645C223A205C225C222C5C6E20202020202020205C2270726F647563744B65795C223A205C225C225C6E202020207D3B5C6E202020202F2F20544F444FEFBC9AE8A7A3E69E90E8AEBEE5A487E6A087E8AF865C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E5C6E2F2A2A5C6E202A20E8A7A3E7A081EFBC9AE8AEBEE5A487E695B0E68DAEE8BDACE5B9B3E58FB0E6A0BCE5BC8F5C6E202A2040706172616D207B537472696E677D207061796C6F616420E8AEBEE5A487E58E9FE5A78BE68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A204072657475726E73207B41727261797D20E5B9B3E58FB0E6A087E58786E6B688E681AFE695B0E7BB84EFBC8CE6AF8FE4B8AAE58583E7B4A0E7BB93E69E84EFBC9A5C6E202A2020205B5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C2250524F504552544945535C222C202F2F20E5B19EE680A7E7B1BBE59E8B5C6E202A2020202020202070726F706572746965733A207B7D202F2F20E8A7A3E69E90E5908EE79A84E5B19EE680A7E994AEE580BCE5AFB9EFBC88E6A0B9E68DAEE789A9E6A8A1E59E8BE58AA8E68081E8B083E695B4EFBC895C6E202A20202020207D2C5C6E202A20202020207B5C6E202A202020202020206D657373616765547970653A205C224556454E545C222C202F2F20E4BA8BE4BBB6E7B1BBE59E8B5C6E202A202020202020206576656E743A205C225C222C202F2F20E4BA8BE4BBB6E5908DE7A7B05C6E202A20202020202020646174613A207B7D202F2F20E4BA8BE4BBB6E58F82E695B05C6E202A20202020207D5C6E202A2020205D5C6E202A2F5C6E766172206465636F6465203D207061796C6F6164203D3E207B5C6E20202020766172207273203D205B5D3B5C6E202020202F2F20E5B19EE680A75C6E202020207661722070726F70657274696573203D207B5C6E20202020202020205C226D657373616765547970655C223A205C2250524F504552544945535C222C5C6E20202020202020205C2270726F706572746965735C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E707573682870726F70657274696573293B5C6E202020205C6E202020202F2F20E4BA8BE4BBB65C6E20202020766172206576656E74203D207B5C6E20202020202020205C226D657373616765547970655C223A205C224556454E545C222C5C6E20202020202020205C226576656E745C223A205C225C222C5C6E20202020202020205C22646174615C223A207B7D5C6E202020207D3B5C6E202020202F2F2072732E70757368286576656E74293B5C6E202020205C6E2020202072657475726E2072733B5C6E7D3B5C6E5C6E2F2F20E8B083E8AF95E7A4BAE4BE8BEFBC88E6ADA3E5BC8FE4BDBFE794A8E8AFB7E588A0E999A4E68896E6B3A8E9878AEFBC895C6E2F2F72657475726E207072654465636F6465285C225C22293B5C6E2F2F2072657475726E20656E636F6465287B7D293B5C6E2F2F2072657475726E206465636F6465285C225C22293B222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E58699222C0A20202263726561746554696D6522203A20313735343134343136373831342C0A20202275706461746554696D6522203A20313735343134343137353933372C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363838636439326664623965303135383837326634616139222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E58699222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('e3857faf66c04d9499c4d1e6a4406036', 1754144187774, NULL, 'api', '开关面板-MQTT-doc编写', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226533383537666166363663303464393439396334643165366134343036303336222C0A20202273637269707422203A2022696D706F7274206C6F675C6E5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E2020202076617220726573756C74203D205C225C223B5C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E20726573756C743B5C6E7D3B5C6E222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E58699222C0A20202263726561746554696D6522203A20313735343134343136373831342C0A20202275706461746554696D6522203A20313735343134343138373734392C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363838636439326664623965303135383837326634616139222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E58699222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('e3857faf66c04d9499c4d1e6a4406036', 1754144224834, NULL, 'api', '开关面板-MQTT-doc编写', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226533383537666166363663303464393439396334643165366134343036303336222C0A20202273637269707422203A2022696D706F7274206C6F675C6E5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E202020206C6F672E696E666F285C22E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E586993D7B7D5C222C7061796C6F6164295C6E202020202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E2020202072657475726E207061796C6F61643B5C6E7D3B5C6E222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E58699222C0A20202263726561746554696D6522203A20313735343134343136373831342C0A20202275706461746554696D6522203A20313735343134343232343739322C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363838636439326664623965303135383837326634616139222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E58699222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('e3857faf66c04d9499c4d1e6a4406036', 1754209302790, NULL, 'api', '开关面板-MQTT-doc编写', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226533383537666166363663303464393439396334643165366134343036303336222C0A20202273637269707422203A2022696D706F7274206C6F675C6E5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E20202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E202072657475726E207061796C6F61643B5C6E7D3B5C6E222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E58699222C0A20202263726561746554696D6522203A20313735343134343136373831342C0A20202275706461746554696D6522203A20313735343230393330323737342C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363838636439326664623965303135383837326634616139222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E58699222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +INSERT INTO `protocol_file_backup` (`id`, `create_date`, `tag`, `type`, `name`, `content`, `create_by`) VALUES ('e3857faf66c04d9499c4d1e6a4406036', 1754209859668, NULL, 'api', '开关面板-MQTT-doc编写', 0x7B0A20202270726F7065727469657322203A207B207D2C0A202022696422203A20226533383537666166363663303464393439396334643165366134343036303336222C0A20202273637269707422203A2022696D706F7274206C6F675C6E5C6E5C6E2F2A2A5C6E202A20E7BC96E7A081EFBC9AE5B9B3E58FB0E68C87E4BBA4E8BDACE8AEBEE5A487E6A0BCE5BC8F5C6E202A2040706172616D207B4F626A6563747D207061796C6F616420E5B9B3E58FB0E68C87E4BBA45C6E202A204072657475726E73207B537472696E677D20E8AEBEE5A487E58FAFE8AF86E588ABE79A84E68AA5E69687EFBC88E4BA8CE8BF9BE588B6E380814A534F4EE380813136E8BF9BE588B6E5AD97E7ACA6E4B8B2E7AD89EFBC895C6E202A2F5C6E76617220656E636F6465203D207061796C6F6164203D3E207B5C6E20207661722072733D746F4A736F6E287061796C6F6164293B5C6E202072732E696F743D27E6BC94E7A4BA696F74275C6E202072732E72616E646F6D4E756D3D72616E646F6D5369676E28293B5C6E20202F2F20544F444FEFBC9AE5AE9EE78EB0E7BC96E7A081E980BBE8BE915C6E202072657475726E2072733B5C6E7D3B5C6E222C0A20202267726F7570496422203A20223737623539393936353439643436333861383538383866376564643831333164222C0A2020226E616D6522203A2022E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E58699222C0A20202263726561746554696D6522203A20313735343134343136373831342C0A20202275706461746554696D6522203A20313735343230393835393633362C0A2020226C6F636B22203A206E756C6C2C0A202022637265617465427922203A202274657374222C0A202022757064617465427922203A202274657374222C0A2020227061746822203A2022363838636439326664623965303135383837326634616139222C0A2020226D6574686F6422203A2022504F5354222C0A202022706172616D657465727322203A205B205D2C0A2020226F7074696F6E7322203A205B205D2C0A20202272657175657374426F647922203A206E756C6C2C0A2020226865616465727322203A205B205D2C0A202022706174687322203A205B205D2C0A202022726573706F6E7365426F647922203A206E756C6C2C0A2020226465736372697074696F6E22203A2022E5BC80E585B3E99DA2E69DBF2D4D5154542D646F63E7BC96E58699222C0A20202272657175657374426F6479446566696E6974696F6E22203A206E756C6C2C0A202022726573706F6E7365426F6479446566696E6974696F6E22203A206E756C6C0A7D, 'test'); +COMMIT; + +-- ---------------------------- +-- Table structure for qrtz_blob_triggers +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_blob_triggers`; +CREATE TABLE `qrtz_blob_triggers` ( + `sched_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度名称', + `trigger_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_triggers表trigger_name的外键', + `trigger_group` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_triggers表trigger_group的外键', + `blob_data` blob COMMENT '存放持久化Trigger对象', + PRIMARY KEY (`sched_name`,`trigger_name`,`trigger_group`) USING BTREE, + CONSTRAINT `qrtz_blob_triggers_ibfk_1` FOREIGN KEY (`sched_name`, `trigger_name`, `trigger_group`) REFERENCES `qrtz_triggers` (`sched_name`, `trigger_name`, `trigger_group`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='Blob类型的触发器表'; + +-- ---------------------------- +-- Records of qrtz_blob_triggers +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for qrtz_calendars +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_calendars`; +CREATE TABLE `qrtz_calendars` ( + `sched_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度名称', + `calendar_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '日历名称', + `calendar` blob NOT NULL COMMENT '存放持久化calendar对象', + PRIMARY KEY (`sched_name`,`calendar_name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='日历信息表'; + +-- ---------------------------- +-- Records of qrtz_calendars +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for qrtz_cron_triggers +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_cron_triggers`; +CREATE TABLE `qrtz_cron_triggers` ( + `sched_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度名称', + `trigger_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_triggers表trigger_name的外键', + `trigger_group` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_triggers表trigger_group的外键', + `cron_expression` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'cron表达式', + `time_zone_id` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '时区', + PRIMARY KEY (`sched_name`,`trigger_name`,`trigger_group`) USING BTREE, + CONSTRAINT `qrtz_cron_triggers_ibfk_1` FOREIGN KEY (`sched_name`, `trigger_name`, `trigger_group`) REFERENCES `qrtz_triggers` (`sched_name`, `trigger_name`, `trigger_group`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='Cron类型的触发器表'; + +-- ---------------------------- +-- Records of qrtz_cron_triggers +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for qrtz_fired_triggers +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_fired_triggers`; +CREATE TABLE `qrtz_fired_triggers` ( + `sched_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度名称', + `entry_id` varchar(95) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度器实例id', + `trigger_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_triggers表trigger_name的外键', + `trigger_group` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_triggers表trigger_group的外键', + `instance_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度器实例名', + `fired_time` bigint NOT NULL COMMENT '触发的时间', + `sched_time` bigint NOT NULL COMMENT '定时器制定的时间', + `priority` int NOT NULL COMMENT '优先级', + `state` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '状态', + `job_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务名称', + `job_group` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '任务组名', + `is_nonconcurrent` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '是否并发', + `requests_recovery` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '是否接受恢复执行', + PRIMARY KEY (`sched_name`,`entry_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='已触发的触发器表'; + +-- ---------------------------- +-- Records of qrtz_fired_triggers +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for qrtz_job_details +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_job_details`; +CREATE TABLE `qrtz_job_details` ( + `sched_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度名称', + `job_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '任务名称', + `job_group` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '任务组名', + `description` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '相关介绍', + `job_class_name` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '执行任务类名称', + `is_durable` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '是否持久化', + `is_nonconcurrent` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '是否并发', + `is_update_data` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '是否更新数据', + `requests_recovery` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '是否接受恢复执行', + `job_data` blob COMMENT '存放持久化job对象', + PRIMARY KEY (`sched_name`,`job_name`,`job_group`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='任务详细信息表'; + +-- ---------------------------- +-- Records of qrtz_job_details +-- ---------------------------- +BEGIN; +INSERT INTO `qrtz_job_details` (`sched_name`, `job_name`, `job_group`, `description`, `job_class_name`, `is_durable`, `is_nonconcurrent`, `is_update_data`, `requests_recovery`, `job_data`) VALUES ('RuoyiScheduler', 'TASK_CLASS_NAME50', 'DEFAULT', NULL, 'com.ruoyi.quartz.util.QuartzDisallowConcurrentExecution', '0', '1', '0', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C7708000000100000000174000F5441534B5F50524F504552544945537372001E636F6D2E72756F79692E71756172747A2E646F6D61696E2E5379734A6F6200000000000000010200084C000A636F6E63757272656E747400124C6A6176612F6C616E672F537472696E673B4C000E63726F6E45787072657373696F6E71007E00094C000C696E766F6B6554617267657471007E00094C00086A6F6247726F757071007E00094C00056A6F6249647400104C6A6176612F6C616E672F4C6F6E673B4C00076A6F624E616D6571007E00094C000D6D697366697265506F6C69637971007E00094C000673746174757371007E000978720027636F6D2E72756F79692E636F6D6D6F6E2E636F72652E646F6D61696E2E42617365456E7469747900000000000000010200074C0008637265617465427971007E00094C000A63726561746554696D657400104C6A6176612F7574696C2F446174653B4C0006706172616D7371007E00034C000672656D61726B71007E00094C000B73656172636856616C756571007E00094C0008757064617465427971007E00094C000A75706461746554696D6571007E000C787074000561646D696E7372000E6A6176612E7574696C2E44617465686A81014B597419030000787077080000017BA5B7A46878707400007070707400013174000C3020302036202A202A203F207400246465766963654F6E6C696E65486973746F72795461736B2E73617665282764617465272974000744454641554C547372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000000000032740021E8AEBEE5A487E59CA8E7BABFE78AB6E68081E4BF9DE5AD98EFBC88E697A5EFBC8974000133740001307800); +INSERT INTO `qrtz_job_details` (`sched_name`, `job_name`, `job_group`, `description`, `job_class_name`, `is_durable`, `is_nonconcurrent`, `is_update_data`, `requests_recovery`, `job_data`) VALUES ('RuoyiScheduler', 'TASK_CLASS_NAME52', 'DEFAULT', NULL, 'com.ruoyi.quartz.util.QuartzDisallowConcurrentExecution', '0', '1', '0', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C7708000000100000000174000F5441534B5F50524F504552544945537372001E636F6D2E72756F79692E71756172747A2E646F6D61696E2E5379734A6F6200000000000000010200084C000A636F6E63757272656E747400124C6A6176612F6C616E672F537472696E673B4C000E63726F6E45787072657373696F6E71007E00094C000C696E766F6B6554617267657471007E00094C00086A6F6247726F757071007E00094C00056A6F6249647400104C6A6176612F6C616E672F4C6F6E673B4C00076A6F624E616D6571007E00094C000D6D697366697265506F6C69637971007E00094C000673746174757371007E000978720027636F6D2E72756F79692E636F6D6D6F6E2E636F72652E646F6D61696E2E42617365456E7469747900000000000000010200074C0008637265617465427971007E00094C000A63726561746554696D657400104C6A6176612F7574696C2F446174653B4C0006706172616D7371007E00034C000672656D61726B71007E00094C000B73656172636856616C756571007E00094C0008757064617465427971007E00094C000A75706461746554696D6571007E000C787074000561646D696E7372000E6A6176612E7574696C2E44617465686A81014B597419030000787077080000017BA5CA9BE078707400007070707400013174000C302030203230204C202A203F7400256465766963654F6E6C696E65486973746F72795461736B2E7361766528276D6F6E7468272974000744454641554C547372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000000000034740021E8AEBEE5A487E59CA8E7BABFE78AB6E68081E4BF9DE5AD98EFBC88E69C88EFBC8974000131740001307800); +INSERT INTO `qrtz_job_details` (`sched_name`, `job_name`, `job_group`, `description`, `job_class_name`, `is_durable`, `is_nonconcurrent`, `is_update_data`, `requests_recovery`, `job_data`) VALUES ('RuoyiScheduler', 'TASK_CLASS_NAME53', 'DEFAULT', NULL, 'com.ruoyi.quartz.util.QuartzDisallowConcurrentExecution', '0', '1', '0', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C7708000000100000000174000F5441534B5F50524F504552544945537372001E636F6D2E72756F79692E71756172747A2E646F6D61696E2E5379734A6F6200000000000000010200084C000A636F6E63757272656E747400124C6A6176612F6C616E672F537472696E673B4C000E63726F6E45787072657373696F6E71007E00094C000C696E766F6B6554617267657471007E00094C00086A6F6247726F757071007E00094C00056A6F6249647400104C6A6176612F6C616E672F4C6F6E673B4C00076A6F624E616D6571007E00094C000D6D697366697265506F6C69637971007E00094C000673746174757371007E000978720027636F6D2E72756F79692E636F6D6D6F6E2E636F72652E646F6D61696E2E42617365456E7469747900000000000000010200074C0008637265617465427971007E00094C000A63726561746554696D657400104C6A6176612F7574696C2F446174653B4C0006706172616D7371007E00034C000672656D61726B71007E00094C000B73656172636856616C756571007E00094C0008757064617465427971007E00094C000A75706461746554696D6571007E000C787074000561646D696E7372000E6A6176612E7574696C2E44617465686A81014B597419030000787077080000017D122632A878707400007070707400013174000D302030202A2F32202A202A203F7400296465766963654F6E6C696E6553746174655461736B2E7570646174654F6E6C696E655374617465282974000744454641554C547372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000000000035740018E8AEBEE5A487E59CA8E7BABFE78AB6E68081E588A4E696AD74000131740001307800); +INSERT INTO `qrtz_job_details` (`sched_name`, `job_name`, `job_group`, `description`, `job_class_name`, `is_durable`, `is_nonconcurrent`, `is_update_data`, `requests_recovery`, `job_data`) VALUES ('UnivScheduler', 'TASK_CLASS_NAME11', '场景联动功能下发', NULL, 'cn.universal.quartz.util.QuartzDisallowConcurrentExecution', '0', '1', '0', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C7708000000100000000174000F5441534B5F50524F5045525449455373720021636E2E756E6976657273616C2E71756172747A2E646F6D61696E2E5379734A6F62000000000000000102000E4C000A636F6E63757272656E747400124C6A6176612F6C616E672F537472696E673B4C0008637265617465427971007E00094C000A63726561746554696D657400104C6A6176612F7574696C2F446174653B4C000E63726F6E45787072657373696F6E71007E00094C000C696E766F6B6554617267657471007E00094C00086A6F6247726F757071007E00094C00056A6F6249647400104C6A6176612F6C616E672F4C6F6E673B4C00076A6F624E616D6571007E00094C000D6D697366697265506F6C69637971007E00094C0006706172616D7371007E00034C000672656D61726B71007E00094C000673746174757371007E00094C0008757064617465427971007E00094C000A75706461746554696D6571007E000A787074000131740003696F747372000E6A6176612E7574696C2E44617465686A81014B59741903000078707708000001983653EF5C7874000F3020313020392C3136202A202A203F7400257363656E654C696E6B616765536572766963652E66756E6374696F6E446F776E2836344C29740018E59CBAE699AFE88194E58AA8E58A9FE883BDE4B88BE58F917372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B0200007870000000000000000B74000A7363656E6549643A3634740001337371007E00053F400000000000007708000000100000000078707400013071007E000E71007E00107800); +COMMIT; + +-- ---------------------------- +-- Table structure for qrtz_locks +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_locks`; +CREATE TABLE `qrtz_locks` ( + `sched_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度名称', + `lock_name` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '悲观锁名称', + PRIMARY KEY (`sched_name`,`lock_name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='存储的悲观锁信息表'; + +-- ---------------------------- +-- Records of qrtz_locks +-- ---------------------------- +BEGIN; +INSERT INTO `qrtz_locks` (`sched_name`, `lock_name`) VALUES ('UnivScheduler', 'STATE_ACCESS'); +INSERT INTO `qrtz_locks` (`sched_name`, `lock_name`) VALUES ('UnivScheduler', 'TRIGGER_ACCESS'); +COMMIT; + +-- ---------------------------- +-- Table structure for qrtz_paused_trigger_grps +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_paused_trigger_grps`; +CREATE TABLE `qrtz_paused_trigger_grps` ( + `sched_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度名称', + `trigger_group` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_triggers表trigger_group的外键', + PRIMARY KEY (`sched_name`,`trigger_group`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='暂停的触发器表'; + +-- ---------------------------- +-- Records of qrtz_paused_trigger_grps +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for qrtz_scheduler_state +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_scheduler_state`; +CREATE TABLE `qrtz_scheduler_state` ( + `sched_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度名称', + `instance_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '实例名称', + `last_checkin_time` bigint NOT NULL COMMENT '上次检查时间', + `checkin_interval` bigint NOT NULL COMMENT '检查间隔时间', + PRIMARY KEY (`sched_name`,`instance_name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='调度器状态表'; + +-- ---------------------------- +-- Records of qrtz_scheduler_state +-- ---------------------------- +BEGIN; +INSERT INTO `qrtz_scheduler_state` (`sched_name`, `instance_name`, `last_checkin_time`, `checkin_interval`) VALUES ('UnivScheduler', 'aleo1756302349868', 1756349454192, 150000); +COMMIT; + +-- ---------------------------- +-- Table structure for qrtz_simple_triggers +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_simple_triggers`; +CREATE TABLE `qrtz_simple_triggers` ( + `sched_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度名称', + `trigger_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_triggers表trigger_name的外键', + `trigger_group` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_triggers表trigger_group的外键', + `repeat_count` bigint NOT NULL COMMENT '重复的次数统计', + `repeat_interval` bigint NOT NULL COMMENT '重复的间隔时间', + `times_triggered` bigint NOT NULL COMMENT '已经触发的次数', + PRIMARY KEY (`sched_name`,`trigger_name`,`trigger_group`) USING BTREE, + CONSTRAINT `qrtz_simple_triggers_ibfk_1` FOREIGN KEY (`sched_name`, `trigger_name`, `trigger_group`) REFERENCES `qrtz_triggers` (`sched_name`, `trigger_name`, `trigger_group`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='简单触发器的信息表'; + +-- ---------------------------- +-- Records of qrtz_simple_triggers +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for qrtz_simprop_triggers +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_simprop_triggers`; +CREATE TABLE `qrtz_simprop_triggers` ( + `sched_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度名称', + `trigger_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_triggers表trigger_name的外键', + `trigger_group` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_triggers表trigger_group的外键', + `str_prop_1` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'String类型的trigger的第一个参数', + `str_prop_2` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'String类型的trigger的第二个参数', + `str_prop_3` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'String类型的trigger的第三个参数', + `int_prop_1` int DEFAULT NULL COMMENT 'int类型的trigger的第一个参数', + `int_prop_2` int DEFAULT NULL COMMENT 'int类型的trigger的第二个参数', + `long_prop_1` bigint DEFAULT NULL COMMENT 'long类型的trigger的第一个参数', + `long_prop_2` bigint DEFAULT NULL COMMENT 'long类型的trigger的第二个参数', + `dec_prop_1` decimal(13,4) DEFAULT NULL COMMENT 'decimal类型的trigger的第一个参数', + `dec_prop_2` decimal(13,4) DEFAULT NULL COMMENT 'decimal类型的trigger的第二个参数', + `bool_prop_1` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'Boolean类型的trigger的第一个参数', + `bool_prop_2` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'Boolean类型的trigger的第二个参数', + PRIMARY KEY (`sched_name`,`trigger_name`,`trigger_group`) USING BTREE, + CONSTRAINT `qrtz_simprop_triggers_ibfk_1` FOREIGN KEY (`sched_name`, `trigger_name`, `trigger_group`) REFERENCES `qrtz_triggers` (`sched_name`, `trigger_name`, `trigger_group`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='同步机制的行锁表'; + +-- ---------------------------- +-- Records of qrtz_simprop_triggers +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for qrtz_triggers +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_triggers`; +CREATE TABLE `qrtz_triggers` ( + `sched_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调度名称', + `trigger_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '触发器的名字', + `trigger_group` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '触发器所属组的名字', + `job_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_job_details表job_name的外键', + `job_group` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'qrtz_job_details表job_group的外键', + `description` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '相关介绍', + `next_fire_time` bigint DEFAULT NULL COMMENT '上一次触发时间(毫秒)', + `prev_fire_time` bigint DEFAULT NULL COMMENT '下一次触发时间(默认为-1表示不触发)', + `priority` int DEFAULT NULL COMMENT '优先级', + `trigger_state` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '触发器状态', + `trigger_type` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '触发器的类型', + `start_time` bigint NOT NULL COMMENT '开始时间', + `end_time` bigint DEFAULT NULL COMMENT '结束时间', + `calendar_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '日程表名称', + `misfire_instr` smallint DEFAULT NULL COMMENT '补偿执行的策略', + `job_data` blob COMMENT '存放持久化job对象', + PRIMARY KEY (`sched_name`,`trigger_name`,`trigger_group`) USING BTREE, + KEY `sched_name` (`sched_name`,`job_name`,`job_group`) USING BTREE, + CONSTRAINT `qrtz_triggers_ibfk_1` FOREIGN KEY (`sched_name`, `job_name`, `job_group`) REFERENCES `qrtz_job_details` (`sched_name`, `job_name`, `job_group`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='触发器详细信息表'; + +-- ---------------------------- +-- Records of qrtz_triggers +-- ---------------------------- +BEGIN; +INSERT INTO `qrtz_triggers` (`sched_name`, `trigger_name`, `trigger_group`, `job_name`, `job_group`, `description`, `next_fire_time`, `prev_fire_time`, `priority`, `trigger_state`, `trigger_type`, `start_time`, `end_time`, `calendar_name`, `misfire_instr`, `job_data`) VALUES ('RuoyiScheduler', 'TASK_CLASS_NAME50', 'DEFAULT', 'TASK_CLASS_NAME50', 'DEFAULT', NULL, 1640296800000, -1, 5, 'WAITING', 'CRON', 1640244959000, 0, NULL, 2, ''); +INSERT INTO `qrtz_triggers` (`sched_name`, `trigger_name`, `trigger_group`, `job_name`, `job_group`, `description`, `next_fire_time`, `prev_fire_time`, `priority`, `trigger_state`, `trigger_type`, `start_time`, `end_time`, `calendar_name`, `misfire_instr`, `job_data`) VALUES ('RuoyiScheduler', 'TASK_CLASS_NAME52', 'DEFAULT', 'TASK_CLASS_NAME52', 'DEFAULT', NULL, 1640952000000, -1, 5, 'WAITING', 'CRON', 1640244959000, 0, NULL, -1, ''); +INSERT INTO `qrtz_triggers` (`sched_name`, `trigger_name`, `trigger_group`, `job_name`, `job_group`, `description`, `next_fire_time`, `prev_fire_time`, `priority`, `trigger_state`, `trigger_type`, `start_time`, `end_time`, `calendar_name`, `misfire_instr`, `job_data`) VALUES ('RuoyiScheduler', 'TASK_CLASS_NAME53', 'DEFAULT', 'TASK_CLASS_NAME53', 'DEFAULT', NULL, 1640246400000, -1, 5, 'WAITING', 'CRON', 1640244959000, 0, NULL, -1, ''); +INSERT INTO `qrtz_triggers` (`sched_name`, `trigger_name`, `trigger_group`, `job_name`, `job_group`, `description`, `next_fire_time`, `prev_fire_time`, `priority`, `trigger_state`, `trigger_type`, `start_time`, `end_time`, `calendar_name`, `misfire_instr`, `job_data`) VALUES ('UnivScheduler', 'TASK_CLASS_NAME11', '场景联动功能下发', 'TASK_CLASS_NAME11', '场景联动功能下发', NULL, 1753319400000, 1753258200000, 5, 'WAITING', 'CRON', 1753258127000, 0, NULL, 2, ''); +COMMIT; + +-- ---------------------------- +-- Table structure for rule_model +-- ---------------------------- +DROP TABLE IF EXISTS `rule_model`; +CREATE TABLE `rule_model` ( + `id` bigint NOT NULL COMMENT 'id', + `rule_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '规则名称', + `data_level` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '数据级别', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '描述', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '状态', + `product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '产品KEY', + `config` json DEFAULT NULL COMMENT '规则配置', + `creator_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='规则模型'; + +-- ---------------------------- +-- Records of rule_model +-- ---------------------------- +BEGIN; +INSERT INTO `rule_model` (`id`, `rule_name`, `data_level`, `description`, `status`, `product_key`, `config`, `creator_id`, `create_time`, `update_time`) VALUES (1938517288869347328, '模拟设备', 'product', '123', 'run', '681c0775c2dc427d0480ab5f', '{\"appId\": \"*\", \"fields\": \"*\", \"targets\": [{\"id\": \"54d669a6-0c1a-46db-99ba-563d78ff8209\", \"url\": \"http://127.0.0.1:9092/test/echo\", \"type\": \"http\"}]}', 'test', '2025-06-27 16:38:32', '2025-08-06 19:06:04'); +INSERT INTO `rule_model` (`id`, `rule_name`, `data_level`, `description`, `status`, `product_key`, `config`, `creator_id`, `create_time`, `update_time`) VALUES (1951455958190497792, '数据转发', 'group', NULL, 'run', NULL, '{\"appId\": \"*\", \"fields\": \"*\", \"targets\": [{\"id\": \"057dbea2-2b5c-4303-af0b-2f080f5c1b56\", \"url\": \"http://127.0.0.1:9092/test/echo\", \"type\": \"http\"}]}', 'iot', '2025-08-02 09:32:11', '2025-08-06 21:40:25'); +COMMIT; + +-- ---------------------------- +-- Table structure for rule_model_instance +-- ---------------------------- +DROP TABLE IF EXISTS `rule_model_instance`; +CREATE TABLE `rule_model_instance` ( + `id` bigint NOT NULL COMMENT 'id', + `model_id` bigint DEFAULT NULL COMMENT '模型id', + `relation_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '关联类型', + `relation_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '关联id', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='规则实例'; + +-- ---------------------------- +-- Records of rule_model_instance +-- ---------------------------- +BEGIN; +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1485428032639234048, 1485419002550185984, 'device', '9f0faba72b5d47718cd9353041e97d40'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1486157429939785728, 1483700361927532544, 'device', '4858f039e4b44fe0987c330392f65815'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1486157429943980032, 1483700361927532544, 'device', 'd125e66e026349bf9a52ac19b126cff2'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1493045086909276160, 1486153439457988608, 'product', 'HBkRhKSj'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1493098852232671232, 1491677415030571008, 'group', '1491675370504503296'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1493493945724760064, 1493493945519239168, 'product', 'test'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1493496334934536192, 1493496334917758976, 'product', 'test'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1493754343145287680, 1493754343090761728, 'product', 'test'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1493785487857373184, 1493785487672823808, 'product', 'HBkRhKSj'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1493905699827015680, 1493830716295647232, 'product', 'HBkRhKSj'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1496786018855018496, 1496786018825658368, 'product', 'HBkRhKSj'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1499592722411155456, 1499592722356629504, 'product', 'HBkRhKSj'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1499636471300337664, 1499636471229034496, 'product', '1111'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1499636605828444160, 1499636605807472640, 'product', '1111'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1600323699565502464, 1497109632061964288, 'product', 'HBkRhKSj'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1600828170421497856, 1554365384672628736, 'product', '62e8aaa9cd68f383e4c18d87'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1602134252485971968, 1600781613110226944, 'product', '632ad1abcd6868ad76930e19'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1610462666376708096, 1610458620202942464, 'group', '1610458307660185600'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1610468879919316992, 1610467431496122368, 'product', '15104868'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1691642130799837184, 1508625094050508800, 'device', '4aaabeb0d3504d3ca39d8184b1844e55'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1692461242133778432, 1692461241966006272, 'group', '1481967386374025216'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1748168860993400832, 1742787987305517056, 'product', '658940dc59765da0afc52cc7'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1759858576130158592, 1759858576100798464, 'product', '65d1bba259767f6aa55801a6'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1927703560096690176, 1927703559882780672, 'product', '682d6ed9e4b0262696ffe0b3'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1940388729789284352, 1938517288869347328, 'product', '681c0775c2dc427d0480ab5f'); +INSERT INTO `rule_model_instance` (`id`, `model_id`, `relation_type`, `relation_id`) VALUES (1953088632411811840, 1951455958190497792, 'group', '1950770676340260864'); +COMMIT; + +-- ---------------------------- +-- Table structure for scene_linkage +-- ---------------------------- +DROP TABLE IF EXISTS `scene_linkage`; +CREATE TABLE `scene_linkage` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `scene_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '场景名称', + `touch` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT 'one' COMMENT '触发条件 all.全部 one.任意一个', + `trigger_condition` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '触发条件', + `exec_action` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '执行动作', + `sleep_cycle` int DEFAULT NULL COMMENT '沉默周期', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` tinyint DEFAULT '0' COMMENT '0启用 1停用', + `dev_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '设备id', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=66 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='场景联动'; + +-- ---------------------------- +-- Records of scene_linkage +-- ---------------------------- +BEGIN; +INSERT INTO `scene_linkage` (`id`, `scene_name`, `touch`, `trigger_condition`, `exec_action`, `sleep_cycle`, `create_by`, `create_time`, `update_by`, `update_time`, `status`, `dev_id`) VALUES (63, '联动', 'one', '[{\"trigger\":\"device\",\"type\":\"event\",\"deviceId\":\"ov00014117\",\"deviceName\":\"ov00014117\",\"modelId\":\"batteryRestore\",\"filters\":[]}]', '[{\"trigger\":\"notice\",\"filters\":[],\"noticeTemplateId\":\"3\",\"noticeTemplateName\":\"Aleo的验证模版\"},{\"trigger\":\"device\",\"type\":\"functions\",\"deviceId\":\"ov0001411\",\"deviceName\":\"ov0001411\",\"modelId\":\"switchControl\",\"filters\":[],\"execData\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}]},{\"trigger\":\"notice\",\"filters\":[],\"noticeTemplateId\":\"5\",\"noticeTemplateName\":\"广丰机房消息推送\"}]', 7200, 'test', '2025-06-27 16:27:38', 'test', '2025-08-04 22:55:19', 0, NULL); +INSERT INTO `scene_linkage` (`id`, `scene_name`, `touch`, `trigger_condition`, `exec_action`, `sleep_cycle`, `create_by`, `create_time`, `update_by`, `update_time`, `status`, `dev_id`) VALUES (65, '01', 'one', '[{\"trigger\":\"manual\",\"filters\":[]}]', '[{\"trigger\":\"device\",\"type\":\"functions\",\"deviceId\":\"ov00014117\",\"deviceName\":\"潜江雅园1号楼底商店铺\",\"modelId\":\"switchControl\",\"filters\":[],\"execData\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}]}]', 5, 'test', '2025-08-26 11:16:30', 'test', '2025-08-26 11:16:30', 0, NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for support_map_areas +-- ---------------------------- +DROP TABLE IF EXISTS `support_map_areas`; +CREATE TABLE `support_map_areas` ( + `id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'id', + `pid` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '父id', + `name` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '地址', + `full_name` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '详细地址', + `deep` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '深度', + `location` geometry NOT NULL COMMENT '核心点坐标', + `polygon` geometry NOT NULL COMMENT '范围坐标', + PRIMARY KEY (`id`) USING BTREE, + KEY `pid_index` (`pid`) USING BTREE, + SPATIAL KEY `sp_index` (`polygon`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Records of support_map_areas +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for sys_config +-- ---------------------------- +DROP TABLE IF EXISTS `sys_config`; +CREATE TABLE `sys_config` ( + `config_id` int NOT NULL AUTO_INCREMENT COMMENT '参数主键', + `config_name` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '参数名称', + `config_key` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '参数键名', + `config_value` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '参数键值', + `config_type` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT 'N' COMMENT '系统内置(Y是 N否)', + `create_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`config_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb3 COMMENT='参数配置表'; + +-- ---------------------------- +-- Records of sys_config +-- ---------------------------- +BEGIN; +INSERT INTO `sys_config` (`config_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'admin', '2021-08-06 15:21:15', '', NULL, '蓝色 skin-blue、绿色 skin-green、紫色 skin-purple、红色 skin-red、黄色 skin-yellow'); +INSERT INTO `sys_config` (`config_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2, '用户管理-账号初始密码', 'sys.user.initPassword', '123456', 'Y', 'admin', '2021-08-06 15:21:15', '', NULL, '初始化密码 123456'); +INSERT INTO `sys_config` (`config_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (3, '主框架页-侧边栏主题', 'sys.index.sideTheme', 'theme-dark', 'Y', 'admin', '2021-08-06 15:21:15', 'admin', '2021-09-09 15:01:46', '深色主题theme-dark,浅色主题theme-light'); +INSERT INTO `sys_config` (`config_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (4, '账号自助-验证码开关', 'sys.account.captchaOnOff', 'false', 'Y', 'admin', '2021-08-06 15:21:15', '', '2021-12-30 09:22:06', '是否开启登录验证码功能(true开启,false关闭)'); +INSERT INTO `sys_config` (`config_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (10, 'OSS云存储服务商', 'sys.oss.cloudStorageService', 'minio', 'Y', 'admin', '2021-08-06 15:21:45', '', NULL, 'OSS云存储服务商(qiniu:七牛云, aliyun:阿里云, qcloud:腾讯云, minio: Minio)'); +INSERT INTO `sys_config` (`config_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (11, 'OSS预览列表资源开关', 'sys.oss.previewListResource', 'true', 'Y', 'admin', '2021-08-06 15:21:45', '', NULL, 'true:开启, false:关闭'); +INSERT INTO `sys_config` (`config_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (12, '授权', 'directUrl', 'ddd', 'Y', 'admin', '2021-10-21 18:33:45', NULL, NULL, NULL); +INSERT INTO `sys_config` (`config_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (13, '修改密码周期(天)', 'updatePasswordCycle', '90', 'Y', 'admin', '2021-11-02 14:37:25', NULL, NULL, NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for sys_dict_data +-- ---------------------------- +DROP TABLE IF EXISTS `sys_dict_data`; +CREATE TABLE `sys_dict_data` ( + `dict_code` bigint NOT NULL AUTO_INCREMENT COMMENT '字典编码', + `dict_sort` int DEFAULT '0' COMMENT '字典排序', + `dict_label` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '字典标签', + `dict_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '字典键值', + `dict_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '字典类型', + `css_class` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '样式属性(其他样式扩展)', + `list_class` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '表格回显样式', + `is_default` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT 'N' COMMENT '是否默认(Y是 N否)', + `status` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '0' COMMENT '状态(0正常 1停用)', + `create_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`dict_code`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=580 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='字典数据表'; + +-- ---------------------------- +-- Records of sys_dict_data +-- ---------------------------- +BEGIN; +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (197, 0, '正常', '0', 'sys_normal_disable', NULL, NULL, NULL, '0', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (198, 1, '停用', '1', 'sys_normal_disable', NULL, NULL, NULL, '0', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (199, 0, '显示', '0', 'sys_show_hide', NULL, NULL, NULL, '0', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (200, 1, '隐藏', '1', 'sys_show_hide', NULL, NULL, NULL, '0', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (201, 0, '是', 'Y', 'sys_yes_no', NULL, NULL, NULL, '0', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (202, 1, '否', 'N', 'sys_yes_no', NULL, NULL, NULL, '0', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (203, 0, '成功', '0', 'sys_common_status', NULL, NULL, NULL, '0', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (204, 1, '失败', '1', 'sys_common_status', NULL, NULL, NULL, '0', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (205, 0, '离线', 'false', 'dev_instance_state', NULL, NULL, NULL, '0', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (206, 1, '在线', 'true', 'dev_instance_state', NULL, NULL, NULL, '0', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (217, 0, '操作人', 'operator', 'product_configuration_ctwing', NULL, NULL, NULL, '0', 'iot', '2021-12-29 14:00:04', 'iot', '2022-08-24 14:45:23', '{\"remark\":\"天翼物联默认账户\",\"type\":\"string\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (218, 0, '产品ID', 'productId', 'product_configuration_ctwing', NULL, NULL, NULL, '0', 'iot', '2021-12-29 14:01:40', 'iot', '2022-08-24 14:45:16', '{\"remark\":\"电信产品ID\",\"type\":\"string\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (219, 0, '设备指令缓存时长', 'ttl', 'product_configuration_ctwing', NULL, NULL, NULL, '0', 'iot', '2021-12-29 14:02:30', 'iot', '2022-08-24 14:45:05', '{\"remark\":\"设备指令缓存时长\",\"type\":\"string\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (220, 0, '指令下发数据类型', 'dataType', 'product_configuration_ctwing', NULL, NULL, NULL, '0', 'iot', '2021-12-29 14:06:45', 'iot', '2022-08-24 14:44:54', '{\"remark\":\"下发指令数据类型:1-字符串,2-16进制\",\"type\":\"string\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (221, 0, '指令下发数据方案', 'downParaType', 'product_configuration_ctwing', NULL, NULL, NULL, '0', 'iot', '2021-12-29 14:12:35', 'iot', '2023-05-08 11:37:50', '{\"remark\":\"0:TCP和LWM2M,透传1:MQTT、LWM2M协议非透传profile:有编解码的指令下发\",\"type\":\"string\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (222, 0, '是否透传', 'passThrough', 'product_configuration_ctwing', NULL, NULL, NULL, '0', 'iot', '2021-12-29 14:13:42', 'iot', '2022-08-24 14:44:04', '{\"remark\":\"TRUE|FALSE\",\"type\":\"boolean\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (223, 0, '产品Master-APIKey', 'masterKey', 'product_configuration_ctwing', NULL, NULL, NULL, '0', 'iot', '2021-12-29 14:14:56', 'iot', '2022-08-24 14:43:51', '{\"remark\":\"天翼物联Master-APIkey\",\"type\":\"string\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (224, 0, '发布', '0', 'product_state', NULL, NULL, NULL, '0', 'iot', '2021-12-30 09:33:52', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (226, 1, '停用', '1', 'product_state', NULL, NULL, NULL, '0', 'iot', '2021-12-30 09:34:21', 'iot', '2021-12-30 10:43:32', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (227, 0, '正常', '0', 'application_delete_status', NULL, NULL, NULL, '0', 'iot', '2021-12-30 13:49:44', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (228, 1, '删除', '1', 'application_delete_status', NULL, NULL, NULL, '0', 'iot', '2021-12-30 13:51:19', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (229, 0, '已发布', '1', 'protocol_state', NULL, NULL, NULL, '0', 'iot', '2022-01-06 14:19:09', 'iot', '2022-01-06 14:22:03', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (230, 1, '未发布', '0', 'protocol_state', NULL, NULL, NULL, '0', 'iot', '2022-01-06 14:19:18', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (231, 0, 'jar', 'jar', 'protocol_type', NULL, NULL, NULL, '1', 'iot', '2022-01-06 14:23:43', 'iot', '2025-07-13 11:35:08', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (232, 1, 'jscript', 'jscript', 'protocol_type', NULL, NULL, NULL, '0', 'iot', '2022-01-06 14:24:23', 'iot', '2022-01-06 14:34:41', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (233, 0, '设备', 'device', 'tsl_source', NULL, NULL, NULL, '0', 'iot', '2022-01-06 16:12:20', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (234, 0, '手动', 'manual', 'tsl_source', NULL, NULL, NULL, '0', 'iot', '2022-01-06 16:14:19', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (235, 0, '规则', 'rule', 'tsl_source', NULL, NULL, NULL, '0', 'iot', '2022-01-06 16:14:25', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (236, 0, 'MQTT', 'MQTT', 'connection_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-12 14:50:36', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (237, 1, 'UDP', 'UDP', 'connection_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-12 14:50:58', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (238, 2, 'CoAp', 'CoAp', 'connection_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-12 14:51:11', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (239, 3, 'TCP', 'TCP', 'connection_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-12 14:51:25', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (240, 4, 'HTTP', 'HTTP', 'connection_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-12 14:51:32', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (241, 5, 'HTTPS', 'HTTPS', 'connection_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-12 14:51:42', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (242, 0, 'int', 'int(整数型)', 'model_basic_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:37:04', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (243, 1, 'long', 'long(长整类型)', 'model_basic_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:37:29', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (244, 2, 'float', 'float(单精度浮点型)', 'model_basic_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:37:57', 'iot', '2022-01-24 16:50:47', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (245, 3, 'double', 'double(双精度浮点型)', 'model_basic_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:38:12', 'iot', '2022-01-24 16:51:00', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (246, 4, 'boolean', 'boolean(布尔型)', 'model_basic_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:39:01', 'iot', '2022-01-24 16:51:20', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (247, 5, 'string', 'string(字符串)', 'model_basic_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:39:29', 'iot', '2022-01-24 16:51:31', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (248, 6, 'text', 'text(文本)', 'model_basic_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:39:36', 'iot', '2022-01-24 16:51:37', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (249, 0, 'date', 'date(时间型)', 'model_other_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:42:18', 'iot', '2022-01-24 16:51:57', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (250, 1, 'array', 'array(数组)', 'model_other_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:42:26', 'iot', '2022-01-24 16:52:14', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (251, 2, 'enum', 'enum(枚举)', 'model_other_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:42:56', 'iot', '2022-01-24 16:52:21', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (252, 3, 'object', 'object(结构体)', 'model_other_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:43:08', 'iot', '2022-01-24 16:52:30', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (253, 4, 'geoPoint', 'geoPoint(地理位置)', 'model_other_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:43:27', 'iot', '2022-01-24 16:52:45', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (254, 5, 'file', 'file(文件)', 'model_other_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:43:41', 'iot', '2022-01-24 16:52:52', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (255, 6, 'password', 'password(密码)', 'model_other_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:43:50', 'iot', '2022-01-24 16:52:58', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (256, 7, 'geoShape', 'geoShape(地理形状)', 'model_other_data', NULL, NULL, NULL, '0', 'iot', '2022-01-24 16:44:43', 'iot', '2022-01-24 16:53:13', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (257, 0, 'utc', 'String类型的UTC时间戳(毫秒)', 'model_data_type', NULL, NULL, NULL, '0', 'iot', '2022-01-24 17:27:24', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (258, 1, 'yyyyMMddHHmmss', 'yyyy-MM-dd HH:mm:ss', 'model_data_type', NULL, NULL, NULL, '0', 'iot', '2022-01-24 17:28:55', 'iot', '2022-01-24 17:29:26', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (259, 0, 'url', 'URL(链接)', 'model_file_type', NULL, NULL, NULL, '0', 'iot', '2022-01-24 17:29:54', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (260, 1, 'base64', 'Base64(Base64编码)', 'model_file_type', NULL, NULL, NULL, '0', 'iot', '2022-01-24 17:30:27', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (261, 2, 'binary', 'Binary(二进制)', 'model_file_type', NULL, NULL, NULL, '0', 'iot', '2022-01-24 17:30:54', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (262, 0, 'often_used_unit', '常用单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:22:59', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (263, 1, 'measurement_unit', '计量单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:23:22', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (264, 2, 'area_unit', '面积单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:23:56', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (265, 3, 'time_unit', '时间单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:24:12', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (266, 4, 'volume_unit', '体积单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:24:32', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (267, 5, 'flow_unit', '流量单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:24:53', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (268, 6, 'capacity_unit', '容积单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:25:13', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (269, 7, 'mass_unit', '质量单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:25:30', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (270, 8, 'pressure_unit', '压力单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:25:49', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (271, 9, 'temperature_unit', '温度单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:26:05', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (272, 10, 'energy_unit', '能量单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:26:24', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (275, 11, 'power_unit', '功率单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:29:45', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (276, 12, 'angle_unit', '角度单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:30:02', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (277, 13, 'frequency_unit', '频率单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:30:19', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (278, 14, 'speed_unit', '速度单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:30:31', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (279, 15, 'electricity_unit', '电单位', 'model_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:30:47', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (280, 0, '%', '百分比 %', 'often_used_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:39:33', 'iot', '2022-01-24 18:42:33', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (281, 1, '次', '次 count', 'often_used_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:39:45', 'iot', '2022-08-09 16:34:02', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (282, 2, 'r/min', '转每分钟 r/min', 'often_used_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:40:16', 'iot', '2022-01-24 18:41:21', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (283, 4, '℃', '摄氏度 ℃', 'often_used_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:41:14', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (284, 5, 'dBm', '信号强度 dBm', 'often_used_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:41:51', 'iot', '2022-01-24 18:42:44', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (285, 6, 'kj', '千焦 kj', 'often_used_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:42:01', 'iot', '2022-01-24 18:42:47', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (286, 7, 'ppm', '百万分率 ppm', 'often_used_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:42:10', 'iot', '2022-01-24 18:42:52', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (287, 3, 'db', '分贝 db', 'often_used_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-24 18:42:19', 'iot', '2022-01-24 18:43:31', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (288, 0, 'nm', '纳米 nm', 'measurement_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:48:42', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (289, 0, 'μm', '微米 μm', 'measurement_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:49:00', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (290, 0, 'mm', '毫米 mm', 'measurement_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:49:09', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (291, 0, 'cm', '厘米 cm', 'measurement_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:49:17', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (292, 0, 'm', '米 m', 'measurement_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:49:26', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (293, 0, 'km', '千米 km', 'measurement_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:49:34', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (297, 0, 'mm²', '平方毫米 mm²', 'area_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:51:04', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (298, 0, 'cm²', '平方厘米 cm²', 'area_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:51:12', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (299, 0, 'm²', '平方米 m²', 'area_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:51:22', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (300, 0, 'km²', '平方千米 km²', 'area_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:51:31', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (301, 0, 'hm²', '公顷 hm²', 'area_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:51:41', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (302, 0, 'd', '天 d', 'time_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:51:57', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (303, 0, 'h', '小时 h', 'time_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:52:05', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (304, 0, 'min', '分钟 min', 'time_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:52:14', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (305, 0, 's', '秒 s', 'time_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:52:23', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (306, 0, 'ms', '毫秒 ms', 'time_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:52:33', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (307, 0, 'μs', '微秒 μs', 'time_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:52:42', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (308, 0, 'ns', '纳秒 ns', 'time_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:52:56', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (309, 0, 'mm³', '立方毫米 mm³', 'volume_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:53:13', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (310, 0, 'cm³', '立方厘米 cm³', 'volume_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:53:21', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (311, 0, 'm³', '立方米 m³', 'volume_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:53:31', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (312, 0, 'km³', '立方千米 km³', 'volume_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:53:39', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (313, 0, 'm³/s', '立方米每秒 m³/s', 'flow_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:53:55', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (314, 0, 'km³/s', '立方千米每秒 km³/s', 'flow_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:54:05', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (315, 0, 'cm³/s', '立方厘米每秒 cm³/s', 'flow_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:54:14', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (316, 0, 'l/s', '升每秒 l/s', 'flow_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:54:22', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (317, 0, 'm³/h', '立方米每小时 m³/h', 'flow_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:54:39', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (318, 0, 'km³/h', '立方千米每小时 km³/h', 'flow_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:54:47', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (319, 0, 'cm³/h', '立方厘米每小时 cm³/h', 'flow_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:54:56', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (320, 0, 'l/h', '升每小时 l/h', 'flow_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:55:08', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (321, 0, 'mL', '毫升 mL', 'capacity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:55:21', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (322, 0, 'L', '升 L', 'capacity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:55:29', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (323, 0, 'mg', '毫克 mg', 'mass_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:55:44', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (324, 0, 'g', '克 g', 'mass_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:55:52', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (325, 0, 'kg', '千克 kg', 'mass_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:56:04', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (326, 0, 't', '吨 t', 'mass_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:56:13', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (327, 0, 'Pa', '帕斯卡 Pa', 'pressure_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:56:29', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (328, 0, 'kPa', '千帕斯卡 kPa', 'pressure_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:56:38', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (329, 0, 'N', '牛顿 N', 'pressure_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:56:46', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (330, 0, 'N.m', '牛·米 N.m', 'pressure_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:56:55', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (331, 0, 'K', '开尔文 K', 'temperature_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:57:10', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (332, 0, '℃', '摄氏度 ℃', 'temperature_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:57:18', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (333, 0, '℉', '华氏度 ℉', 'temperature_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:57:41', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (334, 0, 'J', '焦耳 J', 'energy_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:57:54', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (335, 0, 'cal', '卡 cal', 'energy_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:58:03', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (336, 0, 'W', '瓦特 W', 'power_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:58:18', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (337, 0, 'kW', '千瓦特 kW', 'power_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:58:25', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (338, 0, 'rad', '弧度 rad', 'angle_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:58:40', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (339, 0, '°', '度 °', 'angle_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:58:48', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (340, 0, '′', '[角]分 ′', 'angle_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:58:57', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (341, 0, '″', '[角]秒 ', 'angle_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:59:07', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (342, 0, 'Hz', '赫兹 Hz', 'frequency_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:59:18', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (343, 0, 'MHz', '兆赫兹 MHz', 'frequency_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:59:27', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (344, 0, 'GHz', 'G赫兹 GHz', 'frequency_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:59:34', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (345, 0, 'm/s', '米每秒 m/s', 'speed_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:59:47', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (346, 0, 'km/h', '千米每小时 km/h', 'speed_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 09:59:55', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (347, 0, 'kn', '节 kn', 'speed_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:00:06', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (348, 0, 'V', '伏特 V', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:00:24', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (349, 0, 'kV', '千伏 kV', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:00:31', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (350, 0, 'mV', '毫伏 mV', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:00:38', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (351, 0, 'μV', '微伏 μV', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:00:45', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (352, 0, 'A', '安培 A', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:00:53', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (353, 0, 'mA', '毫安 mA', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:01:00', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (354, 0, 'μA', '微安 μA', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:01:08', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (355, 0, 'nA', '纳安 nA', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:01:15', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (356, 0, 'Ω', '欧姆 Ω', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:01:23', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (357, 0, 'KΩ', '千欧 KΩ', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:01:31', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (358, 0, 'MΩ', '兆欧 MΩ', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:01:39', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (359, 0, 'eV', '电子伏 ', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:01:47', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (360, 0, 'kW·h', '千瓦·时 kW·h', 'electricity_unit', NULL, NULL, NULL, '0', 'iot', '2022-01-25 10:01:55', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (361, 0, '通知', 'notice', 'alarm_level', NULL, NULL, NULL, '0', 'iot', '2022-01-25 18:59:20', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (362, 0, '紧急', 'urgent', 'alarm_level', NULL, NULL, NULL, '0', 'iot', '2022-01-25 18:59:27', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (363, 0, '主要', 'primary', 'alarm_level', NULL, NULL, NULL, '0', 'iot', '2022-01-25 18:59:37', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (364, 0, '严重', 'serious', 'alarm_level', NULL, NULL, NULL, '0', 'iot', '2022-01-25 18:59:45', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (365, 0, '设备', 'DEVICE', 'instance_node', NULL, NULL, NULL, '0', 'iot', '2022-01-26 11:03:35', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (366, 1, '网关', 'GATEWAY', 'instance_node', NULL, NULL, NULL, '0', 'iot', '2022-01-26 11:04:43', 'iot', '2022-01-26 11:05:01', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (367, 2, '网关子设备', 'GATEWAY_SUB_DEVICE', 'instance_node', NULL, NULL, NULL, '0', 'iot', '2022-01-26 11:05:54', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (368, 3, '数据采集终端', 'DTU', 'instance_node', NULL, NULL, NULL, '0', 'iot', '2022-01-26 11:06:10', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (369, 4, '数据采集终端子设备', 'DTU_SUB_DEVICE', 'instance_node', NULL, NULL, NULL, '0', 'iot', '2022-01-26 11:06:26', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (370, 0, '属性上报', 'PROPERTIES', 'data_type', NULL, NULL, NULL, '0', 'iot', '2022-01-26 14:35:51', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (371, 1, '事件上报', 'EVENT', 'data_type', NULL, NULL, NULL, '0', 'iot', '2022-01-26 14:36:03', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (372, 2, '功能调用', 'FUNCTIONS', 'data_type', NULL, NULL, NULL, '0', 'iot', '2022-01-26 14:36:16', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (373, 3, '功能回复', 'REPLY', 'data_type', NULL, NULL, NULL, '0', 'iot', '2022-01-26 14:36:27', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (374, 0, 'NB-IoT', 'NB-IoT', 'transport_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-27 10:24:14', 'iot', '2022-01-27 10:26:08', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (375, 1, 'LoRa', 'LoRa', 'transport_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-27 10:24:26', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (376, 2, 'Wi-Fi', 'Wi-Fi', 'transport_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-27 10:25:01', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (377, 3, 'RS485', 'RS485', 'transport_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-27 10:25:31', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (378, 4, 'RS232', 'RS232', 'transport_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-27 10:25:39', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (379, 5, '4G LTE Cat.1', '4G LTE Cat.1', 'transport_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-27 10:25:48', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (380, 6, '有线', '有线', 'transport_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-27 10:26:02', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (381, 7, 'MQTT', 'MQTT', 'transport_protocol', NULL, NULL, NULL, '0', 'iot', '2022-01-27 10:26:18', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (382, 0, '离线判定时间​', 'offlineThreshold', 'product_configuration_public', NULL, NULL, NULL, '0', 'iot', '2022-02-07 15:33:03', 'iot', '2025-07-28 23:02:59', '{\"remark\":\"设备最后一次通信时间超过阈值,则判定离线;默认12(单位:小时)\",\"type\":\"int\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (383, 0, '保留原始数据', 'requirePayload', 'product_configuration_public', NULL, NULL, NULL, '0', 'iot', '2022-02-08 13:33:15', 'iot', '2025-07-27 13:25:44', '{\"remark\":\"布尔类型;默认false\",\"type\":\"boolean\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (388, 1, '天翼物联', 'ctaiot', 'third_platform', NULL, NULL, NULL, '0', 'iot', '2022-02-14 16:22:29', 'iot', '2025-08-09 20:34:35', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (389, 0, '移动OneNet', 'onenet', 'third_platform', NULL, NULL, NULL, '1', 'iot', '2022-02-14 16:22:53', 'iot', '2025-07-11 09:39:25', '移动新版OneNet'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (420, 0, '云云对接', 'http', 'third_platform', NULL, NULL, NULL, '0', 'iot', '2022-03-17 12:11:54', 'iot', '2022-03-24 13:41:16', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (445, 3, 'magic', 'magic', 'protocol_type', NULL, NULL, NULL, '0', 'iot', '2022-06-08 17:03:01', 'iot', '2022-06-08 17:13:21', '内置大量函数封装'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (446, 0, '第三方指令接收URL', 'thirdDownUrl', 'product_configuration_http', NULL, NULL, NULL, '0', 'iot', '2022-07-13 11:33:21', 'iot', '2025-07-27 14:00:59', '{\"remark\":\"将通过这个URL下发指令,如没有可不填\",\"type\":\"string\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (447, 0, '支持南向云对接', 'thirdDownSupport', 'product_configuration_http', NULL, NULL, NULL, '0', 'iot', '2022-07-13 11:34:26', 'iot', '2022-08-25 09:31:55', '{\"remark\":\"基于物模型将功能参数传给第三方平台,完成功能调用\",\"type\":\"boolean\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (448, 3, '设备SN解析', 'snDecode', 'product_configuration_public', NULL, NULL, NULL, '0', 'iot', '2022-07-14 11:39:24', 'iot', '2022-08-22 20:30:05', '{\"remark\":\"设备sn解析\",\"type\":\"string\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (449, 0, 'NB_IMEI', 'imei', 'device_common_property', NULL, NULL, NULL, '0', 'iot', '2022-08-02 11:35:17', 'iot', NULL, 'NB部分设备功能调用需要'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (450, 0, '表号', 'meterNo', 'device_common_property', NULL, NULL, NULL, '0', 'iot', '2022-08-02 11:35:33', 'iot', NULL, '水电表'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (451, 0, '设备序列号', 'deviceId', 'device_common_property', NULL, NULL, NULL, '0', 'iot', '2022-08-02 11:35:48', 'iot', NULL, '暂时大华设备需要'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (452, 0, '新增', '1', 'sys_oper_type', NULL, NULL, NULL, '0', 'iot', '2022-08-02 14:07:03', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (453, 1, '修改', '2', 'sys_oper_type', NULL, NULL, NULL, '0', 'iot', '2022-08-02 14:07:14', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (454, 3, '删除', '3', 'sys_oper_type', NULL, NULL, NULL, '0', 'iot', '2022-08-02 14:07:25', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (455, 4, '授权', '4', 'sys_oper_type', NULL, NULL, NULL, '0', 'iot', '2022-08-02 14:07:32', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (456, 5, '导出', '5', 'sys_oper_type', NULL, NULL, NULL, '0', 'iot', '2022-08-02 14:07:43', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (457, 6, '导入', '6', 'sys_oper_type', NULL, NULL, NULL, '0', 'iot', '2022-08-02 14:07:50', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (458, 0, '定位设备', 'isGps', 'product_configuration_public', NULL, NULL, NULL, '0', 'iot', '2022-08-05 16:02:53', 'iot', '2025-07-27 13:14:59', '{\"remark\":\"布尔类型;默认false\",\"type\":\"boolean\",\"default\":false}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (459, 0, '是否保存功能下发参数', 'saveFunctionParam', 'product_configuration_public', NULL, NULL, NULL, '0', 'iot', '2022-08-17 14:23:46', 'iot', '2022-08-22 20:29:01', '{\"remark\":\"是否保存功能下发参数\",\"type\":\"boolean\",\"default\":true}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (460, 0, '指令下行转16进制', 'encodeHex', 'product_configuration_public', NULL, NULL, NULL, '1', 'iot', '2022-08-17 17:22:45', 'iot', '2022-08-19 08:34:35', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (461, 7, '解码数据类型', 'decoderType', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-08-22 15:03:30', 'iot', '2025-07-28 22:26:25', '{\"remark\":\"解码数据类型\",\"type\":\"select\",\"url\":\"tcp_data_conver_format\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (462, 0, '字符串(utf-8)', 'STRING', 'tcp_data_conver_format', NULL, NULL, NULL, '0', 'iot', '2022-08-22 15:06:31', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (463, 1, '十六进制', 'HEX', 'tcp_data_conver_format', NULL, NULL, NULL, '0', 'iot', '2022-08-22 15:06:48', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (464, 0, 'TCP', 'tcp', 'third_platform', NULL, NULL, NULL, '0', 'iot', '2022-08-22 15:19:43', 'iot', '2025-07-11 09:42:51', '4G,5G,WiFi'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (467, 14, '空闲间隔时长', 'idleInterval', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-08-23 14:49:15', 'iot', '2022-08-23 20:50:18', '{\"remark\":\"空闲间隔时长\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (468, 13, '发送超时时长', 'sendTimeout', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-08-23 14:50:57', 'iot', '2022-08-23 20:50:13', '{\"remark\":\"发送超时时长\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (469, 12, '读取超时时长', 'readTimeout', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-08-23 14:51:22', 'iot', '2022-08-23 20:50:08', '{\"remark\":\"读取超时时长\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (470, 0, '是否启用', 'enabled', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-08-23 14:51:36', 'iot', '2022-08-24 11:02:11', '{\"remark\":\"是否启用\",\"type\":\"boolean\",\"hide\":\"false\",\"default\":false}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (473, 11, '写(idle)检测时长', 'writerIdleTime', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-08-23 15:03:04', 'iot', '2022-09-22 10:02:39', '{\"remark\":\"writerIdleTime(s)\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (474, 10, '读(idle)检测时长', 'readerIdleTime', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-08-23 15:03:17', 'iot', '2022-09-22 10:00:43', '{\"remark\":\"readerIdleTime(s)\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (475, 9, '读写(allIdle)检测时长', 'allIdleTime', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-08-23 15:03:39', 'iot', '2022-09-22 10:03:10', '{\"remark\":\"allIdle(s)\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (476, 8, '解析器类型', 'parserType', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-08-23 15:05:53', 'iot', '2022-08-23 20:49:47', '{\"remark\":\"解析器\",\"type\":\"select\",\"url\":\"tcp_parser_type\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (477, 0, '不处理', 'DIRECT', 'tcp_parser_type', NULL, NULL, NULL, '0', 'iot', '2022-08-23 15:09:16', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (478, 0, '固定长度', 'FIXED_LENGTH', 'tcp_parser_type', NULL, NULL, NULL, '0', 'iot', '2022-08-23 15:09:45', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (479, 0, '分隔符', 'DELIMITED', 'tcp_parser_type', NULL, NULL, NULL, '0', 'iot', '2022-08-23 15:09:54', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (483, 15, '分隔符(分隔符解析器)', 'delimited', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-08-23 21:26:53', 'iot', '2022-08-23 21:28:43', '{\"remark\":\"分隔符(分隔符解析器)\",\"type\":\"string\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (484, 16, '分隔的最大长度(分隔符解析器)', 'delimitedMaxlength', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-08-23 21:27:28', 'iot', '2022-08-23 21:28:53', '{\"remark\":\"分隔的最大长度(分隔符解析器)\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (485, 17, '固定长度(固定长度解析器)', 'fixedLength', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-08-23 21:28:03', 'iot', '2022-08-23 21:29:01', '{\"remark\":\"固定长度(固定长度解析器)\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (488, 0, '所有', 'ALL', 'subscribe_message_type', NULL, NULL, NULL, '0', 'iot', '2022-09-13 14:22:56', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (489, 1, '事件', 'EVENT', 'subscribe_message_type', NULL, NULL, NULL, '0', 'iot', '2022-09-13 14:23:08', 'iot', '2022-09-13 14:23:27', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (490, 2, '属性', 'PROPERTIES', 'subscribe_message_type', NULL, NULL, NULL, '0', 'iot', '2022-09-13 14:23:39', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (491, 3, '服务调用', 'FUNCTIONS', 'subscribe_message_type', NULL, NULL, NULL, '0', 'iot', '2022-09-13 14:23:52', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (492, 4, '指令回复', 'REPLY', 'subscribe_message_type', NULL, NULL, NULL, '0', 'iot', '2022-09-13 14:24:07', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (493, 0, '产品', 'PRODUCT', 'subscribe_level', NULL, NULL, NULL, '0', 'iot', '2022-09-13 14:25:32', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (494, 1, '设备', 'DEVICE', 'subscribe_level', NULL, NULL, NULL, '0', 'iot', '2022-09-13 14:25:44', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (496, 0, 'MQTT', 'mqtt', 'third_platform', NULL, NULL, NULL, '0', 'iot', '2022-09-21 16:44:41', 'iot', '2025-07-11 09:42:06', '内置和外部部署MQTT'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (497, 0, '指令下行主题', 'downTopic', 'product_configuration_mqtt', NULL, NULL, NULL, '0', 'iot', '2022-09-29 10:02:04', 'iot', '2022-09-29 10:02:47', '{\"remark\":\"指令下行主题\",\"type\":\"string\",\"hide\":\"false\",\"default\":\"\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (498, 0, '解码带入设备影子', 'requireUpShadow', 'product_configuration_public', NULL, NULL, NULL, '0', 'iot', '2022-10-12 09:01:31', 'iot', '2025-07-27 13:28:08', '{\"remark\":\"上行报文是否附加影子\",\"type\":\"boolean\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (499, 3, '下发缓存', 'onlyCache', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-10-18 14:03:54', 'iot', NULL, '{\"remark\":\"无论设备是否在线,是否下发指令总是缓存;true-是;false-否\",\"type\":\"boolean\",\"hide\":\"false\",\"default\":false}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (500, 0, '消息头附带长度', 'FIELD_LENGTH', 'tcp_parser_type', NULL, NULL, NULL, '0', 'iot', '2022-10-19 14:04:23', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (501, 18, '长度域小端在前', 'byteOrderLittle', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-10-20 14:57:42', 'iot', '2022-10-20 15:20:08', '{\"remark\":\"长度域小端在前\",\"type\":\"boolean\",\"default\":\"true\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (502, 19, '最大包长度', 'maxFrameLength', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-10-20 15:00:27', 'iot', '2022-10-20 15:19:37', '{\"remark\":\"最大包长度\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (503, 20, '长度域偏移位', 'lengthFieldOffset', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-10-20 15:01:52', 'iot', '2022-10-20 15:19:40', '{\"remark\":\"长度域偏移位\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (504, 21, '长度域所占字节', 'lengthFieldLength', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-10-20 15:02:50', 'iot', '2022-10-20 15:19:43', '{\"remark\":\"长度域所占字节\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (505, 22, '数据长度偏移量', 'lengthAdjustment', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-10-20 15:04:59', 'iot', '2022-10-20 15:19:46', '{\"remark\":\"数据长度偏移量\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (506, 23, '跳过字节数', 'initialBytesToStrip', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-10-20 15:05:29', 'iot', '2022-10-20 15:19:49', '{\"remark\":\"跳过字节数\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (507, 24, '数据超长异常', 'failFast', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-10-20 15:06:24', 'iot', '2022-10-20 15:20:01', '{\"remark\":\"数据超长异常\",\"type\":\"boolean\",\"default\":\"true\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (509, 0, '永远通过预解码获取设备序列号', 'alwaysPreDecode', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2022-11-10 14:17:30', 'iot', '2022-11-10 14:19:11', '{\"remark\":\"永远预解码\",\"type\":\"boolean\",\"default\":\"false\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (511, 0, 'KB', '流量(KB)', 'flow_unit', NULL, NULL, NULL, '0', 'iot', '2022-12-09 14:53:33', 'iot', '2022-12-09 14:54:07', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (512, 0, 'GB', '流量(Gb)', 'flow_unit', NULL, NULL, NULL, '0', 'iot', '2022-12-09 14:54:01', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (513, 0, 'MB', '流量(Mb)', 'flow_unit', NULL, NULL, NULL, '0', 'iot', '2022-12-09 14:54:21', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (520, 99, '设备消息保存时间', 'logMaxStorage', 'product_configuration_public', NULL, NULL, NULL, '0', 'iot', '2023-02-15 12:28:56', 'iot', '2023-02-15 13:26:46', '{\"remark\":\"单位(天),默认180天\",\"type\":\"int\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (521, 8, '蓝牙', 'Bluetooth', 'transport_protocol', NULL, NULL, NULL, '0', 'iot', '2023-02-16 15:46:01', 'iot', '2023-02-17 08:45:51', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (522, 0, '💉医疗', 'yiliao', 'product_tags', NULL, NULL, NULL, '0', 'iot', '2023-04-03 15:10:11', 'iot', '2025-06-23 19:08:27', ''); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (523, 1, '♻️环保', 'huanbao', 'product_tags', NULL, NULL, NULL, '0', 'iot', '2023-04-03 16:11:40', 'iot', '2025-06-23 19:08:35', NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (524, 0, '指令预缓存', 'preStore', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2023-04-13 11:16:54', 'iot', '2023-04-13 11:20:01', '{\"remark\":\"tcp连接假死时,缓存指令\",\"type\":\"boolean\",\"default\":\"false\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (525, 99, '自动注册新设备​', 'allowInsert', 'product_configuration_public', NULL, NULL, NULL, '0', 'iot', '2023-04-13 16:34:03', 'iot', '2025-07-27 13:26:00', '{\"remark\":\"消息上行时设备不存在时直接新增。布尔类型;默认false\",\"type\":\"boolean\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (526, 0, '换行符', 'LINE_BREAK', 'tcp_parser_type', NULL, NULL, NULL, '0', 'iot', '2023-04-21 15:35:09', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (527, 16, '换行最大长度', 'lineMaxLength', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2023-04-21 15:36:35', 'iot', '2023-04-21 15:49:44', '{\"remark\":\"换行的最大长度(换行符解析器)\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (528, 0, 'pinsu', '14.29.243.72,125.210.48.0/24', 'whitelist', NULL, NULL, NULL, '0', 'iot', '2023-08-11 14:10:45', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (529, 0, 'fuxing', '118.31.57.184', 'whitelist', NULL, NULL, NULL, '0', 'iot', '2023-08-11 14:56:19', 'iot', NULL, '测试'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (531, 0, 'xitang', '47.105.53.60', 'whitelist', NULL, NULL, NULL, '0', 'iot', '2023-08-24 16:12:41', 'iot', NULL, '溪堂上报ip白名单'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (532, 0, 'haikang', '125.210.48.247', 'whitelist', NULL, NULL, NULL, '0', 'iot', '2023-08-24 16:15:11', 'iot', NULL, '海康上行ip白名单'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (533, 0, 'onenet', '183.230.102.85,183.230.40.120,183.230.102.86,183.230.40.121,183.230.102.83,183.230.102.84', 'whitelist', NULL, NULL, NULL, '0', 'iot', '2023-08-24 16:15:11', 'iot', NULL, '移动(明文)上行ip白名单'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (534, 0, 'lechen', '118.31.73.7,118.31.36.102,118.31.35.226,118.31.73.146,118.31.6.178,121.196.220.229', 'whitelist', NULL, NULL, NULL, '0', 'iot', '2023-08-24 16:15:11', 'iot', NULL, '乐橙上行ip白名单'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (549, 0, '养老', 'yanglao', 'product_tags', NULL, NULL, NULL, '0', 'iot', '2025-06-23 19:08:20', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (550, 0, '断开自动重连', 'autoReconnect', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:01:04', 'iot', NULL, '{\"remark\":\"断开重连\",\"type\":\"boolean\",\"default\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (551, 22, '保持会话', 'cleanSession', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:04:59', 'iot', '2025-07-06 11:18:03', '{\"remark\":\"true表示每次新的会话,客户端断开自动销毁。false表示使用固定的clientID保持持久会话\",\"type\":\"boolean\",\"default\":\"false\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (552, 5, '客户端前缀', 'clientIdPrefix', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:06:11', 'iot', '2025-07-06 11:19:04', '{\"remark\":\"与cleanSession对应,如果false,程序会每次用相同的clientId\",\"type\":\"boolean\",\"hide\":\"true\",\"default\":false}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (553, 0, '消息质量', 'qos', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:09:13', 'iot', NULL, '{\"remark\":\"qos:0-最多1次(可能丢);1-至少1次(可重复);2-恰好一次(不丢失且不重复)\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (554, 0, '服务地址', 'host', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:10:09', 'iot', '2025-07-06 11:11:15', '{\"remark\":\"broker地址:tcp://127.0.0.1:1883\",\"type\":\"string\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (555, 0, '心跳机制', 'keepAliveInterval', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:11:01', 'iot', NULL, '{\"remark\":\"心跳检测(s)\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (556, 2, '服务密码', 'password', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:12:01', 'iot', '2025-07-06 11:17:53', '{\"remark\":\"MQTT密码\",\"type\":\"string\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (557, 15, '开启ssl', 'ssl', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:12:39', 'iot', '2025-07-06 11:15:21', '{\"remark\":\"是否启用ssl\",\"type\":\"boolean\",\"hide\":\"false\",\"default\":false}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (558, 17, 'sslCertPath', 'SSL证书路径', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:14:17', 'iot', '2025-07-06 11:16:09', '{\"remark\":\"证书cert\",\"type\":\"string\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (559, 18, 'SSL密钥路径', 'sslKeyPath', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:15:04', 'iot', NULL, '{\"remark\":\"证书key\",\"type\":\"string\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (560, 16, 'CA证书路径', 'sslCaPath', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:15:35', 'iot', NULL, '{\"remark\":\"证书Ca\",\"type\":\"string\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (561, 14, '连接超时', 'connectTimeout', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:16:42', 'iot', NULL, '{\"remark\":\"默认30s\",\"type\":\"int\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (562, 1, '鉴权用户', 'username', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:17:41', 'iot', NULL, '{\"remark\":\"username\",\"type\":\"string\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (563, 99, '订阅主题', 'subscribeTopics', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:20:56', 'iot', NULL, '{\"remark\":\"订阅主题\",\"type\":\"json\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (564, 99, '组件类型', 'networkType', 'mqtt_parser_type', NULL, NULL, NULL, '0', 'iot', '2025-07-06 11:21:51', 'iot', '2025-07-06 11:21:57', 'MQTT_CLIENT,MQTT_SERVER'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (565, 1, '推送消息头', 'AuthHeader', 'product_configuration_http', NULL, NULL, NULL, '0', 'iot', '2025-07-11 09:52:17', 'iot', '2025-07-11 09:53:15', '{\"remark\":\"推送http携带AuthHeader\",\"type\":\"string\"} '); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (566, 0, '推送密钥', 'AuthToken', 'product_configuration_http', NULL, NULL, NULL, '0', 'iot', '2025-07-11 09:52:31', 'iot', '2025-07-11 09:53:05', '{\"remark\":\"推送http携带AuthToken\",\"type\":\"string\"} '); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (569, 0, '推送地址', 'url', 'app_push_http', NULL, NULL, NULL, '0', 'iot', '2025-07-12 10:09:47', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (570, 0, '推送请求头', 'header', 'app_push_http', NULL, NULL, NULL, '0', 'iot', '2025-07-12 10:11:07', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (571, 0, '推送密钥', 'secret', 'app_push_http', NULL, NULL, NULL, '0', 'iot', '2025-07-12 10:11:34', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (572, 0, '访问地址', 'url', 'app_push_mqtt', NULL, NULL, NULL, '0', 'iot', '2025-07-12 10:15:18', 'iot', NULL, 'tcp://127.0.0.1:1883'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (573, 0, '账号', 'username', 'app_push_mqtt', NULL, NULL, NULL, '0', 'iot', '2025-07-12 10:15:35', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (574, 0, '密码', 'password', 'app_push_mqtt', NULL, NULL, NULL, '0', 'iot', '2025-07-12 10:15:48', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (575, 0, '客户端ID', 'clientId', 'app_push_mqtt', NULL, NULL, NULL, '0', 'iot', '2025-07-12 10:18:48', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (576, 0, '订阅主题', 'topic', 'app_push_mqtt', NULL, NULL, NULL, '0', 'iot', '2025-07-12 10:19:08', 'iot', NULL, NULL); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (577, 7, '编码数据类型', 'encoderType', 'product_configuration_tcp', NULL, NULL, NULL, '0', 'iot', '2025-07-26 22:52:52', NULL, NULL, '{\"remark\":\"解码数据类型\",\"type\":\"select\",\"url\":\"tcp_data_conver_format\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (578, 0, '允许IP地址', 'host', 'network_tcp_config', NULL, NULL, NULL, '0', 'iot', '2025-07-27 19:08:37', 'iot', '2025-07-27 19:09:37', '{\"remark\":\"允许设备的IP地址\",\"type\":\"string\",\"default\":\"0.0.0.0\",\"hide\":\"false\"}'); +INSERT INTO `sys_dict_data` (`dict_code`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (579, 0, '绑定端口', 'port', 'network_tcp_config', NULL, NULL, NULL, '0', 'iot', '2025-07-27 19:10:33', NULL, NULL, '{\"remark\":\"绑定端口\",\"type\":\"int\",\"hide\":\"false\"}'); +COMMIT; + +-- ---------------------------- +-- Table structure for sys_dict_type +-- ---------------------------- +DROP TABLE IF EXISTS `sys_dict_type`; +CREATE TABLE `sys_dict_type` ( + `dict_id` bigint NOT NULL AUTO_INCREMENT COMMENT '字典主键', + `dict_name` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '字典名称', + `dict_type` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '字典类型', + `status` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '0' COMMENT '状态(0正常 1停用)', + `create_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`dict_id`) USING BTREE, + UNIQUE KEY `dict_type` (`dict_type`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=190 DEFAULT CHARSET=utf8mb3 COMMENT='字典类型表'; + +-- ---------------------------- +-- Records of sys_dict_type +-- ---------------------------- +BEGIN; +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (118, '通用状态', 'sys_normal_disable', '0', 'iot', '2021-12-27 09:24:34', 'iot', '2021-12-27 09:24:34', NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (119, '菜单状态', 'sys_show_hide', '0', 'iot', '2021-12-27 09:24:34', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (120, '系统是否', 'sys_yes_no', '0', 'iot', '2021-12-27 09:24:34', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (121, '系统状态', 'sys_common_status', '0', 'iot', '2021-12-27 09:24:34', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (122, '设备状态', 'dev_instance_state', '0', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (124, '天翼物联配置字段', 'product_configuration_ctwing', '0', 'iot', '2021-12-29 13:55:27', 'iot', '2022-01-13 13:53:39', '电信NB平台的字典'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (125, '产品状态', 'product_state', '0', 'iot', '2021-12-30 09:33:13', 'iot', NULL, ''); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (126, '应用删除状态', 'application_delete_status', '0', 'iot', '2021-12-30 13:49:22', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (127, '协议状态', 'protocol_state', '0', 'iot', '2022-01-06 14:18:55', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (128, '协议类型', 'protocol_type', '0', 'iot', '2022-01-06 14:23:34', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (129, '属性值来源', 'tsl_source', '0', 'iot', '2022-01-06 16:11:41', 'iot', '2025-08-15 11:48:27', '物模型属性值来源'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (130, '连接协议', 'connection_protocol', '0', 'iot', '2022-01-12 14:50:12', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (131, '物模型数据基本类型', 'model_basic_data', '0', 'iot', '2022-01-24 16:34:16', 'iot', NULL, '物模型数据基本类型'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (132, '物模型数据其他类型', 'model_other_data', '0', 'iot', '2022-01-24 16:34:53', 'iot', NULL, '物模型数据其他类型'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (133, '物模型单位类型', 'model_unit', '0', 'iot', '2022-01-24 16:54:46', 'iot', '2022-01-24 18:31:13', '物模型单位类型'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (134, '物模型时间类型', 'model_data_type', '0', 'iot', '2022-01-24 16:55:11', 'iot', '2022-01-24 18:31:22', '物模型时间类型'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (135, '物模型文件类型', 'model_file_type', '0', 'iot', '2022-01-24 16:55:32', 'iot', '2022-01-24 18:31:28', '物模型文件类型'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (136, '物模型常用单位', 'often_used_unit', '0', 'iot', '2022-01-24 18:31:45', 'iot', '2022-01-24 18:32:14', '物模型常用单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (137, '物模型计量单位', 'measurement_unit', '0', 'iot', '2022-01-24 18:32:30', 'iot', NULL, '物模型计量单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (138, '物模型面积单位', 'area_unit', '0', 'iot', '2022-01-24 18:32:56', 'iot', NULL, '物模型面积单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (139, '物模型时间单位', 'time_unit', '0', 'iot', '2022-01-24 18:33:17', 'iot', NULL, '物模型时间单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (140, '物模型体积单位', 'volume_unit', '0', 'iot', '2022-01-24 18:33:31', 'iot', NULL, '物模型体积单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (141, '物模型流量单位', 'flow_unit', '0', 'iot', '2022-01-24 18:33:42', 'iot', '2022-01-24 18:35:18', '物模型流量单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (142, '物模型容积单位', 'capacity_unit', '0', 'iot', '2022-01-24 18:33:49', 'iot', '2022-01-24 18:36:25', '物模型容积单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (143, '物模型质量单位', 'mass_unit', '0', 'iot', '2022-01-24 18:33:56', 'iot', '2022-01-24 18:36:29', '物模型质量单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (144, '物模型压力单位', 'pressure_unit', '0', 'iot', '2022-01-24 18:34:02', 'iot', '2022-01-24 18:37:00', '物模型压力单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (145, '物模型温度单位', 'temperature_unit', '0', 'iot', '2022-01-24 18:34:10', 'iot', '2022-01-24 18:36:56', '物模型温度单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (146, '物模型能量单位', 'energy_unit', '0', 'iot', '2022-01-24 18:34:17', 'iot', '2022-01-24 18:36:52', '物模型能量单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (147, '物模型功率单位', 'power_unit', '0', 'iot', '2022-01-24 18:34:24', 'iot', '2022-01-24 18:36:49', '物模型功率单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (148, '物模型角度单位', 'angle_unit', '0', 'iot', '2022-01-24 18:34:30', 'iot', '2022-01-24 18:36:45', '物模型角度单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (149, '物模型频率单位', 'frequency_unit', '0', 'iot', '2022-01-24 18:34:37', 'iot', '2022-01-24 18:36:41', '物模型频率单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (150, '物模型速度单位', 'speed_unit', '0', 'iot', '2022-01-24 18:34:43', 'iot', '2022-01-24 18:36:37', '物模型速度单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (151, '物模型电单位', 'electricity_unit', '0', 'iot', '2022-01-24 18:34:50', 'iot', '2022-01-24 18:36:33', '物模型电单位'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (152, '告警等级', 'alarm_level', '0', 'iot', '2022-01-25 18:58:55', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (153, '设备节点', 'instance_node', '0', 'iot', '2022-01-26 11:01:44', 'iot', '2022-01-26 11:02:12', '产品表中设备节点'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (154, '数据类型', 'data_type', '0', 'iot', '2022-01-26 14:30:06', 'iot', '2022-01-26 14:32:39', '数据上行、下行类型'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (155, '传输协议', 'transport_protocol', '0', 'iot', '2022-01-27 10:23:53', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (156, '产品公共配置字段', 'product_configuration_public', '0', 'iot', '2022-02-07 15:32:07', 'iot', NULL, '产品公共配置字段'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (157, '接入方式', 'third_platform', '0', 'iot', '2022-02-14 16:17:45', 'iot', '2025-08-09 20:34:10', '第三方接入平台,包括电信动等'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (170, '云云对接配置字段', 'product_configuration_http', '0', 'iot', '2022-07-13 11:20:08', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (171, '设备公共字段 ', 'device_common_property', '0', 'iot', '2022-08-02 11:34:31', 'iot', NULL, 'imei,deviceId,meterNo'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (173, '操作类型', 'sys_oper_type', '0', 'iot', '2022-08-02 14:06:31', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (174, 'tcp配置字段', 'product_configuration_tcp', '0', 'iot', '2022-08-22 15:02:17', 'iot', NULL, 'tcp配置字段'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (175, 'tcp数据转换格式', 'tcp_data_conver_format', '0', 'iot', '2022-08-22 15:05:25', 'iot', '2022-08-23 15:08:48', NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (176, 'tcp解析器类型', 'tcp_parser_type', '0', 'iot', '2022-08-23 15:08:38', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (177, '订阅消息类型', 'subscribe_message_type', '0', 'iot', '2022-09-13 14:21:45', 'iot', '2022-09-13 14:32:24', NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (178, '订阅级别', 'subscribe_level', '0', 'iot', '2022-09-13 14:22:32', 'iot', '2022-09-13 14:32:34', NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (179, 'mqtt对接配置', 'product_configuration_mqtt', '0', 'iot', '2022-09-29 10:00:01', 'iot', '2022-09-29 10:01:06', NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (182, '产品标签', 'product_tags', '0', 'iot', '2023-04-03 15:07:19', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (183, '接口IP白名单', 'whitelist', '0', 'iot', '2023-08-11 10:36:04', 'iot', '2023-08-11 14:08:21', 'web接口白名单'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (186, 'MQTT配置字段', 'mqtt_parser_type', '0', 'iot', '2025-07-06 10:58:33', 'iot', NULL, 'network的mqtt配置信息'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (187, 'HTTP推送配置', 'app_push_http', '0', 'iot', '2025-07-12 10:06:39', 'iot', '2025-07-12 10:13:06', 'http推送配置'); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (188, 'mqtt推送配置', 'app_push_mqtt', '0', 'iot', '2025-07-12 10:14:33', 'iot', NULL, NULL); +INSERT INTO `sys_dict_type` (`dict_id`, `dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (189, '网络TCP组件配置', 'network_tcp_config', '0', 'iot', '2025-07-27 19:07:12', NULL, NULL, NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for sys_job +-- ---------------------------- +DROP TABLE IF EXISTS `sys_job`; +CREATE TABLE `sys_job` ( + `job_id` bigint NOT NULL AUTO_INCREMENT COMMENT '任务ID', + `job_name` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '' COMMENT '任务名称', + `job_group` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT 'DEFAULT' COMMENT '任务组名', + `invoke_target` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '调用目标字符串', + `cron_expression` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT 'cron执行表达式', + `misfire_policy` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '3' COMMENT '计划执行错误策略(1立即执行 2执行一次 3放弃执行)', + `concurrent` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '1' COMMENT '是否并发执行(0允许 1禁止)', + `status` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '0' COMMENT '状态(0正常 1暂停)', + `create_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '备注信息', + PRIMARY KEY (`job_id`,`job_name`,`job_group`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb3 COMMENT='定时任务调度表'; + +-- ---------------------------- +-- Records of sys_job +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for sys_job_log +-- ---------------------------- +DROP TABLE IF EXISTS `sys_job_log`; +CREATE TABLE `sys_job_log` ( + `job_log_id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID', + `job_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务名称', + `job_group` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务组名', + `invoke_target` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '调用目标字符串', + `status` tinyint NOT NULL COMMENT '执行状态(0成功 1失败)', + `job_message` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '任务信息', + `exception_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '异常信息', + `start_time` datetime DEFAULT NULL COMMENT '开始时间', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`job_log_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='定时任务日志表'; + +-- ---------------------------- +-- Records of sys_job_log +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for sys_logininfor +-- ---------------------------- +DROP TABLE IF EXISTS `sys_logininfor`; +CREATE TABLE `sys_logininfor` ( + `info_id` bigint NOT NULL AUTO_INCREMENT COMMENT '访问ID', + `user_name` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '用户账号', + `ipaddr` varchar(128) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '登录IP地址', + `login_location` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '登录地点', + `browser` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '浏览器类型', + `os` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '操作系统', + `status` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '0' COMMENT '登录状态(0成功 1失败)', + `msg` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '提示消息', + `login_time` datetime DEFAULT NULL COMMENT '访问时间', + PRIMARY KEY (`info_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=72 DEFAULT CHARSET=utf8mb3 COMMENT='系统访问记录'; + +-- ---------------------------- +-- Records of sys_logininfor +-- ---------------------------- +BEGIN; +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (1, 'test', '127.0.0.1', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-12 22:39:10'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (2, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-12 22:47:53'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (3, 'test', '127.0.0.1', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-12 22:54:28'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (4, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-12 23:09:36'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (5, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-12 23:34:40'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (6, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-12 23:38:13'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (7, 'test', '113.215.103.83', NULL, 'MSEdge', 'iPhone', '0', '验证码登录成功', '2025-08-13 05:59:40'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (8, 'iot', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-13 09:06:41'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (9, 'iot', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-13 09:41:50'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (10, 'iot', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-13 11:08:43'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (11, 'iot', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-13 20:01:34'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (12, 'test', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-13 20:52:53'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (13, 'iot', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-13 20:58:02'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (14, 'iot', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-15 10:39:35'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (15, 'test', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-15 14:07:18'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (16, 'test', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-15 14:25:49'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (17, 'iot', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-15 15:20:32'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (18, 'iot', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-15 15:26:15'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (19, 'iot', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-15 15:29:28'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (20, 'iot', '58.100.228.247', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-15 15:36:09'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (21, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-15 23:18:43'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (22, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-15 23:27:51'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (23, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-15 23:32:38'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (24, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-15 23:39:06'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (25, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-16 10:03:33'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (26, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-16 22:04:20'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (27, 'test', '127.0.0.1', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-16 22:06:52'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (28, 'test', '127.0.0.1', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-17 13:33:26'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (29, 'test', '127.0.0.1', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-17 16:04:30'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (30, 'iot', '127.0.0.1', NULL, 'Chrome', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-17 16:22:37'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (31, 'test', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-17 16:25:30'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (32, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-17 16:30:55'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (33, 'test', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-17 16:33:38'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (34, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-17 16:35:35'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (35, 'test', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-17 16:36:26'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (36, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '1', '用户名或密码错误', '2025-08-17 16:40:06'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (37, 'iot', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-17 16:40:12'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (38, 'liulihai', '113.248.40.184', NULL, 'Chrome', 'OSX', '1', '用户名或密码错误', '2025-08-18 11:28:19'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (39, 'iot', '30.22.168.190', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-20 09:21:02'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (40, 'iot', '115.209.55.250', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-21 18:25:13'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (41, 'iot', '115.209.55.250', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-21 18:34:55'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (42, 'iot', '125.85.127.165', NULL, 'Chrome', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-23 10:05:38'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (43, 'iot', '125.85.127.165', NULL, 'Chrome', 'OSX', '0', '验证码登录成功', '2025-08-23 19:01:00'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (44, 'test', '113.215.103.83', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-23 19:30:34'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (45, 'iot', '125.85.127.165', NULL, 'Chrome', 'OSX', '0', '验证码登录成功', '2025-08-24 08:55:30'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (46, 'test', '112.24.73.38', NULL, 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-24 18:34:05'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (47, 'test', '49.88.127.121', NULL, 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-25 15:14:41'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (48, 'iot', '58.100.228.246', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-25 17:39:35'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (49, 'iot', '113.215.176.47', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-25 20:50:26'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (50, 'test', '113.245.15.72', NULL, 'Chrome', 'Windows 10 or Windows Server 2016', '1', '用户名或密码错误', '2025-08-25 22:14:24'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (51, 'test', '113.245.15.72', NULL, 'Chrome', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-25 22:14:36'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (52, 'test', '111.160.8.98', NULL, 'MSEdge', 'Linux', '0', '验证码登录成功', '2025-08-26 08:53:29'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (53, 'iot', '58.100.228.246', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-26 10:10:20'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (54, 'test', '223.160.203.241', NULL, 'MSEdge', 'Android', '0', '验证码登录成功', '2025-08-26 10:28:16'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (55, 'test', '222.75.162.82', NULL, 'Chrome', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-26 11:13:24'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (56, 'iot', '58.100.228.246', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-26 12:46:03'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (57, 'test', '111.160.8.102', NULL, 'MSEdge', 'Linux', '0', '验证码登录成功', '2025-08-26 14:09:58'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (58, 'test', '219.145.104.253', NULL, 'Firefox', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-26 14:52:42'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (59, 'test', '111.160.8.98', NULL, 'Chrome', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-26 15:11:36'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (60, 'test', '111.160.8.98', NULL, 'MSEdge', 'Linux', '0', '验证码登录成功', '2025-08-26 15:16:27'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (61, 'iot', '113.215.176.47', NULL, 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-26 20:15:25'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (62, 'iot', '113.215.176.47', NULL, 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-26 20:46:25'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (63, 'iot', '113.215.176.47', NULL, 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-26 21:01:41'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (64, 'iot', '113.215.176.47', NULL, 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-26 21:23:38'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (65, 'test', '219.145.104.253', NULL, 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-27 08:16:52'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (66, 'test', '219.145.104.253', NULL, 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '验证码登录成功', '2025-08-27 09:53:19'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (67, 'test', '113.215.176.47', NULL, 'MSEdge', 'Android', '0', '验证码登录成功', '2025-08-27 22:33:27'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (68, 'test', '125.120.78.252', NULL, 'Chrome', 'Android', '0', '验证码登录成功', '2025-08-28 02:49:07'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (69, 'iot', '113.215.176.47', NULL, 'MSEdge', 'Android', '0', '验证码登录成功', '2025-08-28 07:43:04'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (70, 'iot', '58.100.228.246', NULL, 'MSEdge', 'OSX', '0', '验证码登录成功', '2025-08-28 09:22:24'); +INSERT INTO `sys_logininfor` (`info_id`, `user_name`, `ipaddr`, `login_location`, `browser`, `os`, `status`, `msg`, `login_time`) VALUES (71, 'test', '124.64.23.72', NULL, 'Chrome', 'OSX', '0', '验证码登录成功', '2025-08-28 10:26:39'); +COMMIT; + +-- ---------------------------- +-- Table structure for sys_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sys_menu`; +CREATE TABLE `sys_menu` ( + `menu_id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID', + `menu_name` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '菜单名称', + `parent_id` bigint DEFAULT '0' COMMENT '父菜单ID', + `order_num` int DEFAULT '0' COMMENT '显示顺序', + `path` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '路由地址', + `component` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '组件路径', + `is_frame` int DEFAULT '1' COMMENT '是否为外链(0是 1否)', + `is_cache` int DEFAULT '0' COMMENT '是否缓存(0缓存 1不缓存)', + `menu_type` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)', + `visible` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)', + `status` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '0' COMMENT '菜单状态(0正常 1停用)', + `perms` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '权限标识', + `icon` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '#' COMMENT '菜单图标', + `create_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '备注', + PRIMARY KEY (`menu_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=1251 DEFAULT CHARSET=utf8mb3 COMMENT='菜单权限表'; + +-- ---------------------------- +-- Records of sys_menu +-- ---------------------------- +BEGIN; +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1, '系统管理', 0, 1, 'system', NULL, 1, 0, 'M', '0', '0', '', 'database', 'iot', '2021-12-21 15:51:34', 'iot', NULL, '系统管理目录'); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (100, '用户管理', 1, 1, 'user', 'system/user/index', 1, 0, 'C', '0', '0', 'system:user:list', 'user', 'iot', '2021-12-21 15:51:34', '', NULL, '用户管理菜单'); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (101, '角色管理', 1, 2, 'role', 'system/role/index', 1, 0, 'C', '0', '0', 'system:role:list', 'peoples', 'iot', '2021-12-21 15:51:34', '', NULL, '角色管理菜单'); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (102, '菜单管理', 1, 3, 'menu', 'system/menu/index', 1, 0, 'C', '0', '0', 'system:menu:list', 'color', 'iot', '2021-12-21 15:51:34', 'iot', NULL, '菜单管理菜单'); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (105, '字典管理', 1, 6, 'dict', 'system/dict/index', 1, 0, 'C', '0', '0', 'system:dict:list', 'dict', 'iot', '2021-12-21 15:51:34', '', NULL, '字典管理菜单'); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (106, '参数设置', 1, 7, 'config', 'system/config/index', 1, 0, 'C', '0', '0', 'system:config:list', 'edit', 'iot', '2021-12-21 15:51:34', '', NULL, '参数设置菜单'); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (115, '代码生成', 1, 2, 'gen', 'tool/gen/index', 1, 0, 'C', '0', '0', 'tool:gen:list', 'code', 'iot', '2021-12-21 15:51:35', 'iot', NULL, '代码生成菜单'); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1001, '用户查询', 100, 1, '', '', 1, 0, 'F', '0', '0', 'system:user:query', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1002, '用户新增', 100, 2, '', '', 1, 0, 'F', '0', '0', 'system:user:add', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1003, '用户修改', 100, 3, '', '', 1, 0, 'F', '0', '0', 'system:user:edit', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1004, '用户删除', 100, 4, '', '', 1, 0, 'F', '0', '0', 'system:user:remove', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1005, '用户导出', 100, 5, '', '', 1, 0, 'F', '0', '0', 'system:user:export', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1006, '用户导入', 100, 6, '', '', 1, 0, 'F', '0', '0', 'system:user:import', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1007, '重置密码', 100, 7, '', '', 1, 0, 'F', '0', '0', 'system:user:resetPwd', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1008, '角色查询', 101, 1, '', '', 1, 0, 'F', '0', '0', 'system:role:query', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1009, '角色新增', 101, 2, '', '', 1, 0, 'F', '0', '0', 'system:role:add', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1010, '角色修改', 101, 3, '', '', 1, 0, 'F', '0', '0', 'system:role:edit', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1011, '角色删除', 101, 4, '', '', 1, 0, 'F', '0', '0', 'system:role:remove', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1012, '角色导出', 101, 5, '', '', 1, 0, 'F', '0', '0', 'system:role:export', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1013, '菜单查询', 102, 1, '', '', 1, 0, 'F', '0', '0', 'system:menu:query', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1014, '菜单新增', 102, 2, '', '', 1, 0, 'F', '0', '0', 'system:menu:add', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1015, '菜单修改', 102, 3, '', '', 1, 0, 'F', '0', '0', 'system:menu:edit', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1016, '菜单删除', 102, 4, '', '', 1, 0, 'F', '0', '0', 'system:menu:remove', '#', 'iot', '2021-12-21 15:51:35', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1026, '字典查询', 105, 1, '#', '', 1, 0, 'F', '0', '0', 'system:dict:query', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1027, '字典新增', 105, 2, '#', '', 1, 0, 'F', '0', '0', 'system:dict:add', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1028, '字典修改', 105, 3, '#', '', 1, 0, 'F', '0', '0', 'system:dict:edit', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1029, '字典删除', 105, 4, '#', '', 1, 0, 'F', '0', '0', 'system:dict:remove', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1030, '字典导出', 105, 5, '#', '', 1, 0, 'F', '0', '0', 'system:dict:export', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1031, '参数查询', 106, 1, '#', '', 1, 0, 'F', '0', '0', 'system:config:query', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1032, '参数新增', 106, 2, '#', '', 1, 0, 'F', '0', '0', 'system:config:add', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1033, '参数修改', 106, 3, '#', '', 1, 0, 'F', '0', '0', 'system:config:edit', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1034, '参数删除', 106, 4, '#', '', 1, 0, 'F', '0', '0', 'system:config:remove', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1035, '参数导出', 106, 5, '#', '', 1, 0, 'F', '0', '0', 'system:config:export', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1055, '生成查询', 115, 1, '#', '', 1, 0, 'F', '0', '0', 'tool:gen:query', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1056, '生成修改', 115, 2, '#', '', 1, 0, 'F', '0', '0', 'tool:gen:edit', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1057, '生成删除', 115, 3, '#', '', 1, 0, 'F', '0', '0', 'tool:gen:remove', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1058, '导入代码', 115, 2, '#', '', 1, 0, 'F', '0', '0', 'tool:gen:import', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1059, '预览代码', 115, 4, '#', '', 1, 0, 'F', '0', '0', 'tool:gen:preview', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1060, '生成代码', 115, 5, '#', '', 1, 0, 'F', '0', '0', 'tool:gen:code', '#', 'iot', '2021-12-21 15:51:36', '', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1081, '规则引擎', 0, 20, 'rule', NULL, 1, 0, 'M', '0', '0', '', 'cluster', 'iot', NULL, 'root', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1082, '数据转发', 1081, 3, 'flow', 'rule/flow/index', 1, 0, 'C', '0', '0', '', 'box-plot', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1093, '产品开发', 0, 2, 'product', NULL, 1, 0, 'M', '0', '0', NULL, 'windows', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1095, '产品管理', 1093, 0, 'product', 'system/product/index', 1, 0, 'C', '0', '0', 'tool:product:list', 'gateway', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1096, '产品分类', 1093, 1, 'sort', 'system/sort/index', 1, 0, 'C', '0', '0', 'tool:sort:list', 'appstore', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1097, '设备管理', 0, 3, 'iot', NULL, 1, 0, 'M', '0', '0', NULL, 'deployment-unit', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1098, '设备列表', 1097, 0, 'device', 'system/instance/index', 1, 0, 'C', '0', '0', 'tool:instance:list', 'bars', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1099, '设备群组', 1097, 1, 'group', 'system/instance/group/index', 1, 0, 'C', '0', '0', 'tool:instance:group:list', 'database', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1100, '协议管理', 1093, 2, 'protocol', 'system/protocol/index', 1, 0, 'C', '0', '0', 'protocol:protocol:list', 'box-plot', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1101, '协议查询', 1100, 0, NULL, NULL, 1, 0, 'F', '0', '0', 'protocol:protocol:query', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1102, '协议新增', 1100, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'protocol:protocol:add', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1103, '协议修改', 1100, 2, NULL, NULL, 1, 0, 'F', '0', '0', 'protocol:protocol:edit', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1104, '协议删除', 1100, 3, NULL, NULL, 1, 0, 'F', '0', '0', 'protocol:protocol:remove', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1105, '协议导出', 1100, 4, NULL, NULL, 1, 0, 'F', '0', '0', 'protocol:protocol:export', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1106, '调试', 1100, 5, NULL, NULL, 1, 0, 'F', '0', '0', 'protocol:protocol:codec', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1107, '应用开发', 1156, 1, 'applicationList', 'application/index', 1, 0, 'C', '0', '0', 'application:application:list', 'codepen-circle', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1108, '用户应用信息查询', 1107, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'application:application:query', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1109, '用户应用信息新增', 1107, 2, NULL, NULL, 1, 0, 'F', '0', '0', 'application:application:add', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1110, '用户应用信息修改', 1107, 3, NULL, NULL, 1, 0, 'F', '0', '0', 'application:application:edit', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1111, '用户应用信息删除', 1107, 4, NULL, NULL, 1, 0, 'F', '0', '0', 'application:application:remove', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1112, '用户应用信息导出', 1107, 5, NULL, NULL, 1, 0, 'F', '0', '0', 'application:application:export', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1113, '应用绑定', 1107, 6, NULL, NULL, 1, 0, 'F', '0', '0', 'application:application:bind', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1114, '应用解绑', 1107, 7, NULL, NULL, 1, 0, 'F', '0', '0', 'application:application:unbind', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1115, '重置密钥', 1107, 8, NULL, NULL, 1, 0, 'F', '0', '0', 'application:application:resetSecret', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1116, '产品配置', 1095, 0, NULL, NULL, 1, 0, 'F', '0', '0', 'system:product:config', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1117, '产品详情', 1095, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'system:product:query', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1118, '产品新增', 1095, 2, NULL, NULL, 1, 0, 'F', '0', '0', 'system:product:add', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1119, '产品修改', 1095, 3, NULL, NULL, 1, 0, 'F', '0', '0', 'system:product:edit', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1120, '产品删除', 1095, 4, NULL, NULL, 1, 0, 'F', '0', '0', 'system:product:remove', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1121, '设备新增', 1098, 0, NULL, NULL, 1, 0, 'F', '0', '0', 'system:instance:add', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1122, '设备修改', 1098, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'system:instance:edit', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1123, '设备删除', 1098, 2, NULL, NULL, 1, 0, 'F', '0', '0', 'system:instance:remove', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1124, '设备详情', 1098, 4, NULL, NULL, 1, 0, 'F', '0', '0', 'system:instance:query', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1125, '物模型修改', 1095, 5, NULL, NULL, 1, 0, 'F', '0', '0', 'system:metadata:edit', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1126, '物模型导入', 1095, 6, NULL, NULL, 1, 0, 'F', '0', '0', 'system:metadata:export', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1127, '物模型新增', 1095, 7, NULL, NULL, 1, 0, 'F', '0', '0', 'system:metadata:add', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1128, '物模型删除', 1095, 8, NULL, NULL, 1, 0, 'F', '0', '0', 'system:metadata:delete', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1130, '详情', 1082, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'rule:flow:detail', '#', 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1131, '修改状态', 1082, 2, NULL, NULL, 1, 0, 'F', '0', '0', 'rule:flow:status', '#', 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1132, '删除', 1082, 3, NULL, NULL, 1, 0, 'F', '0', '0', 'rule:flow:del', '#', 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1133, '新增', 1082, 0, NULL, NULL, 1, 0, 'F', '0', '0', 'rule:flow:add', '#', 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1140, '场景联动', 1081, 1, 'linkage', 'rule/linkage/index', 1, 0, 'C', '0', '0', 'rule:linkage:list', 'file-sync', 'iot', '2022-03-01 10:26:39', 'iot', NULL, '场景联动菜单'); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1141, '场景联动查询', 1140, 1, '#', '', 1, 0, 'F', '0', '0', 'rule:linkage:query', '#', 'iot', '2022-03-01 10:26:39', 'zhuqy', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1142, '场景联动新增', 1140, 2, '#', '', 1, 0, 'F', '0', '0', 'rule:linkage:add', '#', 'iot', '2022-03-01 10:26:39', 'zhuqy', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1143, '场景联动修改', 1140, 3, '#', '', 1, 0, 'F', '0', '0', 'rule:linkage:edit', '#', 'iot', '2022-03-01 10:26:39', 'zhuqy', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1144, '场景联动删除', 1140, 4, '#', '', 1, 0, 'F', '0', '0', 'rule:linkage:remove', '#', 'iot', '2022-03-01 10:26:39', 'zhuqy', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1145, '场景联动导出', 1140, 5, '#', '', 1, 0, 'F', '0', '0', 'rule:linkage:export', '#', 'iot', '2022-03-01 10:26:39', 'zhuqy', NULL, ''); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1153, '设备详情跳转', 1107, 9, NULL, NULL, 1, 0, 'F', '0', '0', 'application:application:details', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1156, '北向应用', 0, 5, 'app', NULL, 1, 0, 'M', '0', '0', NULL, 'codepen-circle', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1157, '应用详情', 1107, 5, NULL, NULL, 1, 0, 'F', '0', '0', 'application:application:bindShow', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1158, '手动触发', 1140, 6, NULL, NULL, 1, 0, 'F', '0', '0', 'rule:linkage:exec', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1172, '日志管理', 1, 8, 'monitor', NULL, 1, 0, 'M', '0', '0', NULL, 'color', 'iot', NULL, 'root', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1174, '登录日志', 1172, 1, 'logininfor', 'monitor/logininfor/index', 1, 1, 'C', '0', '0', 'system:logininfor:list', 'bug', 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1175, '操作日志', 1172, 2, 'operlog', 'monitor/operlog/index', 1, 1, 'C', '0', '0', 'system:operlog:list', 'build', 'iot', NULL, 'root', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1176, '产品导出', 1095, 9, NULL, NULL, 1, 0, 'F', '0', '0', 'system:product:export', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1177, '产品导入', 1095, 10, NULL, NULL, 1, 0, 'F', '0', '0', 'system:product:import', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1178, '设备导入 ', 1095, 11, NULL, NULL, 1, 0, 'F', '0', '0', 'product:device:import', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1179, '批量指令', 1156, 2, 'batchCommand', 'system/instance/config/index', 1, 0, 'C', '0', '0', 'tool:instance:config:list', 'fast-forward', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1181, '批量配置', 1179, 0, NULL, NULL, 1, 0, 'F', '0', '0', 'device:batch:config', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1182, '批量历史', 1179, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'device:batch:history', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1184, '国标视频', 0, 4, 'gb', NULL, 1, 0, 'M', '1', '1', NULL, 'instagram', 'iot', NULL, 'liulihai', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1185, '视频列表', 1184, 1, 'camera', 'gb/camera/index', 1, 0, 'C', '0', '0', 'gb:camera:index', 'branches', 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1186, '第三方应用', 1184, 10, 'thridApplication', 'gb/application/index', 1, 0, 'C', '0', '0', 'gb:thridApplication:index', 'cascader', 'iot', NULL, 'wjj', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1187, '新增', 1185, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'gb:camera:add', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1188, '删除', 1185, 6, NULL, NULL, 1, 0, 'F', '0', '0', 'gb:camera:delete', NULL, 'iot', NULL, 'wjj', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1189, '修改', 1185, 11, NULL, NULL, 1, 0, 'F', '0', '0', 'gb:camera:edit', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1190, '查看', 1185, 16, NULL, NULL, 1, 0, 'F', '0', '0', 'gb:camera:view', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1191, '导入', 1185, 21, NULL, NULL, 1, 0, 'F', '0', '0', 'gb:camera:import', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1192, '导出', 1185, 26, NULL, NULL, 1, 0, 'F', '0', '0', 'gb:camera:export', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1193, '工作台', 0, 0, 'index', 'Index', 1, 1, 'M', '0', '0', '', 'ie', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1194, '网络组件', 0, 4, 'network', NULL, 1, 0, 'M', '0', '0', NULL, 'wifi', 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1195, 'MQTT服务', 1194, 1, 'mqtt', 'MqttNetwork', 1, 1, 'C', '0', '0', 'tool:mqtt:list', 'tree', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1196, 'TCP服务', 1194, 2, 'tcp', 'TcpNetwork', 1, 1, 'C', '0', '0', 'tool:tcp:list', 'tree', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1197, '新增', 1195, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'network:mqtt:add', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1198, '编辑', 1195, 2, NULL, NULL, 1, 0, 'F', '0', '0', 'network:mqtt:edit', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1199, '删除', 1195, 3, NULL, NULL, 1, 0, 'F', '0', '0', 'network:mqtt:del', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1200, '导出', 1195, 4, NULL, NULL, 1, 0, 'F', '0', '0', 'network:mqtt:export', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1201, '新增', 1196, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'network:tcp:add', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1202, '编辑', 1196, 2, NULL, NULL, 1, 0, 'F', '0', '0', 'network:tcp:edit', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1203, '删除', 1196, 3, NULL, NULL, 1, 0, 'F', '0', '0', 'network:tcp:del', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1204, '导出', 1196, 4, NULL, NULL, 1, 0, 'F', '0', '0', 'network:tcp:export', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1205, '启动', 1195, 6, NULL, NULL, 1, 0, 'F', '0', '0', 'network:mqtt:start', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1206, '暂停', 1195, 7, NULL, NULL, 1, 0, 'F', '0', '0', 'network:mqtt:stop', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1207, '重启', 1195, 8, NULL, NULL, 1, 0, 'F', '0', '0', 'network:mqtt:restart', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1208, '启动', 1196, 6, NULL, NULL, 1, 0, 'F', '0', '0', 'network:tcp:start', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1209, '暂停', 1196, 7, NULL, NULL, 1, 0, 'F', '0', '0', 'network:tcp:stop', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1210, '重启', 1196, 8, NULL, NULL, 1, 0, 'F', '0', '0', 'network:tcp:restart', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1211, '通知管理', 0, 6, 'noticeManager', NULL, 1, 0, 'M', '0', '0', NULL, 'guide', 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1212, '通知渠道', 1211, 1, 'channel', 'NoticeChannel', 1, 0, 'C', '0', '0', 'notice:channel:list', 'cascader', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1213, '新增', 1212, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'notice:channel:add', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1214, '编辑', 1212, 2, NULL, NULL, 1, 0, 'F', '0', '0', 'notice:channel:edit', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1215, '详情', 1212, 3, NULL, NULL, 1, 0, 'F', '0', '0', 'notice:channel:detail', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1216, '删除', 1212, 4, NULL, NULL, 1, 0, 'F', '0', '0', 'notice:channel:del', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1217, '通知模版', 1211, 2, 'noticeTemplate', 'NoticeTemplate', 1, 0, 'C', '0', '0', 'notice:template:list', 'dict', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1218, '新增', 1217, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'notice:template:add', NULL, 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1219, '编辑', 1217, 2, NULL, NULL, 1, 0, 'F', '0', '0', 'notice:template:edit', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1220, '详情', 1217, 3, NULL, NULL, 1, 0, 'F', '0', '0', 'notice:template:detail', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1221, '删除', 1217, 4, NULL, NULL, 1, 0, 'F', '0', '0', 'notice:template:del', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1227, '执行流程', 1140, 4, NULL, NULL, 1, 0, 'F', '0', '0', 'rule:visual:execute', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1228, '启用/禁用流程', 1140, 5, NULL, NULL, 1, 0, 'F', '0', '0', 'rule:visual:status', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1229, '删除流程', 1140, 6, NULL, NULL, 1, 0, 'F', '0', '0', 'rule:visual:del', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1230, '流程编排', 0, 99, 'nodeRed', NULL, 1, 0, 'M', '1', '1', NULL, 'documentation', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1231, '流程设计', 1230, 1, 'design', 'NodeRedFlowDesigner', 1, 0, 'C', '0', '0', 'nodered:flow:design', 'code', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1232, '保存', 1231, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'nodered:flow:design', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1233, '部署', 1231, 2, NULL, NULL, 1, 0, 'F', '0', '0', 'nodered:flow:deploy', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1234, '流程列表', 1230, 2, 'flowList', 'NodeRedFlowList', 1, 0, 'C', '0', '0', 'nodered:flow:list', 'button', 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1235, '新建', 1234, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'nodered:flow:create', NULL, 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1236, '编辑', 1234, 2, NULL, NULL, 1, 0, 'F', '0', '0', 'nodered:flow:edit', NULL, 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1237, '部署', 1234, 3, NULL, NULL, 1, 0, 'F', '0', '0', 'nodered:flow:deploy', NULL, 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1238, '删除', 1234, 4, NULL, NULL, 1, 0, 'F', '0', '0', 'nodered:flow:delete', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1245, '执行日志', 1230, 7, 'nodeRedExecutionLogs', 'NodeRedExecutionLogs', 1, 0, 'C', '0', '0', NULL, 'tree', 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1247, '证书管理', 1194, 4, 'cert', 'CertificateManager', 1, 0, 'C', '0', '0', 'network:cert:list', 'dict', 'iot', NULL, 'iot', NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1248, '新增', 1247, 1, NULL, NULL, 1, 0, 'F', '0', '0', 'network:cert:add', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1249, '删除', 1247, 2, NULL, NULL, 1, 0, 'F', '0', '0', 'network:cert:delete', NULL, 'iot', NULL, NULL, NULL, NULL); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1250, '修改', 1247, 3, NULL, NULL, 1, 0, 'F', '0', '0', 'network:cert:edit', NULL, 'iot', NULL, NULL, NULL, NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for sys_oper_log +-- ---------------------------- +DROP TABLE IF EXISTS `sys_oper_log`; +CREATE TABLE `sys_oper_log` ( + `oper_id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志主键', + `title` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '模块标题', + `business_type` int DEFAULT '0' COMMENT '业务类型(0其它 1新增 2修改 3删除)', + `method` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '方法名称', + `request_method` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '请求方式', + `operator_type` int DEFAULT '0' COMMENT '操作类别(0其它 1后台用户 2手机端用户)', + `oper_name` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '操作人员', + `dept_name` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '部门名称', + `oper_url` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '请求URL', + `oper_ip` varchar(128) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '主机地址', + `oper_location` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '操作地点', + `oper_param` varchar(2000) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '请求参数', + `json_result` varchar(2000) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '返回参数', + `status` int DEFAULT '0' COMMENT '操作状态(0正常 1异常)', + `error_msg` varchar(2000) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '错误消息', + `oper_time` datetime DEFAULT NULL COMMENT '操作时间', + PRIMARY KEY (`oper_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=145 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='操作日志记录'; + +-- ---------------------------- +-- Records of sys_oper_log +-- ---------------------------- +BEGIN; +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (1, '删除场景联动', 3, 'cn.universal.admin.system.web.SceneLinkageController.remove()', 'DELETE', 1, 'iot', NULL, '/admin/v1/scene/linkage/64', '113.215.103.83', '113.215.103.83', '{ids=64}', '{\"code\":0,\"msg\":\"success\"}', 0, NULL, '2025-08-12 20:23:59'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (2, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '127.0.0.1', '127.0.0.1', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-12 23:03:21'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (3, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'test', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '127.0.0.1', '127.0.0.1', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"test\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', NULL, 1, '\n### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Unknown column \'device_node\' in \'field list\'\n### The error may exist in cn/universal/persistence/mapper/IoTDeviceMapper.java (best guess)\n### The error may involve cn.universal.persistence.mapper.IoTDeviceMapper.selectOne-Inline\n### The error occurred while setting parameters\n### SQL: SELECT application,areas_id,configuration,coordinate,create_time,creator_id,creator_name,derive_metadata,detail,device_id,device_name,device_node,device_secret,ext_device_id,features,gw_product_key,id,instance,iot_id,nick_name,online_time,product_key,product_name,registry_time,state FROM iot_device WHERE device_id = ? AND product_key = ?\n### Cause: java.sql.SQLSyntaxErrorException: Unknown column \'device_node\' in \'field list\'\n; bad SQL grammar []', '2025-08-12 23:03:29'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (4, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'test', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '127.0.0.1', '127.0.0.1', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"test\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', NULL, 1, '\n### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Unknown column \'device_node\' in \'field list\'\n### The error may exist in cn/universal/persistence/mapper/IoTDeviceMapper.java (best guess)\n### The error may involve cn.universal.persistence.mapper.IoTDeviceMapper.selectOne-Inline\n### The error occurred while setting parameters\n### SQL: SELECT application,areas_id,configuration,coordinate,create_time,creator_id,creator_name,derive_metadata,detail,device_id,device_name,device_node,device_secret,ext_device_id,features,gw_product_key,id,instance,iot_id,nick_name,online_time,product_key,product_name,registry_time,state FROM iot_device WHERE device_id = ? AND product_key = ?\n### Cause: java.sql.SQLSyntaxErrorException: Unknown column \'device_node\' in \'field list\'\n; bad SQL grammar []', '2025-08-12 23:03:30'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (5, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-12 23:09:39'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (6, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-12 23:09:44'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (7, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"e89f57831a6c4a9b8b0fdf635413d660\"}', 0, NULL, '2025-08-12 23:09:48'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (8, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"8fcb737774654918b2bdbac711a73486\"}', 0, NULL, '2025-08-12 23:09:49'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (9, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"55c11987a947458b89b96eb177efe031\"}', 0, NULL, '2025-08-12 23:09:49'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (10, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"fceb1ac348734d3fb6d78be9c23a70bb\"}', 0, NULL, '2025-08-12 23:09:58'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (11, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"5fc7f129fdfb499d83593a99778e43a3\"}', 0, NULL, '2025-08-12 23:09:58'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (12, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"6608d439a9b444ce8aee318223b039b2\"}', 0, NULL, '2025-08-12 23:09:58'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (13, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"A5A50001030201015A5A\"},\"requestId\":\"d64d87db132b4883aa27f29551aa4465\"}', 0, NULL, '2025-08-12 23:12:04'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (14, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"A5A50001030201015A5A\"},\"requestId\":\"126b5b2be0f84681828d6af2ff6e8c6a\"}', 0, NULL, '2025-08-12 23:12:07'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (15, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"A5A50001030201015A5A\"},\"requestId\":\"cf84f320f5b54963a7ca473d6cc8c8e4\"}', 0, NULL, '2025-08-12 23:12:33'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (16, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"A5A50001030201015A5A\"},\"requestId\":\"6417190d508f45c2a2d445d29d47efba\"}', 0, NULL, '2025-08-12 23:12:35'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (17, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"A5A50001030201015A5A\"},\"requestId\":\"03b491c77c9b49e5bb36341b0fdca9aa\"}', 0, NULL, '2025-08-12 23:12:35'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (18, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"A5A50001030201015A5A\"},\"requestId\":\"75913b031ef949bfa8a5d8108bf588be\"}', 0, NULL, '2025-08-12 23:12:36'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (19, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-12 23:34:48'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (20, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"0a44948dec374d68941404b1a2423a3b\"}', 0, NULL, '2025-08-12 23:34:52'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (21, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"fa77d883574346a6b3f25a66dba33422\"}', 0, NULL, '2025-08-12 23:34:57'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (22, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"4d2236ddf04a49e186dde290c054487c\"}', 0, NULL, '2025-08-12 23:35:14'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (23, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"5e742f0bc3654cd3870cbe079368685a\"}', 0, NULL, '2025-08-12 23:36:13'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (24, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"cd4ec080b7af4951b0303d3abfa41368\"}', 0, NULL, '2025-08-12 23:37:47'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (25, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"71064262a82646478856f4ed9177cc1d\"}', 0, NULL, '2025-08-12 23:37:48'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (26, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-12 23:38:17'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (27, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-12 23:38:22'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (28, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"f3025d12aa434e249c249595a6b8258a\"}', 0, NULL, '2025-08-12 23:38:25'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (29, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"7577c36215e54484897ea49370daedef\"}', 0, NULL, '2025-08-12 23:38:33'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (30, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/688cd92fdb9e0158872f4aa9', '113.215.103.83', '113.215.103.83', '688cd92fdb9e0158872f4aa9 {\"appUnionId\":\"iot\",\"productKey\":\"688cd92fdb9e0158872f4aa9\",\"deviceId\":\"docsmqtt004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switch\",\"data\":{\"deviceId\":\"docsmqtt004\",\"state\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"bf3634d903094bdc91b64a9e8fb5f4f0\"}', 0, NULL, '2025-08-12 23:38:34'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (31, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-13 06:00:08'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (32, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-13 09:06:45'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (33, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"meterRead\",\"data\":{\"meterNo\":\"202507070079\"}}}', '{\"code\":723,\"msg\":\"设备与平台连接已断开,指令已暂存3天(同指令以最新为准),重连后自动发送\",\"requestId\":\"965202172e7a4ff4bc4fe536b5eb692c\"}', 0, NULL, '2025-08-13 09:06:54'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (34, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchOnOff\",\"data\":{\"meterNo\":\"202507070079\",\"valveOn\":\"0\"}}}', '{\"code\":723,\"msg\":\"设备与平台连接已断开,指令已暂存3天(同指令以最新为准),重连后自动发送\",\"requestId\":\"33f150c938894a74b9a1df944ecb24ff\"}', 0, NULL, '2025-08-13 09:07:10'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (35, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchOnOff\",\"data\":{\"meterNo\":\"202507070079\",\"valveOn\":\"0\"}}}', '{\"code\":723,\"msg\":\"设备与平台连接已断开,指令已暂存3天(同指令以最新为准),重连后自动发送\",\"requestId\":\"d5672e1d1b234207a9552aecf7cd847e\"}', 0, NULL, '2025-08-13 09:07:13'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (36, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchOnOff\",\"data\":{\"meterNo\":\"202507070079\",\"valveOn\":\"0\"}}}', '{\"code\":723,\"msg\":\"设备与平台连接已断开,指令已暂存3天(同指令以最新为准),重连后自动发送\",\"requestId\":\"e24010dd76a444ba8b3b792a84c662c3\"}', 0, NULL, '2025-08-13 09:07:13'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (37, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-13 09:42:01'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (38, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchOnOff\",\"data\":{\"meterNo\":\"202507070079\",\"valveOn\":\"0\"}}}', '{\"code\":723,\"msg\":\"设备与平台连接已断开,指令已暂存3天(同指令以最新为准),重连后自动发送\",\"requestId\":\"ab0cb37dcc4949d2b0f882e808f467b5\"}', 0, NULL, '2025-08-13 09:42:36'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (39, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-13 09:47:00'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (40, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202505220093\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"meterRead\",\"data\":{\"meterNo\":\"202505220093\"}}}', '{\"code\":723,\"msg\":\"设备与平台连接已断开,指令已暂存3天(同指令以最新为准),重连后自动发送\",\"requestId\":\"315631e709174eaca4399bae92b25290\"}', 0, NULL, '2025-08-13 09:47:07'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (41, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-13 20:41:26'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (42, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-13 20:58:05'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (43, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-13 21:11:57'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (44, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchOnOff\",\"data\":{\"meterNo\":\"202507070079\",\"valveOn\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"68790007072520681c1035333333348967454d3347773b3a443c3216\"},\"requestId\":\"d152dfaf73104d768609954e08b7cdaa\"}', 0, NULL, '2025-08-13 21:12:10'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (45, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchOnOff\",\"data\":{\"meterNo\":\"202507070079\",\"valveOn\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"68790007072520681c1035333333348967454d3347773b3a443c3216\"},\"requestId\":\"b6bc9726ff1a4c90bf7bdbeecfcac4cf\"}', 0, NULL, '2025-08-13 21:13:31'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (46, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchOnOff\",\"data\":{\"meterNo\":\"202507070079\",\"valveOn\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"68790007072520681c1035333333348967454d3347773b3a443c3216\"},\"requestId\":\"1d5021c2a7de45bebcffa2eeac7a1700\"}', 0, NULL, '2025-08-13 21:13:35'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (47, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchOnOff\",\"data\":{\"meterNo\":\"202507070079\",\"valveOn\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"68790007072520681c1035333333348967454e3347773b3a443c3316\"},\"requestId\":\"f8e259a6b7aa483c80ae976e6a2bac54\"}', 0, NULL, '2025-08-13 21:13:57'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (48, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 10:39:40'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (49, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"meterRead\",\"data\":{\"meterNo\":\"202507070079\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"687900070725206811043335c4331016\"},\"requestId\":\"d5a66b891d9f4474bd91c02e92b931d5\"}', 0, NULL, '2025-08-15 10:39:46'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (50, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchOnOff\",\"data\":{\"meterNo\":\"202507070079\",\"valveOn\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"68790007072520681c1035333333348967454d3347773b3a443c3216\"},\"requestId\":\"b8bc5ae66e5c485fb5930ef6bfa79729\"}', 0, NULL, '2025-08-15 10:40:15'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (51, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchOnOff\",\"data\":{\"meterNo\":\"202507070079\",\"valveOn\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"68790007072520681c1035333333348967454e3347773b3a443c3316\"},\"requestId\":\"f3298a3c610d4df9b550d3b0f5f4e8de\"}', 0, NULL, '2025-08-15 10:40:18'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (52, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchOnOff\",\"data\":{\"meterNo\":\"202507070079\",\"valveOn\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"68790007072520681c1035333333348967454e3347773b3a443c3316\"},\"requestId\":\"ab14d85c17694c0f8ceca3fa0aefd726\"}', 0, NULL, '2025-08-15 10:40:28'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (53, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/abcxassxnanahsman', '58.100.228.247', '58.100.228.247', 'abcxassxnanahsman {\"appUnionId\":\"iot\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202507070079\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchOnOff\",\"data\":{\"meterNo\":\"202507070079\",\"valveOn\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"payload\":\"68790007072520681c1035333333348967454d3347773b3a443c3216\"},\"requestId\":\"fd4184f5c6784a7ebcc5859a4cba00c6\"}', 0, NULL, '2025-08-15 10:40:32'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (54, '修改菜单', 2, 'cn.universal.admin.system.web.SysMenuController.edit()', 'PUT', 1, 'iot', NULL, '/admin/system/menu', '58.100.228.247', '58.100.228.247', '{\"menuId\":1193,\"menuName\":\"工作台\",\"parentId\":0,\"orderNum\":\"0\",\"path\":\"index\",\"component\":\"Index\",\"isFrame\":\"1\",\"isCache\":\"1\",\"menuType\":\"M\",\"visible\":\"0\",\"status\":\"0\",\"perms\":\"\",\"icon\":\"ie\",\"createBy\":\"iot\",\"updateBy\":\"iot\",\"params\":{},\"children\":[]}', '{\"code\":0,\"msg\":\"success\"}', 0, NULL, '2025-08-15 13:51:13'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (55, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 14:14:03'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (56, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 14:15:41'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (57, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 14:17:15'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (58, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 14:17:57'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (59, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 14:19:30'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (60, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 14:20:49'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (61, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 14:21:55'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (62, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 14:25:55'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (63, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'test', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '58.100.228.247', '58.100.228.247', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"test\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov00014117\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchControl\",\"data\":{\"open\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"20\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"requestId\":\"0a58a9f068154ac4b13031cb38296fc6\"}', 0, NULL, '2025-08-15 14:32:35'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (64, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'test', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '58.100.228.247', '58.100.228.247', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"test\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov00014117\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchControl\",\"data\":{\"open\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"21\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"1\"},\"function\":\"switchControl\"},\"requestId\":\"0e661ac0767840ee90b59058e7c60a33\"}', 0, NULL, '2025-08-15 14:32:39'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (65, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '58.100.228.247', '58.100.228.247', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 14:34:22'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (66, '新增电信公共产品', 1, 'cn.universal.admin.platform.web.IoTProductController.addCtwingPubPro()', 'POST', 1, 'iot', NULL, '/admin/v1/product/ctwing/pubproadd', '58.100.228.247', '58.100.228.247', '{\"id\":null,\"productId\":\"GENS-1PLO\",\"productKey\":null,\"productSecret\":null,\"thirdPlatform\":\"ctaiot\",\"thirdConfiguration\":null,\"companyNo\":\"上海智洋网络科技有限公司\",\"classifiedId\":\"100100\",\"configuration\":null,\"networkWay\":null,\"projectName\":null,\"projectId\":null,\"classifiedName\":\"智慧电表\",\"messageProtocol\":null,\"orgId\":null,\"name\":null,\"creatorId\":null,\"describe\":null,\"storePolicy\":null,\"storePolicyConfiguration\":null,\"photoUrl\":null,\"protocolName\":null,\"metadata\":null,\"createTime\":null,\"state\":null,\"publicProductID\":\"99000490\"}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"code\":0,\"msg\":\"success\",\"data\":1,\"requestId\":\"7917e4c4392d4792bbabdee5870ac55d\"}}', 0, NULL, '2025-08-15 15:37:03'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (67, '新增电信公共产品', 1, 'cn.universal.admin.platform.web.IoTProductController.addCtwingPubPro()', 'POST', 1, 'iot', NULL, '/admin/v1/product/ctwing/pubproadd', '58.100.228.247', '58.100.228.247', '{\"id\":null,\"productId\":\"GENS-1PLO\",\"productKey\":null,\"productSecret\":null,\"thirdPlatform\":\"ctaiot\",\"thirdConfiguration\":null,\"companyNo\":\"上海智洋网络科技有限公司\",\"classifiedId\":\"100100\",\"configuration\":null,\"networkWay\":null,\"projectName\":null,\"projectId\":null,\"classifiedName\":\"智慧电表\",\"messageProtocol\":null,\"orgId\":null,\"name\":null,\"creatorId\":null,\"describe\":null,\"storePolicy\":null,\"storePolicyConfiguration\":null,\"photoUrl\":null,\"protocolName\":null,\"metadata\":null,\"createTime\":null,\"state\":null,\"publicProductID\":\"99000490\"}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"code\":0,\"msg\":\"success\",\"data\":1,\"requestId\":\"6a480c99bb5f43e895472b559a425b89\"}}', 0, NULL, '2025-08-15 15:37:03'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (68, '删除设备产品', 3, 'cn.universal.admin.platform.web.IoTProductController.remove()', 'DELETE', 1, 'iot', NULL, '/admin/v1/product/540', '58.100.228.247', '58.100.228.247', '{ids=540}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"code\":0,\"msg\":\"success\",\"data\":1,\"requestId\":\"1397159971ab43908593e9a4cddbd012\"}}', 0, NULL, '2025-08-15 15:37:15'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (69, '删除设备产品', 3, 'cn.universal.admin.platform.web.IoTProductController.remove()', 'DELETE', 1, 'iot', NULL, '/admin/v1/product/541', '58.100.228.247', '58.100.228.247', '{ids=541}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"code\":0,\"msg\":\"success\",\"data\":1,\"requestId\":\"611bd69bb2924b97b97f271584ed7308\"}}', 0, NULL, '2025-08-15 15:55:36'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (70, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 23:27:59'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (71, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 23:32:52'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (72, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-15 23:39:13'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (73, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-16 10:43:35'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (74, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-16 22:04:41'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (75, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '127.0.0.1', '127.0.0.1', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-16 22:06:59'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (76, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '127.0.0.1', '127.0.0.1', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-16 22:07:20'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (77, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '127.0.0.1', '127.0.0.1', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-16 22:08:15'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (78, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '127.0.0.1', '127.0.0.1', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-16 22:08:46'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (79, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '127.0.0.1', '127.0.0.1', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-16 22:09:05'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (80, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '127.0.0.1', '127.0.0.1', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-16 22:09:07'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (81, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '127.0.0.1', '127.0.0.1', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-16 22:09:09'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (82, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '127.0.0.1', '127.0.0.1', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 13:33:38'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (83, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '127.0.0.1', '127.0.0.1', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 13:35:54'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (84, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 16:25:54'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (85, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 16:31:04'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (86, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 16:32:16'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (87, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 16:33:08'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (88, '新增设备', 1, 'cn.universal.admin.platform.web.IoTDeviceController.add()', 'POST', 1, 'test', NULL, '/admin/v1/device', '113.215.103.83', '113.215.103.83', '{\"deviceId\":\"ov0000004\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceName\":\"测试验证\",\"latitude\":\"32.169945\",\"longitude\":\"117.242386\",\"params\":{},\"otherParams\":{}}', '{\"code\":0,\"msg\":\"success\",\"requestId\":\"165e02278ff948bd9aec523e329601c6\"}', 0, NULL, '2025-08-17 16:34:33'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (89, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 16:35:53'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (90, '新增设备', 1, 'cn.universal.admin.platform.web.IoTDeviceController.add()', 'POST', 1, 'test', NULL, '/admin/v1/device', '113.215.103.83', '113.215.103.83', '{\"deviceId\":\"ov0000004\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceName\":\"ov0000004\",\"latitude\":\"33.61474\",\"longitude\":\"114.230893\",\"params\":{},\"otherParams\":{}}', '{\"code\":0,\"msg\":\"success\",\"requestId\":\"8a119422cd0f4ac6981458c640bcf4e3\"}', 0, NULL, '2025-08-17 16:39:24'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (91, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 16:40:16'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (92, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 16:43:58'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (93, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.103.83', '113.215.103.83', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchControl\",\"data\":{\"open\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"16\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"requestId\":\"d9e1a3f9c1444e11b5002ff9e9ec3c8c\"}', 0, NULL, '2025-08-17 16:44:07'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (94, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.103.83', '113.215.103.83', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchControl\",\"data\":{\"open\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"17\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"requestId\":\"523dc81139754475ab7bcff42ea4f368\"}', 0, NULL, '2025-08-17 16:45:14'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (95, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 16:49:14'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (96, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.103.83', '113.215.103.83', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchControl\",\"data\":{\"open\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"20\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"requestId\":\"8b7a81a617e64af1a9fac02aa20385a2\"}', 0, NULL, '2025-08-17 16:49:50'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (97, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 17:13:54'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (98, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 17:14:48'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (99, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 17:31:16'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (100, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.103.83', '113.215.103.83', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-17 17:41:16'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (101, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '115.209.55.250', '115.209.55.250', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-21 18:28:39'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (102, '新增设备', 1, 'cn.universal.admin.platform.web.IoTDeviceController.add()', 'POST', 1, 'iot', NULL, '/admin/v1/device', '115.209.55.250', '115.209.55.250', '{\"deviceId\":\"ov0000005\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceName\":\"ov0000005\",\"latitude\":\"30.259898\",\"longitude\":\"120.532174\",\"params\":{},\"otherParams\":{}}', '{\"code\":500,\"msg\":\"deivceId不存在\",\"requestId\":\"1426b7e323904666bd854f9a1a1593e0\"}', 0, NULL, '2025-08-21 18:29:12'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (103, '新增设备', 1, 'cn.universal.admin.platform.web.IoTDeviceController.add()', 'POST', 1, 'iot', NULL, '/admin/v1/device', '115.209.55.250', '115.209.55.250', '{\"deviceId\":\"ov0000005\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceName\":\"ov0000005\",\"latitude\":\"30.259898\",\"longitude\":\"120.532174\",\"params\":{},\"otherParams\":{}}', '{\"code\":500,\"msg\":\"deivceId不存在\",\"requestId\":\"44942fbba58c4d889d778d6d27158e75\"}', 0, NULL, '2025-08-21 18:29:18'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (104, '新增设备', 1, 'cn.universal.admin.platform.web.IoTDeviceController.add()', 'POST', 1, 'iot', NULL, '/admin/v1/device', '115.209.55.250', '115.209.55.250', '{\"deviceId\":\"ov0000005\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceName\":\"ov0000005\",\"latitude\":\"30.259898\",\"longitude\":\"120.532174\",\"params\":{},\"otherParams\":{}}', '{\"code\":0,\"msg\":\"success\",\"requestId\":\"9c0bc1e61dc64bff87f53283cfe153c5\"}', 0, NULL, '2025-08-21 18:35:00'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (105, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '115.209.55.250', '115.209.55.250', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-21 18:35:04'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (106, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '115.209.55.250', '115.209.55.250', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-21 18:35:07'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (107, '修改设备', 2, 'cn.universal.admin.platform.web.IoTDeviceController.edit()', 'PUT', 1, 'iot', NULL, '/admin/v1/device', '115.209.55.250', '115.209.55.250', '{\"id\":56,\"iotId\":\"5967cc77812f42d08352d6aaeeda3cd8\",\"deviceId\":\"ov0000005\",\"productName\":\"智能插座\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceName\":\"模拟删除1\",\"creatorId\":\"iot\",\"state\":false,\"createTime\":1755772499,\"configuration\":\"{}\",\"coordinate\":\"120.532174,30.259898\",\"latitude\":\"120.532174\",\"longitude\":\"30.259898\",\"params\":{\"locationStr\":\"120.532174,30.259898\"},\"hasProtocol\":true,\"otherParams\":{},\"devGroupName\":[]}', '{\"code\":0,\"msg\":\"success\",\"requestId\":\"d225f96e72e44e50bf4f5ca7ecb975be\"}', 0, NULL, '2025-08-21 18:35:21'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (108, '修改设备', 2, 'cn.universal.admin.platform.web.IoTDeviceController.edit()', 'PUT', 1, 'iot', NULL, '/admin/v1/device', '115.209.55.250', '115.209.55.250', '{\"id\":56,\"iotId\":\"5967cc77812f42d08352d6aaeeda3cd8\",\"deviceId\":\"ov0000005\",\"productName\":\"智能插座\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceName\":\"模拟删除1\",\"creatorId\":\"iot\",\"state\":false,\"detail\":\"你撒\",\"createTime\":1755772499,\"configuration\":\"{}\",\"coordinate\":\"30.259898,120.532174\",\"latitude\":\"30.259898\",\"longitude\":\"120.532174\",\"params\":{\"locationStr\":\"30.259898,120.532174\"},\"hasProtocol\":true,\"otherParams\":{},\"devGroupName\":[]}', '{\"code\":0,\"msg\":\"success\",\"requestId\":\"757b1604d0fa4ea9aaa10d0372e3406b\"}', 0, NULL, '2025-08-21 18:35:26'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (109, '删除设备', 3, 'cn.universal.admin.platform.web.IoTDeviceController.remove()', 'DELETE', 1, 'iot', NULL, '/admin/v1/device/56', '115.209.55.250', '115.209.55.250', '{ids=56}', '{\"code\":0,\"msg\":\"success\",\"requestId\":\"98486f53ca6d455cad991a6b6ba82f09\"}', 0, NULL, '2025-08-21 18:35:31'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (110, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '112.24.73.38', '112.24.73.38', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-24 18:37:35'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (111, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '112.24.73.38', '112.24.73.38', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-24 18:38:10'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (112, '产品导出', 5, 'cn.universal.admin.platform.web.IoTProductController.export()', 'POST', 1, 'test', NULL, '/admin/v1/product/export', '112.24.73.38', '112.24.73.38', '{\"id\":539,\"creatorId\":\"test\",\"hasDevice\":false,\"self\":true,\"page\":1,\"size\":10,\"pageNum\":1,\"pageSize\":10,\"halfSize\":5}', NULL, 0, NULL, '2025-08-24 18:45:46'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (113, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '49.88.127.121', '49.88.127.121', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-25 15:16:35'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (114, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '49.88.127.121', '49.88.127.121', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-25 15:20:54'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (115, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '58.100.228.246', '58.100.228.246', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-25 17:48:15'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (116, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '58.100.228.246', '58.100.228.246', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-25 17:54:33'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (117, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '111.160.8.98', '111.160.8.98', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-26 08:55:32'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (118, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '111.160.8.98', '111.160.8.98', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-26 08:55:55'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (119, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '58.100.228.246', '58.100.228.246', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-26 10:26:36'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (120, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '58.100.228.246', '58.100.228.246', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-26 10:26:44'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (121, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '58.100.228.246', '58.100.228.246', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-26 10:27:11'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (122, '新增场景联动', 1, 'cn.universal.admin.system.web.SceneLinkageController.add()', 'POST', 1, 'test', NULL, '/admin/v1/scene/linkage', '222.75.162.82', '222.75.162.82', '{\"id\":65,\"sceneName\":\"01\",\"touch\":\"one\",\"triggerCondition\":[{\"trigger\":\"manual\",\"filters\":[]}],\"execAction\":[{\"trigger\":\"device\",\"type\":\"functions\",\"deviceId\":\"ov00014117\",\"deviceName\":\"潜江雅园1号楼底商店铺\",\"modelId\":\"switchControl\",\"filters\":[],\"execData\":[{\"id\":\"open\",\"name\":\"开关控制\",\"params\":\"0\"}]}],\"sleepCycle\":5,\"status\":0,\"createBy\":\"test\",\"createTime\":1756178190324,\"updateBy\":\"test\",\"updateTime\":1756178190324,\"params\":{}}', '{\"code\":0,\"msg\":\"success\"}', 0, NULL, '2025-08-26 11:16:30'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (123, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '219.145.104.253', '219.145.104.253', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-26 14:57:50'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (124, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '219.145.104.253', '219.145.104.253', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-26 14:59:41'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (125, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '219.145.104.253', '219.145.104.253', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-26 16:05:50'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (126, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'test', NULL, '/admin/v1/device/down/abcxassxnanahsman', '219.145.104.253', '219.145.104.253', 'abcxassxnanahsman {\"appUnionId\":\"test\",\"productKey\":\"abcxassxnanahsman\",\"deviceId\":\"202411190247\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"meterRead\",\"data\":{\"meterNo\":\"202411190247\"}}}', '{\"code\":723,\"msg\":\"设备与平台连接已断开,指令已暂存3天(同指令以最新为准),重连后自动发送\",\"requestId\":\"fc610335ea884d20a3fb0273de2bc329\"}', 0, NULL, '2025-08-26 16:08:13'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (127, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.176.47', '113.215.176.47', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-26 20:47:39'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (128, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.176.47', '113.215.176.47', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-26 21:01:51'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (129, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.176.47', '113.215.176.47', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchControl\",\"data\":{\"open\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"14\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"requestId\":\"b5508db02d424b719a036d91c0ec355c\"}', 0, NULL, '2025-08-26 21:04:48'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (130, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.176.47', '113.215.176.47', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchControl\",\"data\":{\"open\":\"1\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"15\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"1\"},\"function\":\"switchControl\"},\"requestId\":\"8d1b03531f28478b81bee0aa614245ad\"}', 0, NULL, '2025-08-26 21:05:07'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (131, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.176.47', '113.215.176.47', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"newkai\",\"data\":{\"docs\":98}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"16\",\"messageType\":\"FUNCTIONS\",\"data\":{\"docs\":98},\"function\":\"newkai\"},\"requestId\":\"98d6a6b2a7bc45bb9486adaa1d4330d3\"}', 0, NULL, '2025-08-26 21:05:40'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (132, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.176.47', '113.215.176.47', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"newkai\",\"data\":{\"docs\":23}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"17\",\"messageType\":\"FUNCTIONS\",\"data\":{\"docs\":23},\"function\":\"newkai\"},\"requestId\":\"63ef4851656a4b7090c5d5505a866c51\"}', 0, NULL, '2025-08-26 21:05:57'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (133, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.176.47', '113.215.176.47', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"newkai\",\"data\":{\"docs\":23}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"18\",\"messageType\":\"FUNCTIONS\",\"data\":{\"docs\":23},\"function\":\"newkai\"},\"requestId\":\"0db583236a4c4d9cbd8922fcf9fa18b7\"}', 0, NULL, '2025-08-26 21:06:36'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (134, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.176.47', '113.215.176.47', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"newkai\",\"data\":{\"docs\":23}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"19\",\"messageType\":\"FUNCTIONS\",\"data\":{\"docs\":23},\"function\":\"newkai\"},\"requestId\":\"9bcbb873060249e484264bd88bd86414\"}', 0, NULL, '2025-08-26 21:10:57'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (135, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.176.47', '113.215.176.47', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"newkai\",\"data\":{\"docs\":23}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"20\",\"messageType\":\"FUNCTIONS\",\"data\":{\"docs\":23},\"function\":\"newkai\"},\"requestId\":\"309b87d54637422dbf1d19e19ccd4d4c\"}', 0, NULL, '2025-08-26 21:11:19'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (136, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.176.47', '113.215.176.47', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"newkai\",\"data\":{\"docs\":23}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"21\",\"messageType\":\"FUNCTIONS\",\"data\":{\"docs\":23},\"function\":\"newkai\"},\"requestId\":\"b64db29514404b93bbb2d20cea17febd\"}', 0, NULL, '2025-08-26 21:11:41'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (137, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.176.47', '113.215.176.47', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-26 21:23:45'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (138, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.176.47', '113.215.176.47', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchControl\",\"data\":{\"open\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"1bd9324bfcd84497af76bc63306b2228\"}', 0, NULL, '2025-08-26 21:23:56'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (139, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.176.47', '113.215.176.47', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchControl\",\"data\":{\"open\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"msg\":\"设备可能不在线\"},\"requestId\":\"fcd3e7eefa9740fca90175455b06cdb5\"}', 0, NULL, '2025-08-26 21:24:06'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (140, '下行加保存日志', 0, 'cn.universal.admin.platform.web.IoTDeviceController.iotFunctionsDown()', 'POST', 1, 'iot', NULL, '/admin/v1/device/down/681c0775c2dc427d0480ab5f', '113.215.176.47', '113.215.176.47', '681c0775c2dc427d0480ab5f {\"appUnionId\":\"iot\",\"productKey\":\"681c0775c2dc427d0480ab5f\",\"deviceId\":\"ov0000004\",\"cmd\":\"DEV_FUNCTION\",\"function\":{\"messageType\":\"FUNCTIONS\",\"function\":\"switchControl\",\"data\":{\"open\":\"0\"}}}', '{\"code\":0,\"msg\":\"success\",\"data\":{\"dxxc\":\"6\",\"messageType\":\"FUNCTIONS\",\"data\":{\"open\":\"0\"},\"function\":\"switchControl\"},\"requestId\":\"db920086b3c94d40acc04570e0a12282\"}', 0, NULL, '2025-08-26 21:24:11'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (141, '修改设备协议', 2, 'cn.universal.admin.platform.web.IoTDeviceProtocolController.edit()', 'PUT', 1, 'iot', NULL, '/admin/v1/protocol', '113.215.176.47', '113.215.176.47', '{\"name\":\"智能插座\",\"state\":1,\"id\":\"681c0775c2dc427d0480ab5f\",\"type\":\"magic\",\"configuration\":\"{\\\"needBs4Decode\\\":false,\\\"location\\\":\\\"\\\\nimport log;\\\\n\\\\nvar url=\\\\\\\"\\\\\\\";\\\\n\\\\nvar post = (url,body) =>{\\\\n \\\\n}\\\\n\\\\nvar encode = payload => {\\\\n var dt=toJson(payload)\\\\n dt.dxxc=randomSign();\\\\n return dt;\\\\n}\\\\n\\\\n//解码\\\\nvar decode = payload => {\\\\n var result = []\\\\n log.info(\\\\\\\"编解码收到消息={}\\\\\\\",payload)\\\\n var dt=toJson(payload)\\\\n var propertiesObj = {\\\\n \\\\\\\"messageType\\\\\\\": \\\\\\\"PROPERTIES\\\\\\\",\\\\n \\\\\\\"properties\\\\\\\": {}\\\\n }\\\\n //设置属性\\\\n propertiesObj.properties=dt\\\\n propertiesObj.properties.sn=randomSign()\\\\n //设置回复\\\\n propertiesObj.replyPayload=\\\\\\\"xzzxc\\\\\\\"+randomSign()\\\\n propertiesObj.downTopic=\\\\\\\"$thing/down/681c0775c2dc427d0480ab5f/cz001923\\\\\\\"\\\\n propertiesObj.qos=0\\\\n var event={\\\\n \\\\\\\"messageType\\\\\\\": \\\\\\\"EVENT\\\\\\\",\\\\n \\\\\\\"event\\\\\\\":\\\\\\\"batteryRestore\\\\\\\",\\\\n \\\\\\\"properties\\\\\\\": {\\\\n // \\\\\\\"battery\\\\\\\":100\\\\n }\\\\n }\\\\n var lowBatteryAlarm={\\\\n \\\\\\\"messageType\\\\\\\": \\\\\\\"EVENT\\\\\\\",\\\\n \\\\\\\"event\\\\\\\":\\\\\\\"lowBatteryAlarm\\\\\\\",\\\\n \\\\\\\"properties\\\\\\\": {\\\\n //\\\\\\\"csq\\\\\\\":29\\\\n }\\\\n }\\\\n result.push(propertiesObj)\\\\n result.push(event)\\\\n result.push(lowBatteryAlarm)\\\\n return result;\\\\n}\\\\n\\\\n//设备增加\\\\nvar add = payload => {\\\\n var da=post(url,payload)\\\\n var rs={\\\\n \\\\\\\"code\\\\\\\":0,\\\\n \\\\\\\"msg\\\\\\\":\\\\\\\"success\\\\\\\",\\\\n \\\\\\\"codec\\\\\\\":\\\\\\\"add\\\\\\\",\\\\n \\\\\\\"codec\\\\\\\":da\\\\n }\\\\n return rs;\\\\n}\\\\n\\\\n//设备更新\\\\nvar update = payload => {\\\\n var da=post(url,payload)\\\\n var rs={\\\\n \\\\\\\"code\\\\\\\":0,\\\\n \\\\\\\"msg\\\\\\\":\\\\\\\"success\\\\\\\",\\\\n \\\\\\\"codec\\\\\\\":\\\\\\\"update\\\\\\\",\\\\n \\\\\\\"codec\\\\\\\":da\\\\n }\\\\n return rs;\\\\n}\\\\n\\\\n//设备查询\\\\nvar query = payload => {\\\\n var da=post(url,payload)\\\\n var rs={\\\\n \\\\\\\"code\\\\\\\":0,\\\\n \\\\\\\"req\\\\\\\":0,\\\\n ', '{\"code\":0,\"msg\":\"success\"}', 0, NULL, '2025-08-26 21:29:18'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (142, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'iot', NULL, '/admin/v1/subscribe/list', '113.215.176.47', '113.215.176.47', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-26 21:30:26'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (143, '修改设备协议', 2, 'cn.universal.admin.platform.web.IoTDeviceProtocolController.edit()', 'PUT', 1, 'iot', NULL, '/admin/v1/protocol', '113.215.176.47', '113.215.176.47', '{\"name\":\"智能插座\",\"state\":1,\"id\":\"681c0775c2dc427d0480ab5f\",\"type\":\"magic\",\"configuration\":\"{\\\"needBs4Decode\\\":false,\\\"location\\\":\\\"\\\\nimport log;\\\\n\\\\nvar url=\\\\\\\"\\\\\\\";\\\\n\\\\nvar post = (url,body) =>{\\\\n \\\\n}\\\\n\\\\nvar encode = payload => {\\\\n var dt=toJson(payload)\\\\n dt.dxxc=randomSign();\\\\n return dt;\\\\n}\\\\n\\\\n//解码\\\\nvar decode = payload => {\\\\n var result = []\\\\n log.info(\\\\\\\"编解码收到消息={}\\\\\\\",payload)\\\\n var dt=toJson(payload)\\\\n var propertiesObj = {\\\\n \\\\\\\"messageType\\\\\\\": \\\\\\\"PROPERTIES\\\\\\\",\\\\n \\\\\\\"properties\\\\\\\": {}\\\\n }\\\\n\\\\n var repayObj={}\\\\n repayObj.tips=\\\\\\\"当前是在编解码回复\\\\\\\"\\\\n repayObj.randomNo=randomSign()\\\\n repayObj.wings=\\\\\\\"universal-iot\\\\\\\"\\\\n //设置属性\\\\n propertiesObj.properties=dt\\\\n propertiesObj.properties.sn=randomSign()\\\\n //MQTT设置回复\\\\n propertiesObj.replyPayload=repayObj\\\\n // propertiesObj.replyPayload=\\\\\\\"xzzxc\\\\\\\"+randomSign()//设置回复的内容\\\\n propertiesObj.downTopic=\\\\\\\"$thing/down/681c0775c2dc427d0480ab5f/cz001923\\\\\\\" //设置回复的主题\\\\n \\\\n return propertiesObj;\\\\n}\\\\n\\\\n//设备增加\\\\nvar add = payload => {\\\\n var da=post(url,payload)\\\\n var rs={\\\\n \\\\\\\"code\\\\\\\":0,\\\\n \\\\\\\"msg\\\\\\\":\\\\\\\"success\\\\\\\",\\\\n \\\\\\\"codec\\\\\\\":\\\\\\\"add\\\\\\\",\\\\n \\\\\\\"codec\\\\\\\":da\\\\n }\\\\n return rs;\\\\n}\\\\n\\\\n//设备更新\\\\nvar update = payload => {\\\\n var da=post(url,payload)\\\\n var rs={\\\\n \\\\\\\"code\\\\\\\":0,\\\\n \\\\\\\"msg\\\\\\\":\\\\\\\"success\\\\\\\",\\\\n \\\\\\\"codec\\\\\\\":\\\\\\\"update\\\\\\\",\\\\n \\\\\\\"codec\\\\\\\":da\\\\n }\\\\n return rs;\\\\n}\\\\n\\\\n//设备查询\\\\nvar query = payload => {\\\\n var da=post(url,payload)\\\\n var rs={\\\\n \\\\\\\"code\\\\\\\":0,\\\\n \\\\\\\"req\\\\\\\":0,\\\\n \\\\\\\"msg\\\\\\\":\\\\\\\"success\\\\\\\",\\\\n \\\\\\\"codec\\\\\\\":\\\\\\\"query\\\\\\\",\\\\n \\\\\\\"codec\\\\\\\":da\\\\n }\\\\n return rs;\\\\n}\\\\n\\\\n//功能调用\\\\nvar function = payload => {\\\\n var da=post(url,payload)\\\\n var rs={\\\\n \\\\\\\"code\\\\\\\":0,\\\\n \\\\\\\"msg\\\\\\\":\\\\\\\"success\\\\\\\",\\\\n \\\\\\\"codec\\\\', '{\"code\":0,\"msg\":\"success\"}', 0, NULL, '2025-08-26 21:31:48'); +INSERT INTO `sys_oper_log` (`oper_id`, `title`, `business_type`, `method`, `request_method`, `operator_type`, `oper_name`, `dept_name`, `oper_url`, `oper_ip`, `oper_location`, `oper_param`, `json_result`, `status`, `error_msg`, `oper_time`) VALUES (144, '查询订阅列表', 0, 'cn.universal.admin.platform.web.IoTDeviceSubscribeController.list()', 'GET', 1, 'test', NULL, '/admin/v1/subscribe/list', '113.215.176.47', '113.215.176.47', '{}', '{\"total\":0,\"rows\":[],\"code\":0,\"msg\":\"查询成功\"}', 0, NULL, '2025-08-27 22:34:20'); +COMMIT; + +-- ---------------------------- +-- Table structure for sys_oss +-- ---------------------------- +DROP TABLE IF EXISTS `sys_oss`; +CREATE TABLE `sys_oss` ( + `oss_id` bigint NOT NULL AUTO_INCREMENT COMMENT '云存储主键', + `file_name` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '' COMMENT '文件名', + `original_name` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '' COMMENT '原名', + `file_suffix` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '' COMMENT '文件后缀名', + `url` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT 'URL地址', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `create_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '上传人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `update_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '更新人', + `service` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT 'minio' COMMENT '服务商', + PRIMARY KEY (`oss_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='OSS云存储表'; + +-- ---------------------------- +-- Records of sys_oss +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for sys_role +-- ---------------------------- +DROP TABLE IF EXISTS `sys_role`; +CREATE TABLE `sys_role` ( + `role_id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID', + `role_name` varchar(30) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '角色名称', + `role_key` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '角色权限字符串', + `role_sort` int NOT NULL COMMENT '显示顺序', + `data_scope` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '1' COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)', + `menu_check_strictly` tinyint(1) DEFAULT '1' COMMENT '菜单树选择项是否关联显示', + `dept_check_strictly` tinyint(1) DEFAULT '1' COMMENT '部门树选择项是否关联显示', + `status` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '角色状态(0正常 1停用)', + `create_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`role_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb3 COMMENT='角色信息表'; + +-- ---------------------------- +-- Records of sys_role +-- ---------------------------- +BEGIN; +INSERT INTO `sys_role` (`role_id`, `role_name`, `role_key`, `role_sort`, `data_scope`, `menu_check_strictly`, `dept_check_strictly`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1, '超级管理员', 'admin', 0, '4', 0, 0, '0', 'iot', '2021-12-27 10:28:13', NULL, NULL, NULL); +INSERT INTO `sys_role` (`role_id`, `role_name`, `role_key`, `role_sort`, `data_scope`, `menu_check_strictly`, `dept_check_strictly`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (37, '普通', 'common', 0, '4', 1, NULL, '0', 'iot', '2025-06-24 19:55:31', 'iot', '2025-07-30 16:47:08', NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for sys_role_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sys_role_menu`; +CREATE TABLE `sys_role_menu` ( + `role_id` bigint NOT NULL COMMENT '角色ID', + `menu_id` bigint NOT NULL COMMENT '菜单ID', + `uuid` bigint NOT NULL AUTO_INCREMENT, + PRIMARY KEY (`uuid`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=661 DEFAULT CHARSET=utf8mb3 COMMENT='角色和菜单关联表'; + +-- ---------------------------- +-- Records of sys_role_menu +-- ---------------------------- +BEGIN; +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1193, 570); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1133, 571); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1082, 572); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1081, 573); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1130, 574); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1131, 575); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1132, 576); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1140, 577); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1141, 578); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1142, 579); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1143, 580); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1144, 581); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1227, 582); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1145, 583); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1228, 584); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1158, 585); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1229, 586); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1096, 587); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1093, 588); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1095, 589); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1116, 590); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1117, 591); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1118, 592); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1119, 593); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1120, 594); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1125, 595); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1126, 596); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1127, 597); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1128, 598); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1176, 599); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1177, 600); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1178, 601); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1100, 602); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1101, 603); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1102, 604); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1103, 605); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1104, 606); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1105, 607); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1106, 608); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1099, 609); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1097, 610); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1098, 611); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1121, 612); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1122, 613); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1123, 614); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1124, 615); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1108, 616); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1107, 617); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1156, 618); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1109, 619); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1110, 620); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1111, 621); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1112, 622); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1157, 623); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1113, 624); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1114, 625); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1115, 626); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1153, 627); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1179, 628); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1181, 629); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1182, 630); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1197, 631); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1195, 632); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1194, 633); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1198, 634); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1199, 635); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1200, 636); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1205, 637); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1206, 638); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1207, 639); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1196, 640); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1201, 641); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1202, 642); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1203, 643); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1204, 644); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1208, 645); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1209, 646); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1210, 647); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1247, 648); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1248, 649); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1249, 650); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1250, 651); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1213, 652); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1212, 653); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1214, 654); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1215, 655); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1216, 656); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1220, 657); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1221, 658); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1211, 659); +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `uuid`) VALUES (37, 1217, 660); +COMMIT; + +-- ---------------------------- +-- Table structure for sys_user_role +-- ---------------------------- +DROP TABLE IF EXISTS `sys_user_role`; +CREATE TABLE `sys_user_role` ( + `uuid` bigint NOT NULL AUTO_INCREMENT, + `union_id` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '用户unionId', + `role_id` bigint NOT NULL COMMENT '角色ID', + PRIMARY KEY (`uuid`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb3 COMMENT='用户和角色关联表'; + +-- ---------------------------- +-- Records of sys_user_role +-- ---------------------------- +BEGIN; +INSERT INTO `sys_user_role` (`uuid`, `union_id`, `role_id`) VALUES (1, 'iot', 1); +INSERT INTO `sys_user_role` (`uuid`, `union_id`, `role_id`) VALUES (3, 'view', 37); +INSERT INTO `sys_user_role` (`uuid`, `union_id`, `role_id`) VALUES (5, 'test', 37); +COMMIT; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/cn-universal-plugins/cn-universal-plugins-extend-jdbc/pom.xml b/cn-universal-plugins/cn-universal-plugins-extend-jdbc/pom.xml new file mode 100644 index 0000000..1f03f12 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-extend-jdbc/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + cn.universal.iot + cn-universal-plugins + 1.5-SNAPSHOT + + + cn-universal-plugins-extend-jdbc + + + 21 + 21 + UTF-8 + + + + org.slf4j + slf4j-api + + + org.springframework.security + spring-security-core + + + spring-boot-starter-web + + + slf4j-log4j12 + org.slf4j + + + + org.springframework.boot + ${spring-boot.version} + provided + + + cn.universal.iot + cn-universal-core + ${project.version} + compile + + + + \ No newline at end of file diff --git a/cn-universal-plugins/cn-universal-plugins-extend-jdbc/src/main/resources/application-extend-jdbc.properties b/cn-universal-plugins/cn-universal-plugins-extend-jdbc/src/main/resources/application-extend-jdbc.properties new file mode 100644 index 0000000..cf54a05 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-extend-jdbc/src/main/resources/application-extend-jdbc.properties @@ -0,0 +1 @@ +# 插件开关 - 设置为true时启用插件 diff --git a/cn-universal-plugins/cn-universal-plugins-extend-kafka/pom.xml b/cn-universal-plugins/cn-universal-plugins-extend-kafka/pom.xml new file mode 100644 index 0000000..82bb250 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-extend-kafka/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + cn.universal.iot + cn-universal-plugins + 1.5-SNAPSHOT + + + cn-universal-plugins-extend-kafka + + + 21 + 21 + UTF-8 + + + + + spring-boot-starter-web + + + slf4j-log4j12 + org.slf4j + + + + org.springframework.boot + ${spring-boot.version} + provided + + + + org.springframework + spring-web + + + + org.springframework.boot + spring-boot-starter-jdbc + provided + + + org.springframework.boot + spring-boot-starter-log4j2 + provided + + + com.alibaba + fastjson + provided + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework + spring-jdbc + + + cn.universal.iot + cn-universal-core + ${project.version} + compile + + + + \ No newline at end of file diff --git a/cn-universal-plugins/cn-universal-plugins-extend-kafka/src/main/resources/application-extend-kafka.properties b/cn-universal-plugins/cn-universal-plugins-extend-kafka/src/main/resources/application-extend-kafka.properties new file mode 100644 index 0000000..cf54a05 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-extend-kafka/src/main/resources/application-extend-kafka.properties @@ -0,0 +1 @@ +# 插件开关 - 设置为true时启用插件 diff --git a/cn-universal-plugins/cn-universal-plugins-message-format/pom.xml b/cn-universal-plugins/cn-universal-plugins-message-format/pom.xml new file mode 100644 index 0000000..0648641 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-message-format/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + cn.universal.iot + cn-universal-plugins + 1.5-SNAPSHOT + + + cn-universal-plugins-message-format + + + 21 + 21 + UTF-8 + + + + org.slf4j + slf4j-api + + + org.springframework.security + spring-security-core + + + spring-boot-starter-web + + + slf4j-log4j12 + org.slf4j + + + + org.springframework.boot + ${spring-boot.version} + provided + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework + spring-web + + + org.springframework.boot + spring-boot-starter-websocket + + + org.springframework.boot + spring-boot-starter-jdbc + provided + + + commons-beanutils + commons-beanutils + 1.9.4 + + + commons-io + commons-io + 2.7 + + + + org.apache.commons + commons-compress + 1.21 + + + org.apache.commons + commons-lang3 + + + org.springframework.boot + spring-boot-starter-log4j2 + provided + + + com.alibaba + fastjson + provided + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework + spring-jdbc + + + cn.universal.iot + cn-universal-core + ${project.version} + compile + + + + \ No newline at end of file diff --git a/cn-universal-plugins/cn-universal-plugins-message-format/src/main/resources/application-message-format.properties b/cn-universal-plugins/cn-universal-plugins-message-format/src/main/resources/application-message-format.properties new file mode 100644 index 0000000..cf54a05 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-message-format/src/main/resources/application-message-format.properties @@ -0,0 +1 @@ +# 插件开关 - 设置为true时启用插件 diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/pom.xml b/cn-universal-plugins/cn-universal-plugins-web-debug/pom.xml new file mode 100644 index 0000000..441c3d7 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + cn.universal.iot + cn-universal-plugins + 1.5-SNAPSHOT + + + cn-universal-plugins-web-debug + IoT-Connector-Debug的IDE不开放前端源码了 + + 21 + 21 + UTF-8 + + + + org.slf4j + slf4j-api + + + org.springframework.security + spring-security-core + + + spring-boot-starter-web + + + slf4j-log4j12 + org.slf4j + + + + org.springframework.boot + ${spring-boot.version} + provided + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework + spring-web + + + org.springframework.boot + spring-boot-starter-websocket + + + org.springframework.boot + spring-boot-starter-jdbc + provided + + + commons-beanutils + commons-beanutils + 1.9.4 + + + commons-io + commons-io + 2.7 + + + + org.apache.commons + commons-compress + 1.21 + + + org.apache.commons + commons-lang3 + + + org.springframework.boot + spring-boot-starter-log4j2 + provided + + + com.alibaba + fastjson + provided + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework + spring-jdbc + + + cn.universal.iot + cn-universal-core + ${project.version} + compile + + + + \ No newline at end of file diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/auth/CustomAuthorizationInterceptor.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/auth/CustomAuthorizationInterceptor.java new file mode 100644 index 0000000..7b88f30 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/auth/CustomAuthorizationInterceptor.java @@ -0,0 +1,93 @@ +package cn.universal.plugins.protocolapi.auth; + +import cn.hutool.core.util.StrUtil; +import cn.universal.plugins.protocolapi.core.context.MagicUser; +import cn.universal.plugins.protocolapi.core.exception.MagicLoginException; +import cn.universal.plugins.protocolapi.core.interceptor.Authorization; +import cn.universal.plugins.protocolapi.core.interceptor.AuthorizationInterceptor; +import cn.universal.plugins.protocolapi.core.model.Group; +import cn.universal.plugins.protocolapi.core.model.MagicEntity; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Component; + +/** 自定义用户名密码登录 */ +@Component // 注入到Spring容器中 +@Slf4j +public class CustomAuthorizationInterceptor implements AuthorizationInterceptor { + + private Map user = new HashMap<>(); + @Autowired private UserDetailsService userDetailsService; + + /** 配置是否需要登录 */ + @Override + public boolean requireLogin() { + return true; + } + + /** 根据Token获取User */ + @Override + public MagicUser getUserByToken(String token) throws MagicLoginException { + String username = SymmetricEncryptionUtil.desDecryptWithBuiltinKey(token); + if (user.containsKey(username)) { + return new MagicUser( + username, username, SymmetricEncryptionUtil.desEncryptWithBuiltinKey(username)); + } + throw new MagicLoginException("token无效"); + } + + @Override + public MagicUser login(String username, String password) throws MagicLoginException { + // 根据实际情况进行修改,如查询数据库。。 + if (StrUtil.isBlank(username) || StrUtil.isBlank(password)) { + throw new MagicLoginException("用户名或密码不能为空"); + } + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + // 注释密码验证,专注解决类型转换问题 + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + if (!passwordEncoder.matches(password, userDetails.getPassword())) { + throw new MagicLoginException("用户名或密码错误"); + } + user.put(username, SymmetricEncryptionUtil.desEncryptWithBuiltinKey(username)); + return new MagicUser( + username, username, SymmetricEncryptionUtil.desEncryptWithBuiltinKey(username)); + } + + @Override + public boolean allowVisit( + MagicUser magicUser, + MagicHttpServletRequest request, + Authorization authorization, + MagicEntity entity) { + if (entity.getCreateBy() == null) { + return true; + } + if (magicUser.getUsername().equalsIgnoreCase(entity.getCreateBy())) { + return true; + } + log.info("magicUser:{}, request:{}, entity:{}", magicUser, request, entity); + return true; + } + + @Override + public boolean allowVisit( + MagicUser magicUser, + MagicHttpServletRequest request, + Authorization authorization, + Group group) { + log.info("magicUser:{}, request:{}, group:{}", magicUser, request, group); + if (group.getCreateBy() == null) { + return true; + } + if (magicUser.getUsername().equalsIgnoreCase(group.getCreateBy())) { + return true; + } + return false; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/auth/CustomRequestInterceptor.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/auth/CustomRequestInterceptor.java new file mode 100644 index 0000000..3c3435e --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/auth/CustomRequestInterceptor.java @@ -0,0 +1,40 @@ +package cn.universal.plugins.protocolapi.auth; + +import cn.universal.core.engine.MagicScriptContext; +import cn.universal.plugins.protocolapi.core.config.Constants; +import cn.universal.plugins.protocolapi.core.interceptor.RequestInterceptor; +import cn.universal.plugins.protocolapi.core.model.ApiInfo; +import cn.universal.plugins.protocolapi.core.model.JsonBean; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletRequest; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** 自定义未授权访问 */ +@Component +@Slf4j +public class CustomRequestInterceptor implements RequestInterceptor { + + @Override + public Object preHandle( + ApiInfo info, + MagicScriptContext context, + MagicHttpServletRequest request, + MagicHttpServletResponse response) + throws Exception { + String token = request.getHeader(Constants.MAGIC_TOKEN_HEADER); + + if (token == null || token.isEmpty()) { + return new JsonBean<>(403, "未授权访问"); + } + String username = SymmetricEncryptionUtil.desDecryptWithBuiltinKey(token); + if (username == null || username.isEmpty()) { + return new JsonBean<>(403, "用户无权限"); + } + if (!username.equals(info.getCreateBy())) { + return new JsonBean<>(403, "你没有调用权限"); + } + log.info("info={},context={}", info, context); + return null; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/auth/SymmetricEncryptionUtil.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/auth/SymmetricEncryptionUtil.java new file mode 100644 index 0000000..e5062d3 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/auth/SymmetricEncryptionUtil.java @@ -0,0 +1,219 @@ +package cn.universal.plugins.protocolapi.auth; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * JDK 21 对称加密工具类,支持AES和DES算法 + */ +public class SymmetricEncryptionUtil { + + // 密钥长度常量 + public static final int AES_128_KEY_SIZE = 128; + public static final int AES_192_KEY_SIZE = 192; + public static final int AES_256_KEY_SIZE = 256; + public static final int DES_KEY_SIZE = 64; + + // 加密算法 + private static final String AES_ALGORITHM = "AES"; + private static final String DES_ALGORITHM = "DES"; + private static final String AES_GCM_TRANSFORMATION = "AES/GCM/NoPadding"; + private static final String DES_TRANSFORMATION = "DES/ECB/PKCS5Padding"; + + // GCM参数 + private static final int GCM_IV_LENGTH = 12; + private static final int GCM_TAG_LENGTH = 128; + + // 内置密钥(实际应用中建议从配置文件或环境变量读取) + private static final String BUILTIN_AES_KEY = "yourSecureAESKey1234567890123456"; // 32字节(256位) + private static final String BUILTIN_DES_KEY = "ABX12JNS"; // 8字节(64位) + + // 内置密钥的Base64编码 + private static final String BUILTIN_AES_KEY_BASE64; + private static final String BUILTIN_DES_KEY_BASE64; + + static { + try { + // 初始化内置密钥的Base64编码 + BUILTIN_AES_KEY_BASE64 = Base64.getEncoder() + .encodeToString(BUILTIN_AES_KEY.getBytes(StandardCharsets.UTF_8)); + BUILTIN_DES_KEY_BASE64 = Base64.getEncoder() + .encodeToString(BUILTIN_DES_KEY.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new RuntimeException("初始化内置密钥失败", e); + } + } + + /** + * 使用内置AES密钥加密 + * + * @param plaintext 明文 + * @return 加密结果(格式:IV:密文:标签) + */ + public static String aesGcmEncryptWithBuiltinKey(String plaintext) throws Exception { + return aesGcmEncrypt(plaintext, BUILTIN_AES_KEY_BASE64); + } + + /** + * 使用内置AES密钥解密 + * + * @param ciphertext 密文(格式:IV:密文:标签) + * @return 明文 + */ + public static String aesGcmDecryptWithBuiltinKey(String ciphertext) throws Exception { + return aesGcmDecrypt(ciphertext, BUILTIN_AES_KEY_BASE64); + } + + /** + * 使用内置DES密钥加密 + * + * @param plaintext 明文 + * @return 加密结果的Base64编码 + */ + public static String desEncryptWithBuiltinKey(String plaintext) { + return desEncrypt(plaintext, BUILTIN_DES_KEY_BASE64); + } + + /** + * 使用内置DES密钥解密 + * + * @param ciphertext 密文的Base64编码 + * @return 明文 + */ + public static String desDecryptWithBuiltinKey(String ciphertext) { + return desDecrypt(ciphertext, BUILTIN_DES_KEY_BASE64); + } + + /** + * 使用AES/GCM加密 + * + * @param plaintext 明文 + * @param base64Key 密钥的Base64编码 + * @return 加密结果(格式:IV:密文:标签) + */ + public static String aesGcmEncrypt(String plaintext, String base64Key) throws Exception { + // 解码密钥 + byte[] keyBytes = Base64.getDecoder().decode(base64Key); + SecretKeySpec secretKey = new SecretKeySpec(keyBytes, AES_ALGORITHM); + + // 生成随机IV + SecureRandom secureRandom = new SecureRandom(); + byte[] iv = new byte[GCM_IV_LENGTH]; + secureRandom.nextBytes(iv); + + // 初始化加密器 + Cipher cipher = Cipher.getInstance(AES_GCM_TRANSFORMATION); + GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec); + + // 加密 + byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); + + // 拼接IV、密文和标签(GCM会自动处理标签) + return Base64.getEncoder().encodeToString(iv) + ":" + Base64.getEncoder() + .encodeToString(ciphertext); + } + + /** + * 使用AES/GCM解密 + * + * @param ciphertext 密文(格式:IV:密文:标签) + * @param base64Key 密钥的Base64编码 + * @return 明文 + */ + public static String aesGcmDecrypt(String ciphertext, String base64Key) throws Exception { + // 分割IV和密文 + String[] parts = ciphertext.split(":"); + byte[] iv = Base64.getDecoder().decode(parts[0]); + byte[] encrypted = Base64.getDecoder().decode(parts[1]); + + // 解码密钥 + byte[] keyBytes = Base64.getDecoder().decode(base64Key); + SecretKeySpec secretKey = new SecretKeySpec(keyBytes, AES_ALGORITHM); + + // 初始化解密器 + Cipher cipher = Cipher.getInstance(AES_GCM_TRANSFORMATION); + GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); + cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec); + + // 解密 + byte[] decrypted = cipher.doFinal(encrypted); + return new String(decrypted, StandardCharsets.UTF_8); + } + + /** + * 使用DES加密 + * + * @param plaintext 明文 + * @param base64Key 密钥的Base64编码 + * @return 加密结果的Base64编码 + */ + public static String desEncrypt(String plaintext, String base64Key) { + // 解码密钥 + byte[] keyBytes = Base64.getDecoder().decode(base64Key); + SecretKeySpec secretKey = new SecretKeySpec(keyBytes, DES_ALGORITHM); + + // 初始化加密器 + Cipher cipher = null; + try { + cipher = Cipher.getInstance(DES_TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(encrypted); + } catch (Exception e) { + } + return null; + } + + /** + * 使用DES解密 + * + * @param ciphertext 密文的Base64编码 + * @param base64Key 密钥的Base64编码 + * @return 明文 + */ + public static String desDecrypt(String ciphertext, String base64Key) { + // 解码密钥 + byte[] keyBytes = Base64.getDecoder().decode(base64Key); + SecretKeySpec secretKey = new SecretKeySpec(keyBytes, DES_ALGORITHM); + // 初始化解密器 + Cipher cipher = null; + try { + cipher = Cipher.getInstance(DES_TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + // 解密 + byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(ciphertext)); + return new String(decrypted, StandardCharsets.UTF_8); + } catch (Exception e) { + } + return null; + } + + // 其他方法保持不变... + + public static void main(String[] args) { + try { + // 内置密钥示例 + String plaintext = "Hello, 内置密钥加密!"; + + System.out.println("\n=== 使用内置AES密钥 ==="); + String aesCiphertext = aesGcmEncryptWithBuiltinKey(plaintext); + String aesDecrypted = aesGcmDecryptWithBuiltinKey(aesCiphertext); + System.out.println("AES密文: " + aesCiphertext); + System.out.println("AES解密结果: " + aesDecrypted); + + System.out.println("\n=== 使用内置DES密钥 ==="); + String desCiphertext = desEncryptWithBuiltinKey(plaintext); + String desDecrypted = desDecryptWithBuiltinKey(desCiphertext); + System.out.println("DES密文: " + desCiphertext); + System.out.println("DES解密结果: " + desDecrypted); + + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/backup/model/Backup.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/backup/model/Backup.java new file mode 100644 index 0000000..d4977ad --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/backup/model/Backup.java @@ -0,0 +1,100 @@ +package cn.universal.plugins.protocolapi.backup.model; + +/** + * 备份记录 + * + * @author mxd + */ +public class Backup { + + /** 记录ID */ + private String id; + + /** 备份时间 */ + private Long createDate = 0L; + + /** 标签,只允许有一个 */ + private String tag; + + /** 备份类型,api function datasource */ + private String type; + + /** 原名称 */ + private String name; + + /** 备份内容 */ + private byte[] content; + + /** 操作人,取用户名,空为系统记录 */ + private String createBy; + + public Backup() {} + + public Backup(String id, String type, String name, byte[] content) { + this.id = id; + this.type = type; + this.name = name; + this.content = content; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Long getCreateDate() { + return createDate; + } + + public void setCreateDate(Long createDate) { + this.createDate = createDate; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public byte[] getContent() { + return content; + } + + public void setContent(byte[] content) { + this.content = content; + } + + public String getCreateBy() { + return createBy; + } + + public void setCreateBy(String createBy) { + this.createBy = createBy; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Backup small() { + setContent(null); + return this; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/backup/service/MagicBackupService.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/backup/service/MagicBackupService.java new file mode 100644 index 0000000..71e503b --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/backup/service/MagicBackupService.java @@ -0,0 +1,67 @@ +package cn.universal.plugins.protocolapi.backup.service; + +import cn.universal.plugins.protocolapi.backup.model.Backup; +import java.io.IOException; +import java.util.List; + +/** + * 数据备份接口 + * + * @author mxd + */ +public interface MagicBackupService { + + int FETCH_SIZE = 100; + + /** + * 执行备份动作 + * + * @param backup 备份对象 + */ + void doBackup(Backup backup); + + void doBackupAll(String name, String createBy) throws IOException; + + /** + * 根据时间戳查询最近的 FETCH_SIZE 条记录 + * + * @param timestamp 时间戳 + * @return 返回备份记录 + */ + List backupList(long timestamp); + + /** + * 根据对象ID查询备份记录 + * + * @param id 对象ID + * @return 返回备份记录 + */ + List backupById(String id); + + /** 根据对象ID和备份时间查询 */ + Backup backupInfo(String id, long timestamp); + + /** + * 根据标签查询备份记录 + * + * @param tag 标签 + * @return 返回备份记录 + */ + List backupByTag(String tag); + + /** + * 删除备份 + * + * @param id 对象ID + * @return 返回删除的记录数 + */ + long removeBackup(String id); + + /** + * 根据13位时间戳删除备份记录(清除小于该值的记录) + * + * @param timestamp 时间戳 + * @return 返回删除的记录数 + */ + long removeBackupByTimestamp(long timestamp); +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/backup/service/MagicDatabaseBackupService.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/backup/service/MagicDatabaseBackupService.java new file mode 100644 index 0000000..fe1f506 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/backup/service/MagicDatabaseBackupService.java @@ -0,0 +1,186 @@ +package cn.universal.plugins.protocolapi.backup.service; + +import cn.universal.plugins.protocolapi.backup.model.Backup; +import cn.universal.plugins.protocolapi.core.config.MagicConfiguration; +import cn.universal.plugins.protocolapi.core.event.FileEvent; +import cn.universal.plugins.protocolapi.core.event.GroupEvent; +import cn.universal.plugins.protocolapi.core.model.Group; +import cn.universal.plugins.protocolapi.core.model.MagicEntity; +import cn.universal.plugins.protocolapi.utils.JsonUtils; +import cn.universal.plugins.protocolapi.utils.WebUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.event.EventListener; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * 数据库备份实现 + * + * @author mxd + */ +public class MagicDatabaseBackupService implements MagicBackupService { + + private static final String DEFAULT_COLUMNS = "id,create_date,tag,type,name,create_by"; + + private final JdbcTemplate template; + + private final String INSERT_SQL; + + private final String FIND_BY_ID; + + private final String FIND_BY_TAG; + + private final String FIND_BY_TIMESTAMP; + + private final String FIND_BY_ID_AND_TIMESTAMP; + + private final String DELETE_BY_ID; + + private final String DELETE_BY_TIMESTAMP; + + private final BeanPropertyRowMapper rowMapper = new BeanPropertyRowMapper<>(Backup.class); + + private static final Logger logger = LoggerFactory.getLogger(MagicDatabaseBackupService.class); + + public MagicDatabaseBackupService(JdbcTemplate template, String tableName) { + this.template = template; + this.template.setMaxRows(FETCH_SIZE); + this.INSERT_SQL = + String.format( + "insert into %s(%s,content) values(?,?,?,?,?,?,?)", tableName, DEFAULT_COLUMNS); + this.FIND_BY_ID = + String.format( + "select %s from %s where id = ? order by create_date desc", DEFAULT_COLUMNS, tableName); + this.DELETE_BY_ID = String.format("delete from %s where id = ?", tableName); + this.FIND_BY_TAG = + String.format( + "select %s from %s where tag = ? order by create_date desc", + DEFAULT_COLUMNS, tableName); + this.FIND_BY_TIMESTAMP = + String.format( + "select %s from %s where create_date < ? order by create_date desc", + DEFAULT_COLUMNS, tableName); + this.DELETE_BY_TIMESTAMP = String.format("delete from %s where create_date < ?", tableName); + this.FIND_BY_ID_AND_TIMESTAMP = + String.format("select * from %s where id = ? and create_date = ?", tableName); + } + + @Override + public void doBackupAll(String name, String createBy) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + MagicConfiguration.getMagicResourceService().export(null, null, baos); + Backup backup = new Backup(); + backup.setId("full"); + backup.setType("full"); + backup.setName(name); + backup.setCreateBy(createBy); + backup.setContent(baos.toByteArray()); + doBackup(backup); + } + + @Override + public void doBackup(Backup backup) { + try { + if (backup.getCreateDate() == 0) { + backup.setCreateDate(System.currentTimeMillis()); + } + if (backup.getCreateBy() == null) { + backup.setCreateBy(WebUtils.currentUserName()); + } + template.update( + INSERT_SQL, + backup.getId(), + backup.getCreateDate(), + backup.getTag(), + backup.getType(), + backup.getName(), + backup.getCreateBy(), + backup.getContent()); + } catch (Exception e) { + logger.warn("备份失败", e); + } + } + + @Override + public List backupList(long timestamp) { + return template.query(FIND_BY_TIMESTAMP, rowMapper, timestamp); + } + + @Override + public List backupById(String id) { + return template.query(FIND_BY_ID, rowMapper, id); + } + + @Override + public Backup backupInfo(String id, long timestamp) { + return template.queryForObject(FIND_BY_ID_AND_TIMESTAMP, rowMapper, id, timestamp); + } + + @Override + public List backupByTag(String tag) { + return template.query(FIND_BY_TAG, rowMapper, tag); + } + + @Override + public long removeBackup(String id) { + return template.update(DELETE_BY_ID, id); + } + + @Override + public long removeBackupByTimestamp(long timestamp) { + try { + return template.update(DELETE_BY_TIMESTAMP, timestamp); + } catch (Exception e) { + logger.warn("删除备份失败", e); + return -1; + } + } + + @EventListener( + condition = + "#event!=null && #event.source != T(cn.universal.plugins.protocolapi.core.config.Constants)" + + ".EVENT_SOURCE_NOTIFY") + public void onFileEvent(FileEvent event) { + switch (event.getAction()) { + case SAVE: + case CREATE: + case MOVE: + break; + default: + return; + } + MagicEntity entity = event.getEntity(); + doBackup(entity.getId(), JsonUtils.toJsonBytes(entity), entity.getName(), event.getType()); + } + + @EventListener( + condition = + "#event!=null &&#event.source != T(cn.universal.plugins.protocolapi.core.config.Constants)" + + ".EVENT_SOURCE_NOTIFY") + public void onFolderEvent(GroupEvent event) { + switch (event.getAction()) { + case SAVE: + case CREATE: + case MOVE: + break; + default: + return; + } + Group group = event.getGroup(); + doBackup( + group.getId(), JsonUtils.toJsonBytes(group), group.getName(), group.getType() + "-group"); + } + + private void doBackup(String id, byte[] content, String name, String type) { + Backup backup = new Backup(); + backup.setName(name); + backup.setId(id); + backup.setContent(content); + backup.setType(type); + doBackup(backup); + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/backup/web/MagicBackupController.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/backup/web/MagicBackupController.java new file mode 100644 index 0000000..daf75d7 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/backup/web/MagicBackupController.java @@ -0,0 +1,100 @@ +package cn.universal.plugins.protocolapi.backup.web; + +import cn.universal.plugins.protocolapi.backup.model.Backup; +import cn.universal.plugins.protocolapi.backup.service.MagicBackupService; +import cn.universal.plugins.protocolapi.core.config.Constants; +import cn.universal.plugins.protocolapi.core.config.MagicConfiguration; +import cn.universal.plugins.protocolapi.core.model.Group; +import cn.universal.plugins.protocolapi.core.model.JsonBean; +import cn.universal.plugins.protocolapi.core.model.MagicEntity; +import cn.universal.plugins.protocolapi.core.service.MagicDynamicRegistry; +import cn.universal.plugins.protocolapi.core.web.MagicController; +import cn.universal.plugins.protocolapi.core.web.MagicExceptionHandler; +import cn.universal.plugins.protocolapi.utils.JsonUtils; +import cn.universal.plugins.protocolapi.utils.WebUtils; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +public class MagicBackupController extends MagicController implements MagicExceptionHandler { + + private final MagicBackupService service; + + public MagicBackupController(MagicConfiguration configuration) { + super(configuration); + this.service = configuration.getMagicBackupService(); + } + + @GetMapping("/backups") + @ResponseBody + public JsonBean> backups(Long timestamp) { + if (service == null) { + return new JsonBean<>(Collections.emptyList()); + } + return new JsonBean<>( + service.backupList(timestamp == null ? System.currentTimeMillis() : timestamp)); + } + + @GetMapping("/backup/{id}") + @ResponseBody + public JsonBean> backups(@PathVariable("id") String id) { + if (service == null || StringUtils.isBlank(id)) { + return new JsonBean<>(Collections.emptyList()); + } + return new JsonBean<>(service.backupById(id)); + } + + @PostMapping("/backup/rollback") + @ResponseBody + public JsonBean rollback(String id, Long timestamp) throws IOException { + notNull(service, BACKUP_NOT_ENABLED); + Backup backup = service.backupInfo(id, timestamp); + if ("full".equals(id)) { + service.doBackupAll("还原全量备份前,系统自动全量备份", WebUtils.currentUserName()); + configuration + .getMagicAPIService() + .upload(new ByteArrayInputStream(backup.getContent()), Constants.UPLOAD_MODE_FULL); + return new JsonBean<>(true); + } + if (backup.getType().endsWith("-group")) { + Group group = JsonUtils.readValue(backup.getContent(), Group.class); + return new JsonBean<>(MagicConfiguration.getMagicResourceService().saveGroup(group)); + } + MagicEntity entity = + configuration.getMagicDynamicRegistries().stream() + .map(MagicDynamicRegistry::getMagicResourceStorage) + .filter(it -> it.folder().equals(backup.getType())) + .map(it -> it.read(backup.getContent())) + .findFirst() + .orElse(null); + if (entity != null) { + return new JsonBean<>(MagicConfiguration.getMagicResourceService().saveFile(entity)); + } + return new JsonBean<>(false); + } + + @GetMapping("/backup") + @ResponseBody + public JsonBean backup(Long timestamp, String id) { + notNull(service, BACKUP_NOT_ENABLED); + notBlank(id, PARAMETER_INVALID); + notNull(timestamp, PARAMETER_INVALID); + Backup backup = service.backupInfo(id, timestamp); + MagicEntity entity = JsonUtils.readValue(backup.getContent(), MagicEntity.class); + return new JsonBean<>(entity == null ? null : entity.getScript()); + } + + @PostMapping("/backup/full") + @ResponseBody + public JsonBean doBackup() throws IOException { + notNull(service, BACKUP_NOT_ENABLED); + service.doBackupAll("主动全量备份", WebUtils.currentUserName()); + return new JsonBean<>(true); + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/annotation/MagicModule.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/annotation/MagicModule.java new file mode 100644 index 0000000..2adca68 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/annotation/MagicModule.java @@ -0,0 +1,21 @@ +package cn.universal.plugins.protocolapi.core.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 模块,主要用于import指令,import时根据模块名获取当前类如:import assert; + * + * @author mxd + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MagicModule { + + /** 模块名 */ + String value(); +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/annotation/Message.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/annotation/Message.java new file mode 100644 index 0000000..d1640e4 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/annotation/Message.java @@ -0,0 +1,24 @@ +package cn.universal.plugins.protocolapi.core.annotation; + +import cn.universal.plugins.protocolapi.core.config.MessageType; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * WebSocket 消息 + * + * @author mxd + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Message { + + /** + * @return 消息类型 + */ + MessageType value(); +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/annotation/Valid.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/annotation/Valid.java new file mode 100644 index 0000000..677101c --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/annotation/Valid.java @@ -0,0 +1,28 @@ +package cn.universal.plugins.protocolapi.core.annotation; + +import cn.universal.plugins.protocolapi.core.interceptor.Authorization; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 接口验证信息 + * + * @author mxd + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Valid { + + /** 验证是否有该权限 */ + Authorization authorization() default Authorization.NONE; + + /** 验证是否是只读模式 */ + boolean readonly() default true; + + /** 验证是否需要登录 */ + boolean requireLogin() default true; +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Backup.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Backup.java new file mode 100644 index 0000000..d9f9132 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Backup.java @@ -0,0 +1,54 @@ +package cn.universal.plugins.protocolapi.core.config; + +/** + * 备份配置 + * + * @author mxd + * @since 2.0.0 + */ +public class Backup { + + /** 是否启用备份配置,默认不启用 */ + private boolean enable = false; + + /** 保留天数,<=0 为不限制 */ + private int maxHistory = -1; + + /** 使用数据库存储时的表名 */ + private String tableName; + + /** 使用数据库存储时使用的数据源 */ + private String datasource; + + public int getMaxHistory() { + return maxHistory; + } + + public void setMaxHistory(int maxHistory) { + this.maxHistory = maxHistory; + } + + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public String getDatasource() { + return datasource; + } + + public void setDatasource(String datasource) { + this.datasource = datasource; + } + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Cache.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Cache.java new file mode 100644 index 0000000..dfbf03e --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Cache.java @@ -0,0 +1,42 @@ +package cn.universal.plugins.protocolapi.core.config; + +/** + * 缓存配置 + * + * @author mxd + */ +public class Cache { + + /** 是否启用缓存 */ + private boolean enable = false; + + /** 默认缓存容量 */ + private int capacity = 10000; + + /** 默认过期时间,单位为毫秒,-1为不过期 */ + private long ttl = -1; + + public int getCapacity() { + return capacity; + } + + public void setCapacity(int capacity) { + this.capacity = capacity; + } + + public long getTtl() { + return ttl; + } + + public void setTtl(long ttl) { + this.ttl = ttl; + } + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Constants.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Constants.java new file mode 100644 index 0000000..89f54f2 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Constants.java @@ -0,0 +1,95 @@ +package cn.universal.plugins.protocolapi.core.config; + +public class Constants { + + /** true 常量 */ + public static final String CONST_STRING_TRUE = "true"; + + /** 空值 */ + public static final String EMPTY = ""; + + /** 表达式验证 */ + public static final String VALIDATE_TYPE_EXPRESSION = "expression"; + + /** 正则验证 */ + public static final String VALIDATE_TYPE_PATTERN = "pattern"; + + /** 表达式验证中变量的默认名称 */ + public static final String EXPRESSION_DEFAULT_VAR_NAME = "value"; + + /** 脚本中session的变量名 */ + public static final String VAR_NAME_SESSION = "session"; + + /** 脚本中cookie的变量名 */ + public static final String VAR_NAME_COOKIE = "cookie"; + + /** 脚本中路径变量的变量名 */ + public static final String VAR_NAME_PATH_VARIABLE = "path"; + + /** 脚本中header的变量名 */ + public static final String VAR_NAME_HEADER = "header"; + + /** 脚本中query的变量名 */ + public static final String VAR_NAME_QUERY = "query"; + + /** /** 脚本中RequestBody的变量名 */ + public static final String VAR_NAME_REQUEST_BODY = "body"; + + /** 脚本中RequestBody的变量值字段类型 */ + public static final String VAR_NAME_REQUEST_BODY_VALUE_TYPE_OBJECT = "object"; + + /** 脚本中RequestBody的变量名字段类型 */ + public static final String VAR_NAME_REQUEST_BODY_VALUE_TYPE_ARRAY = "array"; + + public static final String HEADER_REQUEST_SCRIPT_ID = "Magic-Request-Script-Id"; + + public static final String HEADER_REQUEST_CLIENT_ID = "Magic-Request-Client-Id"; + + public static final String HEADER_REQUEST_BREAKPOINTS = "Magic-Request-Breakpoints"; + + public static final String ATTRIBUTE_MAGIC_USER = "MAGIC_API_ATTRIBUTE_USER"; + + public static final String MAGIC_TOKEN_HEADER = "Magic-Token"; + + public static final String GROUP_METABASE = "group.json"; + + public static final String UPLOAD_MODE_FULL = "full"; + + public static final String LOCK = "1"; + + public static final String UNLOCK = "0"; + + public static final String ROOT_ID = "0"; + + public static final String EVENT_TYPE_FILE = "file"; + + public static final String EVENT_SOURCE_NOTIFY = "notify"; + + public static final String WEBSOCKET_ATTRIBUTE_FILE_ID = "fileId"; + + public static final String WEBSOCKET_ATTRIBUTE_USER_ID = "id"; + + public static final String WEBSOCKET_ATTRIBUTE_USER_NAME = "username"; + + public static final String WEBSOCKET_ATTRIBUTE_USER_IP = "ip"; + + public static final String WEBSOCKET_ATTRIBUTE_CLIENT_ID = "cid"; + + /** 执行成功的code值 */ + public static int RESPONSE_CODE_SUCCESS = 1; + + /** 执行成功的message值 */ + public static final String RESPONSE_MESSAGE_SUCCESS = "success"; + + /** 执行出现异常的code值 */ + public static int RESPONSE_CODE_EXCEPTION = -1; + + /** 参数验证未通过的code值 */ + public static int RESPONSE_CODE_INVALID = 0; + + /** 空数组 */ + public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + /** The Es driver. */ + public static final String ES_DRIVER = "org.elasticsearch.xpack.sql.jdbc.EsDriver"; +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Crud.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Crud.java new file mode 100644 index 0000000..b7e9b50 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Crud.java @@ -0,0 +1,33 @@ +package cn.universal.plugins.protocolapi.core.config; + +/** + * CRUD 配置 + * + * @author 冰点 + * @date 2021-7-15 09:26:17 + * @since 1.3.4 + */ +public class Crud { + + /** 逻辑删除列 */ + private String logicDeleteColumn = "is_valid"; + + /** 逻辑删除值 */ + private String logicDeleteValue = "0"; + + public String getLogicDeleteColumn() { + return logicDeleteColumn; + } + + public void setLogicDeleteColumn(String logicDeleteColumn) { + this.logicDeleteColumn = logicDeleteColumn; + } + + public String getLogicDeleteValue() { + return logicDeleteValue; + } + + public void setLogicDeleteValue(String logicDeleteValue) { + this.logicDeleteValue = logicDeleteValue; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Debug.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Debug.java new file mode 100644 index 0000000..ca635b3 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Debug.java @@ -0,0 +1,20 @@ +package cn.universal.plugins.protocolapi.core.config; + +/** + * Debug配置 + * + * @author mxd + */ +public class Debug { + + /** 断点超时时间 */ + private int timeout = 60; + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/JsonCodeConstants.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/JsonCodeConstants.java new file mode 100644 index 0000000..a2d74aa --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/JsonCodeConstants.java @@ -0,0 +1,102 @@ +package cn.universal.plugins.protocolapi.core.config; + +import cn.universal.plugins.protocolapi.core.exception.InvalidArgumentException; +import cn.universal.plugins.protocolapi.core.model.JsonCode; +import org.apache.commons.lang3.StringUtils; + +public interface JsonCodeConstants { + + JsonCode SUCCESS = new JsonCode(1, Constants.RESPONSE_MESSAGE_SUCCESS); + + JsonCode IS_READ_ONLY = new JsonCode(-2, "当前为只读模式,无法操作"); + + JsonCode PERMISSION_INVALID = new JsonCode(-10, "无权限操作."); + + JsonCode GROUP_NOT_FOUND = new JsonCode(1001, "找不到分组信息"); + + JsonCode NOT_SUPPORTED_GROUP_TYPE = new JsonCode(1002, "不支持该分组类型"); + + JsonCode TARGET_IS_REQUIRED = new JsonCode(1003, "目标网址不能为空"); + + JsonCode SECRET_KEY_IS_REQUIRED = new JsonCode(1004, "secretKey不能为空"); + + JsonCode MOVE_NAME_CONFLICT = new JsonCode(1005, "移动后名称会重复,请修改名称后在试。"); + + JsonCode SRC_GROUP_CONFLICT = new JsonCode(1006, "源对象和分组不能一致"); + + JsonCode FILE_NOT_FOUND = new JsonCode(1007, "找不到对应文件或分组"); + + JsonCode RESOURCE_LOCKED = new JsonCode(1008, "当前资源已被锁定,请解锁后在操作。"); + + JsonCode PATH_CONFLICT = new JsonCode(1009, "该路径已被使用,请换一个路径在试"); + + JsonCode RESOURCE_PATH_CONFLICT = new JsonCode(1010, "资源中[%s]有冲突,请检查"); + + JsonCode MOVE_PATH_CONFLICT = new JsonCode(1011, "移动后路径会冲突,请换一个路径在试"); + + JsonCode SAVE_GROUP_PATH_CONFLICT = new JsonCode(1036, "保存后路径会冲突,请换一个路径在试"); + + JsonCode REQUEST_METHOD_REQUIRED = new JsonCode(1012, "请求方法不能为空"); + + JsonCode REQUEST_PATH_REQUIRED = new JsonCode(1013, "请求路径不能为空"); + + JsonCode FUNCTION_PATH_REQUIRED = new JsonCode(1014, "函数路径不能为空"); + + JsonCode FILE_PATH_NOT_EXISTS = new JsonCode(1015, "配置的文件路径不存在,请检查"); + + JsonCode REQUEST_PATH_CONFLICT = new JsonCode(1016, "接口[%s(%s)]与应用冲突,无法注册"); + + JsonCode SCRIPT_REQUIRED = new JsonCode(1017, "脚本内容不能为空"); + + JsonCode NAME_REQUIRED = new JsonCode(1018, "名称不能为空"); + + JsonCode PATH_REQUIRED = new JsonCode(1019, "路径不能为空"); + + JsonCode DS_URL_REQUIRED = new JsonCode(1020, "jdbcURL不能为空"); + + JsonCode DS_KEY_REQUIRED = new JsonCode(1021, "key不能为空"); + + JsonCode DS_KEY_CONFLICT = new JsonCode(1022, "数据源key已被使用,请更换后在试"); + + JsonCode GROUP_ID_REQUIRED = new JsonCode(1023, "请选择分组"); + + JsonCode CRON_ID_REQUIRED = new JsonCode(1024, "cron表达式不能为空"); + + JsonCode NAME_INVALID = new JsonCode(1025, "名称不能包含特殊字符,只允许中文、数字、字母以及+_-.()的组合且不能.开头"); + + JsonCode DATASOURCE_KEY_INVALID = new JsonCode(1026, "数据源Key不能包含特殊字符,只允许中文、数字、字母以及_组合"); + + JsonCode FILE_SAVE_FAILURE = new JsonCode(1027, "保存失败,同一组下分组名称不能重复且不能包含特殊字符。"); + + JsonCode PARAMETER_INVALID = new JsonCode(1028, "参数验证失败"); + + JsonCode HEADER_INVALID = new JsonCode(1029, "header验证失败"); + + JsonCode PATH_VARIABLE_INVALID = new JsonCode(1030, "路径变量验证失败"); + + JsonCode BODY_INVALID = new JsonCode(1031, "body验证失败"); + + JsonCode FILE_IS_REQUIRED = new JsonCode(1032, "请上传文件"); + + JsonCode SIGN_IS_INVALID = new JsonCode(1033, "签名验证失败,请检查秘钥是否正确"); + + JsonCode BACKUP_NOT_ENABLED = new JsonCode(1034, "未启用备份,无法操作"); + + JsonCode API_NOT_FOUND = new JsonCode(1035, "找不到接口"); + + default void notNull(Object value, JsonCode jsonCode) { + if (value == null) { + throw new InvalidArgumentException(jsonCode); + } + } + + default void isTrue(boolean value, JsonCode jsonCode) { + if (!value) { + throw new InvalidArgumentException(jsonCode); + } + } + + default void notBlank(String value, JsonCode jsonCode) { + isTrue(StringUtils.isNotBlank(value), jsonCode); + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicAPIProperties.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicAPIProperties.java new file mode 100644 index 0000000..6bd37f0 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicAPIProperties.java @@ -0,0 +1,417 @@ +package cn.universal.plugins.protocolapi.core.config; + +import cn.universal.plugins.protocolapi.core.web.RequestHandler; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + * magic-api配置信息 + * + * @author mxd + */ +@ConfigurationProperties(prefix = "magic-api") +public class MagicAPIProperties { + + /** 版本号 */ + private final String version = RequestHandler.class.getPackage().getImplementationVersion(); + + /** web页面入口 */ + private String web; + + /** 接口路径前缀 */ + private String prefix; + + /** 打印banner */ + private boolean banner = true; + + /** 是否抛出异常 */ + private boolean throwException = false; + + /** + * 自动导入的模块,多个用","分隔 + * + * @since 0.3.2 + */ + private String autoImportModule = "db"; + + /** + * 可自动导入的包(目前只支持以.*结尾的通配符),多个用","分隔 + * + * @since 0.4.0 + */ + private String autoImportPackage; + + /** + * 是否允许覆盖应用接口,默认为false + * + * @since 0.4.0 + */ + private boolean allowOverride = false; + + /** + * SQL列名转换 + * + * @since 0.5.0 + */ + private String sqlColumnCase = "default"; + + /** + * 线程核心数,需要>0,<=0时采用默认配置,即CPU核心数 * 2 + * + * @since 0.4.5 + */ + private int threadPoolExecutorSize = 0; + + /** + * 编辑器配置文件路径(js) + * + * @since 0.6.1 + */ + private String editorConfig; + + /** + * 是否启用跨域支持 + * + * @since 1.0.0 + */ + private boolean supportCrossDomain = true; + + /** + * JSON响应结构表达式 + * + * @since 1.0.0 + */ + private String response; + + /** + * 远程推送时的秘钥,未配置则不开启 + * + * @since 1.2.1 + */ + private String secretKey; + + /** + * 远程推送的路径,默认为/_magic-api-sync + * + * @since 1.2.1 + */ + private String pushPath = "/_magic-api-sync"; + + /** 启动完成之后打印地址 */ + private boolean showUrl = true; + + /** + * 是否要打印SQL + * + * @since 1.5.0 + */ + private boolean showSql = true; + + /** 插件开关 - 设置为true时启用插件 */ + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * 请求参数日期格式 + * + * @since 1.5.2 + */ + private String[] datePattern = + new String[] {"yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyyMMddHHmmss", "yyyyMMdd"}; + + /** + * 编译缓存容量 + * + * @since 1.5.3 + */ + private int compileCacheSize = 500; + + /** + * 是否持久化保存ResponseBody + * + * @since 1.5.3 + */ + private boolean persistenceResponseBody = true; + + /** 实例ID,集群环境下,要保证每台机器不同。默认启动后随机生成uuid */ + private String instanceId = UUID.randomUUID().toString(); + + @NestedConfigurationProperty private Security security = new Security(); + + @NestedConfigurationProperty private Page page = new Page(); + + @NestedConfigurationProperty private Cache cache = new Cache(); + + @NestedConfigurationProperty private Debug debug = new Debug(); + + @NestedConfigurationProperty private Resource resource = new Resource(); + + @NestedConfigurationProperty private ResponseCode responseCode = new ResponseCode(); + + @NestedConfigurationProperty private Crud crud = new Crud(); + + @NestedConfigurationProperty private Backup backup = new Backup(); + + public String getWeb() { + if (StringUtils.isBlank(web)) { + return null; + } + if (web.endsWith("/**")) { + return web.substring(0, web.length() - 3); + } + if (web.endsWith("/*")) { + return web.substring(0, web.length() - 2); + } + if (web.endsWith("/")) { + return web.substring(0, web.length() - 1); + } + return web; + } + + public void setWeb(String web) { + this.web = web; + } + + public String getSqlColumnCase() { + return sqlColumnCase; + } + + public void setSqlColumnCase(String sqlColumnCase) { + this.sqlColumnCase = sqlColumnCase; + } + + public boolean isBanner() { + return banner; + } + + public void setBanner(boolean banner) { + this.banner = banner; + } + + public List getAutoImportModuleList() { + return Arrays.asList(autoImportModule.replaceAll("\\s", "").split(",")); + } + + public List getAutoImportPackageList() { + if (autoImportPackage == null) { + return Collections.emptyList(); + } + return Arrays.asList(autoImportPackage.replaceAll("\\s", "").split(",")); + } + + public String getVersion() { + return version; + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public boolean isThrowException() { + return throwException; + } + + public void setThrowException(boolean throwException) { + this.throwException = throwException; + } + + public String getAutoImportModule() { + return autoImportModule; + } + + public void setAutoImportModule(String autoImportModule) { + this.autoImportModule = autoImportModule; + } + + public String getAutoImportPackage() { + return autoImportPackage; + } + + public void setAutoImportPackage(String autoImportPackage) { + this.autoImportPackage = autoImportPackage; + } + + public boolean isAllowOverride() { + return allowOverride; + } + + public void setAllowOverride(boolean allowOverride) { + this.allowOverride = allowOverride; + } + + public int getThreadPoolExecutorSize() { + return threadPoolExecutorSize; + } + + public void setThreadPoolExecutorSize(int threadPoolExecutorSize) { + this.threadPoolExecutorSize = threadPoolExecutorSize; + } + + public String getEditorConfig() { + return editorConfig; + } + + public void setEditorConfig(String editorConfig) { + this.editorConfig = editorConfig; + } + + public boolean isSupportCrossDomain() { + return supportCrossDomain; + } + + public void setSupportCrossDomain(boolean supportCrossDomain) { + this.supportCrossDomain = supportCrossDomain; + } + + public String getResponse() { + return response; + } + + public void setResponse(String response) { + this.response = response; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public String getPushPath() { + return pushPath; + } + + public void setPushPath(String pushPath) { + this.pushPath = pushPath; + } + + public boolean isShowUrl() { + return showUrl; + } + + public void setShowUrl(boolean showUrl) { + this.showUrl = showUrl; + } + + public boolean isShowSql() { + return showSql; + } + + public void setShowSql(boolean showSql) { + this.showSql = showSql; + } + + public String[] getDatePattern() { + return datePattern; + } + + public void setDatePattern(String[] datePattern) { + this.datePattern = datePattern; + } + + public int getCompileCacheSize() { + return compileCacheSize; + } + + public void setCompileCacheSize(int compileCacheSize) { + this.compileCacheSize = compileCacheSize; + } + + public boolean isPersistenceResponseBody() { + return persistenceResponseBody; + } + + public void setPersistenceResponseBody(boolean persistenceResponseBody) { + this.persistenceResponseBody = persistenceResponseBody; + } + + public String getInstanceId() { + return instanceId; + } + + public void setInstanceId(String instanceId) { + this.instanceId = instanceId; + } + + public Security getSecurity() { + return security; + } + + public void setSecurity(Security security) { + this.security = security; + } + + public Page getPage() { + return page; + } + + public void setPage(Page page) { + this.page = page; + } + + public Cache getCache() { + return cache; + } + + public void setCache(Cache cache) { + this.cache = cache; + } + + public Debug getDebug() { + return debug; + } + + public void setDebug(Debug debug) { + this.debug = debug; + } + + public Resource getResource() { + return resource; + } + + public void setResource(Resource resource) { + this.resource = resource; + } + + public ResponseCode getResponseCode() { + return responseCode; + } + + public void setResponseCode(ResponseCode responseCode) { + this.responseCode = responseCode; + } + + public Crud getCrud() { + return crud; + } + + public void setCrud(Crud crud) { + this.crud = crud; + } + + public Backup getBackup() { + return backup; + } + + public void setBackup(Backup backup) { + this.backup = backup; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicConfiguration.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicConfiguration.java new file mode 100644 index 0000000..5c1c1bd --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicConfiguration.java @@ -0,0 +1,203 @@ +package cn.universal.plugins.protocolapi.core.config; + +import cn.universal.plugins.protocolapi.backup.service.MagicBackupService; +import cn.universal.plugins.protocolapi.core.interceptor.AuthorizationInterceptor; +import cn.universal.plugins.protocolapi.core.interceptor.RequestInterceptor; +import cn.universal.plugins.protocolapi.core.interceptor.ResultProvider; +import cn.universal.plugins.protocolapi.core.model.MagicEntity; +import cn.universal.plugins.protocolapi.core.resource.Resource; +import cn.universal.plugins.protocolapi.core.service.MagicAPIService; +import cn.universal.plugins.protocolapi.core.service.MagicDynamicRegistry; +import cn.universal.plugins.protocolapi.core.service.MagicNotifyService; +import cn.universal.plugins.protocolapi.core.service.MagicResourceService; +import cn.universal.plugins.protocolapi.core.web.RequestHandler; +import cn.universal.plugins.protocolapi.datasource.model.MagicDynamicDataSource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.springframework.http.converter.HttpMessageConverter; + +public class MagicConfiguration { + + /** 拦截器 */ + private final List requestInterceptors = new ArrayList<>(); + + /** 编辑器配置文件 */ + private String editorConfig; + + private MagicAPIService magicAPIService; + + private MagicDynamicDataSource magicDynamicDataSource; + + /** 请求出错时,是否抛出异常 */ + private boolean throwException = false; + + /** 结果处理器 */ + private ResultProvider resultProvider; + + private MagicNotifyService magicNotifyService; + + private String instanceId; + + private Resource workspace; + + private List> httpMessageConverters = new ArrayList<>(); + + private AuthorizationInterceptor authorizationInterceptor; + + private MagicBackupService magicBackupService; + + private static MagicResourceService magicResourceService; + + private List> magicDynamicRegistries; + + /** debug 超时时间 */ + private int debugTimeout; + + private boolean enableWeb = false; + + public void addRequestInterceptor(RequestInterceptor requestInterceptor) { + this.requestInterceptors.add(requestInterceptor); + } + + public AuthorizationInterceptor getAuthorizationInterceptor() { + return authorizationInterceptor; + } + + public void setAuthorizationInterceptor(AuthorizationInterceptor authorizationInterceptor) { + this.authorizationInterceptor = authorizationInterceptor; + } + + public List getRequestInterceptors() { + return requestInterceptors; + } + + public boolean isThrowException() { + return throwException; + } + + public void setThrowException(boolean throwException) { + this.throwException = throwException; + } + + public ResultProvider getResultProvider() { + return resultProvider; + } + + public void setResultProvider(ResultProvider resultProvider) { + this.resultProvider = resultProvider; + } + + public List> getHttpMessageConverters() { + return httpMessageConverters; + } + + public void setHttpMessageConverters(List> httpMessageConverters) { + this.httpMessageConverters = httpMessageConverters; + } + + public int getDebugTimeout() { + return debugTimeout; + } + + public void setDebugTimeout(int debugTimeout) { + this.debugTimeout = debugTimeout; + } + + public boolean isEnableWeb() { + return enableWeb; + } + + public void setEnableWeb(boolean enableWeb) { + this.enableWeb = enableWeb; + } + + public String getEditorConfig() { + return editorConfig; + } + + public void setEditorConfig(String editorConfig) { + this.editorConfig = editorConfig; + } + + public Resource getWorkspace() { + return workspace; + } + + public void setWorkspace(Resource workspace) { + this.workspace = workspace; + } + + public MagicAPIService getMagicAPIService() { + return magicAPIService; + } + + public void setMagicAPIService(MagicAPIService magicAPIService) { + this.magicAPIService = magicAPIService; + } + + public MagicNotifyService getMagicNotifyService() { + return magicNotifyService; + } + + public void setMagicNotifyService(MagicNotifyService magicNotifyService) { + this.magicNotifyService = magicNotifyService; + } + + public String getInstanceId() { + return instanceId; + } + + public void setInstanceId(String instanceId) { + this.instanceId = instanceId; + } + + public MagicBackupService getMagicBackupService() { + return magicBackupService; + } + + public void setMagicBackupService(MagicBackupService magicBackupService) { + this.magicBackupService = magicBackupService; + } + + public MagicDynamicDataSource getMagicDynamicDataSource() { + return magicDynamicDataSource; + } + + public void setMagicDynamicDataSource(MagicDynamicDataSource magicDynamicDataSource) { + this.magicDynamicDataSource = magicDynamicDataSource; + } + + public static MagicResourceService getMagicResourceService() { + return MagicConfiguration.magicResourceService; + } + + public void setMagicResourceService(MagicResourceService magicResourceService) { + MagicConfiguration.magicResourceService = magicResourceService; + } + + public List> getMagicDynamicRegistries() { + return magicDynamicRegistries; + } + + public void setMagicDynamicRegistries( + List> magicDynamicRegistries) { + this.magicDynamicRegistries = magicDynamicRegistries; + } + + /** 打印banner */ + public void printBanner(List plugins) { + System.out.println(" __ __ _ _ ____ ___ "); + System.out.println(" | \\/ | __ _ __ _ (_) ___ / \\ | _ \\|_ _|"); + System.out.println(" | |\\/| | / _` | / _` || | / __| / _ \\ | |_) || | "); + System.out.println(" | | | || (_| || (_| || || (__ / ___ \\ | __/ | | "); + System.out.println(" |_| |_| \\__,_| \\__, ||_| \\___|/_/ \\_\\|_| |___|"); + System.out.println( + " |___/ " + + Objects.toString(RequestHandler.class.getPackage().getImplementationVersion(), "")); + if (!plugins.isEmpty()) { + System.out.println("集成插件:"); + plugins.stream().peek(it -> System.out.print("- ")).forEach(System.out::println); + } + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicCorsFilter.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicCorsFilter.java new file mode 100644 index 0000000..45842fd --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicCorsFilter.java @@ -0,0 +1,27 @@ +package cn.universal.plugins.protocolapi.core.config; + +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletRequest; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletResponse; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpHeaders; + +public abstract class MagicCorsFilter { + + public void process(MagicHttpServletRequest request, MagicHttpServletResponse response) { + if (StringUtils.isNotBlank(Constants.HEADER_REQUEST_CLIENT_ID)) { + String value = request.getHeader(HttpHeaders.ORIGIN); + response.setHeader( + HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, StringUtils.isBlank(value) ? "*" : value); + response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, Constants.CONST_STRING_TRUE); + value = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + if (StringUtils.isNotBlank(value)) { + response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, value); + } + value = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD); + response.setHeader( + HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, + StringUtils.isBlank(value) ? "GET,POST,OPTIONS,PUT,DELETE" : value); + } + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicFunction.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicFunction.java new file mode 100644 index 0000000..198490d --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicFunction.java @@ -0,0 +1,8 @@ +package cn.universal.plugins.protocolapi.core.config; + +/** + * 函数,主要用于脚本中直接可使用的函数,如 now(); + * + * @author mxd + */ +public interface MagicFunction {} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicPluginConfiguration.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicPluginConfiguration.java new file mode 100644 index 0000000..7dd45fc --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MagicPluginConfiguration.java @@ -0,0 +1,14 @@ +package cn.universal.plugins.protocolapi.core.config; + +import cn.universal.plugins.protocolapi.core.model.Plugin; +import cn.universal.plugins.protocolapi.core.web.MagicControllerRegister; + +public interface MagicPluginConfiguration { + + Plugin plugin(); + + /** 注册Controller */ + default MagicControllerRegister controllerRegister() { + return (mapping, configuration) -> {}; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MessageType.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MessageType.java new file mode 100644 index 0000000..fd6789f --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/MessageType.java @@ -0,0 +1,44 @@ +package cn.universal.plugins.protocolapi.core.config; + +/** 消息类型 */ +public enum MessageType { + /* S -> C message */ + /* 日志消息 */ + LOG, + /* 多个日志消息 */ + LOGS, + /* 进入断点 */ + BREAKPOINT, + /* 请求接口发生异常 */ + EXCEPTION, + /* 登录结果 */ + LOGIN_RESPONSE, + /* 刷新token */ + REFRESH_TOKEN, + /* 通知客户端,有用户上线 */ + USER_LOGIN, + /* 通知客户端,有用户下线 */ + USER_LOGOUT, + /* 通知客户端,当前机器在线人数 */ + ONLINE_USERS, + /* 通知客户端,他人进入文件*/ + INTO_FILE_ID, + /* ping */ + PING, + + /* C -> S message */ + /* 设置断点 */ + SET_BREAKPOINT, + /* 恢复断点 */ + RESUME_BREAKPOINT, + /* 登录 */ + LOGIN, + /* 设置当前所在文件 */ + SET_FILE_ID, + /* PONG */ + PONG, + + /* S <-> S -> C message*/ + /* 获取当前在线用户 */ + SEND_ONLINE +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Page.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Page.java new file mode 100644 index 0000000..6606543 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Page.java @@ -0,0 +1,64 @@ +package cn.universal.plugins.protocolapi.core.config; + +/** + * 分页配置 + * + * @author mxd + */ +public class Page { + + /** 默认page表达式 */ + private String page = "page"; + + /** 默认size表达式 */ + private String size = "size"; + + /** 默认首页 */ + private long defaultPage = 1; + + /** 默认页大小 */ + private long defaultSize = 10; + + /** 最大页大小, -1 为不限制 */ + private long maxPageSize = -1; + + public String getPage() { + return page; + } + + public void setPage(String page) { + this.page = page; + } + + public String getSize() { + return size; + } + + public void setSize(String size) { + this.size = size; + } + + public long getDefaultPage() { + return defaultPage; + } + + public void setDefaultPage(long defaultPage) { + this.defaultPage = defaultPage; + } + + public long getDefaultSize() { + return defaultSize; + } + + public void setDefaultSize(long defaultSize) { + this.defaultSize = defaultSize; + } + + public long getMaxPageSize() { + return maxPageSize; + } + + public void setMaxPageSize(long maxPageSize) { + this.maxPageSize = maxPageSize; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Resource.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Resource.java new file mode 100644 index 0000000..5f59219 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Resource.java @@ -0,0 +1,75 @@ +package cn.universal.plugins.protocolapi.core.config; + +/** + * 接口存储配置 + * + * @author mxd + */ +public class Resource { + + /** 存储类型,默认是文件 */ + private String type = "file"; + + /** 文件存储位置 */ + private String location = "/data/magic-api/"; + + /** 是否是只读模式 */ + private boolean readonly = false; + + /** 前缀 */ + private String prefix = "magic-api"; + + /** 使用数据库存储时的表名 */ + private String tableName = "magic_api_file"; + + /** 使用数据库存储时使用的数据源 */ + private String datasource; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public boolean isReadonly() { + return readonly; + } + + public void setReadonly(boolean readonly) { + this.readonly = readonly; + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public String getDatasource() { + return datasource; + } + + public void setDatasource(String datasource) { + this.datasource = datasource; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/ResponseCode.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/ResponseCode.java new file mode 100644 index 0000000..6e87700 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/ResponseCode.java @@ -0,0 +1,43 @@ +package cn.universal.plugins.protocolapi.core.config; + +/** + * json结果code配置 + * + * @author mxd + * @since 1.1.2 + */ +public class ResponseCode { + + /** 执行成功的code值 */ + private int success = 1; + + /** 参数验证未通过的code值 */ + private int invalid = 0; + + /** 执行出现异常的code值 */ + private int exception = -1; + + public int getSuccess() { + return success; + } + + public void setSuccess(int success) { + this.success = success; + } + + public int getInvalid() { + return invalid; + } + + public void setInvalid(int invalid) { + this.invalid = invalid; + } + + public int getException() { + return exception; + } + + public void setException(int exception) { + this.exception = exception; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Security.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Security.java new file mode 100644 index 0000000..e748643 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/Security.java @@ -0,0 +1,32 @@ +package cn.universal.plugins.protocolapi.core.config; + +/** + * 安全配置 + * + * @author mxd + * @since 0.4.0 + */ +public class Security { + + /** 登录用的用户名 */ + private String username; + + /** 登录用的密码 */ + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/WebSocketSessionManager.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/WebSocketSessionManager.java new file mode 100644 index 0000000..2a2ccb5 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/config/WebSocketSessionManager.java @@ -0,0 +1,241 @@ +package cn.universal.plugins.protocolapi.core.config; + +import cn.universal.core.engine.MagicScriptDebugContext; +import cn.universal.plugins.protocolapi.core.context.MagicConsoleSession; +import cn.universal.plugins.protocolapi.core.event.EventAction; +import cn.universal.plugins.protocolapi.core.model.MagicNotify; +import cn.universal.plugins.protocolapi.core.model.Pair; +import cn.universal.plugins.protocolapi.core.service.MagicNotifyService; +import cn.universal.plugins.protocolapi.utils.JsonUtils; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.socket.TextMessage; + +public class WebSocketSessionManager { + + private static final Logger logger = LoggerFactory.getLogger(WebSocketSessionManager.class); + + private static final Map SESSIONS = new ConcurrentHashMap<>(); + + private static MagicNotifyService magicNotifyService; + + private static final Map CONTEXTS = new ConcurrentHashMap<>(); + + private static String instanceId; + + private static final int CHECK_INTERVAL = 20; + + private static final int KEEPALIVE_TIMEOUT = 60 * 1000; + + private static final List> MESSAGE_CACHE = new ArrayList<>(200); + + public static void add(MagicConsoleSession session) { + SESSIONS.put(session.getClientId(), session); + } + + public static MagicConsoleSession getConsoleSession(String clientId) { + return SESSIONS.get(clientId); + } + + static { + // 1秒1次发送日志 + new ScheduledThreadPoolExecutor(1, r -> new Thread(r, "magic-api-send-log-task")) + .scheduleAtFixedRate(WebSocketSessionManager::flushLog, 1, 1, TimeUnit.SECONDS); + // 60秒检测一次是否在线 + new ScheduledThreadPoolExecutor(1, r -> new Thread(r, "magic-api-websocket-clean-task")) + .scheduleAtFixedRate( + WebSocketSessionManager::checkSession, + CHECK_INTERVAL, + CHECK_INTERVAL, + TimeUnit.SECONDS); + } + + public static Collection getSessions() { + return SESSIONS.values(); + } + + public static void remove(MagicConsoleSession session) { + if (session.getClientId() != null) { + remove(session.getClientId()); + } + } + + public static void remove(String sessionId) { + SESSIONS.remove(sessionId); + } + + public static void sendToAll(MessageType messageType, Object... values) { + String content = buildMessage(messageType, values); + sendToAll(content); + } + + private static void sendToAll(String content) { + getSessions().stream() + .filter(MagicConsoleSession::writeable) + .forEach(session -> sendBySession(session, content)); + sendToMachineByClientId(null, content); + } + + public static void sendLogs(String sessionId, String message) { + synchronized (MESSAGE_CACHE) { + MESSAGE_CACHE.add(Pair.of(sessionId, message)); + if (MESSAGE_CACHE.size() >= 100) { + flushLog(); + } + } + } + + public static void flushLog() { + try { + Map> messages; + synchronized (MESSAGE_CACHE) { + messages = + MESSAGE_CACHE.stream() + .collect( + Collectors.groupingBy( + Pair::getFirst, Collectors.mapping(Pair::getSecond, Collectors.toList()))); + MESSAGE_CACHE.clear(); + } + messages.forEach( + (clientId, logs) -> + sendByClientId(clientId, logs.size() > 1 ? MessageType.LOGS : MessageType.LOG, logs)); + } catch (Exception e) { + logger.warn("发送日志失败", e); + } + } + + public static void sendByClientId(String clientId, MessageType messageType, Object... values) { + MagicConsoleSession session = findSession(clientId); + String content = buildMessage(messageType, values); + if (session != null && session.writeable()) { + sendBySession(session, content); + } else { + sendToMachineByClientId(clientId, content); + } + } + + public static void sendToOther( + String excludeClientId, MessageType messageType, Object... values) { + String content = buildMessage(messageType, values); + getSessions().stream() + .filter(MagicConsoleSession::writeable) + .filter(it -> !it.getClientId().equals(excludeClientId)) + .forEach(session -> sendBySession(session, content)); + sendToMachineByClientId(null, content); + } + + public static void sendToMachineByClientId(String clientId, String content) { + if (magicNotifyService != null) { + // 通知其他机器去发送消息 + magicNotifyService.sendNotify( + new MagicNotify(instanceId, EventAction.WS_S_C, clientId, content)); + } + } + + public static void sendToMachine(MessageType messageType, Object... args) { + if (magicNotifyService != null) { + // 通知其他机器去发送消息 + magicNotifyService.sendNotify( + new MagicNotify(instanceId, EventAction.WS_S_S, null, buildMessage(messageType, args))); + } + } + + public static String buildMessage(MessageType messageType, Object... values) { + StringBuilder builder = new StringBuilder(messageType.name().toLowerCase()); + if (values != null) { + for (int i = 0, len = values.length; i < len; i++) { + builder.append(","); + Object value = values[i]; + if (i + 1 < len || value instanceof CharSequence || value instanceof Number) { + builder.append(value); + } else { + builder.append(JsonUtils.toJsonString(value)); + } + } + } + return builder.toString(); + } + + public static void sendByClientId(String clientId, String content) { + if (clientId == null) { + getSessions().stream() + .filter(MagicConsoleSession::writeable) + .forEach(session -> sendBySession(session, content)); + } else { + MagicConsoleSession session = findSession(clientId); + if (session != null) { + sendBySession(session, content); + } + } + } + + public static void sendBySession(MagicConsoleSession session, String content) { + try { + if (session != null) { + synchronized (session.getClientId()) { + session.getWebSocketSession().sendMessage(new TextMessage(content)); + } + } + } catch (Exception e) { + logger.warn("发送WebSocket消息失败: {}", e.getMessage()); + } + } + + public static MagicConsoleSession findSession(String clientId) { + return getSessions().stream() + .filter(it -> Objects.equals(clientId, it.getClientId())) + .findFirst() + .orElse(null); + } + + public static void setMagicNotifyService(MagicNotifyService magicNotifyService) { + WebSocketSessionManager.magicNotifyService = magicNotifyService; + } + + public static void setInstanceId(String instanceId) { + WebSocketSessionManager.instanceId = instanceId; + } + + public static void addMagicScriptContext( + String sessionAndScriptId, MagicScriptDebugContext context) { + CONTEXTS.put(sessionAndScriptId, context); + } + + public static MagicScriptDebugContext findMagicScriptContext(String sessionAndScriptId) { + return CONTEXTS.get(sessionAndScriptId); + } + + public static void removeMagicScriptContext(String sessionAndScriptId) { + CONTEXTS.remove(sessionAndScriptId); + } + + private static void checkSession() { + try { + long activateTime = System.currentTimeMillis() - KEEPALIVE_TIMEOUT; + SESSIONS.entrySet().stream() + .peek( + it -> + WebSocketSessionManager.sendBySession( + it.getValue(), WebSocketSessionManager.buildMessage(MessageType.PING))) + .filter(it -> it.getValue().getActivateTime() < activateTime) + .collect(Collectors.toList()) + .forEach( + entry -> { + MagicConsoleSession session = entry.getValue(); + SESSIONS.remove(entry.getKey()); + session.close(); + sendToAll(MessageType.USER_LOGOUT, session.getAttributes()); + }); + } catch (Exception ignored) { + } + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/CookieContext.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/CookieContext.java new file mode 100644 index 0000000..2e462cd --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/CookieContext.java @@ -0,0 +1,31 @@ +package cn.universal.plugins.protocolapi.core.context; + +import cn.universal.plugins.protocolapi.core.servlet.MagicCookie; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletRequest; +import java.util.HashMap; + +/** + * Cookie Context 用于脚本中获取cookie信息 + * + * @author mxd + */ +public class CookieContext extends HashMap { + + private final MagicCookie[] cookies; + + public CookieContext(MagicHttpServletRequest request) { + this.cookies = request.getCookies(); + } + + @Override + public String get(Object key) { + if (cookies != null) { + for (MagicCookie cookie : cookies) { + if (cookie.getName().equalsIgnoreCase("" + key)) { + return cookie.getValue(); + } + } + } + return null; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/MagicConsoleSession.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/MagicConsoleSession.java new file mode 100644 index 0000000..5b10c9f --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/MagicConsoleSession.java @@ -0,0 +1,107 @@ +package cn.universal.plugins.protocolapi.core.context; + +import cn.universal.plugins.protocolapi.core.config.Constants; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketSession; + +public class MagicConsoleSession { + + private static final Map cached = new ConcurrentHashMap<>(); + + private String clientId; + + private WebSocketSession webSocketSession; + + private final Map attributes = new HashMap<>(); + + private long activateTime = System.currentTimeMillis(); + + private MagicUser user; + + private long timeout; + + public MagicConsoleSession(WebSocketSession webSocketSession) { + this.webSocketSession = webSocketSession; + } + + public String getClientId() { + return clientId; + } + + public WebSocketSession getWebSocketSession() { + return webSocketSession; + } + + public boolean writeable() { + return webSocketSession != null && webSocketSession.isOpen(); + } + + public static MagicConsoleSession from(WebSocketSession session) { + MagicConsoleSession magicConsoleSession = cached.get(session.getId()); + if (magicConsoleSession == null) { + magicConsoleSession = new MagicConsoleSession(session); + cached.put(session.getId(), magicConsoleSession); + } + return magicConsoleSession; + } + + public static void remove(WebSocketSession session) { + cached.remove(session.getId()); + } + + public Object getAttribute(String key) { + return attributes.get(key); + } + + public void setAttribute(String key, Object value) { + attributes.put(key, value); + } + + public Map getAttributes() { + return attributes; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + setAttribute(Constants.WEBSOCKET_ATTRIBUTE_CLIENT_ID, clientId); + } + + public long getActivateTime() { + return activateTime; + } + + public void setActivateTime(long activateTime) { + this.activateTime = activateTime; + } + + public void close() { + if (this.webSocketSession != null) { + remove(this.webSocketSession); + try { + this.webSocketSession.close(CloseStatus.SESSION_NOT_RELIABLE); + } catch (Exception ignored) { + + } + this.webSocketSession = null; + } + } + + public MagicUser getUser() { + return user; + } + + public void setUser(MagicUser user) { + this.user = user; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/MagicUser.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/MagicUser.java new file mode 100644 index 0000000..7a32cc5 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/MagicUser.java @@ -0,0 +1,69 @@ +package cn.universal.plugins.protocolapi.core.context; + +/** + * magic 用户对象 + * + * @author mxd + */ +public class MagicUser { + + private String id; + + private String username; + + private String token; + + /** token 有效期,<=0 为永不过期 */ + private long timeout = -1; + + public MagicUser() {} + + public MagicUser(String id, String username, String token) { + this.id = id; + this.username = username; + this.token = token; + } + + public MagicUser(String id, String username, String token, long timeout) { + this.id = id; + this.username = username; + this.token = token; + this.timeout = timeout; + } + + public static MagicUser guest() { + return new MagicUser(null, "guest", null); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/RequestContext.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/RequestContext.java new file mode 100644 index 0000000..73e1650 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/RequestContext.java @@ -0,0 +1,37 @@ +package cn.universal.plugins.protocolapi.core.context; + +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletRequest; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletResponse; + +/** + * 请求上下文 + * + * @author mxd + */ +public class RequestContext { + + private static final ThreadLocal REQUEST_ENTITY_THREAD_LOCAL = + new InheritableThreadLocal<>(); + + public static MagicHttpServletRequest getHttpServletRequest() { + RequestEntity requestEntity = REQUEST_ENTITY_THREAD_LOCAL.get(); + return requestEntity == null ? null : requestEntity.getRequest(); + } + + public static MagicHttpServletResponse getHttpServletResponse() { + RequestEntity requestEntity = REQUEST_ENTITY_THREAD_LOCAL.get(); + return requestEntity == null ? null : requestEntity.getResponse(); + } + + public static RequestEntity getRequestEntity() { + return REQUEST_ENTITY_THREAD_LOCAL.get(); + } + + public static void setRequestEntity(RequestEntity requestEntity) { + REQUEST_ENTITY_THREAD_LOCAL.set(requestEntity); + } + + public static void remove() { + REQUEST_ENTITY_THREAD_LOCAL.remove(); + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/RequestEntity.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/RequestEntity.java new file mode 100644 index 0000000..083d26c --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/RequestEntity.java @@ -0,0 +1,136 @@ +package cn.universal.plugins.protocolapi.core.context; + +import cn.universal.core.engine.MagicScriptContext; +import cn.universal.plugins.protocolapi.core.model.ApiInfo; +import cn.universal.plugins.protocolapi.core.model.DebugRequest; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletRequest; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletResponse; +import java.util.Map; +import java.util.UUID; + +/** + * 请求信息 + * + * @author mxd + */ +public class RequestEntity { + + private final Long requestTime = System.currentTimeMillis(); + private final String requestId = UUID.randomUUID().toString().replace("-", ""); + private ApiInfo apiInfo; + private MagicHttpServletRequest request; + private MagicHttpServletResponse response; + private boolean requestedFromTest; + private Map parameters; + private Map pathVariables; + private MagicScriptContext magicScriptContext; + private Object requestBody; + private DebugRequest debugRequest; + + private Map headers; + + private RequestEntity() {} + + public static RequestEntity create() { + return new RequestEntity(); + } + + public ApiInfo getApiInfo() { + return apiInfo; + } + + public RequestEntity info(ApiInfo apiInfo) { + this.apiInfo = apiInfo; + return this; + } + + public MagicHttpServletRequest getRequest() { + return request; + } + + public RequestEntity request(MagicHttpServletRequest request) { + this.request = request; + this.debugRequest = DebugRequest.create(request); + return this; + } + + public MagicHttpServletResponse getResponse() { + return response; + } + + public RequestEntity response(MagicHttpServletResponse response) { + this.response = response; + return this; + } + + public boolean isRequestedFromTest() { + return requestedFromTest; + } + + public RequestEntity requestedFromTest(boolean requestedFromTest) { + this.requestedFromTest = requestedFromTest; + return this; + } + + public boolean isRequestedFromDebug() { + return requestedFromTest && !this.debugRequest.getRequestedBreakpoints().isEmpty(); + } + + public Map getParameters() { + return parameters; + } + + public RequestEntity parameters(Map parameters) { + this.parameters = parameters; + return this; + } + + public Map getPathVariables() { + return pathVariables; + } + + public RequestEntity pathVariables(Map pathVariables) { + this.pathVariables = pathVariables; + return this; + } + + public Long getRequestTime() { + return requestTime; + } + + public MagicScriptContext getMagicScriptContext() { + return magicScriptContext; + } + + public RequestEntity setMagicScriptContext(MagicScriptContext magicScriptContext) { + this.magicScriptContext = magicScriptContext; + return this; + } + + public Map getHeaders() { + return headers; + } + + public RequestEntity setHeaders(Map headers) { + this.headers = headers; + return this; + } + + public String getRequestId() { + return requestId; + } + + /** 获取 RequestBody */ + public Object getRequestBody() { + return this.requestBody; + } + + public RequestEntity setRequestBody(Object requestBody) { + this.requestBody = requestBody; + return this; + } + + public DebugRequest getDebugRequest() { + return debugRequest; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/SessionContext.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/SessionContext.java new file mode 100644 index 0000000..a3e0e1f --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/context/SessionContext.java @@ -0,0 +1,30 @@ +package cn.universal.plugins.protocolapi.core.context; + +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpSession; +import java.util.HashMap; + +/** + * Session Context 用于脚本中获取Session信息 + * + * @author mxd + */ +public class SessionContext extends HashMap { + + private final MagicHttpSession session; + + public SessionContext(MagicHttpSession session) { + this.session = session; + } + + @Override + public Object get(Object key) { + return session != null ? session.getAttribute(key.toString()) : null; + } + + @Override + public Object put(String key, Object value) { + Object oldValue = session.getAttribute(key); + session.setAttribute(key, value); + return oldValue; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/EventAction.class b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/EventAction.class new file mode 100644 index 0000000..ddf330c Binary files /dev/null and b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/EventAction.class differ diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/EventAction.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/EventAction.java new file mode 100644 index 0000000..cc90097 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/EventAction.java @@ -0,0 +1,14 @@ +package cn.universal.plugins.protocolapi.core.event; + +public enum EventAction { + CREATE, + SAVE, + DELETE, + MOVE, + LOAD, + WS_C_S, + WS_S_C, + RELOAD, + CLEAR, + WS_S_S +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/FileEvent.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/FileEvent.java new file mode 100644 index 0000000..0bbd46e --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/FileEvent.java @@ -0,0 +1,22 @@ +package cn.universal.plugins.protocolapi.core.event; + +import cn.universal.plugins.protocolapi.core.model.MagicEntity; + +public class FileEvent extends MagicEvent { + + private final MagicEntity entity; + + public FileEvent(String type, EventAction action, MagicEntity entity) { + super(type, action); + this.entity = entity; + } + + public FileEvent(String type, EventAction action, MagicEntity entity, String source) { + super(type, action, source); + this.entity = entity; + } + + public MagicEntity getEntity() { + return entity; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/GroupEvent.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/GroupEvent.java new file mode 100644 index 0000000..7d7641c --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/GroupEvent.java @@ -0,0 +1,41 @@ +package cn.universal.plugins.protocolapi.core.event; + +import cn.universal.plugins.protocolapi.core.model.Group; +import cn.universal.plugins.protocolapi.core.model.MagicEntity; +import java.util.Collections; +import java.util.List; + +public class GroupEvent extends MagicEvent { + + /** 分组信息 */ + private Group group; + + /** 子分组 */ + private List entities; + + public GroupEvent(String type, EventAction action, Group group) { + this(type, action, group, Collections.emptyList()); + } + + public GroupEvent(String type, EventAction action, Group group, List entities) { + super(type, action); + this.group = group; + this.entities = entities; + } + + public Group getGroup() { + return group; + } + + public void setGroup(Group group) { + this.group = group; + } + + public List getEntities() { + return entities; + } + + public void setEntities(List entities) { + this.entities = entities; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/MagicEvent.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/MagicEvent.java new file mode 100644 index 0000000..aaea88d --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/MagicEvent.java @@ -0,0 +1,39 @@ +package cn.universal.plugins.protocolapi.core.event; + +public class MagicEvent { + + /** 消息类型 */ + private final String type; + + /** 消息动作 */ + private final EventAction action; + + /** 消息来源 */ + private String source; + + public MagicEvent(String type, EventAction action, String source) { + this.type = type; + this.action = action; + this.source = source; + } + + public MagicEvent(String type, EventAction action) { + this(type, action, null); + } + + public String getType() { + return type; + } + + public EventAction getAction() { + return action; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/NotifyEvent.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/NotifyEvent.java new file mode 100644 index 0000000..0292794 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/event/NotifyEvent.java @@ -0,0 +1,18 @@ +package cn.universal.plugins.protocolapi.core.event; + +import cn.universal.plugins.protocolapi.core.config.Constants; +import cn.universal.plugins.protocolapi.core.model.MagicNotify; + +public class NotifyEvent extends MagicEvent { + + private String id; + + public NotifyEvent(MagicNotify notify) { + super(notify.getType(), notify.getAction(), Constants.EVENT_SOURCE_NOTIFY); + this.id = notify.getId(); + } + + public String getId() { + return id; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/InvalidArgumentException.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/InvalidArgumentException.java new file mode 100644 index 0000000..4e05a5a --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/InvalidArgumentException.java @@ -0,0 +1,22 @@ +package cn.universal.plugins.protocolapi.core.exception; + +import cn.universal.plugins.protocolapi.core.model.JsonCode; + +/** + * 参数错误异常 + * + * @author mxd + */ +public class InvalidArgumentException extends RuntimeException { + + private final transient JsonCode jsonCode; + + public InvalidArgumentException(JsonCode jsonCode) { + super(jsonCode.getMessage()); + this.jsonCode = jsonCode; + } + + public int getCode() { + return jsonCode.getCode(); + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/MagicAPIException.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/MagicAPIException.java new file mode 100644 index 0000000..55a9069 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/MagicAPIException.java @@ -0,0 +1,17 @@ +package cn.universal.plugins.protocolapi.core.exception; + +/** + * magic-api异常对象 + * + * @author mxd + */ +public class MagicAPIException extends RuntimeException { + + public MagicAPIException(String message) { + super(message); + } + + public MagicAPIException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/MagicLoginException.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/MagicLoginException.java new file mode 100644 index 0000000..f9147ce --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/MagicLoginException.java @@ -0,0 +1,13 @@ +package cn.universal.plugins.protocolapi.core.exception; + +/** + * 登录异常对象 + * + * @author mxd + */ +public class MagicLoginException extends Exception { + + public MagicLoginException(String message) { + super(message); + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/MagicResourceNotFoundException.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/MagicResourceNotFoundException.java new file mode 100644 index 0000000..a0f4a47 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/MagicResourceNotFoundException.java @@ -0,0 +1,13 @@ +package cn.universal.plugins.protocolapi.core.exception; + +/** + * 资源未找到 + * + * @author mxd + */ +public class MagicResourceNotFoundException extends RuntimeException { + + public MagicResourceNotFoundException(String message) { + super(message); + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/ValidateException.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/ValidateException.java new file mode 100644 index 0000000..18b94ec --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/exception/ValidateException.java @@ -0,0 +1,22 @@ +package cn.universal.plugins.protocolapi.core.exception; + +import cn.universal.plugins.protocolapi.core.model.JsonCode; + +/** + * 接口验证异常 + * + * @author mxd + */ +public class ValidateException extends RuntimeException { + + private final JsonCode jsonCode; + + public ValidateException(JsonCode jsonCode, String message) { + super(message); + this.jsonCode = jsonCode; + } + + public JsonCode getJsonCode() { + return jsonCode; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/handler/MagicCoordinationHandler.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/handler/MagicCoordinationHandler.java new file mode 100644 index 0000000..006721c --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/handler/MagicCoordinationHandler.java @@ -0,0 +1,20 @@ +package cn.universal.plugins.protocolapi.core.handler; + +import cn.universal.plugins.protocolapi.core.annotation.Message; +import cn.universal.plugins.protocolapi.core.config.Constants; +import cn.universal.plugins.protocolapi.core.config.MessageType; +import cn.universal.plugins.protocolapi.core.config.WebSocketSessionManager; +import cn.universal.plugins.protocolapi.core.context.MagicConsoleSession; + +public class MagicCoordinationHandler { + + @Message(MessageType.SET_FILE_ID) + public void setFileId(MagicConsoleSession session, String fileId) { + session.setAttribute(Constants.WEBSOCKET_ATTRIBUTE_FILE_ID, fileId); + WebSocketSessionManager.sendToOther( + session.getClientId(), + MessageType.INTO_FILE_ID, + session.getAttribute(Constants.WEBSOCKET_ATTRIBUTE_CLIENT_ID), + fileId); + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/handler/MagicDebugHandler.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/handler/MagicDebugHandler.java new file mode 100644 index 0000000..32794e2 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/handler/MagicDebugHandler.java @@ -0,0 +1,55 @@ +package cn.universal.plugins.protocolapi.core.handler; + +import cn.universal.core.engine.MagicScriptDebugContext; +import cn.universal.plugins.protocolapi.core.annotation.Message; +import cn.universal.plugins.protocolapi.core.config.MessageType; +import cn.universal.plugins.protocolapi.core.config.WebSocketSessionManager; +import cn.universal.plugins.protocolapi.core.context.MagicConsoleSession; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; + +/** + * WebSocket Debug 处理器 + * + * @author mxd + */ +public class MagicDebugHandler { + + /** 设置断点 当本机没有该Session时,通知其他机器处理 */ + @Message(MessageType.SET_BREAKPOINT) + public boolean setBreakPoint(MagicConsoleSession session, String scriptId, String breakpoints) { + MagicScriptDebugContext context = + WebSocketSessionManager.findMagicScriptContext(session.getClientId() + scriptId); + if (context != null) { + context.setBreakpoints( + Stream.of(breakpoints.split(",")).map(Integer::valueOf).collect(Collectors.toList())); + return true; + } + return false; + } + + /** 恢复断点 当本机没有该Session时,通知其他机器处理 */ + @Message(MessageType.RESUME_BREAKPOINT) + public boolean resumeBreakpoint( + MagicConsoleSession session, String scriptId, String stepInto, String breakpoints) { + MagicScriptDebugContext context = + WebSocketSessionManager.findMagicScriptContext(session.getClientId() + scriptId); + if (context != null) { + context.setStepInto("1".equals(stepInto)); + if (StringUtils.isNotBlank(breakpoints)) { + context.setBreakpoints( + Stream.of(breakpoints.split("\\|")).map(Integer::valueOf).collect(Collectors.toList())); + } else { + context.setBreakpoints(Collections.emptyList()); + } + try { + context.singal(); + } catch (InterruptedException ignored) { + } + return true; + } + return false; + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/handler/MagicWebSocketDispatcher.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/handler/MagicWebSocketDispatcher.java new file mode 100644 index 0000000..43be734 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/handler/MagicWebSocketDispatcher.java @@ -0,0 +1,132 @@ +package cn.universal.plugins.protocolapi.core.handler; + +import static cn.universal.plugins.protocolapi.core.config.Constants.EMPTY_OBJECT_ARRAY; + +import cn.universal.core.engine.reflection.MethodInvoker; +import cn.universal.plugins.protocolapi.core.annotation.Message; +import cn.universal.plugins.protocolapi.core.config.MessageType; +import cn.universal.plugins.protocolapi.core.config.WebSocketSessionManager; +import cn.universal.plugins.protocolapi.core.context.MagicConsoleSession; +import cn.universal.plugins.protocolapi.core.event.EventAction; +import cn.universal.plugins.protocolapi.core.model.MagicNotify; +import cn.universal.plugins.protocolapi.core.service.MagicNotifyService; +import cn.universal.plugins.protocolapi.utils.JsonUtils; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +/** + * WebSocket 分发器 + * + * @author mxd + */ +public class MagicWebSocketDispatcher extends TextWebSocketHandler { + + private static final Logger logger = LoggerFactory.getLogger(MagicWebSocketDispatcher.class); + + private static final Map HANDLERS = new HashMap<>(); + + private final String instanceId; + + private final MagicNotifyService magicNotifyService; + + public MagicWebSocketDispatcher( + String instanceId, + MagicNotifyService magicNotifyService, + List websocketMessageHandlers) { + this.instanceId = instanceId; + this.magicNotifyService = magicNotifyService; + WebSocketSessionManager.setMagicNotifyService(magicNotifyService); + WebSocketSessionManager.setInstanceId(instanceId); + websocketMessageHandlers.forEach( + websocketMessageHandler -> + Stream.of(websocketMessageHandler.getClass().getDeclaredMethods()) + .filter(it -> it.getAnnotation(Message.class) != null) + .forEach( + method -> + HANDLERS.put( + method.getAnnotation(Message.class).value().name().toLowerCase(), + new MethodInvoker(method, websocketMessageHandler)))); + } + + private static Object findHandleAndInvoke(MagicConsoleSession session, String payload) { + // messageType[, data][,data] + int index = payload.indexOf(","); + String msgType = index == -1 ? payload : payload.substring(0, index); + MethodInvoker invoker = HANDLERS.get(msgType); + if (invoker != null) { + Object returnValue; + try { + Class[] pTypes = invoker.getParameterTypes(); + int pCount = pTypes.length; + if (pCount == 0) { + returnValue = invoker.invoke0(invoker.getDefaultTarget(), null, EMPTY_OBJECT_ARRAY); + } else { + Object[] pValues = new Object[pCount]; + for (int i = 0; i < pCount; i++) { + Class pType = pTypes[i]; + if (pType == MagicConsoleSession.class) { + pValues[i] = session; + } else if (pType == String.class) { + int subIndex = payload.indexOf(",", index + 1); + if (subIndex > -1) { + pValues[i] = payload.substring(index + 1, index = subIndex); + } else if (index > -1) { + pValues[i] = payload.substring(index + 1); + } + } else { + pValues[i] = JsonUtils.readValue(payload, pType); + } + } + returnValue = invoker.invoke0(invoker.getDefaultTarget(), null, pValues); + } + return returnValue; + } catch (Throwable e) { + logger.error("WebSocket消息处理出错", e); + } + } + return null; + } + + public static void processMessageReceived(String clientId, String payload) { + MagicConsoleSession session = WebSocketSessionManager.findSession(clientId); + if (session != null) { + findHandleAndInvoke(session, payload); + } + } + + public static void processWebSocketEventMessage(String payload) { + findHandleAndInvoke(null, payload); + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + MagicConsoleSession mcsession = MagicConsoleSession.from(session); + WebSocketSessionManager.remove(mcsession); + MagicConsoleSession.remove(session); + if (mcsession.getClientId() != null + && mcsession.getAttributes() != null + && !mcsession.getAttributes().isEmpty()) { + WebSocketSessionManager.sendToAll(MessageType.USER_LOGOUT, mcsession.getAttributes()); + } + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) { + MagicConsoleSession consoleSession = MagicConsoleSession.from(session); + Object returnValue = findHandleAndInvoke(consoleSession, message.getPayload()); + // 如果未成功处理消息,则通知其他机器去处理消息 + if (Boolean.FALSE.equals(returnValue)) { + magicNotifyService.sendNotify( + new MagicNotify( + instanceId, EventAction.WS_C_S, consoleSession.getClientId(), message.getPayload())); + } + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/handler/MagicWorkbenchHandler.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/handler/MagicWorkbenchHandler.java new file mode 100644 index 0000000..54482ee --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/handler/MagicWorkbenchHandler.java @@ -0,0 +1,123 @@ +package cn.universal.plugins.protocolapi.core.handler; + +import cn.universal.plugins.protocolapi.core.annotation.Message; +import cn.universal.plugins.protocolapi.core.config.Constants; +import cn.universal.plugins.protocolapi.core.config.MessageType; +import cn.universal.plugins.protocolapi.core.config.WebSocketSessionManager; +import cn.universal.plugins.protocolapi.core.context.MagicConsoleSession; +import cn.universal.plugins.protocolapi.core.context.MagicUser; +import cn.universal.plugins.protocolapi.core.interceptor.AuthorizationInterceptor; +import cn.universal.plugins.protocolapi.utils.IpUtils; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; + +/** + * UI上其它操作处理 + * + * @author mxd + */ +public class MagicWorkbenchHandler { + + private final AuthorizationInterceptor authorizationInterceptor; + + private static final MagicUser guest = new MagicUser("guest", "游客", "unauthorization"); + + private static final Logger logger = LoggerFactory.getLogger(MagicWorkbenchHandler.class); + + public MagicWorkbenchHandler(AuthorizationInterceptor authorizationInterceptor) { + this.authorizationInterceptor = authorizationInterceptor; + } + + @Message(MessageType.LOGIN) + public void onLogin(MagicConsoleSession session, String token, String clientId) { + session.setClientId(clientId); + MagicUser user = null; + try { + user = authorizationInterceptor.getUserByToken(token); + } catch (Exception e) { + if (!authorizationInterceptor.requireLogin()) { + user = guest; + } + } + if (user != null) { + String ip = + Optional.ofNullable(session.getWebSocketSession().getRemoteAddress()) + .map(it -> it.getAddress().getHostAddress()) + .orElse("unknown"); + HttpHeaders headers = session.getWebSocketSession().getHandshakeHeaders(); + ip = IpUtils.getRealIP(ip, headers::getFirst, null); + if (user.getTimeout() > 0) { + session.setUser(user); + session.setTimeout(user.getTimeout() * 1000 + System.currentTimeMillis()); + } + session.setAttribute(Constants.WEBSOCKET_ATTRIBUTE_USER_ID, user.getId()); + session.setAttribute( + Constants.WEBSOCKET_ATTRIBUTE_USER_IP, StringUtils.defaultIfBlank(ip, "unknown")); + session.setAttribute(Constants.WEBSOCKET_ATTRIBUTE_USER_NAME, user.getUsername()); + session.setActivateTime(System.currentTimeMillis()); + synchronized (MagicWorkbenchHandler.class) { + if (WebSocketSessionManager.getConsoleSession(clientId) != null) { + WebSocketSessionManager.sendBySession( + session, WebSocketSessionManager.buildMessage(MessageType.LOGIN_RESPONSE, "-1")); + return; + } + WebSocketSessionManager.add(session); + } + WebSocketSessionManager.sendBySession( + session, + WebSocketSessionManager.buildMessage( + MessageType.LOGIN_RESPONSE, "1", session.getAttributes())); + List> messages = getOnlineUsers(); + if (!messages.isEmpty()) { + WebSocketSessionManager.sendByClientId( + session.getClientId(), + WebSocketSessionManager.buildMessage(MessageType.ONLINE_USERS, messages)); + } + WebSocketSessionManager.sendToMachine(MessageType.SEND_ONLINE, session.getClientId()); + WebSocketSessionManager.sendToOther( + session.getClientId(), MessageType.USER_LOGIN, session.getAttributes()); + } else { + WebSocketSessionManager.sendBySession( + session, WebSocketSessionManager.buildMessage(MessageType.LOGIN_RESPONSE, "0")); + } + } + + @Message(MessageType.SEND_ONLINE) + public void sendOnline(String clientId) { + List> messages = getOnlineUsers(); + if (!messages.isEmpty()) { + WebSocketSessionManager.sendToMachineByClientId( + clientId, WebSocketSessionManager.buildMessage(MessageType.ONLINE_USERS, messages)); + } + } + + @Message(MessageType.PONG) + public String pong(MagicConsoleSession session) { + session.setActivateTime(System.currentTimeMillis()); + MagicUser user = session.getUser(); + if (user != null && session.getTimeout() - System.currentTimeMillis() < 60 * 1000) { + String oldToken = user.getToken(); + authorizationInterceptor.refreshToken(user); + String newToken = user.getToken(); + session.setTimeout(System.currentTimeMillis() + user.getTimeout() * 1000); + if (!Objects.equals(newToken, oldToken)) { + WebSocketSessionManager.sendBySession( + session, WebSocketSessionManager.buildMessage(MessageType.REFRESH_TOKEN, newToken)); + } + } + return null; + } + + private List> getOnlineUsers() { + return WebSocketSessionManager.getSessions().stream() + .map(MagicConsoleSession::getAttributes) + .collect(Collectors.toList()); + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/Authorization.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/Authorization.java new file mode 100644 index 0000000..f08d275 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/Authorization.java @@ -0,0 +1,29 @@ +package cn.universal.plugins.protocolapi.core.interceptor; + +/** + * 鉴权类型枚举 + * + * @author mxd + */ +public enum Authorization { + /** 无实际意义 */ + NONE, + /** 执行保存动作 */ + SAVE, + /** 执行查看详情、列表动作 */ + VIEW, + /** 执行删除动作 */ + DELETE, + /** 执行导出动作 */ + DOWNLOAD, + /** 执行上传动作 */ + UPLOAD, + /** 执行推送动作 */ + PUSH, + /** 锁定动作 */ + LOCK, + /** 解锁动作 */ + UNLOCK, + /** 重新加载 */ + RELOAD +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/AuthorizationInterceptor.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/AuthorizationInterceptor.java new file mode 100644 index 0000000..d7f5137 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/AuthorizationInterceptor.java @@ -0,0 +1,109 @@ +package cn.universal.plugins.protocolapi.core.interceptor; + +import cn.universal.plugins.protocolapi.core.context.MagicUser; +import cn.universal.plugins.protocolapi.core.exception.MagicLoginException; +import cn.universal.plugins.protocolapi.core.model.Group; +import cn.universal.plugins.protocolapi.core.model.MagicEntity; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletRequest; + +/** + * UI权限拦截器 + * + * @author mxd + */ +public interface AuthorizationInterceptor { + + /** + * 是否需要登录 + * + * @return true 需要登录, false 不需要登录 + */ + default boolean requireLogin() { + return false; + } + + /** + * 根据Token获取User对象 + * + * @param token token值 + * @return 登录成功后返回MagicUser对象 + * @throws MagicLoginException 登录失败抛出 + */ + default MagicUser getUserByToken(String token) throws MagicLoginException { + return null; + } + + /** + * 根据用户名,密码登录 + * + * @param username 用户名 + * @param password 密码 + * @return 登录成功后返回MagicUser对象 + * @throws MagicLoginException 登录失败抛出 + */ + default MagicUser login(String username, String password) throws MagicLoginException { + return null; + } + + /** + * 退出登录 + * + * @param token token值 + */ + default void logout(String token) {} + + /** + * 是否拥有页面按钮的权限 + * + * @param magicUser 登录的用户对象 + * @param request HttpServletRequest + * @param authorization 鉴权方法 + * @return true 有权限访问, false 无权限访问 + */ + default boolean allowVisit( + MagicUser magicUser, MagicHttpServletRequest request, Authorization authorization) { + return true; + } + + /** + * 是否拥有对该接口的增删改权限 + * + * @param magicUser 登录的用户对象 + * @param request HttpServletRequest + * @param authorization 鉴权方法 + * @param entity 接口、函数、数据源信息 + * @return true 有权限访问, false 无权限访问 + */ + default boolean allowVisit( + MagicUser magicUser, + MagicHttpServletRequest request, + Authorization authorization, + MagicEntity entity) { + return allowVisit(magicUser, request, authorization); + } + + /** + * 是否拥有对该分组的增删改权限 + * + * @param magicUser 登录的用户对象 + * @param request HttpServletRequest + * @param authorization 鉴权方法 + * @param group 分组信息 + * @return true 有权限访问, false 无权限访问 + */ + default boolean allowVisit( + MagicUser magicUser, + MagicHttpServletRequest request, + Authorization authorization, + Group group) { + return allowVisit(magicUser, request, authorization); + } + + /** + * 刷新 token, 重新赋值对象内的token和timeout + * + * @param user + * @return + */ + default void refreshToken(MagicUser user) {} +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/DefaultAuthorizationInterceptor.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/DefaultAuthorizationInterceptor.java new file mode 100644 index 0000000..8d9efcb --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/DefaultAuthorizationInterceptor.java @@ -0,0 +1,48 @@ +package cn.universal.plugins.protocolapi.core.interceptor; + +import cn.universal.plugins.protocolapi.core.context.MagicUser; +import cn.universal.plugins.protocolapi.core.exception.MagicLoginException; +import cn.universal.plugins.protocolapi.utils.MD5Utils; +import java.util.Objects; + +/** + * 默认UI鉴权实现 + * + * @author mxd + */ +public class DefaultAuthorizationInterceptor implements AuthorizationInterceptor { + + private final boolean requireLogin; + private String validToken; + private MagicUser configMagicUser; + + public DefaultAuthorizationInterceptor(String username, String password) { + if (this.requireLogin = username != null && password != null) { + this.validToken = MD5Utils.encrypt(String.format("%s||%s", username, password)); + this.configMagicUser = new MagicUser(username, username, this.validToken); + } + } + + @Override + public boolean requireLogin() { + return this.requireLogin; + } + + @Override + public MagicUser getUserByToken(String token) throws MagicLoginException { + if (requireLogin && Objects.equals(validToken, token)) { + return configMagicUser; + } + throw new MagicLoginException("token无效"); + } + + @Override + public MagicUser login(String username, String password) throws MagicLoginException { + if (requireLogin + && Objects.equals( + MD5Utils.encrypt(String.format("%s||%s", username, password)), this.validToken)) { + return configMagicUser; + } + throw new MagicLoginException("用户名或密码不正确"); + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/DefaultResultProvider.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/DefaultResultProvider.java new file mode 100644 index 0000000..c5b8662 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/DefaultResultProvider.java @@ -0,0 +1,49 @@ +package cn.universal.plugins.protocolapi.core.interceptor; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import cn.universal.core.engine.MagicScriptContext; +import cn.universal.plugins.protocolapi.core.context.RequestEntity; +import cn.universal.plugins.protocolapi.utils.ScriptManager; +import java.util.Map; + +/** 默认结果封装实现 */ +public class DefaultResultProvider implements ResultProvider { + + private final String responseScript; + + public DefaultResultProvider(String responseScript) { + this.responseScript = responseScript; + } + + @Override + public Object buildResult(RequestEntity requestEntity, int code, String message, Object data) { + long timestamp = System.currentTimeMillis(); + if (this.responseScript != null) { + MagicScriptContext context = new MagicScriptContext(); + context.setScriptName(requestEntity.getMagicScriptContext().getScriptName()); + // context.set("code", code); + // context.set("message", message); + String v = (String) data; + if (JSONUtil.isJson(v)) { + context.putMapIntoContext(JSONUtil.toBean(v, Map.class)); + } else { + context.set("data", data); + } + // context.set("apiInfo", requestEntity.getApiInfo()); + // context.set("request", requestEntity.getRequest()); + // context.set("response", requestEntity.getResponse()); + // context.set("timestamp", timestamp); + // context.set("requestTime", requestEntity.getRequestTime()); + // context.set("executeTime", timestamp - requestEntity.getRequestTime()); + return ScriptManager.executeExpression(responseScript, context); + } else { + if (code == -1 && ObjectUtil.isNull(data)) { + return message; + } + return data; + // return new JsonBean<>(code, message, data, (int) (timestamp - + // requestEntity.getRequestTime())); + } + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/MagicWebRequestInterceptor.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/MagicWebRequestInterceptor.java new file mode 100644 index 0000000..9d8ce09 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/MagicWebRequestInterceptor.java @@ -0,0 +1,48 @@ +package cn.universal.plugins.protocolapi.core.interceptor; + +import cn.universal.plugins.protocolapi.core.annotation.Valid; +import cn.universal.plugins.protocolapi.core.config.Constants; +import cn.universal.plugins.protocolapi.core.config.MagicCorsFilter; +import cn.universal.plugins.protocolapi.core.exception.MagicLoginException; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletRequest; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletResponse; +import cn.universal.plugins.protocolapi.core.web.MagicController; +import org.springframework.web.method.HandlerMethod; + +public abstract class MagicWebRequestInterceptor { + + private final MagicCorsFilter magicCorsFilter; + + private final AuthorizationInterceptor authorizationInterceptor; + + public MagicWebRequestInterceptor( + MagicCorsFilter magicCorsFilter, AuthorizationInterceptor authorizationInterceptor) { + this.magicCorsFilter = magicCorsFilter; + this.authorizationInterceptor = authorizationInterceptor; + } + + public void handle( + Object handler, MagicHttpServletRequest request, MagicHttpServletResponse response) + throws MagicLoginException { + HandlerMethod handlerMethod; + if (handler instanceof HandlerMethod) { + handlerMethod = (HandlerMethod) handler; + handler = handlerMethod.getBean(); + if (handler instanceof MagicController) { + if (magicCorsFilter != null) { + magicCorsFilter.process(request, response); + } + Valid valid = handlerMethod.getMethodAnnotation(Valid.class); + boolean requiredLogin = authorizationInterceptor.requireLogin(); + boolean validRequiredLogin = (valid == null || valid.requireLogin()); + if (validRequiredLogin && requiredLogin) { + request.setAttribute( + Constants.ATTRIBUTE_MAGIC_USER, + authorizationInterceptor.getUserByToken( + request.getHeader(Constants.MAGIC_TOKEN_HEADER))); + } + ((MagicController) handler).doValid(request, valid); + } + } + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/RequestInterceptor.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/RequestInterceptor.java new file mode 100644 index 0000000..2984651 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/RequestInterceptor.java @@ -0,0 +1,121 @@ +package cn.universal.plugins.protocolapi.core.interceptor; + +import cn.universal.core.engine.MagicScriptContext; +import cn.universal.plugins.protocolapi.core.context.RequestEntity; +import cn.universal.plugins.protocolapi.core.model.ApiInfo; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletRequest; +import cn.universal.plugins.protocolapi.core.servlet.MagicHttpServletResponse; + +/** + * 请求拦截器 + * + * @author mxd + */ +public interface RequestInterceptor { + + /** + * 请求之前执行 + * + * @param requestEntity 请求对象 + * @return 当返回对象时,直接将此对象返回到页面,返回null时,继续执行后续操作 + */ + default Object preHandle(RequestEntity requestEntity) throws Exception { + return preHandle( + requestEntity.getApiInfo(), + requestEntity.getMagicScriptContext(), + requestEntity.getRequest(), + requestEntity.getResponse()); + } + + /** + * 请求之前执行 + * + * @param info 接口信息 + * @param context 脚本上下文 + * @param request HttpServletRequest + * @param response HttpServletResponse + * @return 当返回对象时,直接将此对象返回到页面,返回null时,继续执行后续操作 + * @throws Exception 处理失败时抛出的异常 + */ + default Object preHandle( + ApiInfo info, + MagicScriptContext context, + MagicHttpServletRequest request, + MagicHttpServletResponse response) + throws Exception { + return null; + } + + /** + * 执行完毕之后执行 + * + * @param info 接口信息 + * @param context 脚本上下文 + * @param returnValue 即将要返回到页面的值 + * @param request HttpServletRequest + * @param response HttpServletResponse + * @return 返回到页面的对象, 当返回null时执行后续拦截器,否则直接返回该值,不执行后续拦截器 + * @throws Exception 处理失败时抛出的异常 + */ + default Object postHandle( + ApiInfo info, + MagicScriptContext context, + Object returnValue, + MagicHttpServletRequest request, + MagicHttpServletResponse response) + throws Exception { + return null; + } + + /** + * 执行完毕之后执行 + * + * @param requestEntity 请求对象 + * @param returnValue 即将要返回到页面的值 + * @return 返回到页面的对象, 当返回null时执行后续拦截器,否则直接返回该值,不执行后续拦截器 + */ + default Object postHandle(RequestEntity requestEntity, Object returnValue) throws Exception { + return postHandle( + requestEntity.getApiInfo(), + requestEntity.getMagicScriptContext(), + returnValue, + requestEntity.getRequest(), + requestEntity.getResponse()); + } + + /** + * 接口执行完毕之后执行 + * + * @param requestEntity 请求对象 + * @param returnValue 即将要返回到页面的值 + * @param throwable 异常对象 + */ + default void afterCompletion( + RequestEntity requestEntity, Object returnValue, Throwable throwable) { + afterCompletion( + requestEntity.getApiInfo(), + requestEntity.getMagicScriptContext(), + returnValue, + requestEntity.getRequest(), + requestEntity.getResponse(), + throwable); + } + + /** + * 接口执行完毕之后执行 + * + * @param info 接口信息 + * @param context 脚本上下文 + * @param returnValue 即将要返回到页面的值 + * @param request HttpServletRequest + * @param response HttpServletResponse + * @param throwable 异常对象 + */ + default void afterCompletion( + ApiInfo info, + MagicScriptContext context, + Object returnValue, + MagicHttpServletRequest request, + MagicHttpServletResponse response, + Throwable throwable) {} +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/ResultProvider.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/ResultProvider.java new file mode 100644 index 0000000..0cb480e --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/interceptor/ResultProvider.java @@ -0,0 +1,119 @@ +package cn.universal.plugins.protocolapi.core.interceptor; + +import static cn.universal.plugins.protocolapi.core.config.Constants.RESPONSE_CODE_EXCEPTION; +import static cn.universal.plugins.protocolapi.core.config.Constants.RESPONSE_CODE_SUCCESS; +import static cn.universal.plugins.protocolapi.core.config.Constants.RESPONSE_MESSAGE_SUCCESS; + +import cn.universal.core.engine.exception.MagicScriptAssertException; +import cn.universal.core.engine.exception.MagicScriptException; +import cn.universal.core.engine.functions.ObjectConvertExtension; +import cn.universal.core.engine.runtime.ExitValue; +import cn.universal.plugins.protocolapi.core.context.RequestEntity; +import cn.universal.plugins.protocolapi.modules.db.model.Page; +import cn.universal.plugins.protocolapi.modules.db.model.PageResult; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 结果构建接口 + * + * @author mxd + */ +public interface ResultProvider { + + Logger logger = LoggerFactory.getLogger(ResultProvider.class); + + /** + * 根据异常内容构建结果 + * + * @param requestEntity 请求信息 + * @param root 异常对象 + */ + default Object buildResult(RequestEntity requestEntity, Throwable root) { + MagicScriptException se = null; + Throwable parent = root; + do { + if (parent instanceof MagicScriptAssertException) { + MagicScriptAssertException sae = (MagicScriptAssertException) parent; + return buildResult(requestEntity, sae.getCode(), sae.getMessage()); + } + if (parent instanceof MagicScriptException) { + se = (MagicScriptException) parent; + } + } while ((parent = parent.getCause()) != null); + logger.error("调用接口出错", root); + if (se != null) { + return buildException(requestEntity, se); + } + return buildException(requestEntity, root); + } + + /** + * 构建JSON返回结果,code和message 默认为 1 success + * + * @param requestEntity 请求相关信息 + * @param data 返回内容 + */ + default Object buildResult(RequestEntity requestEntity, Object data) { + if (data instanceof ExitValue) { + ExitValue exitValue = (ExitValue) data; + Object[] values = exitValue.getValues(); + int code = + values.length > 0 + ? ObjectConvertExtension.asInt(values[0], RESPONSE_CODE_SUCCESS) + : RESPONSE_CODE_SUCCESS; + String message = + values.length > 1 + ? Objects.toString(values[1], RESPONSE_MESSAGE_SUCCESS) + : RESPONSE_MESSAGE_SUCCESS; + return buildResult(requestEntity, code, message, values.length > 2 ? values[2] : null); + } + return buildResult(requestEntity, RESPONSE_CODE_SUCCESS, RESPONSE_MESSAGE_SUCCESS, data); + } + + /** + * 构建JSON返回结果 + * + * @param requestEntity 请求相关信息 + * @param code 状态码 + * @param message 状态说明 + */ + default Object buildResult(RequestEntity requestEntity, int code, String message) { + return buildResult(requestEntity, code, message, null); + } + + /** + * 构建异常返回结果 + * + * @param requestEntity 请求相关信息 + * @param throwable 异常信息 + * @since 1.2.2 + */ + default Object buildException(RequestEntity requestEntity, Throwable throwable) { + return buildResult(requestEntity, RESPONSE_CODE_EXCEPTION, throwable.getMessage()); + } + + /** + * 构建JSON返回结果 + * + * @param requestEntity 请求相关信息 + * @param code 状态码 + * @param message 状态说明 + * @param data 数据内容,可以通过data的类型判断是否是分页结果进行区分普通结果集和分页结果集 + */ + Object buildResult(RequestEntity requestEntity, int code, String message, Object data); + + /** + * @param requestEntity 请求相关信息 + * @param page 分页对象 + * @param total 总数 + * @param data 数据内容 + */ + default Object buildPageResult( + RequestEntity requestEntity, Page page, long total, List> data) { + return new PageResult<>(total, data); + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/Formatter.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/Formatter.java new file mode 100644 index 0000000..3bd45af --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/Formatter.java @@ -0,0 +1,281 @@ +package cn.universal.plugins.protocolapi.core.logging; + +import ch.qos.logback.classic.ClassicConstants; +import ch.qos.logback.core.CoreConstants; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class Formatter { + + private static final String[] SPACES = { + " ", + " ", + " ", + " ", // 1,2,4,8 + // spaces + " ", // 16 spaces + " " + }; // 32 spaces + + private static final CachingDateFormatter CACHING_DATE_FORMATTER = + new CachingDateFormatter("yyyy-MM-dd HH:mm:ss.SSS"); + + private static final TargetLengthBasedClassNameAbbreviator ABBREVIATOR = + new TargetLengthBasedClassNameAbbreviator(39); + + private StringBuilder buf = new StringBuilder(); + + private Formatter() {} + + public static Formatter create() { + return new Formatter(); + } + + private static void leftPad(StringBuilder buf, String s, int desiredLength) { + int actualLen = 0; + if (s != null) { + actualLen = s.length(); + } + if (actualLen < desiredLength) { + spacePad(buf, desiredLength - actualLen); + } + if (s != null) { + buf.append(s); + } + } + + private static void rightPad(StringBuilder buf, String s, int desiredLength) { + int actualLen = 0; + if (s != null) { + actualLen = s.length(); + } + if (s != null) { + buf.append(s); + } + if (actualLen < desiredLength) { + spacePad(buf, desiredLength - actualLen); + } + } + + /** Fast space padding method. */ + private static void spacePad(StringBuilder sbuf, int length) { + while (length >= 32) { + sbuf.append(SPACES[5]); + length -= 32; + } + + for (int i = 4; i >= 0; i--) { + if ((length & (1 << i)) != 0) { + sbuf.append(SPACES[i]); + } + } + } + + public Formatter timestamp(long timestamp) { + buf.append(CACHING_DATE_FORMATTER.format(timestamp)); + return this; + } + + public Formatter space() { + buf.append(SPACES[0]); + return this; + } + + public Formatter value(String value) { + buf.append(value); + return this; + } + + public Formatter newline() { + buf.append("\n"); + return this; + } + + public Formatter thread(String value) { + return alignment(value, 15, 15, true, true); + } + + public Formatter level(String value) { + return alignment(value, 5, 2147483647, true, true); + } + + public Formatter loggerName(String value) { + return alignment(ABBREVIATOR.abbreviate(value), 40, 40, true, false); + } + + @Override + public String toString() { + return buf.toString(); + } + + public Formatter throwable(Throwable throwable) { + if (throwable != null) { + this.newline(); + StringWriter sw = new StringWriter(1024); + PrintWriter writer = new PrintWriter(sw); + throwable.printStackTrace(writer); + writer.close(); + buf.append(sw.getBuffer()); + this.newline(); + } + return this; + } + + private Formatter alignment( + String value, int min, int max, boolean leftTruncate, boolean leftPad) { + if (value == null) { + if (0 < min) { + spacePad(buf, min); + } + } else { + int len = value.length(); + if (len > max) { + if (leftTruncate) { + buf.append(value.substring(len - max)); + } else { + buf.append(value, 0, max); + } + } else if (len < min) { + if (leftPad) { + leftPad(buf, value, min); + } else { + rightPad(buf, value, min); + } + } else { + buf.append(value); + } + } + return this; + } + + private static class CachingDateFormatter { + + final SimpleDateFormat sdf; + long lastTimestamp = -1; + String cachedStr = null; + + public CachingDateFormatter(String pattern) { + sdf = new SimpleDateFormat(pattern); + } + + public final String format(long now) { + + // SimpleDateFormat is not thread safe. + + // See also the discussion in http://jira.qos.ch/browse/LBCLASSIC-36 + // DateFormattingThreadedThroughputCalculator and SelectiveDateFormattingRunnable + // are also noteworthy + + // The now == lastTimestamp guard minimizes synchronization + synchronized (this) { + if (now != lastTimestamp) { + lastTimestamp = now; + cachedStr = sdf.format(new Date(now)); + } + return cachedStr; + } + } + } + + private static class TargetLengthBasedClassNameAbbreviator { + + final int targetLength; + + public TargetLengthBasedClassNameAbbreviator(int targetLength) { + this.targetLength = targetLength; + } + + public String abbreviate(String fqClassName) { + StringBuilder buf = new StringBuilder(targetLength); + if (fqClassName == null) { + throw new IllegalArgumentException("Class name may not be null"); + } + + int inLen = fqClassName.length(); + if (inLen < targetLength) { + return fqClassName; + } + + int[] dotIndexesArray = new int[ClassicConstants.MAX_DOTS]; + // a.b.c contains 2 dots but 2+1 parts. + // see also http://jira.qos.ch/browse/LBCLASSIC-110 + int[] lengthArray = new int[ClassicConstants.MAX_DOTS + 1]; + + int dotCount = computeDotIndexes(fqClassName, dotIndexesArray); + + // System.out.println(); + // System.out.println("Dot count for [" + className + "] is " + dotCount); + // if there are not dots than abbreviation is not possible + if (dotCount == 0) { + return fqClassName; + } + // printArray("dotArray: ", dotArray); + computeLengthArray(fqClassName, dotIndexesArray, lengthArray, dotCount); + // printArray("lengthArray: ", lengthArray); + for (int i = 0; i <= dotCount; i++) { + if (i == 0) { + buf.append(fqClassName.substring(0, lengthArray[i] - 1)); + } else { + buf.append( + fqClassName.substring( + dotIndexesArray[i - 1], dotIndexesArray[i - 1] + lengthArray[i])); + } + // System.out.println("i=" + i + ", buf=" + buf); + } + + return buf.toString(); + } + + int computeDotIndexes(final String className, int[] dotArray) { + int dotCount = 0; + int k = 0; + while (true) { + // ignore the $ separator in our computations. This is both convenient + // and sensible. + k = className.indexOf(CoreConstants.DOT, k); + if (k != -1 && dotCount < ClassicConstants.MAX_DOTS) { + dotArray[dotCount] = k; + dotCount++; + k++; + } else { + break; + } + } + return dotCount; + } + + void computeLengthArray( + final String className, int[] dotArray, int[] lengthArray, int dotCount) { + int toTrim = className.length() - targetLength; + // System.out.println("toTrim=" + toTrim); + + // int toTrimAvarage = 0; + + int len; + for (int i = 0; i < dotCount; i++) { + int previousDotPosition = -1; + if (i > 0) { + previousDotPosition = dotArray[i - 1]; + } + int available = dotArray[i] - previousDotPosition - 1; + // System.out.println("i=" + i + ", available = " + available); + + len = (available < 1) ? available : 1; + // System.out.println("i=" + i + ", toTrim = " + toTrim); + + if (toTrim > 0) { + len = (available < 1) ? available : 1; + } else { + len = available; + } + toTrim -= (available - len); + lengthArray[i] = len + 1; + } + + int lastDotIndex = dotCount - 1; + lengthArray[dotCount] = className.length() - dotArray[lastDotIndex]; + } + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/Log4j2LoggerContext.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/Log4j2LoggerContext.java new file mode 100644 index 0000000..adf4448 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/Log4j2LoggerContext.java @@ -0,0 +1,65 @@ +package cn.universal.plugins.protocolapi.core.logging; + +import java.nio.charset.StandardCharsets; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.layout.PatternLayout; + +/** + * 对接Log4j2 + * + * @author mxd + */ +public class Log4j2LoggerContext implements MagicLoggerContext { + + @Override + public void generateAppender() { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + Configuration configuration = context.getConfiguration(); + LoggerConfig logger = configuration.getRootLogger(); + PatternLayout layout = + PatternLayout.newBuilder() + .withCharset(StandardCharsets.UTF_8) + .withConfiguration(configuration) + .withPattern("%d %t %p %X{TracingMsg} %c - %m%n") + .build(); + MagicLog4j2Appender appender = new MagicLog4j2Appender("Magic", logger.getFilter(), layout); + appender.start(); + configuration.addAppender(appender); + logger.addAppender(appender, logger.getLevel(), logger.getFilter()); + context.updateLoggers(configuration); + } + + static class MagicLog4j2Appender extends AbstractAppender { + + MagicLog4j2Appender(String name, Filter filter, Layout layout) { + super(name, filter, layout, true, Property.EMPTY_ARRAY); + } + + @Override + public void append(LogEvent event) { + String message = + Formatter.create() + .timestamp(event.getTimeMillis()) + .space() + .level(event.getLevel().toString()) + .value(" --- [") + .thread(event.getThreadName()) + .value("] ") + .loggerName(event.getLoggerName()) + .value(": ") + .value(event.getMessage().getFormattedMessage()) + .newline() + .throwable(event.getThrown()) + .toString(); + MagicLoggerContext.println(message); + } + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/Log4jLoggerContext.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/Log4jLoggerContext.java new file mode 100644 index 0000000..cb5fe12 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/Log4jLoggerContext.java @@ -0,0 +1,57 @@ +// package cn.universal.plugins.protocolapi.core.logging; +// +// import org.apache.log4j.AppenderSkeleton; +// import org.apache.log4j.LogManager; +// import org.apache.log4j.PatternLayout; +// import org.apache.log4j.spi.LoggingEvent; +// import org.apache.log4j.spi.RootLogger; +// +// +/// ** +// * 对接Log4j +// * +// * @author mxd +// */ +// public class Log4jLoggerContext implements MagicLoggerContext { +// @Override +// public void generateAppender() { +// RootLogger logger = (RootLogger) LogManager.getRootLogger(); +// PatternLayout patternLayout = new PatternLayout("%d %p [%c] - %m%n"); +// MagicLog4jAppender magicLog4jAppender = new MagicLog4jAppender(); +// magicLog4jAppender.setLayout(patternLayout); +// logger.addAppender(magicLog4jAppender); +// } +// +// static class MagicLog4jAppender extends AppenderSkeleton { +// +// +// @Override +// protected void append(LoggingEvent event) { +// String message = Formatter.create() +// .timestamp(event.getTimeStamp()) +// .space() +// .level(event.getLevel().toString()) +// .value(" --- [") +// .thread(event.getThreadName()) +// .value("] ") +// .loggerName(event.getLoggerName()) +// .value(": ") +// .value(event.getRenderedMessage()) +// .newline() +// .throwable(event.getThrowableInformation() == null ? null : +// event.getThrowableInformation().getThrowable()) +// .toString(); +// MagicLoggerContext.println(message); +// } +// +// @Override +// public void close() { +// +// } +// +// @Override +// public boolean requiresLayout() { +// return false; +// } +// } +// } diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/LogbackLoggerContext.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/LogbackLoggerContext.java new file mode 100644 index 0000000..e53d44e --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/LogbackLoggerContext.java @@ -0,0 +1,52 @@ +package cn.universal.plugins.protocolapi.core.logging; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.ThrowableProxy; +import ch.qos.logback.core.UnsynchronizedAppenderBase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 对接Logback + * + * @author mxd + */ +public class LogbackLoggerContext implements MagicLoggerContext { + + @Override + public void generateAppender() { + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + ch.qos.logback.classic.Logger logger = context.getLogger(Logger.ROOT_LOGGER_NAME); + MagicLogbackAppender appender = new MagicLogbackAppender(); + appender.setContext(context); + appender.setName(LOGGER_NAME); + appender.start(); + logger.addAppender(appender); + } + + static class MagicLogbackAppender extends UnsynchronizedAppenderBase { + + @Override + protected void append(ILoggingEvent event) { + Formatter formatter = + Formatter.create() + .timestamp(event.getTimeStamp()) + .space() + .level(event.getLevel().toString()) + .value(" --- [") + .thread(event.getThreadName()) + .value("] ") + .loggerName(event.getLoggerName()) + .value(": ") + .value(event.getFormattedMessage()) + .newline(); + IThrowableProxy proxy = event.getThrowableProxy(); + if (proxy instanceof ThrowableProxy) { + formatter.throwable(((ThrowableProxy) proxy).getThrowable()); + } + MagicLoggerContext.println(formatter.toString()); + } + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/LoggerManager.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/LoggerManager.java new file mode 100644 index 0000000..7ab3622 --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/LoggerManager.java @@ -0,0 +1,35 @@ +package cn.universal.plugins.protocolapi.core.logging; + +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 日志管理 + * + * @author mxd + */ +public class LoggerManager { + + private static final Logger logger = LoggerFactory.getLogger(LoggerManager.class); + + /** 创建一个新的appender至项目中,用于UI界面 */ + public static void createMagicAppender() { + ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); + String loggerFactoryClassName = loggerFactory.getClass().getName(); + MagicLoggerContext magicLoggerContext = null; + // logback + if ("ch.qos.logback.classic.LoggerContext".equalsIgnoreCase(loggerFactoryClassName)) { + magicLoggerContext = new LogbackLoggerContext(); + } else if ("org.apache.logging.slf4j.Log4jLoggerFactory" + .equalsIgnoreCase(loggerFactoryClassName)) { + // log4j2 + magicLoggerContext = new Log4j2LoggerContext(); + } + if (magicLoggerContext == null) { + logger.error("无法识别LoggerContext:{}", loggerFactoryClassName); + } else { + magicLoggerContext.generateAppender(); + } + } +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/MagicLoggerContext.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/MagicLoggerContext.java new file mode 100644 index 0000000..d59a3cf --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/logging/MagicLoggerContext.java @@ -0,0 +1,36 @@ +package cn.universal.plugins.protocolapi.core.logging; + +import cn.universal.plugins.protocolapi.core.config.WebSocketSessionManager; + +/** + * 日志上下文 + * + * @author mxd + */ +public interface MagicLoggerContext { + + String LOGGER_NAME = "magic"; + + ThreadLocal SESSION = new InheritableThreadLocal<>(); + + /** + * 打印日志 re + * + * @param logInfo 日志信息 + */ + static void println(String logInfo) { + // 获取SessionId + String sessionId = SESSION.get(); + if (sessionId != null) { + WebSocketSessionManager.sendLogs(sessionId, logInfo); + } + } + + /** 移除ThreadLocal中的sessionId */ + static void remove() { + SESSION.remove(); + } + + /** 生成appender */ + void generateAppender(); +} diff --git a/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/model/ApiInfo.java b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/model/ApiInfo.java new file mode 100644 index 0000000..3a128ba --- /dev/null +++ b/cn-universal-plugins/cn-universal-plugins-web-debug/src/main/java/cn/universal/plugins/protocolapi/core/model/ApiInfo.java @@ -0,0 +1,236 @@ +package cn.universal.plugins.protocolapi.core.model; + +import cn.universal.plugins.protocolapi.core.config.MagicConfiguration; +import cn.universal.plugins.protocolapi.utils.JsonUtils; +import com.fasterxml.jackson.core.type.TypeReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** 接口信息 */ +public class ApiInfo extends PathMagicEntity { + + /** 请求方法 */ + private String method = "GET"; + + /** 设置的请求参数 */ + private List parameters = Collections.emptyList(); + + /** 设置的接口选项 */ + private List