Browse Source

首次提交

wks 1 year ago
commit
43a2390806
100 changed files with 13132 additions and 0 deletions
  1. 22 0
      .gitignore
  2. 674 0
      LICENSE
  3. 368 0
      README.md
  4. 83 0
      config/run/bootstrap.yml
  5. 2 0
      linkwe-ai/.gitattributes
  6. 33 0
      linkwe-ai/.gitignore
  7. 19 0
      linkwe-ai/.mvn/wrapper/maven-wrapper.properties
  8. 164 0
      linkwe-ai/pom.xml
  9. 52 0
      linkwe-ai/src/main/java/com/linkwechat/coal/LinkweAiApplication.java
  10. 14 0
      linkwe-ai/src/main/java/com/linkwechat/coal/config/Constant.java
  11. 80 0
      linkwe-ai/src/main/java/com/linkwechat/coal/datasource/AiDataSourceConfig.java
  12. 87 0
      linkwe-ai/src/main/java/com/linkwechat/coal/datasource/QwDataSourceConfig.java
  13. 130 0
      linkwe-ai/src/main/java/com/linkwechat/coal/exception/SystemHandlerException.java
  14. 39 0
      linkwe-ai/src/main/java/com/linkwechat/coal/filter/HttpServletRequestWrapperFilter.java
  15. 129 0
      linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/controller/AliAiController.java
  16. 63 0
      linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/controller/FileController.java
  17. 159 0
      linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/controller/ZmAiController.java
  18. 30 0
      linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/domain/dto/AliAiTextDto.java
  19. 20 0
      linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/domain/dto/RequestIdDto.java
  20. 15 0
      linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/mapper/SysUserMapper2.java
  21. 106 0
      linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/service/impl/FtpStorageService.java
  22. 232 0
      linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/utils/AliAiUtils.java
  23. 45 0
      linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/utils/FileUtils.java
  24. 90 0
      linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/utils/SSEHelper.java
  25. 57 0
      linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/utils/WordUtils.java
  26. 15 0
      linkwe-ai/src/main/java/com/linkwechat/coal/modules/qw/mapper/SysUserMapper1.java
  27. 8 0
      linkwe-ai/src/main/resources/application.properties
  28. 11 0
      linkwe-ai/src/main/resources/banner.txt
  29. 203 0
      linkwe-ai/src/main/resources/logback-spring.xml
  30. 4 0
      linkwe-ai/src/main/resources/mapper/ai/SysUserMapper2.xml
  31. 4 0
      linkwe-ai/src/main/resources/mapper/qw/SysUserMapper1.xml
  32. 24 0
      linkwe-ai/src/main/resources/spy.properties
  33. 13 0
      linkwe-ai/src/test/java/com/linkwechat/coal/LinkweAiApplicationTests.java
  34. 1 0
      linkwe-api/jdkReplace/README.txt
  35. BIN
      linkwe-api/jdkReplace/US_export_policy.jar
  36. BIN
      linkwe-api/jdkReplace/local_policy.jar
  37. 134 0
      linkwe-api/pom.xml
  38. 31 0
      linkwe-api/src/main/java/com/linkwechat/LinkWeApiApplication.java
  39. 58 0
      linkwe-api/src/main/java/com/linkwechat/aspecj/RequestArgsLogAspect.java
  40. 133 0
      linkwe-api/src/main/java/com/linkwechat/config/SwaggerConfig.java
  41. 20 0
      linkwe-api/src/main/java/com/linkwechat/config/WebConfig.java
  42. 133 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeAgentController.java
  43. 100 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCallBackController.java
  44. 138 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCallBackJsonController.java
  45. 104 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCallBackXmlController.java
  46. 93 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCategoryController.java
  47. 87 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeChatCollectionController.java
  48. 142 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeChatContactMsgController.java
  49. 95 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeChatItemController.java
  50. 59 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeChatSideController.java
  51. 146 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCommunityGroupSopController.java
  52. 121 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCommunityH5Controller.java
  53. 100 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCommunityKeywordGroupController.java
  54. 153 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCommunityNewGroupController.java
  55. 123 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCommunityPresTagGroupController.java
  56. 49 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeConfigController.java
  57. 55 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeContentActionController.java
  58. 161 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeContentTalkController.java
  59. 82 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCorpAccountController.java
  60. 311 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCustomerController.java
  61. 289 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCustomerPortraitController.java
  62. 273 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCustomerSeasController.java
  63. 45 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeCustomerTrackRecordController.java
  64. 274 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeFissionController.java
  65. 83 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeFormSurveyAnswerController.java
  66. 101 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeFormSurveyCatalogueController.java
  67. 62 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeFormSurveyCategoryController.java
  68. 675 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeFormSurveyStatisticsController.java
  69. 151 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeGroupChatController.java
  70. 189 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeGroupCodeController.java
  71. 113 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeGroupMessageTemplateController.java
  72. 94 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeGroupRobotController.java
  73. 138 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeIndexController.java
  74. 122 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeLeaveUserController.java
  75. 307 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeLiveController.java
  76. 151 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeLxQrCodeController.java
  77. 414 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeMaterialController.java
  78. 104 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeMomentsController.java
  79. 105 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeMsgTlpController.java
  80. 380 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeOperationCenterController.java
  81. 526 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeProductAnalyzeController.java
  82. 125 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeProductController.java
  83. 174 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeProductOrderController.java
  84. 63 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeProductOrderRefundController.java
  85. 154 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeQrCodeController.java
  86. 571 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeRedEnvelopesController.java
  87. 120 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeSensitiveActController.java
  88. 105 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeSensitiveController.java
  89. 131 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeShortLinkController.java
  90. 540 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeStoreCodeController.java
  91. 100 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeStrackStageController.java
  92. 67 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeStrategicCrowdController.java
  93. 94 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeSysFieldTemplateController.java
  94. 164 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeTagController.java
  95. 104 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeTalkController.java
  96. 63 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeTicketController.java
  97. 65 0
      linkwe-api/src/main/java/com/linkwechat/controller/WeTrajectoryController.java
  98. 56 0
      linkwe-api/src/main/java/com/linkwechat/interceptor/RequestContextInterceptor.java
  99. 10 0
      linkwe-api/src/main/resources/banner.txt
  100. 206 0
      linkwe-api/src/main/resources/logback-spring.xml

+ 22 - 0
.gitignore

@@ -0,0 +1,22 @@
+target
+bak
+.pmd
+.project
+.settings
+.classpath
+.idea.xml
+.idea
+*.class
+*.bak
+*.iml
+*.ipr
+*.iws
+bak
+null/
+tree.log
+tmp/
+velocity.log
+.DS_Store
+logs
+.m2
+*.log

+ 674 - 0
LICENSE

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.

+ 368 - 0
README.md

@@ -0,0 +1,368 @@
+<p></p>
+<p></p>
+
+![输入图片说明](readme-pic/logo.png)
+
+<div align="center">
+
+[![star](https://gitee.com/LinkWeChat/link-wechat/badge/star.svg?theme=gvp)](https://gitee.com/LinkWeChat/link-wechat/stargazers)
+[![fork](https://gitee.com/LinkWeChat/link-wechat/badge/fork.svg?theme=gvp)](https://gitee.com/LinkWeChat/link-wechat/members)
+[![LinkWeChat](https://img.shields.io/badge/LinkWeChat-V4.1-brightgreen)](https://www.yuque.com/linkwechat/help/ci1n9p)
+[![license](http://img.shields.io/badge/license-GPL%203.0-orange)](https://gitee.com/LinkWeChat/link-wechat/blob/master/LICENSE)
+[![木兰社区](https://img.shields.io/badge/Mulan-incubating-blue)](https://portal.mulanos.cn//)
+
+[官方网站](http://www.linkwechat.net/) | [官方论坛](http://community.linkwechat.net/) | [在线演示](http://demo.linkwechat.net/)
+
+[项目手册](https://www.yuque.com/linkwechat/help) | [用户手册](https://docs.qq.com/pdf/DZXR0ZUJub29wTldS?) | [常见问题](https://www.yuque.com/linkwechat/help/qk5gl6) | [V4.x功能清单](https://docs.qq.com/sheet/DZUNUV2pRZU5kSnlU?tab=BB08J3)  
+
+
+</div>
+
+<p></p>
+<p></p>
+<p></p>
+<p></p>
+
+---
+
+**<p align="center">【📣最近更新】还有什么想要产品/技术上友好的建议或意见,<a href="https://docs.qq.com/sheet/DZWxGU0JGVFRVdWZV" target="_blank">点击告诉我们~</a>一经采纳,将纳入开源贡献者并官网致谢,还会收到社区奖励的小礼品哦!~</p>**
+
+**<p align="center">LinkWeChat V4.0 正式发布啦,新版“生态绿”视觉,更多能力逐步升级,欢迎大家提前体验!<a href="http://demo.linkwechat.net/" target="_blank">点击立即体验~</a></p>**
+
+**<p align="center">LinkWeChat V4.0 采用前后端分离架构,前端项目请点击<a href="https://gitee.com/LinkWeChat/link-we-chat-front" target="_blank">LinkWeChat-Front</a></p>**
+
+
+**<p align="center">如果您觉得我们的开源项目很有帮助,请点击 :star: Star 支持 LinkWeChat 开源团队:heart:</p>**
+
+---
+
+# 版权声明
+
+开源不代表免费,LinkWeChat 遵循 [GPL-3.0](https://gitee.com/LinkWeChat/link-wechat/blob/master/LICENSE) 开源协议发布,并提供技术交流学习,但**绝不允许修改后和衍生的代码做为闭源的商业软件发布和销售!** 如果需要将本产品在本地进行任何附带商业化性质行为使用,**请联系项目负责人进行商业授权**,以遵守 GPL 协议保证您的正常使用。
+
+[帮助手册——LinkWeChat 需要付费吗?](https://www.yuque.com/linkwechat/help/xql2ts)
+
+目前在国内 GPL 协议**具备合同特征,是一种民事法律行为** ,属于我国《中华人民共和国民法典》调整的范围。LinkWeChat 项目团队保留一切诉讼权利。
+
+[相关案例:违反 GPL 协议赔偿 50 万,国内首例!](https://mp.weixin.qq.com/s/YQ6sNjbDS-P7BViLZIsaoA)
+
+LinkWeChat 开源团队拥有对本开源协议的最终解释权。
+
+# 一、 关于 LinkWeChat
+
+## 1.1 介绍
+
+> LinkWeChat, Link to WeChat.
+
+LinkWeChat 是基于企业微信的开源 SCRM 系统,是企业私域流量管理与营销的综合解决方案。
+
+LinkWeChat 基于企业微信开放能力,不仅集成了企微强大的后台管理及基础的客户管理功能,而且提供了多种渠道、多个方式连接微信客户。并通过客情维系、聊天增强等灵活高效的客户运营模块,让客户与企业之间建立强链接,从而进一步通过多元化的营销工具,帮助企业提高客户运营效率,强化营销能力,拓展盈利空间。
+
+主要运用于电商、零售、教育、金融、政务等服务行业领域。
+
+[更多 LinkWeChat 的故事](https://www.yuque.com/linkwechat/help/dsatfs)
+
+## 1.2 能力
+
+LinkWeChat SCRM 基于企业私域流量营销闭环,遵从获取新客到管理并维护客户,再到建立社群进行客户营销,最后在营销过程中再次获取新客的循环流程。
+
+![输入图片说明](readme-pic/yewujiagou.png)
+
+LinkWeChat V4.x 大版本规划来啦~!V4.x 版本将把重点以下三部分:
+
+* 全面对接三方:不止于微信生态,让私域建设更全面、更彻底;
+* 精准营销能力:基于人群和策略,加强后链路转化能力;
+* 升级CRM能力:销售赋能,让线索到客户再到成交的链路更简单;
+
+整个系统共分为十一大模块:
+
+*   **运营中心** :客户、客群、会话等全功能数据报表,数据一目了然;
+*   **引流获客** :活码、群活码、公海、客服等多渠道引流,实现精准获客;
+*   **销售中心** :承接引流获客模块客户线索,高效协作跟进;
+*   **客户中心** :助力企业搭建私域流量池,高效运营客户;
+*   **客群中心** :客群运营场景全覆盖;
+*   **客情维系** :企业客户运营精细化,朋友圈、红包工具提高客户活跃度;
+*   **内容中心** :搭建企业自有内容库,多类型素材一键调用;
+*   **全能营销** :提供多类型、多场景客户营销工具;
+*   **商城中心** :自有小程序商城,快捷搭建企业转化链路;
+*   **企业风控** :会话合规存档,敏感内容全局风控;
+*   **企业管理** :组织架构、自建应用全融合,实现“一个后台”;
+
+[LinkWeChat 社区版 V4.x 详细功能清单](https://docs.qq.com/sheet/DZUNUV2pRZU5kSnlU?tab=BB08J3)
+
+我们欢迎感兴趣的贡献者积极参与 LinkWeChat 新版的开源中,如有兴趣通过下方的联系方式联系我们哦~
+
+## 1.3 优势
+
+LinkWeChat 基于企业微信从服务内部到对外全面开放能力,可为电商、零售、教育、金融等垂直场景提供企微私域管理基础架构,主要优势有:
+
+* 全面对接企微开放 API,无需重复对接,快速上手
+* 二次整合封装企微 API,避免重复踩坑
+* 采用主流 Java 架构,具备高拓展性、灵活性,避免 PHP 架构的缺点
+* 对外提供内部 API,低成本二次开发
+
+![输入图片说明](readme-pic/youshi.png)
+
+## 1.4 愿景
+
+LinkWeChat 不做私域营销专家,而是通过开源让每个企业都是自己的私域流量营销专家:
+
+* 集成企业微信基础能力,拿来即用
+* 针对垂直行业提供一站式营销解决方案
+* 开放自身平台能力,让更多的开发者参与进来
+* 与企业微信共同助力企业数字化转型
+
+# 二、关于项目
+
+## 2.1 在线体验
+
+开源演示地址:[http://demo.linkwechat.net/](http://demo.linkwechat.net/)
+
+默认账号密码:lw,123456
+
+目前支持两种方式体验:
+
+* 在首页扫码二维码申请加入测试企业,申请通过后即可扫码登录,体验系统的完整功能;
+* 点击账密登录并勾选`演示账号登录`即可在线体验后台系统,但不支持体验移动端相关功能;
+
+_(注意:演示环境已屏蔽管理权限和相关操作)_
+
+
+## 2.2 系统架构
+
+LinkWeChat V3.0 架构升级,全面升级为微服务架构,整体系统架构如下:
+
+![系统架构](readme-pic/jiagou.png)
+
+## 2.3 技术栈
+
+本项目基于 [RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud) 后台开发框架,感谢 [RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud) 的开源。
+
+* 前端技术栈:ES6、Vue、Vuex、Vue-router、Vue-cli、Axios、Element-ui;
+* 后端技术栈:Spring Boot、Spring Cloud & Alibaba、Nacos、Mybatis-plus、xxljob、RabbitMQ、Forest;
+
+## 2.4 项目结构
+
+**后端结构**
+
+```
+├── linkwe-api                // 系统业务接口模块
+├── linkwe-auth               // 角色权限部门用户认证等模块
+├── linkwe-common             // 公共组件模块
+├── linkwe-fileservice	      // 文件服务模块
+├── linkwe-framework	      // 框架配置
+├── linkwe-gateway            // 网关服务
+├── linkwe-scheduler	      // 定时任务相关模块
+├── linkwe-service            // 系统service层抽取,与数据库相关交互
+├── linkwe-wecome             // 企微接口实现
+├── linkwe-wx-api             // 系统中设计微信公众号相关接口模块
+
+```
+
+**前端结构**
+
+```
+├── linkwe-pc			// 后台项目
+├── linkwe-mobile		// 移动端项目包含移动工作台、任务宝、群裂变等H5
+├── linkwe-uniapp		// uniapp短链小程序项目
+```
+
+## 2.5 项目部署
+
+点击下方链接进入官方语雀帮助手册查看项目部署方式:
+
+[帮助手册——如何快速部署 LinkWeChat](https://www.yuque.com/linkwechat/help/gt1myw)
+
+建议服务器最低配置:
+
+| 类型 | 配置 |
+|:-:|:-:|
+|操作系统|CentOS8.2|
+|CPU|4核|
+|内存|16G|
+|带宽|5M|
+|硬盘|100G|
+
+# 三、常见问题
+
+由于 LinkWeChat 项目基于企业微信官方第三方应用开发文档,所以请各位小伙伴在学习和使用 LinkWeChat 之前, **务必仔细了解并熟悉** 官方 API 已有功能。
+
+[企业微信-企业内部开发-API 文档](https://open.work.weixin.qq.com/api/doc/90000/90003/90556)
+
+点击下方链接进入官方语雀帮助手册查看常见问题的相关描述及更多帮助:
+
+[帮助手册——常见问题](https://www.yuque.com/linkwechat/help/qk5gl6)
+
+# 四、最近更新
+
+## V4.1 更新日志
+
+- [x] 系统素材相关功能,全面升级为内容中心;
+- [x] 离职继承、流失提醒等相关功能优化;
+- [x] 修复系统相关bug;
+
+## V4.0 更新日志
+
+- [x] 系统UI全面更新;
+- [x] 新增客户画像表单自定义,商机阶段;
+- [x] 部分功能优化等;
+- [x] 修复相关bug;
+
+## V3.4 更新日志
+
+- [x] 智能短链;
+- [x] 修复了一些已知问题;
+- [x] 其余各模块UI细节调优;
+
+## V3.3 更新日志
+
+- [x] 客户/客群SOP发布;
+- [x] 内部机器人前端发布;
+- [x] 企微商品画册功能重构;
+- [x] 修复了一些已知问题;
+- [x] 其余各模块UI细节调优;
+
+## V3.2 更新日志
+
+- [x] 新增智能表单:通过画布的形式自定义H5表单,可以是报名表、问卷等形式,通过链接形式发放给公域或微信客户,客户主动留资后收集手机号,进行后续营销;
+- [x] 新增:封装企业微信直播 API;
+- [x] 新增:应用管理;
+- [x] 新增:机器人 API;
+- [x] 修复了一些已知问题;
+- [x] 其余各模块UI细节调优;
+
+## V3.0 更新日志
+
+LinkWeChat V3.0 主要围绕系统架构、引流获客、客情维系、内容中心及移动端等模块进行升级。
+
+主要的升级内容如下:
+
+- [x] 架构调整,升级微服务架构;
+- [x] 拆分客户与客群中心,原素材中心升级为内容中心;
+- [x] 新增门店活码:基于客户实时位置,引导客户快捷添加门店导购或专属群;
+- [x] 新增内容中心,支持多种类型素材、话术及内容模板;
+- [x] 客群活码重新设计,支持群满自动建群;
+- [x] 新增客群画像,支持在侧边栏中快捷查看及跟进客群详情;
+- [x] 新增角色与菜单管理,快捷设置企业员工数据与菜单权限;
+- [x] 新增移动工作台,支持在移动端快捷查看企业数据与动态;支持查看客户与客群画像详情,一键联系;工作台快捷使用小工具;
+- [x] 新增公众号配置,用于快捷绑定公众号;
+- [x] 新增扫码登录,企业员工快捷加入;
+- [x] 新增企业动态,全局洞察企业各维度进展;
+- [x] 优化系统首页相关模块及数据统计逻辑;
+- [x] 优化组织管理相关逻辑;
+- [x] 优化微信客服分配逻辑;
+- [x] 优化会话存档相关解密逻辑,UI页面调优;
+- [x] 优化新客拉群相关数据统计逻辑;
+- [x] 优化全局菜单结构,业务流程更加清晰;
+- [x] 修复了一些已知问题;
+- [x] 其余各模块UI细节调优。
+
+点击下方链接进入官方语雀帮助手册查看历史版本更新日志:
+
+[帮助手册——历史更新](https://www.yuque.com/linkwechat/help/pcmd0o)
+
+# 五、奖项荣誉
+
+LinkWeChat 凭借着出色的产品能力和专业的技术能力,已经获取如下奖项:
+
+![输入图片说明](readme-pic/jiangxiang2023.png)
+
+目前 LinkWeChat 已成功加入木兰开源社区孵化项目,得到了更加专业的技术支持与相关知识产权扶持,未来将会以更加严谨规范的专业能力服务各企业与行业。
+
+![输入图片说明](https://images.gitee.com/uploads/images/2021/1108/150256_14f4ba8d_1480777.png "仟微助手企业微信SCRM智能营销系统-V1.5_40.png")
+
+# 六、联系我们
+
+如果你想加入我们的开源交流群,请扫码添加 LinkWeChat 项目作者,加入群聊:
+
+![输入图片说明](readme-pic/contact1.png)
+
+如果你有任何 LinkWeChat 产品上的想法、意见或建议,或商务上的合作需求,请扫码添加 LinkWeChat 项目团队进一步沟通:
+
+![输入图片说明](readme-pic/contact2.png)
+
+# 七、捐赠支持
+
+## 7.1 来一杯卡布奇洛
+
+如果您觉得我们的开源项目 `LinkWeChat` 对您有帮助,那就请项目开发者们来一杯卡布奇诺吧!当前我们接受来自于**微信**、**支付宝**或者**码云**的捐赠,请在捐赠时备注自己的昵称或附言。
+
+您的捐赠将用于支付该项目的一些费用支出,并激励开发者们以便更好的推动项目的发展,同时欢迎捐赠**公网服务器**用于提高在线演示系统体验。
+
+![输入图片说明](https://images.gitee.com/uploads/images/2021/0222/221344_727a0e80_1480777.png "image-20210222214357456.png")
+
+## 7.2 长期捐赠
+
+如果您是企业的经营者并且有计划将 `LinkWeChat` 用在公司的经营产品中,欢迎进行长期捐赠。长期捐赠有商业上的益处有:
+
+* 积极响应,快速维护,及时更新;
+* 企业名称、Logo 及官网链接将长期展示在开源仓库、`LinkWeChat` 官网及宣发材料中;
+* 捐赠金额同比例抵扣未来 `LinkWeChat` 的付费产品价格。
+
+如果您对长期赞助 `LinkWeChat` 团队感兴趣,或者有其他好想法,欢迎联系开发团队微信 `sxjiangdongqin`,或发送邮件到 iamxiarui@foxmail.com。
+
+## 7.3 捐赠记录
+
+
+`LinkWeChat` 全体开发团队感谢以下全部小伙伴们的赞助(排名不分先后):
+
+|   昵称   |  金额   | 渠道 |        时间         |        附言        |
+| :------: | :-----: | :--: | :-----------------: | :----------------: |
+|   yang   | ¥10.00 | 码云 | 2021-02-22 22:08:59 | 感谢您的开源项目! |
+| 水库浪子 | ¥1.00  | 码云 | 2021-02-22 22:09:03 | 感谢您的开源项目! |
+|     楼*     |   ¥166.60      | 微信     |       2021-02-22 22:27:25              |     希望这个好项目能长久发展               |
+|     joygezxp     |   ¥66.60      | 微信     |       2021-02-22 22:39:41              |     愿项目一路666               |
+|     iamxiarui     |   ¥10.00      | 微信     |       2021-02-22 22:37:52              |     坚持开源不容易               |
+|     godricV     |   ¥10.00      | 微信     |       2021-02-22 22:28:55              |     感谢您的开源项目!               |
+|     *标     |   ¥20.00      | 微信     |       2021-03-05 16:03:19              |     感谢您的开源项目!               |
+|     *涯     |   ¥30.00      | 微信     |       2021-03-09 12:14:02              |      加油,感谢开源!               |
+|     *魂     |   ¥30.00      | 微信     |       2021-03-11 10:52:47              |       感谢开源供大家学习               |
+|     *J     |   ¥10.00      | 微信     |       2021-03-12 14:20:22              |       感谢开源供大家学习               |
+|     杨*源     |   ¥10.00      | 码云     |       2021-03-23 11:07:22              |       期待月底的大更新               |
+|     骆*升     |   ¥66.66      |  支付宝     |       2021-03-23 11:31:58              |       感谢开源供大家学习               |
+|     曲*旭     |   ¥20.00      |   码云     |       2021-03-23 11:31:58              |       感谢您的开源项目!               |
+|     郑*     |   ¥ 50.00      |  码云     |       2021-03-23 11:31:58      | 虽然之前做过类似的,还是感谢下开源和分享的奉献精神       |
+|     *喵     |   ¥  66.00      |   微信     |       2021-03-24 11:06:31      | 感谢您的开源项目!       |
+|     q*s     |   ¥ 50.00      |  微信     |       2021-03-29 19:57:49      | 感谢您的开源项目!       |
+|     *海     |   ¥  1.00      |  微信     |       2021-03-30 10:47:03      | 感谢您的开源项目!       |
+|     m*r     |   ¥ 50.00      |  微信     |       2021-03-30 17:55:30      | 感谢您的开源项目!       |
+|     *祺     |   ¥ 20.00      |  支付宝     |       2021-03-30 19:03:59      | 感谢您的开源项目!       |
+|     *桥     |   ¥  66.66      |  支付宝     |       2021-04-07 16:38:31      |  祝项目一路 666       |
+|     *力     |   ¥  66.00      |  微信     |       2021-04-09 10:30:11      |   支持开源       |
+|     *生     |   ¥  66.00      |  微信     |       2021-04-15 21:36:01      |   支持开源       |
+|     Q*N     |   ¥  10.00      |  微信     |       2021-04-19 20:24:11      |   感谢分享       |
+|     大*k     |   ¥  10.00      |  微信     |       2021-04-22 12:25:02      |   感谢您的开源       |
+|     J*s     |   ¥  10.00      |  微信     |       2021-05-19 17:33:10      |   感谢您的开源       |
+|     *茶     |   ¥  10.00      |  微信     |       2021-05-17 20:46:05      |   感谢您的开源       |
+|     悟空     |   ¥   500.00      |   支付宝     |       2021- 11-17 11:05:05      |   感谢您的开源       |
+|     羊一只     |   ¥   20.00      |   微信     |       2022-09-05 11:10:22      |   感谢您的开源       |
+
+
+`LinkWeChat` 全体开发团队感谢以下全部合作伙伴的服务器赞助(排名不分先后):
+
+|   公司/个人名称   |  服务器   | 
+| :------: | :-----: | 
+|   上海六感科技有限公司   | 八核 16G 服务器一台 |
+|    Happy   | 两核 8G 服务器一台 |
+|  平山阑槛倚晴空    | 两核 4G 服务器一台 |
+
+## 7.4 优秀开源社区
+[IoTSharp](https://gitee.com/IoTSharp) IoTSharp 是一个 基于.Net Core 开源的物联网基础平台, 支持 HTTP、MQTT 、CoAp 协议
+
+[流之云](https://gitee.com/ntdgg) 信息化、数字化服务提供商
+
+[translate.js](https://gitee.com/mail_osc/translate) 网页自动翻译,页面无需另行改造,加入两行js即可让你的网页快速具备多国语言切换能力!
+
+## 7.5 社会价值
+
+LinkWeChat 开源团队将目前所收项目捐赠全额捐出,仅尽绵薄之力,在此我们也感谢大家的捐赠和支持,希望河南尽早渡过难关。
+
+同时也希望 LinkWeChat 不仅能为国内开源社区建设做一点贡献,也能为社会产生一些价值。
+
+![输入图片说明](https://images.gitee.com/uploads/images/2021/1108/150157_2762cfc4_1480777.png "屏幕截图.png")
+
+---
+
+**<p align="center">如果您觉得我们的开源项目很有帮助,请点击 :star: Star 支持 LinkWeChat 开源团队:heart:</p>**
+
+---

+ 83 - 0
config/run/bootstrap.yml

@@ -0,0 +1,83 @@
+spring:
+  # profiles: local
+  main:
+    allow-bean-definition-overriding: true
+    allow-circular-references: true
+  cloud:
+    nacos:
+      username: nacos
+      password: weCaht@zm2023
+      # 配置中心
+      config:
+        # 命名空间id(此处不用public,因public初始化的空间, id为空)  4e237601-cea8-414d-b7b9-d7adc8cbcf95
+#        namespace: 41746f91-0939-4e2b-b88e-095d52518465 #生产
+        namespace: 0c644b7e-505b-4c39-bee2-297e828abc17  #测试
+        # nacos的ip地址和端口
+        server-addr: 10.1.4.71:8848
+        # 这个就表示 在我们nacos命名空间id为 dev中 有一个data-id 为 demo-service.yml 的配置文件 读取这个里面的配置
+        file-extension: yml
+        config-retry-time: 300000
+        # 共享配置, 可以把公共配置放在同个命名空间下,然后创建一个 common.yml 文件 ,里面可以放共用的配置
+        shared-configs[0]:
+          dataId: linkwe-common.yml
+          refresh: true
+      # 发布到注册中心 (如果没有使用可以不配)
+      discovery:
+        # 命名空间id(此处不用public,因public初始化的空间, id为空)
+        namespace: ${spring.cloud.nacos.config.namespace}
+        # nacos的ip地址和端口
+        server-addr: ${spring.cloud.nacos.config.server-addr}
+#---
+#spring:
+#  profiles: dev
+#  main:
+#    allow-bean-definition-overriding: true
+#    allow-circular-references: true
+#  cloud:
+#    nacos:
+#      # 配置中心
+#      config:
+#        # 命名空间id(此处不用public,因public初始化的空间, id为空)
+#        namespace: 5633d616-30dc-40e4-8816-f82e88f329ab
+#        # nacos的ip地址和端口
+#        server-addr: 127.0.0.1:8848
+#        # 这个就表示 在我们nacos命名空间id为 dev中 有一个data-id 为 demo-service.yml 的配置文件 读取这个里面的配置
+#        file-extension: yml
+#        config-retry-time: 300000
+#        # 共享配置, 可以把公共配置放在同个命名空间下,然后创建一个 common.yml 文件 ,里面可以放共用的配置
+#        shared-configs[0]:
+#          dataId: linkwe-common.yml
+#          refresh: true
+#      # 发布到注册中心 (如果没有使用可以不配)
+#      discovery:
+#        # 命名空间id(此处不用public,因public初始化的空间, id为空)
+#        namespace: ${spring.cloud.nacos.config.namespace}
+#        # nacos的ip地址和端口
+#        server-addr: ${spring.cloud.nacos.config.server-addr}
+#---
+#spring:
+#  profiles: prod
+#  main:
+#    allow-bean-definition-overriding: true
+#    allow-circular-references: true
+#  cloud:
+#    nacos:
+#      # 配置中心
+#      config:
+#        # 命名空间id(此处不用public,因public初始化的空间, id为空)
+#        namespace: 4e237601-cea8-414d-b7b9-d7adc8cbcf95
+#        # nacos的ip地址和端口
+#        server-addr:  127.0.0.1:8848
+#        # 这个就表示 在我们nacos命名空间id为 dev中 有一个data-id 为 demo-service.yml 的配置文件 读取这个里面的配置
+#        file-extension: yml
+#        config-retry-time: 300000
+#        # 共享配置, 可以把公共配置放在同个命名空间下,然后创建一个 common.yml 文件 ,里面可以放共用的配置
+#        shared-configs[0]:
+#          dataId: linkwe-common.yml
+#          refresh: true
+#      # 发布到注册中心 (如果没有使用可以不配)
+#      discovery:
+#        # 命名空间id(此处不用public,因public初始化的空间, id为空)
+#        namespace: ${spring.cloud.nacos.config.namespace}
+#        # nacos的ip地址和端口
+#        server-addr: ${spring.cloud.nacos.config.server-addr}

+ 2 - 0
linkwe-ai/.gitattributes

@@ -0,0 +1,2 @@
+/mvnw text eol=lf
+*.cmd text eol=crlf

+ 33 - 0
linkwe-ai/.gitignore

@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/

+ 19 - 0
linkwe-ai/.mvn/wrapper/maven-wrapper.properties

@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip

+ 164 - 0
linkwe-ai/pom.xml

@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>linkwechat-cloud</artifactId>
+        <groupId>com.linkwechat</groupId>
+        <version>3.1.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <artifactId>linkwe-ai</artifactId>
+
+    <description>
+        AI助手功能(该项目没有用到)
+    </description>
+
+    <properties>
+        <java.version>1.8</java.version>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+        <maven.build.timestamp.format>yyyyMMddHHmmss</maven.build.timestamp.format>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- 阿里数据库连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- JSON工具类 -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+
+        <!-- 阿里JSON解析器 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>2.0.12</version>
+        </dependency>
+        <!-- 阿里JSON解析器 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>p6spy</groupId>
+            <artifactId>p6spy</artifactId>
+        </dependency>
+
+        <!-- redis 缓存操作 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <!--mybatis-plus多数据源配置-->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+            <version>3.4.0</version>
+        </dependency>
+
+        <!--mybatis-plus多数据源配置-->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+
+        <!--邮件发送-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-mail</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+            <version>2021.1</version>
+        </dependency>
+
+        <!--校验字段是否为空依赖-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+
+        <!-- SpringCloud Alibaba Nacos Config -->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-common</artifactId>
+            <version>2.9.2</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.linkwechat</groupId>
+            <artifactId>linkwe-service</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>dashscope-sdk-java</artifactId>
+            <!-- 请将 'the-latest-version' 替换为最新版本号:https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
+            <version>2.18.2</version>
+        </dependency>
+
+        <!-- Apache Commons Net for FTP -->
+        <dependency>
+            <groupId>commons-net</groupId>
+            <artifactId>commons-net</artifactId>
+            <version>3.8.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <finalName>lw-ai</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.3.3.RELEASE</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+        </plugins>
+    </build>
+
+</project>

+ 52 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/LinkweAiApplication.java

@@ -0,0 +1,52 @@
+package com.linkwechat.coal;
+
+import com.linkwechat.common.config.fegin.FeginConfig;
+import org.hibernate.validator.HibernateValidator;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.ServletComponentScan;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+import javax.validation.ValidatorFactory;
+
+@MapperScan({"com.linkwechat.**.mapper","com.linkwechat.mapper"})
+@ComponentScan({"com.linkwechat.**"})
+@ServletComponentScan(basePackages={"com.linkwechat.coal.filter.**"})   //开启servlet组件扫描
+@EnableFeignClients(basePackages = "com.linkwechat.**", defaultConfiguration = FeginConfig.class)
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+public class LinkweAiApplication {
+
+    public static void main(String[] args) {
+        try {
+            new SpringApplicationBuilder(LinkweAiApplication.class)
+                    .properties("spring.config.name:bootstrap", "config/run/bootstrap.yml")
+                    .properties("spring.application.name=linkwe-ai")
+                    .build().run(args);
+            System.out.println("(♥◠‿◠)ノ゙  LinkWe-AI启动成功   ლ(´ڡ`ლ)゙ ");
+        } catch (Exception e) {
+            e.printStackTrace();
+
+        }
+
+    }
+
+    /**
+     * Spring Validation默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启Fali Fast模式,一旦校验失败就立即返回
+     *
+     * @return
+     */
+    @Bean
+    public Validator validator() {
+        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
+                .configure()
+                .failFast(true)
+                .buildValidatorFactory();
+        return validatorFactory.getValidator();
+    }
+}

+ 14 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/config/Constant.java

@@ -0,0 +1,14 @@
+package com.linkwechat.coal.config;
+
+public class Constant {
+
+    public static String KEY ="sk-1f095e5103dd4567a9afec6b17ec0187";
+
+    public static String DEEPSEEK_R1 ="deepseek-r1" ;
+
+    public static String QWEN_PLUS ="qwen-plus" ;
+
+    public static String QWQ_32B ="qwq-32b" ;
+
+    public static final String FILE_UPLOAD_PATH = "path/to/upload/directory"; // 替换为实际路径
+}

+ 80 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/datasource/AiDataSourceConfig.java

@@ -0,0 +1,80 @@
+package com.linkwechat.coal.datasource;
+
+import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
+import org.apache.ibatis.logging.stdout.StdOutImpl;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionTemplate;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.transaction.PlatformTransactionManager;
+
+import javax.sql.DataSource;
+
+/**
+ * 主数据源配置
+ */
+@Configuration
+@Primary
+@MapperScan(basePackages = "com.linkwechat.coal.modules.ai.mapper", sqlSessionFactoryRef = "aiSqlSessionFactory")
+public class AiDataSourceConfig {
+    
+
+    @Bean(name = "aiDataSource")
+    @ConfigurationProperties(prefix = "spring.datasource.ai")
+    @Primary
+    public DataSource aiDataSource() {
+        return DruidDataSourceBuilder.create().build();
+    }
+
+    @Bean(name = "aiSqlSessionFactory")
+    @Primary
+    public SqlSessionFactory aiSqlSessionFactory(@Qualifier("aiDataSource") DataSource datasource)
+            throws Exception {
+        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
+        //configuration配置bean
+        MybatisConfiguration configuration = new MybatisConfiguration();
+        configuration.setMapUnderscoreToCamelCase(true);
+        configuration.setCacheEnabled(false);
+        // 配置打印sql语句s
+        configuration.setLogImpl(StdOutImpl.class);
+        // 添加自定义SQL注入
+        bean.setConfiguration(configuration);
+
+        //插件对象
+        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
+
+        //分页插件
+        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+        bean.setDataSource(datasource);
+        // 设置mybatis的xml所在位置
+        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/ai/*.xml"));
+        bean.setPlugins(mybatisPlusInterceptor);
+        return bean.getObject();
+    }
+
+    @Bean("aiSqlSessionTemplate")
+    @Primary
+    public SqlSessionTemplate aiSqlSessionTemplate(
+            @Qualifier("aiSqlSessionFactory") SqlSessionFactory sessionFactory) {
+        return new SqlSessionTemplate(sessionFactory);
+    }
+
+    @Bean("aiTransactionManager")
+    @Primary
+    public PlatformTransactionManager aiTransactionManager(@Qualifier("aiDataSource") DataSource dataSource) {
+        return new DataSourceTransactionManager(dataSource);
+    }
+
+
+}

+ 87 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/datasource/QwDataSourceConfig.java

@@ -0,0 +1,87 @@
+package com.linkwechat.coal.datasource;
+
+import cn.hutool.core.collection.ListUtil;
+import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
+import org.apache.ibatis.logging.stdout.StdOutImpl;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionTemplate;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.transaction.PlatformTransactionManager;
+
+import javax.sql.DataSource;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 从数据源配置
+ */
+@Configuration
+@MapperScan(basePackages = {"com.linkwechat.coal.modules.qw.mapper", "com.linkwechat.mapper"}, sqlSessionFactoryRef = "qwSqlSessionFactory")
+public class QwDataSourceConfig {
+
+
+    @Bean(name = "qwDataSource")
+    @ConfigurationProperties(prefix = "spring.datasource.qw")
+    public DataSource qwDataSource() {
+        return DruidDataSourceBuilder.create().build();
+    }
+
+    @Bean(name = "qwSqlSessionFactory")
+    public SqlSessionFactory qwSqlSessionFactory(@Qualifier("qwDataSource") DataSource datasource)
+            throws Exception {
+        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
+        //configuration配置bean
+        MybatisConfiguration configuration = new MybatisConfiguration();
+        configuration.setMapUnderscoreToCamelCase(true);
+        configuration.setCacheEnabled(false);
+        // 配置打印sql语句s
+        configuration.setLogImpl(StdOutImpl.class);
+        // 添加自定义SQL注入
+        bean.setConfiguration(configuration);
+
+        //插件对象
+        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
+        
+        //分页插件
+        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+        bean.setDataSource(datasource);
+
+        // 动态加载多个路径
+        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
+        List<Resource> resources = new ArrayList<>();
+
+        // 添加第一个路径
+        resources.addAll(ListUtil.of(resolver.getResources("classpath:mapper/qw/*.xml")));
+        // 添加第二个路径
+        resources.addAll(ListUtil.of(resolver.getResources("classpath*:mapper/sys/*.xml")));
+        resources.addAll(ListUtil.of(resolver.getResources("classpath*:mapper/wecom/*.xml")));
+        bean.setMapperLocations(resources.toArray(new Resource[0]));
+        bean.setPlugins(mybatisPlusInterceptor);
+        return bean.getObject();
+    }
+
+    @Bean("qwSqlSessionTemplate")
+    public SqlSessionTemplate qwSqlSessionTemplate(
+            @Qualifier("qwSqlSessionFactory") SqlSessionFactory sessionFactory) {
+        return new SqlSessionTemplate(sessionFactory);
+    }
+
+    @Bean("qwTransactionManager")
+    public PlatformTransactionManager qwTransactionManager(@Qualifier("qwDataSource") DataSource dataSource) {
+        return new DataSourceTransactionManager(dataSource);
+    }
+
+
+}

+ 130 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/exception/SystemHandlerException.java

@@ -0,0 +1,130 @@
+package com.linkwechat.coal.exception;
+
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.exception.SystemException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.ObjectUtils;
+import org.springframework.validation.BeanPropertyBindingResult;
+import org.springframework.validation.BindException;
+import org.springframework.validation.ObjectError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @Author: shixk
+ * @create: 2022/5/31 17:25
+ * description: 全局控制层异常处理类
+ */
+@Slf4j
+@RestControllerAdvice
+public class SystemHandlerException {
+
+    @ExceptionHandler(Exception.class)
+    @ResponseStatus(HttpStatus.OK)
+    public AjaxResult APIExceptionHandler(Exception e) {
+        e.printStackTrace();
+        if (e instanceof SystemException) {
+            SystemException se = (SystemException)e;
+            return AjaxResult.error(se.getCode(), se.getMessage());
+        }
+
+        String err = StringUtils.isEmpty(e.getMessage()) ? e.toString() : e.getMessage();
+        log.error(err);
+        String errid = "[错误ID:" + System.currentTimeMillis()+"]" + err;
+        return AjaxResult.error(errid);
+    }
+
+    /**
+     * 用来处理bean validation异常
+     * @param ex
+     * @return
+     */
+    @ExceptionHandler(BindException.class)
+    @ResponseStatus(HttpStatus.OK)
+    @ResponseBody
+    public  AjaxResult beanPropertyBindingResultException(BindException ex){
+        ex.printStackTrace();
+        AjaxResult httpResult = new AjaxResult();
+        BeanPropertyBindingResult bindingResult = (BeanPropertyBindingResult)ex.getBindingResult();
+        if(!ObjectUtils.isEmpty(bindingResult)){
+            StringBuilder msgBuilder = new StringBuilder();
+            for(ObjectError error :bindingResult.getAllErrors()){
+                msgBuilder.append(error.getDefaultMessage()).append(",");
+            }
+
+            String errorMessage = msgBuilder.toString();
+            if(errorMessage.length()>1){
+                errorMessage = errorMessage.substring(0,errorMessage.length()-1);
+            }
+            httpResult.setCode(org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR);
+            httpResult.setMsg(errorMessage);
+            return httpResult;
+        }
+        httpResult.setMsg(ex.getMessage());
+        return httpResult;
+    }
+
+    /**
+     * 用来处理bean validation异常
+     * @param ex
+     * @return
+     */
+    @ExceptionHandler(ConstraintViolationException.class)
+    @ResponseStatus(HttpStatus.OK)
+    @ResponseBody
+    public  AjaxResult resolveConstraintViolationException(ConstraintViolationException ex){
+        ex.printStackTrace();
+        AjaxResult httpResult = new AjaxResult();
+        Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
+        if(!CollectionUtils.isEmpty(constraintViolations)){
+            StringBuilder msgBuilder = new StringBuilder();
+            for(ConstraintViolation constraintViolation :constraintViolations){
+                msgBuilder.append(constraintViolation.getMessage()).append(",");
+            }
+            String errorMessage = msgBuilder.toString();
+            if(errorMessage.length()>1){
+                errorMessage = errorMessage.substring(0,errorMessage.length()-1);
+            }
+            httpResult.setCode(org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR);
+            httpResult.setMsg(errorMessage);
+            return httpResult;
+        }
+        httpResult.setMsg(ex.getMessage());
+        return httpResult;
+    }
+
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    @ResponseStatus(HttpStatus.OK)
+    @ResponseBody
+    public AjaxResult resolveMethodArgumentNotValidException(MethodArgumentNotValidException ex){
+        ex.printStackTrace();
+        AjaxResult httpResult = new AjaxResult();
+        List<ObjectError> objectErrors = ex.getBindingResult().getAllErrors();
+        if(!CollectionUtils.isEmpty(objectErrors)) {
+            StringBuilder msgBuilder = new StringBuilder();
+            for (ObjectError objectError : objectErrors) {
+                msgBuilder.append(objectError.getDefaultMessage()).append(",");
+            }
+            String errorMessage = msgBuilder.toString();
+            if (errorMessage.length() > 1) {
+                errorMessage = errorMessage.substring(0, errorMessage.length() - 1);
+            }
+            httpResult.setCode(org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR);
+            httpResult.setMsg(errorMessage);
+            return httpResult;
+        }
+        httpResult.setMsg(ex.getMessage());
+        return httpResult;
+    }
+}

+ 39 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/filter/HttpServletRequestWrapperFilter.java

@@ -0,0 +1,39 @@
+package com.linkwechat.coal.filter;
+
+import lombok.extern.slf4j.Slf4j;
+
+import javax.servlet.*;
+import javax.servlet.annotation.WebFilter;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+
+@Slf4j
+@WebFilter(filterName = "httpServletRequestWrapperFilter", urlPatterns = {"/*"}, asyncSupported = true)
+public class HttpServletRequestWrapperFilter implements Filter {
+
+    @Override
+    public void init(FilterConfig filterConfig) {
+        log.info("---->init");
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+        log.info("---->进入过滤器");
+
+        if (request instanceof HttpServletRequest) {
+            HttpServletRequest httpRequest = (HttpServletRequest) request;
+            String requestURI = httpRequest.getRequestURI();
+            log.info("请求路径:" + requestURI);
+
+        }
+        chain.doFilter(request, response);
+    }
+
+
+    @Override
+    public void destroy() {
+        log.info("destroy<-----");
+    }
+}

+ 129 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/controller/AliAiController.java

@@ -0,0 +1,129 @@
+package com.linkwechat.coal.modules.ai.controller;
+
+import com.alibaba.dashscope.aigc.generation.GenerationResult;
+import com.alibaba.dashscope.common.Message;
+import com.alibaba.dashscope.exception.ApiException;
+import com.alibaba.dashscope.exception.InputRequiredException;
+import com.alibaba.dashscope.exception.NoApiKeyException;
+import com.alibaba.dashscope.utils.JsonUtils;
+import com.linkwechat.coal.config.Constant;
+import com.linkwechat.coal.modules.ai.domain.dto.AliAiTextDto;
+import com.linkwechat.coal.modules.ai.domain.dto.RequestIdDto;
+import com.linkwechat.coal.modules.ai.utils.AliAiUtils;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.redis.RedisService;
+import com.linkwechat.common.exception.SystemException;
+import com.linkwechat.common.utils.StringUtils;
+import io.reactivex.Flowable;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+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;
+
+import javax.servlet.annotation.WebServlet;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping("/aliAi")
+public class AliAiController {
+
+    @Autowired
+    private RedisService redisService;
+
+    /**
+     * 单次对话
+     *
+     * @param dto
+     * @return
+     */
+    @PostMapping("/single")
+    public AjaxResult single(@RequestBody AliAiTextDto dto) {
+        GenerationResult result = null;
+        try {
+            result = AliAiUtils.callWithMessage1(dto, Constant.QWQ_32B);
+            System.out.println(JsonUtils.toJson(result));
+        } catch (ApiException | NoApiKeyException | InputRequiredException e) {
+            e.printStackTrace();
+            // 使用日志框架记录异常信息
+            throw new SystemException("出现异常!" + e.getMessage());
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new SystemException("未知异常!" + e.getMessage());
+        }
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 多轮对话
+     *
+     * @param dto
+     * @return
+     */
+    @PostMapping("/multiwheel")
+    public AjaxResult multiwheel(@RequestBody AliAiTextDto dto) {
+        GenerationResult result = null;
+        try {
+            result = AliAiUtils.callWithMessage2(dto, Constant.QWQ_32B);
+            System.out.println(JsonUtils.toJson(result));
+        } catch (ApiException | NoApiKeyException | InputRequiredException e) {
+            // 使用日志框架记录异常信息
+            e.printStackTrace();
+            throw new SystemException("出现异常!" + e.getMessage());
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new SystemException("未知异常!" + e.getMessage());
+        }
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 流式对话
+     *
+     * @param dto
+     * @return
+     */
+    @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public Flowable<GenerationResult> stream(@RequestBody AliAiTextDto dto) {
+        List<Message> messageList = null;
+        RequestIdDto requestId = new RequestIdDto();
+        try {
+//            if (StringUtils.isNotBlank(dto.getRequestId())) {
+//                messageList = redisService.getCacheList(dto.getRequestId());
+//            } else {
+//                messageList = new ArrayList<>();
+//            }
+            messageList = new ArrayList<>();
+
+
+            Flowable<GenerationResult> result = AliAiUtils.callWithMessage3(dto, Constant.QWQ_32B, messageList);
+            List<Message> finalMessageList = messageList;
+            return result.doOnNext(genResult -> {
+                log.info("流式处理: {}", JsonUtils.toJson(genResult));
+                // 逐个处理每个 GenerationResult
+                AliAiUtils.handleGenerationResult(genResult, requestId);
+            }).doOnError(error -> {
+                // 记录错误日志
+                log.error("流式对话出现错误: {}", error.getMessage());
+            }).doOnComplete(() -> {
+                log.info("处理完成!!!!!!!!!!!");
+                AliAiUtils.callWithMessage3EndEnd(finalMessageList, requestId);
+            });
+
+        } catch (ApiException | NoApiKeyException | InputRequiredException e) {
+            e.printStackTrace();
+            // 使用日志框架记录异常信息
+            throw new SystemException("出现异常!" + e.getMessage());
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new SystemException("未知异常!" + e.getMessage());
+        }
+    }
+
+
+}

+ 63 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/controller/FileController.java

@@ -0,0 +1,63 @@
+package com.linkwechat.coal.modules.ai.controller;
+
+import com.linkwechat.coal.modules.ai.service.impl.FtpStorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+@RestController
+@RequestMapping("/files")
+public class FileController {
+
+    @Autowired
+    private FtpStorageService ftpStorageService;
+
+    /**
+     * 文件上传功能
+     *
+     * @param file 上传的文件
+     * @return 文件保存的位置的ID
+     * @throws IOException 文件上传异常
+     */
+    @PostMapping("/upload")
+    public String uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
+        return ftpStorageService.store(file);
+    }
+
+    /**
+     * 根据文件ID下载文件
+     *
+     * @param fileId 文件ID
+     * @return 文件资源
+     * @throws IOException 文件下载异常
+     */
+    @GetMapping("/download/{fileId}")
+    @ResponseBody
+    public ResponseEntity<InputStreamResource> downloadFile(@PathVariable String fileId) throws IOException {
+        InputStream inputStream = ftpStorageService.loadFile(fileId);
+        InputStreamResource resource = new InputStreamResource(inputStream);
+
+        return ResponseEntity.ok()
+                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileId + "\"")
+                .contentType(MediaType.APPLICATION_OCTET_STREAM)
+                .body(resource);
+    }
+
+    /**
+     * 根据文件ID删除文件
+     *
+     * @param fileId 文件ID
+     * @throws IOException 文件删除异常
+     */
+    @DeleteMapping("/delete/{fileId}")
+    public void deleteFile(@PathVariable String fileId) throws IOException {
+        ftpStorageService.deleteFile(fileId);
+    }
+}

+ 159 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/controller/ZmAiController.java

@@ -0,0 +1,159 @@
+package com.linkwechat.coal.modules.ai.controller;
+
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.linkwechat.coal.modules.ai.domain.dto.AliAiTextDto;
+import com.linkwechat.coal.modules.ai.utils.SSEHelper;
+import com.linkwechat.coal.modules.ai.utils.WordUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/zmAi")
+public class ZmAiController {
+
+
+    @Autowired
+    private SSEHelper sseHelper;
+
+
+    //条款校验
+    @CrossOrigin
+    @PostMapping(path = "/chat")
+    @ResponseBody
+    public SseEmitter chat(@RequestBody AliAiTextDto dto) throws IOException {
+
+        // 请求内容
+        String wordText = WordUtils.escapeControlCharacters(WordUtils.readWordToString(dto.getFilePath()));
+
+        String content = wordText;
+        if (StringUtils.isNotBlank(dto.getContent())) {
+            dto.setContent(WordUtils.escapeControlCharacters(com.linkwechat.common.utils.StringUtils.isNotBlank(wordText) ? ("附件内容:" + wordText + "\r\n") + ("问题:" + dto.getContent()) : ("问题:" + dto.getContent())));
+        }
+        // 获取请求体
+        String requestBody = "";
+        SseEmitter sseEmitter = null;
+        switch ("R1") {
+            case "R1":
+                requestBody = getALiString(content);
+                log.info("请求体:" + requestBody);
+                sseEmitter = sseHelper.receiveSSE(requestBody,
+                        "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
+                        "Bearer sk-2643c6ba68874228b8102bd81f517bce");
+                break;
+            case "LOCAL":
+                requestBody = getLocalString(content);
+                log.info("请求体:" + requestBody);
+                sseEmitter = sseHelper.receiveSSE(requestBody,
+                        "http://10.1.42.64:3001/api/v1/workspace/qinfl/stream-chat",
+                        "Bearer YQDEDSA-H054VWX-G99277J-WHH8FST");
+                break;
+            default:
+                log.info("无法识别的类型!!!");
+                throw new RuntimeException("无法识别的请求类型!");
+        }
+        // 返回是SseEmitter对象
+        return sseEmitter;
+    }
+
+
+    /**
+     * 获得请求体信息
+     */
+    private static String getRequestBody(HttpServletRequest request) throws IOException {
+        StringBuilder requestBody = new StringBuilder();
+        BufferedReader reader = request.getReader();
+        String line;
+        while ((line = reader.readLine()) != null) {
+            requestBody.append(line);
+        }
+        return requestBody.toString();
+    }
+
+    /**
+     * 阿里的请求参数
+     *
+     * @param content
+     * @return
+     * @throws IOException
+     */
+    private String getALiString(String content) {
+        String systemCon = "\"content\": \"" + content + "\"";
+
+        String requestBody = "{\n" +
+                "    \"model\": \"deepseek-r1\",\n" +
+                "    \"messages\": [\n" +
+                "        {\n" +
+                "            \"role\": \"system\",\n" +
+                systemCon +
+                "        },\n" +
+                "        {\n" +
+                "            \"role\": \"user\",\n" +
+                "            \"content\": \"" + content + "\"" +
+                "        }\n" +
+                "    ],\n" +
+                "    \"stream\": true\n" +
+                "}";
+        String decodedStr = formatJson(requestBody);
+        return decodedStr;
+    }
+
+
+
+    /**
+     * 本地大模型请求参数
+     *
+     * @param content
+     * @return
+     */
+    private String getLocalString(String content) {
+        String requestBody = "{\n" +
+                "    \"mode\": \"chat\",\n" +
+                "    \"message\": \"" + content + "\"" +
+                "}";
+        String decodedStr = formatJson(requestBody);
+        return decodedStr;
+    }
+
+    /**
+     * json格式化
+     *
+     * @param json
+     * @return
+     */
+    private String formatJson(String json) {
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            mapper.enable(SerializationFeature.INDENT_OUTPUT);
+
+            // 将JSON字符串解析为Map
+            Map<String, Object> jsonMap = mapper.readValue(json, new TypeReference<Map<String, Object>>() {
+            });
+
+            // 将Map格式化为美化的JSON字符串
+            return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonMap);
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new RuntimeException("转json失败!" + json);
+        }
+    }
+
+
+
+
+
+}

+ 30 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/domain/dto/AliAiTextDto.java

@@ -0,0 +1,30 @@
+package com.linkwechat.coal.modules.ai.domain.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AliAiTextDto {
+
+    /**
+     * 问题
+     */
+    private String content;
+
+    /**
+     * 文件路径
+     */
+    private String filePath;
+
+    /**
+     * 对话ID
+     */
+    private String requestId;
+
+
+}

+ 20 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/domain/dto/RequestIdDto.java

@@ -0,0 +1,20 @@
+package com.linkwechat.coal.modules.ai.domain.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class RequestIdDto {
+
+    /**
+     * 对话ID
+     */
+    private String requestId;
+
+
+}

+ 15 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/mapper/SysUserMapper2.java

@@ -0,0 +1,15 @@
+package com.linkwechat.coal.modules.ai.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.linkwechat.common.core.domain.entity.SysUser;
+
+/**
+ * Created with IntelliJ IDEA.
+ *
+ * @Author: Shixk
+ * @Date: 2023/11/24/11:02
+ * @Description:
+ */
+public interface SysUserMapper2 extends BaseMapper<SysUser> {
+
+}

+ 106 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/service/impl/FtpStorageService.java

@@ -0,0 +1,106 @@
+package com.linkwechat.coal.modules.ai.service.impl;
+
+import org.apache.commons.net.ftp.FTP;
+import org.apache.commons.net.ftp.FTPClient;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@Service
+public class FtpStorageService {
+
+    @Value("${ftp.server}")
+    private String server;
+
+    @Value("${ftp.port}")
+    private int port;
+
+    @Value("${ftp.username}")
+    private String username;
+
+    @Value("${ftp.password}")
+    private String password;
+
+    @Value("${ftp.base-dir}")
+    private String baseDir;
+
+    private final Map<String, String> fileMap = new HashMap<>();
+
+    public String store(MultipartFile file) throws IOException {
+        FTPClient ftpClient = new FTPClient();
+        try {
+            ftpClient.connect(server, port);
+            ftpClient.login(username, password);
+            ftpClient.enterLocalPassiveMode();
+            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
+
+            String fileName = UUID.randomUUID().toString() + "_" + file.getOriginalFilename();
+            String remoteFilePath = baseDir + "/" + fileName;
+
+            try (InputStream inputStream = file.getInputStream()) {
+                boolean success = ftpClient.storeFile(remoteFilePath, inputStream);
+                if (success) {
+                    String fileId = UUID.randomUUID().toString();
+                    fileMap.put(fileId, remoteFilePath);
+                    return fileId;
+                } else {
+                    throw new RuntimeException("Failed to store file on FTP server.");
+                }
+            }
+        } finally {
+            ftpClient.logout();
+            ftpClient.disconnect();
+        }
+    }
+
+    public InputStream loadFile(String fileId) throws IOException {
+        FTPClient ftpClient = new FTPClient();
+        try {
+            ftpClient.connect(server, port);
+            ftpClient.login(username, password);
+            ftpClient.enterLocalPassiveMode();
+            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
+
+            String remoteFilePath = fileMap.get(fileId);
+            if (remoteFilePath == null) {
+                throw new RuntimeException("File not found: " + fileId);
+            }
+
+            return ftpClient.retrieveFileStream(remoteFilePath);
+        } finally {
+            ftpClient.logout();
+            ftpClient.disconnect();
+        }
+    }
+
+    public void deleteFile(String fileId) throws IOException {
+        FTPClient ftpClient = new FTPClient();
+        try {
+            ftpClient.connect(server, port);
+            ftpClient.login(username, password);
+            ftpClient.enterLocalPassiveMode();
+            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
+
+            String remoteFilePath = fileMap.get(fileId);
+            if (remoteFilePath == null) {
+                throw new RuntimeException("File not found: " + fileId);
+            }
+
+            boolean success = ftpClient.deleteFile(remoteFilePath);
+            if (success) {
+                fileMap.remove(fileId);
+            } else {
+                throw new RuntimeException("Failed to delete file from FTP server.");
+            }
+        } finally {
+            ftpClient.logout();
+            ftpClient.disconnect();
+        }
+    }
+}

+ 232 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/utils/AliAiUtils.java

@@ -0,0 +1,232 @@
+package com.linkwechat.coal.modules.ai.utils;
+
+import com.alibaba.dashscope.aigc.generation.Generation;
+import com.alibaba.dashscope.aigc.generation.GenerationParam;
+import com.alibaba.dashscope.aigc.generation.GenerationResult;
+import com.alibaba.dashscope.common.Message;
+import com.alibaba.dashscope.common.Role;
+import com.alibaba.dashscope.exception.ApiException;
+import com.alibaba.dashscope.exception.InputRequiredException;
+import com.alibaba.dashscope.exception.NoApiKeyException;
+import com.linkwechat.coal.config.Constant;
+import com.linkwechat.coal.modules.ai.domain.dto.AliAiTextDto;
+import com.linkwechat.coal.modules.ai.domain.dto.RequestIdDto;
+import com.linkwechat.common.core.redis.RedisService;
+import com.linkwechat.common.utils.StringUtils;
+import io.reactivex.Flowable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class AliAiUtils {
+
+    private static RedisService redisService;
+
+    private static StringBuilder reasoningContent = new StringBuilder();
+    private static StringBuilder finalContent = new StringBuilder();
+    private static boolean isFirstPrint = true;
+
+    @Autowired
+    private RedisService redisService2;
+
+    @PostConstruct
+    public void beforeInit() {
+        redisService = redisService2;
+    }
+
+    /**
+     * 单次对话
+     *
+     * @param dto
+     * @param model
+     * @return
+     * @throws ApiException
+     * @throws NoApiKeyException
+     * @throws InputRequiredException
+     */
+    public static GenerationResult callWithMessage1(AliAiTextDto dto, String model) throws ApiException, NoApiKeyException, InputRequiredException, IOException {
+        // 请求内容
+        String wordText = WordUtils.escapeControlCharacters(WordUtils.readWordToString(dto.getFilePath()));
+        if (StringUtils.isNotBlank(wordText)) {
+            dto.setContent(WordUtils.escapeControlCharacters(StringUtils.isNotBlank(wordText) ? ("附件内容:" + wordText + "\r\n") + ("问题:" + dto.getContent()) : ("问题:" + dto.getContent())));
+        }
+
+        Generation gen = new Generation();
+        Message userMsg = Message.builder()
+                .role(Role.USER.getValue())
+                .content(dto.getContent())
+                .build();
+        GenerationParam param = GenerationParam.builder()
+                // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
+                .apiKey(Constant.KEY)
+                .model(StringUtils.isBlank(model) ? "deepseek-r1" : model)
+                .messages(Arrays.asList(userMsg))
+                // 不可以设置为"text"
+                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
+                .build();
+        GenerationResult call = gen.call(param);
+        String reasoningContent = call.getOutput().getChoices().get(0).getMessage().getReasoningContent();
+        System.out.println("思考过程:" + reasoningContent);
+        String resContent = call.getOutput().getChoices().get(0).getMessage().getContent();
+        System.out.println("回复内容:" + resContent);
+        return call;
+    }
+
+    /**
+     * 多轮对话
+     *
+     * @return
+     * @throws ApiException
+     * @throws NoApiKeyException
+     * @throws InputRequiredException
+     */
+    public static GenerationResult callWithMessage2(AliAiTextDto dto, String model) throws ApiException, NoApiKeyException, InputRequiredException, IOException {
+        String wordText = WordUtils.escapeControlCharacters(WordUtils.readWordToString(dto.getFilePath()));
+        if (StringUtils.isNotBlank(wordText)) {
+            dto.setContent(WordUtils.escapeControlCharacters(StringUtils.isNotBlank(wordText) ? ("附件内容:" + wordText + "\r\n") + ("问题:" + dto.getContent()) : ("问题:" + dto.getContent())));
+        }
+        List<Message> messageList = null;
+//        if (StringUtils.isNotBlank(dto.getRequestId())) {
+//            messageList = redisService.getCacheList(dto.getRequestId());
+//        } else {
+//            messageList = new ArrayList<>();
+//        }
+        messageList = new ArrayList<>();
+
+        // 初始化
+        Generation gen = new Generation();
+
+        Message userMsg1 = Message.builder()
+                .role(Role.USER.getValue())
+                .content(dto.getContent())
+                .build();
+        // 设置对话
+        messageList.add(userMsg1);
+
+        GenerationParam param = GenerationParam.builder()
+                // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
+                .apiKey(Constant.KEY)
+                .model(model)
+                .messages(messageList)
+                // 不可以设置为"text"
+                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
+                .build();
+        // 清空
+        messageList.clear();
+        // 重新添加下
+        messageList.add(userMsg1);
+        // 回复
+        GenerationResult call = gen.call(param);
+        String reasoningContent = call.getOutput().getChoices().get(0).getMessage().getReasoningContent();
+        System.out.println("思考过程:" + reasoningContent);
+        String content = call.getOutput().getChoices().get(0).getMessage().getContent();
+        System.out.println("回复内容:" + content);
+        // 回复内容
+        Message assistantMsg = Message.builder()
+                .role(Role.ASSISTANT.getValue())
+                .content(content)
+                .build();
+        // 清空
+        messageList.clear();
+        // 重新添加下
+        messageList.add(userMsg1);
+        messageList.add(assistantMsg);
+        String requestId = call.getRequestId();
+        if (StringUtils.isNotBlank(dto.getRequestId())) {
+            redisService.deleteObject(dto.getRequestId());
+        }
+//        redisService.setCacheListExpiration(requestId, messageList, 8, TimeUnit.HOURS);
+        return call;
+    }
+
+    /**
+     * 流式输出
+     *
+     * @param dto
+     * @param model
+     * @return
+     * @throws ApiException
+     * @throws NoApiKeyException
+     * @throws InputRequiredException
+     */
+    public static Flowable<GenerationResult> callWithMessage3(AliAiTextDto dto, String model, List<Message> messageList) throws NoApiKeyException, InputRequiredException, IOException {
+
+        String wordText = WordUtils.escapeControlCharacters(WordUtils.readWordToString(dto.getFilePath()));
+        if (StringUtils.isNotBlank(wordText)) {
+            dto.setContent(WordUtils.escapeControlCharacters(StringUtils.isNotBlank(wordText) ? ("附件内容:" + wordText + "\r\n") + ("问题:" + dto.getContent()) : ("问题:" + dto.getContent())));
+        }
+
+        Generation gen = new Generation();
+        Message userMsg = Message.builder().role(Role.USER.getValue()).content(dto.getContent()).build();
+        // 添加请求
+        messageList.add(userMsg);
+
+        GenerationParam param = GenerationParam.builder()
+                // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
+                .apiKey(Constant.KEY)
+                .model(model)
+                .messages(messageList)
+                // 不可以设置为"text"
+                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
+                .incrementalOutput(true)
+                .build();
+        Flowable<GenerationResult> result = gen.streamCall(param);
+        return result;
+
+    }
+
+    public static void callWithMessage3EndEnd(List<Message> messageList, RequestIdDto requestId) {
+        System.out.println("\n====================思考过程====================");
+        System.out.println(reasoningContent.toString());
+        System.out.println("\n====================完整回复====================");
+        System.out.println(finalContent.toString());
+        //  最终结果
+        if (finalContent.length() > 0) {
+            // 回复内容
+            Message assistantMsg = Message.builder()
+                    .role(Role.ASSISTANT.getValue())
+                    .content(finalContent.toString())
+                    .build();
+            messageList.add(assistantMsg);
+            if (StringUtils.isNotBlank(requestId.getRequestId())) {
+                redisService.deleteObject(requestId.getRequestId());
+            }
+//            redisService.setCacheListExpiration(requestId.getRequestId(), messageList, 8, TimeUnit.HOURS);
+        }
+    }
+
+
+    public static void handleGenerationResult(GenerationResult message, RequestIdDto requestId) {
+        String reasoning = message.getOutput().getChoices().get(0).getMessage().getReasoningContent();
+        String content = message.getOutput().getChoices().get(0).getMessage().getContent();
+        if (StringUtils.isBlank(requestId.getRequestId())) {
+            requestId.setRequestId(message.getRequestId());
+        }
+        if (StringUtils.isNotBlank(reasoning)) {
+            reasoningContent.append(reasoning);
+            if (isFirstPrint) {
+                System.out.println("====================思考过程====================");
+                isFirstPrint = false;
+            }
+
+            System.out.print(reasoning);
+        }
+
+        if (StringUtils.isNotBlank(content)) {
+            finalContent.append(content);
+            if (!isFirstPrint) {
+                System.out.println("\n====================完整回复====================");
+                isFirstPrint = true;
+            }
+            System.out.print(content);
+        }
+    }
+
+}

+ 45 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/utils/FileUtils.java

@@ -0,0 +1,45 @@
+package com.linkwechat.coal.modules.ai.utils;
+
+import com.linkwechat.coal.config.Constant;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class FileUtils {
+
+    /**
+     * 文件上传功能
+     *
+     * @param file 上传的文件
+     * @return 文件存储路径
+     * @throws IOException 文件上传异常
+     */
+    public String uploadFile(MultipartFile file) throws IOException {
+        if (file == null || file.isEmpty()) {
+            throw new IllegalArgumentException("文件不能为空");
+        }
+
+        // 获取文件名
+        String fileName = file.getOriginalFilename();
+        if (fileName == null) {
+            throw new IllegalArgumentException("文件名不能为空");
+        }
+
+        // 创建文件存储路径
+        Path uploadPath = Paths.get(Constant.FILE_UPLOAD_PATH);
+        if (!Files.exists(uploadPath)) {
+            Files.createDirectories(uploadPath);
+        }
+
+        // 构建文件完整路径
+        Path filePath = uploadPath.resolve(fileName);
+
+        // 保存文件
+        Files.copy(file.getInputStream(), filePath);
+
+        return filePath.toString();
+    }
+}

+ 90 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/utils/SSEHelper.java

@@ -0,0 +1,90 @@
+package com.linkwechat.coal.modules.ai.utils;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.codec.DecodingException;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.codec.ServerSentEvent;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import reactor.core.publisher.Flux;
+
+import java.io.IOException;
+
+@Service
+@Slf4j
+public class SSEHelper {
+    // 发送数据
+    public void sendSSE(SseEmitter sseEmitter, Object data) {
+        try {
+            log.info("推送消息: {}", data);
+
+            sseEmitter.send(SseEmitter.event()
+                    .data(data));
+            log.info("推送消息成功: {}", data);
+
+        } catch (IOException e) {
+            log.error("【1】SEE推送消息失败, 失败消息: {}", data, e);
+            throw new RuntimeException("【1】SEE推送消息失败, 失败消息:" + data, e);
+        } catch (Exception e) {
+            log.error("【2】SEE推送消息失败, 失败消息: {}", data, e);
+            throw new RuntimeException("【2】SEE推送消息失败, 失败消息:" + data, e);
+
+        }
+    }
+
+    /**
+     * 接收数据,发送请求
+     *
+     * @param jsonBody 请求体
+     * @param url      请求路径
+     * @param token    秘钥、token
+     * @return 返回流
+     */
+    public SseEmitter receiveSSE(String jsonBody, String url, String token) {
+        SseEmitter sseEmitter = new SseEmitter(300_000L);
+
+        // SSE服务器端地址
+        WebClient webClient = WebClient.create(url);
+        Flux<ServerSentEvent> sseStream = webClient.post()
+                // 请求头信息
+                .accept(MediaType.TEXT_EVENT_STREAM)
+                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // 设置请求头
+                .header(HttpHeaders.AUTHORIZATION, token)
+                .body(BodyInserters.fromObject(jsonBody))
+                .retrieve()
+                .bodyToFlux(ServerSentEvent.class);
+        // 这里Flux.from()是异步的,该方法会先把SseEmitter对象返回出去
+        Flux.from(sseStream)
+                .doOnNext(sseEvent -> {
+                    log.info("请求成功:" + sseEvent.data());
+                    // 推送消息给客户端
+                    sendSSE(sseEmitter, sseEvent.data());
+                })
+                .doOnError(error -> {
+                    if (error instanceof DecodingException
+                            && error.getMessage().contains("DONE")) {
+                        log.error("处理完成!!!!!!!!!!!");
+                    }else {
+                        log.error("连接断开: {}", error.getMessage());
+                        sseEmitter.completeWithError(error);
+                        error.printStackTrace();
+                    }
+
+                })
+                .doOnComplete(() -> {
+                    log.info("处理完成!!!!!!!!!!!");
+                    // 正常完成
+                    sseEmitter.complete();
+                }).subscribe();
+
+
+        // Flux.form()是异步的,SseEmitter对象是先返回给前端
+        return sseEmitter;
+    }
+
+
+}
+

+ 57 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/modules/ai/utils/WordUtils.java

@@ -0,0 +1,57 @@
+package com.linkwechat.coal.modules.ai.utils;
+
+import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+
+public class WordUtils {
+
+    /**
+     * 读取 Word 文档并返回其内容为字符串
+     *
+     * @param filePath Word 文档路径
+     * @return 文档内容的字符串
+     * @throws IOException 如果文件读取失败
+     */
+    public static String readWordToString(String filePath) throws IOException {
+        if (filePath == null || filePath.isEmpty()) {
+            return "";
+        } else if (filePath.startsWith("http:") || filePath.startsWith("https:")) {
+            URL url = new URL(filePath);
+            URLConnection connection = url.openConnection();
+            try (InputStream is = connection.getInputStream();
+                 XWPFDocument document = new XWPFDocument(is);
+                 XWPFWordExtractor extractor = new XWPFWordExtractor(document)) {
+
+                // 提取文档内容
+                return extractor.getText();
+            }
+        } else {
+            try (FileInputStream fis = new FileInputStream(filePath);
+                 XWPFDocument document = new XWPFDocument(fis);
+                 XWPFWordExtractor extractor = new XWPFWordExtractor(document)) {
+
+                // 提取文档内容
+                return extractor.getText();
+            }
+        }
+
+    }
+
+    /**
+     * 转义控制字符
+     */
+    public static String escapeControlCharacters(String input) {
+        return input.replaceAll("\\n", "\\\\n")  // 换行符
+                .replaceAll("\\t", "\\\\t")  // 制表符
+                .replaceAll("\\r", "\\\\r")  // 回车符
+                .replaceAll("\\b", "\\\\b")  // 退格符
+                .replaceAll("\\f", "\\\\f")  // 换页符
+                .replaceAll("\"", "\\\\\""); // 双引号
+    }
+}

+ 15 - 0
linkwe-ai/src/main/java/com/linkwechat/coal/modules/qw/mapper/SysUserMapper1.java

@@ -0,0 +1,15 @@
+package com.linkwechat.coal.modules.qw.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.linkwechat.common.core.domain.entity.SysUser;
+
+/**
+ * Created with IntelliJ IDEA.
+ *
+ * @Author: Shixk
+ * @Date: 2023/11/24/11:02
+ * @Description:
+ */
+public interface SysUserMapper1 extends BaseMapper<SysUser> {
+
+}

+ 8 - 0
linkwe-ai/src/main/resources/application.properties

@@ -0,0 +1,8 @@
+spring.application.name=linkwe-ai
+
+# application.properties
+ftp.server=ftp.example.com
+ftp.port=21
+ftp.username=your_username
+ftp.password=your_password
+ftp.base-dir=/path/to/base/directory

+ 11 - 0
linkwe-ai/src/main/resources/banner.txt

@@ -0,0 +1,11 @@
+${AnsiColor.GREEN}
+   ________               __          __      __             __
+  / ____/ /_  ___  _____/ /___ ___  / /___  / /____  _  __/ /
+ / /   / __ \/ _ \/ ___/ / __ `/ / / / __ \/ //_/ _ \| |/_/ /
+/ /___/ / / /  __/ /__/ / /_/ / /_/ / /_/ / ,< /  __/>  </_
+\____/_/ /_/\___/\___/_/\__,_/\__,_/\____/_/|_|\___/_/|_/(_)
+
+${AnsiColor.WHITE}
+
+Spring Application Name: ${spring.application.name}
+

+ 203 - 0
linkwe-ai/src/main/resources/logback-spring.xml

@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <!-- 日志输出格式 -->
+    <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS,CTT} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+    <!--  邮件 -->
+    <!-- SMTP server的地址,必需指定。如网易的SMTP服务器地址是: smtp.163.com -->
+    <property name="smtpHost" value="smtp.163.com"/><!--填入要发送邮件的smtp服务器地址-->
+    <!-- SMTP server的端口地址。默认值:25 -->
+    <property name="smtpPort" value="25"/>
+    <!-- 发送邮件账号,默认为null -->
+    <property name="username" value="*"/><!--发件人账号-->
+    <!-- 发送邮件密码,默认为null -->
+    <property name="password" value="*"/><!--发件人密码-->
+    <!-- 如果设置为true,appender将会使用SSL连接到日志服务器。默认值:false -->
+    <property name="SSL" value="true"/>
+    <!-- 指定发送到那个邮箱,可设置多个<to>属性,指定多个目的邮箱 -->
+    <property name="email_to" value="*"/>
+    <!--收件人账号多个可以逗号隔开-->
+    <!-- 指定发件人名称。如果设置成“&lt;ADMIN&gt; ”,则邮件发件人将会是“<ADMIN> ” -->
+    <property name="email_from" value="1" />
+    <!-- 指定emial的标题,它需要满足PatternLayout中的格式要求。如果设置成“Log: %logger - %msg ”,就案例来讲,则发送邮件时,标题为“【Error】: com.foo.Bar - Hello World ”。 默认值:"%logger{20} - %m". -->
+    <property name="email_subject" value="【linkwechat-ai】【Error】: %logger" />
+
+
+    <property name="app_name" value="linkwe-ai"/>
+    <!-- 日志根目录-->
+    <springProperty scope="context" name="log.path" source="logging.path"
+                    defaultValue="./logs/linkwe-ai"/>
+
+    <!-- 控制台输出 -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+
+    <!-- 系统日志输出 -->
+    <!-- 循环文件输出(基于时间戳的分文件,是实际项目中用途最广的一种情况) -->
+    <appender name="file_debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/debug/${app_name}.debug.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <!-- rollover daily -->
+            <fileNamePattern>${log.path}/debug/${app_name}_%d{yyyy-MM-dd}.debug.%i.log</fileNamePattern>
+            <!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
+            <maxFileSize>50MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>DEBUG</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <!-- 异步输出 -->
+    <appender name="arollingfile_debug" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>1</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_debug"/>
+    </appender>
+
+    <!-- 循环文件输出(基于时间戳的分文件,是实际项目中用途最广的一种情况) -->
+    <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/info/${app_name}.info.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <!-- rollover daily -->
+            <fileNamePattern>${log.path}/info/${app_name}_%d{yyyy-MM-dd}.info.%i.log</fileNamePattern>
+            <!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
+            <maxFileSize>50MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>INFO</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <!-- 异步输出 -->
+    <appender name="arollingfile_info" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>1</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_info"/>
+    </appender>
+
+    <!-- 循环文件输出(基于时间戳的分文件,是实际项目中用途最广的一种情况) -->
+    <appender name="file_warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/warn/${app_name}.warn.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <!-- rollover daily -->
+            <fileNamePattern>${log.path}/warn/${app_name}_%d{yyyy-MM-dd}.warn.%i.log</fileNamePattern>
+            <!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
+            <maxFileSize>50MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>WARN</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <!-- 异步输出 -->
+    <appender name="arollingfile_warn" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>1</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_warn"/>
+    </appender>
+
+    <!-- 循环文件输出(基于时间戳的分文件,是实际项目中用途最广的一种情况) -->
+    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/error/${app_name}.error.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <!-- rollover daily -->
+            <fileNamePattern>${log.path}/error/${app_name}_%d{yyyy-MM-dd}.error.%i.log</fileNamePattern>
+            <!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
+            <maxFileSize>50MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <!-- 异步输出 -->
+    <appender name="arollingfile_error" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>1</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_error"/>
+    </appender>
+
+    <!-- ERROR邮件发送 -->
+    <appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender">
+        <smtpHost>${smtpHost}</smtpHost>
+        <smtpPort>${smtpPort}</smtpPort>
+        <username>${username}</username>
+        <password>${password}</password>
+        <!-- <asynchronousSending>true</asynchronousSending>-->
+        <SSL>${SSL}</SSL>
+        <!--<STARTTLS>true</STARTTLS>-->
+        <to>${email_to}</to>
+        <from>${email_from}</from>
+        <subject>${email_subject}</subject>
+        <!-- html格式-->
+        <layout class="ch.qos.logback.classic.html.HTMLLayout">
+            <Pattern>%date%level%thread%logger{0}%line%message</Pattern>
+        </layout>
+        <!-- 这里采用等级过滤器 指定等级相符才发送 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+        <cyclicBufferTracker class="ch.qos.logback.core.spi.CyclicBufferTracker">
+            <!-- 每个电子邮件只发送一个日志条目 -->
+            <bufferSize>1</bufferSize>
+        </cyclicBufferTracker>
+    </appender>
+
+
+    <!-- 系统模块日志级别控制  -->
+    <logger name="cn.linkwechat" level="info" />
+    <!-- Spring日志级别控制  -->
+    <logger name="org.springframework" level="warn" />
+
+    <!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="console" />
+        <appender-ref ref="arollingfile_debug" />
+        <appender-ref ref="arollingfile_info" />
+        <appender-ref ref="arollingfile_warn" />
+        <appender-ref ref="arollingfile_error" />
+        <!-- <appender-ref ref="alarm_error" />-->
+        <appender-ref ref="EMAIL" />
+    </root>
+</configuration>

+ 4 - 0
linkwe-ai/src/main/resources/mapper/ai/SysUserMapper2.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.linkwechat.coal.modules.ai.mapper.SysUserMapper2">
+</mapper>

+ 4 - 0
linkwe-ai/src/main/resources/mapper/qw/SysUserMapper1.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.linkwechat.coal.modules.qw.mapper.SysUserMapper1">
+</mapper>

+ 24 - 0
linkwe-ai/src/main/resources/spy.properties

@@ -0,0 +1,24 @@
+#3.2.1\u4EE5\u4E0A\u4F7F\u7528
+modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
+#3.2.1\u4EE5\u4E0B\u4F7F\u7528\u6216\u8005\u4E0D\u914D\u7F6E
+#modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
+# \u81EA\u5B9A\u4E49\u65E5\u5FD7\u6253\u5370
+logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
+#\u65E5\u5FD7\u8F93\u51FA\u5230\u63A7\u5236\u53F0
+appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
+# \u4F7F\u7528\u65E5\u5FD7\u7CFB\u7EDF\u8BB0\u5F55 sql
+#appender=com.p6spy.engine.spy.appender.Slf4JLogger
+# \u8BBE\u7F6E p6spy driver \u4EE3\u7406
+deregisterdrivers=true
+# \u53D6\u6D88JDBC URL\u524D\u7F00
+useprefix=true
+# \u914D\u7F6E\u8BB0\u5F55 Log \u4F8B\u5916,\u53EF\u53BB\u6389\u7684\u7ED3\u679C\u96C6\u6709error,info,batch,debug,statement,commit,rollback,result,resultset.
+excludecategories=info,debug,result,commit,resultset
+# \u65E5\u671F\u683C\u5F0F
+dateformat=yyyy-MM-dd HH:mm:ss
+# \u5B9E\u9645\u9A71\u52A8\u53EF\u591A\u4E2A
+#driverlist=oracle.jdbc.driver.OracleDriver
+# \u662F\u5426\u5F00\u542F\u6162SQL\u8BB0\u5F55
+outagedetection=true
+# \u6162SQL\u8BB0\u5F55\u6807\u51C6 2 \u79D2
+outagedetectioninterval=2

+ 13 - 0
linkwe-ai/src/test/java/com/linkwechat/coal/LinkweAiApplicationTests.java

@@ -0,0 +1,13 @@
+package com.linkwechat.coal;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class LinkweAiApplicationTests {
+
+    @Test
+    void contextLoads() {
+    }
+
+}

+ 1 - 0
linkwe-api/jdkReplace/README.txt

@@ -0,0 +1 @@
+2024011014:35

BIN
linkwe-api/jdkReplace/US_export_policy.jar


BIN
linkwe-api/jdkReplace/local_policy.jar


+ 134 - 0
linkwe-api/pom.xml

@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>linkwechat-cloud</artifactId>
+        <groupId>com.linkwechat</groupId>
+        <version>3.1.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>linkwe-api</artifactId>
+
+    <properties>
+        <java.version>1.8</java.version>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+        <maven.build.timestamp.format>yyyyMMddHHmmss</maven.build.timestamp.format>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!-- 阿里数据库连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-test</artifactId>
+            <version>2.6.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- liquibase -->
+        <!--<dependency>
+            <groupId>org.liquibase</groupId>
+            <artifactId>liquibase-core</artifactId>
+        </dependency>-->
+
+        <!-- swagger2-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.linkwechat</groupId>
+            <artifactId>linkwe-service</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+            <version>2021.1</version>
+        </dependency>
+
+        <!-- SpringCloud Alibaba Nacos Config -->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+
+
+    </dependencies>
+
+
+    <build>
+        <finalName>lw-api</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.3.3.RELEASE</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.liquibase</groupId>
+                <artifactId>liquibase-maven-plugin</artifactId>
+                <version>4.8.0</version>
+                <dependencies>
+                    <dependency>
+                        <groupId>org.liquibase</groupId>
+                        <artifactId>liquibase-core</artifactId>
+                        <version>4.8.0</version>
+                    </dependency>
+                </dependencies>
+                <configuration>
+                    <propertyFile>src/main/resources/liquibase-db.properties</propertyFile>
+                    <changeLogFile>src/main/resources/db/changelog-master.xml</changeLogFile>
+                    <diffChangeLogFile>src/main/resources/db/changelog/changelog_${maven.build.timestamp}.xml</diffChangeLogFile>
+                    <outputChangeLogFile>src/main/resources/db/changelog/changelog_original.xml</outputChangeLogFile>
+                    <dropFirst>false</dropFirst>
+                    <defaultSchemaName/>
+                    <verbose>true</verbose>
+                    <logging>debug</logging>
+                    <promptOnNonLocalDatabase>true</promptOnNonLocalDatabase>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 31 - 0
linkwe-api/src/main/java/com/linkwechat/LinkWeApiApplication.java

@@ -0,0 +1,31 @@
+package com.linkwechat;
+
+import com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration;
+import com.linkwechat.common.config.fegin.FeginConfig;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+@MapperScan("com.linkwechat.**.mapper")
+@SpringBootApplication(exclude = {PageHelperAutoConfiguration.class})
+@EnableAsync
+@EnableFeignClients(defaultConfiguration = FeginConfig.class)
+public class LinkWeApiApplication {
+    public static void main(String[] args) {
+
+        try {
+            new SpringApplicationBuilder(LinkWeApiApplication.class)
+                    .properties("spring.config.name:bootstrap", "config/run/bootstrap.yml")
+                    .properties("spring.application.name=linkwe-api")
+                    .build().run(args);
+            System.out.println("(♥◠‿◠)ノ゙  LinkWe-api启动成功   ლ(´ڡ`ლ)゙ ");
+        }catch (Exception e){
+            e.printStackTrace();
+
+        }
+
+    }
+
+}

+ 58 - 0
linkwe-api/src/main/java/com/linkwechat/aspecj/RequestArgsLogAspect.java

@@ -0,0 +1,58 @@
+package com.linkwechat.aspecj;
+
+import com.alibaba.fastjson.JSONObject;
+import com.linkwechat.common.utils.ip.IpUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Objects;
+
+/**
+ * 请求入参日志记录处理
+ *
+ * @author ruoyi
+ */
+//@Aspect
+//@Component
+public class RequestArgsLogAspect {
+    private static final Logger log = LoggerFactory.getLogger(RequestArgsLogAspect.class);
+
+
+    // 配置织入点
+    @Pointcut("execution(public * com.linkwechat.controller..*.*(..)))")
+    public void logPointCut() {
+    }
+
+    /**
+     * 处理请求前日志打印
+     */
+    @Before(value = "logPointCut()")
+    public void doBefore(JoinPoint joinPoint) {
+
+        Object[] args = joinPoint.getArgs();
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (Objects.isNull(attributes)) {
+            return;
+        }
+        HttpServletRequest request = attributes.getRequest();
+        String url = request.getRequestURL().toString();
+        String ipAddr = IpUtils.getIpAddr(request);
+        Object[] params = new Object[args.length];
+        for (int i = 0; i< args.length;i++) {
+            if (!(args[i] instanceof HttpServletRequest) && !(args[i] instanceof HttpServletResponse) && !(args[i] instanceof MultipartFile)) {
+                params[i]=args[i];
+            }
+        }
+        log.info("api 接口请求 url:{},HTTP:{},IP:{},params:{}", url, request.getMethod(), ipAddr, JSONObject.toJSONString(params));
+    }
+}

+ 133 - 0
linkwe-api/src/main/java/com/linkwechat/config/SwaggerConfig.java

@@ -0,0 +1,133 @@
+package com.linkwechat.config;
+
+import com.linkwechat.common.config.LinkWeChatConfig;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.*;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spi.service.contexts.SecurityContext;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Swagger2的接口配置
+ * 
+ * @author ruoyi
+ */
+@Configuration
+@EnableSwagger2
+public class SwaggerConfig
+{
+
+
+    /** 系统基础配置 */
+    @Autowired
+    private LinkWeChatConfig linkWeChatConfig;
+
+    /** 是否开启swagger */
+    @Value("${swagger.enabled}")
+    private boolean enabled;
+
+    /** 设置请求的统一前缀 */
+    @Value("${swagger.pathMapping}")
+    private String pathMapping;
+    /** 标题 */
+    @Value("${swagger.title}")
+    private String title;
+    /** 描述 */
+    @Value("${swagger.description}")
+    private String description;
+    /** 版本 */
+    @Value("${swagger.version}")
+    private String version;
+
+    /**
+     * 创建API
+     */
+    @Bean
+    public Docket createRestApi()
+    {
+        return new Docket(DocumentationType.SWAGGER_2)
+                // 是否启用Swagger
+                .enable(enabled)
+                // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
+                .apiInfo(apiInfo())
+                // 设置哪些接口暴露给Swagger展示
+                .select()
+                // 扫描所有有注解的api,用这种方式更灵活
+                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
+                // 扫描指定包中的swagger注解
+                // .apis(RequestHandlerSelectors.basePackage("com.ruoyi.project.tool.swagger"))
+                // 扫描所有 .apis(RequestHandlerSelectors.any())
+                .paths(PathSelectors.any())
+                .build()
+                /* 设置安全模式,swagger可以设置访问token */
+                .securitySchemes(securitySchemes())
+                .securityContexts(securityContexts())
+                .pathMapping(pathMapping);
+    }
+
+    /**
+     * 安全模式,这里指定token通过Authorization头请求头传递
+     */
+    private List<ApiKey> securitySchemes()
+    {
+        List<ApiKey> apiKeyList = new ArrayList<ApiKey>();
+        apiKeyList.add(new ApiKey("Authorization", "Authorization", "header"));
+        return apiKeyList;
+    }
+
+    /**
+     * 安全上下文
+     */
+    private List<SecurityContext> securityContexts()
+    {
+        List<SecurityContext> securityContexts = new ArrayList<>();
+        securityContexts.add(
+                SecurityContext.builder()
+                        .securityReferences(defaultAuth())
+                        .forPaths(PathSelectors.regex("^(?!auth).*$"))
+                        .build());
+        return securityContexts;
+    }
+
+    /**
+     * 默认的安全上引用
+     */
+    private List<SecurityReference> defaultAuth()
+    {
+        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
+        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
+        authorizationScopes[0] = authorizationScope;
+        List<SecurityReference> securityReferences = new ArrayList<>();
+        securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
+        return securityReferences;
+    }
+
+    /**
+     * 添加摘要信息
+     */
+    private ApiInfo apiInfo()
+    {
+        // 用ApiInfoBuilder进行定制
+        return new ApiInfoBuilder()
+                // 设置标题
+                .title(title)
+                // 描述
+                .description(description)
+                // 作者信息
+                .contact(new Contact(linkWeChatConfig.getName(), null, null))
+                // 版本
+                .version("版本号:" + version)
+                .build();
+    }
+}

+ 20 - 0
linkwe-api/src/main/java/com/linkwechat/config/WebConfig.java

@@ -0,0 +1,20 @@
+package com.linkwechat.config;
+
+import com.linkwechat.interceptor.RequestContextInterceptor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * @author sxw
+ * @description 拦截器配置
+ * @date 2022/4/9 22:58
+ **/
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(new RequestContextInterceptor());
+    }
+}

+ 133 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeAgentController.java

@@ -0,0 +1,133 @@
+package com.linkwechat.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.utils.SecurityUtils;
+import com.linkwechat.domain.WeAgentMsg;
+import com.linkwechat.domain.agent.query.WeAgentAddQuery;
+import com.linkwechat.domain.agent.query.WeAgentEditQuery;
+import com.linkwechat.domain.agent.query.WeAgentMsgAddQuery;
+import com.linkwechat.domain.agent.query.WeAgentMsgListQuery;
+import com.linkwechat.domain.agent.vo.LwAgentListVo;
+import com.linkwechat.domain.agent.vo.WeAgentMsgListVo;
+import com.linkwechat.domain.agent.vo.WeAgentMsgVo;
+import com.linkwechat.service.IWeAgentInfoService;
+import com.linkwechat.service.IWeAgentMsgService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author danmo
+ * @description 应用管理
+ * @date 2022/11/03 18:22
+ **/
+@Slf4j
+@RestController
+@RequestMapping(value = "agent")
+@Api(tags = "应用管理")
+public class WeAgentController extends BaseController {
+
+    @Autowired
+    private IWeAgentInfoService weAgentInfoService;
+
+    @Autowired
+    private IWeAgentMsgService weAgentMsgService;
+
+    @ApiOperation(value = "新增应用信息", httpMethod = "POST")
+    @PostMapping("/add")
+    public AjaxResult addAgent(@RequestBody @Validated WeAgentAddQuery query) {
+        log.info("新增应用信息入参query:{}", JSONObject.toJSONString(query));
+        Integer id = weAgentInfoService.addAgent(query);
+        return AjaxResult.success(id);
+    }
+
+    @ApiOperation(value = "拉取应用信息", httpMethod = "GET")
+    @GetMapping("/pull/{id}")
+    public AjaxResult pullAgent(@PathVariable("id") Integer id) {
+        log.info("拉取应用信息入参query:{}", id);
+        weAgentInfoService.pullAgent(id);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "编辑应用信息", httpMethod = "PUT")
+    @PutMapping("/update/{id}")
+    public AjaxResult update(@PathVariable("id") Integer id, @RequestBody WeAgentEditQuery query) {
+        query.setId(id);
+        log.info("编辑应用信息入参query:{}", JSONObject.toJSONString(query));
+        weAgentInfoService.update(query);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "获取应用列表", httpMethod = "GET")
+    @GetMapping("/list")
+    public AjaxResult<LwAgentListVo> getList() {
+        log.info("获取应用列表接口调用");
+        List<LwAgentListVo> list = weAgentInfoService.getList();
+        return AjaxResult.success(list);
+    }
+
+    @ApiOperation(value = "删除应用", httpMethod = "DELETE")
+    @DeleteMapping("/delete/{id}")
+    public AjaxResult<LwAgentListVo> deleteAgent(@PathVariable("id") Integer id) {
+        log.info("删除应用入参query:{}", id);
+        weAgentInfoService.deleteAgent(id);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "获取历史消息列表", httpMethod = "GET")
+    @GetMapping("/msg/list")
+    public TableDataInfo<WeAgentMsgListVo> getMsgList(WeAgentMsgListQuery query) {
+        this.startPage();
+        List<WeAgentMsgListVo> list = weAgentMsgService.getMsgList(query);
+        return  getDataTable(list);
+    }
+
+    @ApiOperation(value = "新增应用消息", httpMethod = "POST")
+    @PostMapping("/msg/add")
+    public AjaxResult addMsg(@RequestBody WeAgentMsgAddQuery query) {
+        log.info("新增应用消息入参query:{}", JSONObject.toJSONString(query));
+        weAgentMsgService.addMsg(query);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "修改应用消息", httpMethod = "PUT")
+    @PutMapping("/msg/update/{id}")
+    public AjaxResult updateMsg(@PathVariable("id") Long id, @RequestBody WeAgentMsgAddQuery query) {
+        query.setId(id);
+        log.info("修改应用消息入参query:{}", JSONObject.toJSONString(query));
+        weAgentMsgService.updateMsg(query);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "删除应用消息", httpMethod = "DELETE")
+    @DeleteMapping("/msg/delete/{id}")
+    public AjaxResult deleteMsg(@PathVariable("id") Long id) {
+        log.info("删除应用消息入参query:{}", id);
+        weAgentMsgService.deleteMsg(id);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "应用消息详情", httpMethod = "GET")
+    @GetMapping("/msg/get/{id}")
+    public AjaxResult<WeAgentMsgVo> getMsgInfo(@PathVariable("id") Long id) {
+        log.info("应用消息详情入参query:{}", id);
+        return AjaxResult.success(weAgentMsgService.getMsgInfo(id));
+    }
+
+    @ApiOperation(value = "撤销应用消息", httpMethod = "GET")
+    @GetMapping("/msg/revoke/{id}")
+    public AjaxResult revokeMsgInfo(@PathVariable("id") Long id) {
+        log.info("撤销消息详情入参query:{}", id);
+        weAgentMsgService.revokeMsgInfo(id);
+        return AjaxResult.success();
+    }
+
+}

+ 100 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCallBackController.java

@@ -0,0 +1,100 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.exception.AesException;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.common.utils.wecom.WxCryptUtil;
+import com.linkwechat.domain.WeCorpAccount;
+import com.linkwechat.service.ISxkWelcomeMsgService;
+import com.linkwechat.service.IWeCorpAccountService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * @author Shixk
+ * @description 企微回调
+ * @date 2023/11/1 11:31
+ **/
+@Api(tags = "企微回调通知")
+@Slf4j
+@RestController
+@RequestMapping("/wecom/callback")
+public class WeCallBackController {
+
+    @Lazy
+    @Autowired
+    private ISxkWelcomeMsgService sxkWelcomeMsgService;
+
+    @Autowired
+    private IWeCorpAccountService weCorpAccountService;
+
+    @ApiModelProperty("post数据接收")
+    @PostMapping(value = "/recive/{corpId}")
+    public String recive(@RequestBody String msg, @RequestParam(name = "msg_signature") String signature,
+                         String timestamp, String nonce, @PathVariable("corpId") String corpId) {
+        log.info("post数据接收>>>>>>>>>>corpId:{},msg:{}", corpId, msg);
+        log.info("post参数>>>>>>>>>>msg_signature:{}==timestamp:{}==nonce:{}==corpId:{}==msg:{}",
+                signature, timestamp, nonce, corpId, msg);
+        WeCorpAccount weCorpAccount = weCorpAccountService.getCorpAccountByCorpId(corpId);
+        WxCryptUtil wxCryptUtil = new WxCryptUtil(weCorpAccount.getToken(), weCorpAccount.getEncodingAesKey(), corpId);
+        try {
+            String decrypt = wxCryptUtil.decrypt(signature, timestamp, nonce, msg);
+            log.info("----------------post数据解密后:{}", decrypt);
+            sxkWelcomeMsgService.msgHandle(decrypt);
+            log.info("后续步骤异步执行----------------返回数据", decrypt);
+            return decrypt;
+        } catch (Exception e) {
+            log.error("post数据接收>>>>>>error", e);
+            String sRespData = WxCryptUtil.getTextRespData("success");
+            return wxCryptUtil.encrypt(sRespData);
+        }
+    }
+
+    @ApiModelProperty("get数据校验")
+    @GetMapping(value = "/recive/{corpId}")
+    public String recive(HttpServletRequest request, @PathVariable("corpId") String corpId) {
+        log.info("get数据校验>>>>>>>>>>corpId:{}", corpId);
+        // 微信加密签名
+        String sVerifyMsgSig = request.getParameter("msg_signature");
+        // 时间戳
+        String sVerifyTimeStamp = request.getParameter("timestamp");
+        // 随机数
+        String sVerifyNonce = request.getParameter("nonce");
+        // 随机字符串
+        String sVerifyEchoStr = request.getParameter("echostr");
+        log.info("get参数>>>>>>>>>>msg_signature:{}==timestamp:{}==nonce:{}==sVerifyEchoStr:{}",
+                sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr);
+
+        WeCorpAccount weCorpAccount = weCorpAccountService.getCorpAccountByCorpId(corpId);
+        WxCryptUtil wxCryptUtil = new WxCryptUtil(weCorpAccount.getToken(), weCorpAccount.getEncodingAesKey(), corpId);
+        try {
+            if (StringUtils.isEmpty(sVerifyEchoStr)) {
+                return "success";
+            }
+            String s = wxCryptUtil.verifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr);
+            log.info("----------------GET数据解密后:{}", s);
+
+            return s;
+        } catch (Exception e) {
+            log.error("get数据校验>>>>>>error", e);
+            return "error";
+        }
+    }
+
+    public static void main(String[] args) throws AesException {
+        String sVerifyMsgSig = "1b7276a1856ac52dc7ae408d3ee1b923780f60ca";
+        String sVerifyTimeStamp = "1704793285";
+        String sVerifyNonce = "1704681796";
+
+        String sVerifyEchoStr = "LoWs4zLC+WzW7u5lpaHlIMD7i0fnIN7rek4L8hj1vpXxvG9oDu4aInFfQ7ZRTok1P2592kNOfeBug37xyIgLJQ==";
+        WxCryptUtil wxCryptUtil = new WxCryptUtil("dPvUu3mF2DBw559i0RhMGNFkKDi", "iUBcItTG64fjIjbMDNPqLIoeLjYdzOo4ghVHonb7wNB", "wx73e9b369d87bf285");
+        String s = wxCryptUtil.verifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr);
+        System.out.println(s);
+
+    }
+}

+ 138 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCallBackJsonController.java

@@ -0,0 +1,138 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.linkwechat.common.exception.AesException;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.common.utils.wecom.WxCryptUtil;
+import com.linkwechat.common.utils.wecom.json.WXBizJsonMsgCryptUtil;
+import com.linkwechat.domain.WeCorpAccount;
+import com.linkwechat.domain.wecom.callback.json.RobotMsgBasic;
+import com.linkwechat.domain.wecom.callback.json.RobotMsgResText;
+import com.linkwechat.domain.wecom.callback.json.RobotMsgText;
+import com.linkwechat.service.IRobotMsgService;
+import com.linkwechat.service.ISxkWelcomeMsgService;
+import com.linkwechat.service.IWeCorpAccountService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.extern.slf4j.Slf4j;
+import org.json.JSONObject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * @author Shixk
+ * @description Json企微回调
+ * @date 2023/11/1 11:31
+ **/
+@Api(tags = "企微回调通知")
+@Slf4j
+@RestController
+@RequestMapping("/wecom/callback/json")
+public class WeCallBackJsonController {
+
+    @Lazy
+    @Autowired
+    private IRobotMsgService robotMsgService;
+
+    @Autowired
+    private IWeCorpAccountService weCorpAccountService;
+
+    @ApiModelProperty("post数据接收")
+    @PostMapping(value = "/recive/{corpId}")
+    public String recive(@RequestBody String msg, @RequestParam(name = "msg_signature") String signature,
+                         String timestamp, String nonce, @PathVariable("corpId") String corpId) {
+        log.info("JSON-post数据接收>>>>>>>>>>corpId:{},msg:{}", corpId, msg);
+        log.info("JSON-post参数>>>>>>>>>>msg_signature:{}==timestamp:{}==nonce:{}==corpId:{}==msg:{}",
+                signature, timestamp, nonce, corpId, msg);
+        WeCorpAccount weCorpAccount = weCorpAccountService.getCorpAccountByCorpId(corpId);
+        WXBizJsonMsgCryptUtil wxBizJsonMsgCryptUtil = null;
+        try {
+            wxBizJsonMsgCryptUtil = new WXBizJsonMsgCryptUtil(weCorpAccount.getToken(), weCorpAccount.getEncodingAesKey(), "");
+
+            String sMsg = wxBizJsonMsgCryptUtil.DecryptMsg(signature, timestamp, nonce, msg);
+            log.info("JSON----------------post数据解密后:{}", sMsg);
+            String resDate = robotMsgService.msgHandle(sMsg);
+            log.info("【回复:{}】", resDate);
+            // 返回
+            String sEncryptMsg = wxBizJsonMsgCryptUtil.EncryptMsg(resDate, timestamp, nonce);
+            log.info("JSON--------返回:{}", sEncryptMsg);
+            return sEncryptMsg;
+
+        } catch (Exception e) {
+            // 解密失败,失败原因请查看异常
+            e.printStackTrace();
+            return null;
+
+        }
+    }
+
+    public static void main(String[] args) {
+        String msg = "{\"msgid\":\"4831d3ca6dc493e323d498eef7a787f7\",\"aibotid\":\"aiblLBTKqIka1UJi8DGFXMHV1VsJKJGP_K3\",\"chattype\":\"single\",\"from\":{\"userid\":\"sxk\"},\"msgtype\":\"text\",\"text\":{\"content\":\"你是谁\"}}";
+        ObjectMapper mapper = new ObjectMapper();
+        try {
+            RobotMsgBasic basic = mapper.readValue(msg, RobotMsgBasic.class);
+            if (ObjectUtil.isEmpty(basic)) {
+                log.error("企微机器人接收消息-消息处理失败,解析JSON报文为空!");
+            }
+            String msgtype = basic.getMsgtype();
+
+            if ("text".equalsIgnoreCase(msgtype)) {
+                // 文本消息
+                RobotMsgText readValue = mapper.readValue(msg, RobotMsgText.class);
+                log.info("企微机器人接收消息-文本消息:{}", readValue.getText().getContent());
+                RobotMsgResText resText = new RobotMsgResText();
+                resText.setMsgtype("text");
+                RobotMsgResText.Text text = new RobotMsgResText.Text();
+                text.setContent("测试111111111111111");
+                resText.setText(text);
+                System.out.println("json:" + mapper.writeValueAsString(resText));
+            }else if ("image".equalsIgnoreCase(msgtype)) {
+                // 图文混排
+            }else if ("mixed".equalsIgnoreCase(msgtype)) {
+                // 文件消息
+            }else if ("stream".equalsIgnoreCase(msgtype)) {}
+
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+
+    }
+
+    @ApiModelProperty("get数据校验")
+    @GetMapping(value = "/recive/{corpId}")
+    public String recive(HttpServletRequest request, @PathVariable("corpId") String corpId) {
+        log.info("JSON-get数据校验>>>>>>>>>>corpId:{}", corpId);
+        // 微信加密签名
+        String sVerifyMsgSig = request.getParameter("msg_signature");
+        // 时间戳
+        String sVerifyTimeStamp = request.getParameter("timestamp");
+        // 随机数
+        String sVerifyNonce = request.getParameter("nonce");
+        // 随机字符串
+        String sVerifyEchoStr = request.getParameter("echostr");
+        log.info("JSON-get参数>>>>>>>>>>msg_signature:{}==timestamp:{}==nonce:{}==sVerifyEchoStr:{}",
+                sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr);
+        log.info("JSON开始解密---------------->");
+
+        WeCorpAccount weCorpAccount = weCorpAccountService.getCorpAccountByCorpId(corpId);
+        try {
+            log.info("JSON开始解密---------------->");
+            WXBizJsonMsgCryptUtil wxBizJsonMsgCryptUtil = new WXBizJsonMsgCryptUtil(weCorpAccount.getToken(), weCorpAccount.getEncodingAesKey(), "");
+            String sEchoStr = wxBizJsonMsgCryptUtil.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,
+                    sVerifyNonce, sVerifyEchoStr);
+            log.info("JSON----------------GET数据解密后:{}", sEchoStr);
+            return sEchoStr;
+        } catch (Exception e1) {
+            log.error("JSON-get数据校验>>>>>>error", e1);
+            return "error";
+        }
+
+
+    }
+
+}

+ 104 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCallBackXmlController.java

@@ -0,0 +1,104 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.exception.AesException;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.common.utils.wecom.WxCryptUtil;
+import com.linkwechat.common.utils.xml.XmlUtils;
+import com.linkwechat.domain.WeCorpAccount;
+import com.linkwechat.service.ISxkWelcomeMsgService;
+import com.linkwechat.service.IWeCorpAccountService;
+import com.linkwechat.service.IXmlMsgService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * @author Shixk
+ * @description 企微回调
+ * @date 2023/11/1 11:31
+ **/
+@Api(tags = "企微回调通知")
+@Slf4j
+@RestController
+@RequestMapping("/wecom/callback/xml")
+public class WeCallBackXmlController {
+
+    @Lazy
+    @Autowired
+    private IXmlMsgService xmlMsgService;
+
+    @Autowired
+    private IWeCorpAccountService weCorpAccountService;
+
+    @ApiModelProperty("post数据接收")
+    @PostMapping(value = "/recive/{corpId}")
+    public String recive(@RequestBody String msg, @RequestParam(name = "msg_signature") String signature,
+                         String timestamp, String nonce, @PathVariable("corpId") String corpId) {
+        log.info("XML-post数据接收>>>>>>>>>>corpId:{},msg:{}", corpId, msg);
+        log.info("XML-post参数>>>>>>>>>>msg_signature:{}==timestamp:{}==nonce:{}==corpId:{}==msg:{}",
+                signature, timestamp, nonce, corpId, msg);
+        WeCorpAccount weCorpAccount = weCorpAccountService.getCorpAccountByCorpId(corpId);
+        WxCryptUtil wxCryptUtil = new WxCryptUtil(weCorpAccount.getToken(), weCorpAccount.getEncodingAesKey(), corpId);
+        try {
+            String decrypt = wxCryptUtil.decrypt(signature, timestamp, nonce, msg);
+            log.info("XML-----------------post数据解密后:{}", decrypt);
+            String resDate = xmlMsgService.msgHandle(decrypt);
+            log.info("XML-----------------逻辑完成后返回数据[1]:{}", resDate);
+            String res = wxCryptUtil.encrypt(resDate);
+            log.info("XML-----------------最终返回数据[3]:{}", res);
+            return res;
+        } catch (Exception e) {
+            log.error("XML-post数据接收>>>>>>error", e);
+            String sRespData = WxCryptUtil.getTextRespData("success");
+            return wxCryptUtil.encrypt(sRespData);
+        }
+    }
+
+    @ApiModelProperty("get数据校验")
+    @GetMapping(value = "/recive/{corpId}")
+    public String recive(HttpServletRequest request, @PathVariable("corpId") String corpId) {
+        log.info("XML-get数据校验>>>>>>>>>>corpId:{}", corpId);
+        // 微信加密签名
+        String sVerifyMsgSig = request.getParameter("msg_signature");
+        // 时间戳
+        String sVerifyTimeStamp = request.getParameter("timestamp");
+        // 随机数
+        String sVerifyNonce = request.getParameter("nonce");
+        // 随机字符串
+        String sVerifyEchoStr = request.getParameter("echostr");
+        log.info("XML-get参数>>>>>>>>>>msg_signature:{}==timestamp:{}==nonce:{}==sVerifyEchoStr:{}",
+                sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr);
+
+        WeCorpAccount weCorpAccount = weCorpAccountService.getCorpAccountByCorpId(corpId);
+        WxCryptUtil wxCryptUtil = new WxCryptUtil(weCorpAccount.getToken(), weCorpAccount.getEncodingAesKey(), corpId);
+        try {
+            if (StringUtils.isEmpty(sVerifyEchoStr)) {
+                return "success";
+            }
+            String s = wxCryptUtil.verifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr);
+            log.info("XML-----------------GET数据解密后:{}", s);
+
+            return s;
+        } catch (Exception e) {
+            log.error("XML-get数据校验>>>>>>error", e);
+            return "error";
+        }
+    }
+
+    public static void main(String[] args) throws AesException {
+        String sVerifyMsgSig = "1b7276a1856ac52dc7ae408d3ee1b923780f60ca";
+        String sVerifyTimeStamp = "1704793285";
+        String sVerifyNonce = "1704681796";
+
+        String sVerifyEchoStr = "LoWs4zLC+WzW7u5lpaHlIMD7i0fnIN7rek4L8hj1vpXxvG9oDu4aInFfQ7ZRTok1P2592kNOfeBug37xyIgLJQ==";
+        WxCryptUtil wxCryptUtil = new WxCryptUtil("dPvUu3mF2DBw559i0RhMGNFkKDi", "iUBcItTG64fjIjbMDNPqLIoeLjYdzOo4ghVHonb7wNB", "wx73e9b369d87bf285");
+        String s = wxCryptUtil.verifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr);
+        System.out.println(s);
+
+    }
+}

+ 93 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCategoryController.java

@@ -0,0 +1,93 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.domain.entity.SysUser;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.domain.material.entity.WeCategory;
+import com.linkwechat.domain.material.vo.WeCategoryNewVo;
+import com.linkwechat.domain.material.vo.WeCategoryVo;
+import com.linkwechat.service.IWeCategoryService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+/**
+ * 企业微信素材分类Controller
+ *
+ * @author leejoker
+ * @date 2022-04-30
+ */
+@RestController
+@RequestMapping("category")
+@Api(tags = "企业微信素材分类")
+public class WeCategoryController extends BaseController {
+
+    @Resource
+    private IWeCategoryService weCategoryService;
+
+
+    @GetMapping("/list")
+    @ApiOperation("类目树")
+    public AjaxResult<WeCategoryVo> list(@RequestParam("mediaType") String mediaType) {
+
+        return AjaxResult.success(weCategoryService.findWeCategoryByMediaType(mediaType));
+    }
+
+    /**
+     * 通过id查询类目详细信息
+     */
+    @GetMapping(value = "/{id}")
+    @ApiOperation("通过id查询类目详细信息")
+    public AjaxResult<WeCategoryVo> getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(weCategoryService.getById(id));
+    }
+
+    /**
+     * 添加类目
+     */
+    @Log(title = "添加类目", businessType = BusinessType.INSERT)
+    @PostMapping
+    @ApiOperation("添加类目")
+    public AjaxResult add(@Validated @RequestBody WeCategory category) {
+        weCategoryService.insertWeCategory(category);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 更新目录
+     */
+    @Log(title = "更新目录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    @ApiOperation("更新目录")
+    public AjaxResult edit(@Validated @RequestBody WeCategory category) {
+        weCategoryService.updateWeCategory(category);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 删除类目
+     */
+    @Log(title = "删除类目", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    @ApiOperation("删除类目")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        weCategoryService.deleteWeCategoryByIds(ids);
+        return AjaxResult.success();
+    }
+
+    @Log(title = "批量删除或移动", businessType = BusinessType.DELETE)
+    @DeleteMapping("/delOrMuchMove")
+    @ApiOperation("批量删除或移动")
+    public AjaxResult delOrMuchMove(@RequestBody WeCategoryNewVo weCategoryNewVo) {
+        weCategoryService.delOrMuchMove(weCategoryNewVo);
+        return AjaxResult.success();
+    }
+
+
+}

+ 87 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeChatCollectionController.java

@@ -0,0 +1,87 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.enums.MediaType;
+import com.linkwechat.common.utils.SecurityUtils;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.domain.material.ao.WePoster;
+import com.linkwechat.domain.material.entity.WeMaterial;
+import com.linkwechat.domain.material.query.LinkMediaCollectQuery;
+import com.linkwechat.domain.side.vo.WeChatSideVo;
+import com.linkwechat.service.IWeChatCollectionService;
+import com.linkwechat.service.IWeMaterialService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 聊天工具 侧边栏 素材收藏
+ *
+ * @author sxw
+ */
+@Api(tags = "素材收藏接口")
+@RequestMapping(value = "/chat/collection")
+@RestController
+public class WeChatCollectionController extends BaseController {
+
+
+    @Autowired
+    private IWeChatCollectionService weChatCollectionService;
+
+    @Autowired
+    private IWeMaterialService weMaterialService;
+
+
+    /**
+     * 添加收藏
+     */
+    @ApiOperation(value = "添加收藏",httpMethod = "POST")
+    @PostMapping("addCollection")
+    public AjaxResult addCollection(@RequestBody LinkMediaCollectQuery query) {
+        weChatCollectionService.addCollection(query.getMaterialId());
+        return  AjaxResult.success();
+    }
+
+
+    /**
+     * 取消收藏
+     */
+    @ApiOperation(value = "取消收藏",httpMethod = "POST")
+    @PostMapping(value = "cancleCollection")
+    public AjaxResult cancleCollection(@RequestBody LinkMediaCollectQuery query) {
+        weChatCollectionService.cancleCollection(query.getMaterialId());
+        return  AjaxResult.success();
+    }
+
+    /**
+     * 收藏列表
+     */
+    @ApiOperation(value = "收藏列表",httpMethod = "GET")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(value = "keyword",required = false) String keyword) {
+        startPage();
+        List<WeChatSideVo> collections = weChatCollectionService.collections(SecurityUtils.getUserId(),keyword);
+        if(CollectionUtil.isNotEmpty(collections)){
+            collections.forEach(k->{
+                if(StringUtils.isEmpty(k.getMediaType())){//为空则为海报
+                    WeMaterial weMaterial = weMaterialService.getById(k.getMaterialId());
+                    if(null != weMaterial){
+                        k.setMediaType(MediaType.POSTER.getType());
+                        k.setMaterialName(weMaterial.getMaterialName());
+                        k.setMaterialUrl(weMaterial.getMaterialUrl());
+                    }
+                }
+            });
+        }
+        return getDataTable(collections);
+    }
+
+}

+ 142 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeChatContactMsgController.java

@@ -0,0 +1,142 @@
+package com.linkwechat.controller;
+
+import com.alibaba.excel.EasyExcel;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.utils.SecurityUtils;
+import com.linkwechat.common.utils.ServletUtils;
+import com.linkwechat.common.utils.poi.LwExcelUtil;
+import com.linkwechat.domain.WeChatContactMsg;
+import com.linkwechat.domain.msgaudit.vo.WeChatContactMsgVo;
+import com.linkwechat.service.IWeChatContactMsgService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+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.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.List;
+
+/**
+ * 会话消息Controller
+ * 
+ * @author ruoyi
+ * @date 2021-07-28
+ */
+@Api(tags = "会话存档管理")
+@RestController
+@RequestMapping("/chat/msg" )
+public class WeChatContactMsgController extends BaseController {
+
+    @Autowired
+    private IWeChatContactMsgService iWeChatContactMsgService;
+
+    /**
+     * 查询会话消息列表
+     */
+    @ApiOperation("查询会话消息列表")
+    @GetMapping("/list")
+    public TableDataInfo<List<WeChatContactMsg>> list(WeChatContactMsg weChatContactMsg) {
+        startPage();
+        List<WeChatContactMsg> list = iWeChatContactMsgService.queryList(weChatContactMsg);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出会话消息列表
+     */
+    @ApiOperation("导出会话消息列表")
+    @Log(title = "会话消息", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public void export(WeChatContactMsg weChatContactMsg) throws IOException {
+        List<WeChatContactMsg> list = iWeChatContactMsgService.queryList(weChatContactMsg);
+        HttpServletResponse response = ServletUtils.getResponse();
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode("会话消息", "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+        EasyExcel.write(response.getOutputStream(), WeChatContactMsg.class).sheet("会话消息").doWrite(list);
+    }
+
+    /**
+     * 获取会话消息详细信息
+     */
+    @ApiOperation("获取会话消息详细信息")
+    @GetMapping(value = "/{id}" )
+    public AjaxResult<WeChatContactMsg> getInfo(@PathVariable("id" ) Long id) {
+        return AjaxResult.success(iWeChatContactMsgService.getById(id));
+    }
+
+
+    /**
+     * 外部联系人/单聊 会话列表
+     */
+    @ApiOperation("外部联系人 会话列表")
+    @Log(title = "外部联系人 会话列表" , businessType = BusinessType.OTHER)
+    @GetMapping("/selectExternalChatList/{fromId}" )
+    public AjaxResult<List<WeChatContactMsgVo>> selectExternalChatList(@PathVariable("fromId") String fromId) {
+        return AjaxResult.success(iWeChatContactMsgService.selectExternalChatList(fromId));
+    }
+
+    /**
+     * 外部联系人/单聊 会话列表
+     */
+    @ApiOperation("单聊 会话列表")
+    @Log(title = "单聊 会话列表" , businessType = BusinessType.OTHER)
+    @GetMapping("/selectAloneChatList" )
+    public AjaxResult<List<WeChatContactMsgVo>> selectAloneChatList(WeChatContactMsg weChatContactMsg) {
+        return AjaxResult.success(iWeChatContactMsgService.selectAloneChatList(weChatContactMsg));
+    }
+
+    /**
+     * 内部联系人 会话列表
+     */
+    @ApiOperation("内部联系人 会话列表")
+    @Log(title = "内部联系人 会话列表" , businessType = BusinessType.OTHER)
+    @GetMapping("/selectInternalChatList/{fromId}" )
+    public AjaxResult<List<WeChatContactMsgVo>> selectInternalChatList(@PathVariable("fromId") String fromId) {
+        return AjaxResult.success(iWeChatContactMsgService.selectInternalChatList(fromId));
+    }
+
+    /**
+     * 群聊 会话列表
+     */
+    @ApiOperation("群聊 会话列表")
+    @Log(title = "群聊 会话列表" , businessType = BusinessType.OTHER)
+    @GetMapping("/selectGroupChatList/{fromId}" )
+    public AjaxResult<List<WeChatContactMsgVo>> selectGroupChatList(@PathVariable("fromId") String fromId) {
+        return AjaxResult.success(iWeChatContactMsgService.selectGroupChatList(fromId));
+    }
+
+    /**
+     * 全文检索 会话列表
+     */
+    @ApiOperation("全文检索 会话列表")
+    @Log(title = "全文检索 会话列表" , businessType = BusinessType.OTHER)
+    @GetMapping("/selectFullSearchChatList" )
+    public TableDataInfo<List<WeChatContactMsgVo>> selectFullSearchChatList(WeChatContactMsg weChatContactMsg) {
+        startPage();
+        List<WeChatContactMsgVo> list = iWeChatContactMsgService.selectFullSearchChatList(weChatContactMsg);
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 全文检索 会话列表
+     */
+    @ApiOperation("全文检索 导出列表")
+    @GetMapping("/selectFullSearchChatList/export")
+    public void fullSearchChatListExport(WeChatContactMsg weChatContactMsg) {
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeChatContactMsgVo.class, iWeChatContactMsgService.selectFullSearchChatList(weChatContactMsg),"全文检索"
+        );
+    }
+}

+ 95 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeChatItemController.java

@@ -0,0 +1,95 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.domain.material.entity.WeMaterial;
+import com.linkwechat.domain.material.query.LinkMediaQuery;
+import com.linkwechat.domain.side.WeChatItem;
+import com.linkwechat.domain.side.dto.WeChatItemDto;
+import com.linkwechat.domain.side.query.WeChatItemQuery;
+import com.linkwechat.domain.side.vo.WeChatSideVo;
+import com.linkwechat.service.IWeChatItemService;
+import com.linkwechat.service.IWeMaterialService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 聊天工具侧边栏
+ *
+ * @author kewen
+ */
+@RestController
+@RequestMapping(value = "/chat/item")
+public class WeChatItemController extends BaseController {
+
+    @Autowired
+    private IWeChatItemService weChatItemService;
+
+    @Autowired
+    private IWeMaterialService materialService;
+
+
+    /**
+     * 获取侧边栏选中或者未选中素材
+     * @param query
+     * @return
+     */
+    @GetMapping("/mList")
+    public TableDataInfo mList(WeChatItemQuery query) {
+        startPage();
+        List<WeMaterial> list=materialService.findWeMaterials(LinkMediaQuery.builder()
+                .categoryId(query.getCategoryId())
+                .search(query.getSearch())
+                .mediaType(query.getMediaType())
+                .build());
+        if(CollectionUtil.isNotEmpty(list)){
+            List<WeChatItem> weChatItems = weChatItemService.list(new LambdaQueryWrapper<WeChatItem>()
+                    .in(WeChatItem::getMaterialId,list.stream().map(WeMaterial::getId).collect(Collectors.toList())));
+            if(CollectionUtil.isNotEmpty(weChatItems)){
+                list.stream().forEach(k->{
+                    k.setIsCheck(
+                            weChatItems.stream().filter(item -> item.getMaterialId()
+                                    .equals(k.getId())).findFirst().isPresent()?true:false
+                    );
+                });
+            }
+        }
+
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 侧边栏抓取素材
+     */
+    @Log(title = "侧边栏抓取素材", businessType = BusinessType.INSERT)
+    @PutMapping
+    public AjaxResult add(@RequestBody WeChatItemDto chatItemDto) {
+        weChatItemService.checkItems(chatItemDto);
+        return AjaxResult.success();
+    }
+
+    /**
+     * h5素材列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(WeChatItemQuery query) {
+        startPage();
+        return getDataTable(
+                weChatItemService.chatItems(query.getSideId(),query.getKeyword(),query.getMediaType(),query.getUserId())
+        );
+    }
+
+
+
+
+
+}
+

+ 59 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeChatSideController.java

@@ -0,0 +1,59 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.domain.side.WeChatSide;
+import com.linkwechat.service.IWeChatSideService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import java.util.List;
+
+
+/**
+ * 聊天工具侧边栏
+ *
+ * @author kewen
+ */
+@RestController
+@RequestMapping(value = "/chat/side")
+public class WeChatSideController extends BaseController {
+
+
+    @Autowired
+    public IWeChatSideService weChatSideService;
+
+    /**
+     * 群发侧边栏列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list() {
+        startPage();
+        List<WeChatSide> weChatSides = weChatSideService.chatSides("0");
+        return getDataTable(weChatSides);
+    }
+
+    /**
+     * 更新侧边栏信息
+     */
+    @Log(title = "更新侧边栏信息", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody WeChatSide weChatSide) {
+        weChatSideService.updateWeChatSide(weChatSide);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 群发侧边栏列表
+     */
+    @GetMapping("/h5List")
+    public TableDataInfo h5List() {
+        startPage();
+        List<WeChatSide> weChatSides = weChatSideService.chatSides("1");
+        return getDataTable(weChatSides);
+    }
+
+}
+

+ 146 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCommunityGroupSopController.java

@@ -0,0 +1,146 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.constant.HttpStatus;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.domain.community.WeGroupSop;
+import com.linkwechat.domain.community.query.WeGroupSopQuery;
+import com.linkwechat.domain.community.vo.WeGroupSopVo;
+import com.linkwechat.service.IWeGroupSopService;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import java.util.List;
+
+
+/**
+ * 社群运营群sop
+ */
+@RestController
+@RequestMapping(value = "/communityGroupSop")
+public class WeCommunityGroupSopController extends BaseController {
+
+    @Autowired
+    private IWeGroupSopService iWeGroupSopService;
+
+
+
+    /**
+     * 通过过滤条件获取群sop列表
+     *
+     * @param ruleName  规则名称
+     * @param createBy  创建者
+     * @param beginTime 创建区间 - 开始时间
+     * @param endTime   创建区间 - 结束时间
+     * @return 群sop规则列表
+     */
+    @GetMapping(path = "/list")
+    public TableDataInfo<List<WeGroupSopVo>> getSopList(
+            @RequestParam(value = "ruleName", required = false) String ruleName,
+            @RequestParam(value = "createBy", required = false) String createBy,
+            @RequestParam(value = "beginTime", required = false) String beginTime,
+            @RequestParam(value = "endTime", required = false) String endTime
+    )
+    {
+        startPage();
+        List<WeGroupSopVo> groupSopVoList = iWeGroupSopService.getGroupSopList(ruleName, createBy, beginTime, endTime);
+        return getDataTable(groupSopVoList);
+    }
+
+
+    /**
+     * 通过规则id获取sop规则
+     *
+     * @param ruleId 规则id
+     * @return 结果
+     */
+    @GetMapping(path = "/{ruleId}")
+    public AjaxResult<WeGroupSopVo> getGroupSop(@PathVariable("ruleId") Long ruleId) {
+        WeGroupSopVo groupSopVo = iWeGroupSopService.getGroupSopById(ruleId);
+        if (null == groupSopVo) {
+            return AjaxResult.error(HttpStatus.NOT_FOUND, "该群SOP规则不存在");
+        }
+        return AjaxResult.success(groupSopVo);
+    }
+
+    /**
+     * 新增SOP规则
+     *
+     * @param groupSopDto 更新数据
+     * @return 结果
+     */
+    @PostMapping(path = "/")
+    public AjaxResult addGroupSop(@Validated @RequestBody WeGroupSopQuery groupSopDto) {
+
+        WeGroupSop weGroupSop = new WeGroupSop();
+        BeanUtils.copyProperties(groupSopDto, weGroupSop);
+
+        if (iWeGroupSopService.isNameOccupied(weGroupSop.getRuleName())) {
+            return AjaxResult.error(HttpStatus.BAD_REQUEST, "规则名称已存在");
+        }
+
+        try {
+            iWeGroupSopService.addGroupSop(weGroupSop, groupSopDto.getChatIdList(), groupSopDto.getMaterialIdList(), groupSopDto.getPicList());
+
+
+        }catch (Exception e){
+            return AjaxResult.error(e.getMessage());
+        }
+
+        return AjaxResult.success();
+    }
+
+
+
+    /**
+     * 更改SOP规则
+     *
+     * @param ruleId      SOP规则 id
+     * @param groupSopDto 更新数据
+     * @return 结果
+     */
+    @PutMapping(path = "/{ruleId}")
+    public AjaxResult updateGroupSop(@PathVariable Long ruleId, @Validated @RequestBody WeGroupSopQuery groupSopDto) {
+        // 校验是否存在
+        WeGroupSop oldWeGroupSop = iWeGroupSopService.getById(ruleId);
+
+        if (null == oldWeGroupSop) {
+            return AjaxResult.error(HttpStatus.NOT_FOUND, "该群SOP规则不存在");
+        }
+        WeGroupSop weGroupSop = new WeGroupSop();
+        weGroupSop.setRuleId(ruleId);
+        BeanUtils.copyProperties(groupSopDto, weGroupSop);
+
+        // 校验规则名是否可用
+        if(!oldWeGroupSop.getRuleName().equals(groupSopDto.getRuleName())){
+            if (iWeGroupSopService.isNameOccupied(weGroupSop.getRuleName())) {
+                return AjaxResult.error(HttpStatus.BAD_REQUEST, "规则名称已存在");
+            }
+        }
+
+
+
+        iWeGroupSopService.updateGroupSop(weGroupSop, groupSopDto.getChatIdList(), groupSopDto.getMaterialIdList(), groupSopDto.getPicList());
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 根据id列表批量删除群sop规则
+     *
+     * @param ids 群sop规则列表
+     * @return 结果
+     */
+    @DeleteMapping(path = "/{ids}")
+    public AjaxResult batchDeleteSopRule(@PathVariable("ids") Long[] ids) {
+        iWeGroupSopService.batchRemoveGroupSopByIds(ids);
+        return AjaxResult.success();
+    }
+
+
+
+
+}

+ 121 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCommunityH5Controller.java

@@ -0,0 +1,121 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.MessageNoticeType;
+import com.linkwechat.domain.community.WeKeywordGroupTask;
+import com.linkwechat.domain.community.vo.WeEmplTaskVo;
+import com.linkwechat.service.IWeCommunityKeywordToGroupService;
+import com.linkwechat.service.IWeGroupSopService;
+import com.linkwechat.service.IWePresTagGroupTaskService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 社区运营H5接口
+ *
+ * @Author Hang
+ * @Date 2021/3/24 10:54
+ */
+@RestController
+@RequestMapping(value = "/community/h5")
+public class WeCommunityH5Controller extends BaseController {
+
+    @Autowired
+    private IWeCommunityKeywordToGroupService keywordToGroupService;
+
+    @Autowired
+    private IWeGroupSopService iWeGroupSopService;
+
+
+    @Autowired
+    private IWePresTagGroupTaskService iWePresTagGroupTaskService;
+
+
+    /**
+     * 获取任务对应的执行人列表
+     * @param taskId 任务id
+     * @param type 任务类型 1:标签建群任务 2:sop任务
+     * @return
+     */
+    @GetMapping("/scope/{taskId}")
+    public AjaxResult getTaskScopeList(@PathVariable("taskId") Long taskId, @RequestParam(value = "type") Integer type) {
+        if (type.equals(MessageNoticeType.TAG.getType())) {
+            return AjaxResult.success(iWePresTagGroupTaskService.getScopeListByTaskId(taskId));
+        } else {
+            return AjaxResult.success(iWeGroupSopService.getScopeListByRuleId(taskId));
+        }
+    }
+
+
+    /**
+     * h5页面根据员工id获取老客标签建群和群sop任务信息
+     *
+     * @param emplId 员工id
+     * @param type   数据类型,0:全部数据 1:老客标签建群数据 2:群SOP数据
+     * @return
+     */
+    @GetMapping("/{emplId}")
+    public AjaxResult<WeEmplTaskVo> getEmplTask(@PathVariable("emplId") String emplId, @RequestParam(value = "type") Integer type) {
+        WeEmplTaskVo weEmplTaskVo=new WeEmplTaskVo();
+        if (type.equals(MessageNoticeType.TAG.getType())) {
+            // 老客标签建群数据
+            weEmplTaskVo.setDone(iWePresTagGroupTaskService.getFollowerTaskList(emplId, 1));
+            weEmplTaskVo.setTodo(iWePresTagGroupTaskService.getFollowerTaskList(emplId, 0));
+        } else if (type.equals(MessageNoticeType.SOP.getType())) {
+            // 群SOP数据
+            weEmplTaskVo.setTodo(iWeGroupSopService.getEmplTaskList(emplId, false));
+            weEmplTaskVo.setDone(iWeGroupSopService.getEmplTaskList(emplId, true));
+        } else {
+            // 全部数据
+            List todoList = new ArrayList();
+            List doneList = new ArrayList();
+            todoList.addAll(iWePresTagGroupTaskService.getFollowerTaskList(emplId, 0));
+            todoList.addAll(iWeGroupSopService.getEmplTaskList(emplId, false));
+            weEmplTaskVo.setTodo(todoList);
+            doneList.addAll(iWePresTagGroupTaskService.getFollowerTaskList(emplId, 1));
+            doneList.addAll(iWeGroupSopService.getEmplTaskList(emplId, true));
+            weEmplTaskVo.setDone(doneList);
+        }
+        return AjaxResult.success(
+                weEmplTaskVo
+        );
+    }
+
+    /**
+     * 员工发送老客标签建群任务信息或者发送sop到其客户群之后,变更其任务状态
+     *
+     * @param taskId 老客标签建群时代表任务id,sop时,代表规则id
+     * @param emplId 老客标签建群时代表员工id,sop时,代表群主
+     * @param type   类型 0:老客标签建群 1:sop
+     * @return 结果
+     */
+    @GetMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestParam("taskId") Long taskId, @RequestParam("emplId") String emplId, @RequestParam("type") Integer type) {
+        if (type.equals(0)) {
+
+            return toAjax(iWePresTagGroupTaskService.updateFollowerTaskStatus(taskId, emplId));
+        } else {
+            return toAjax(iWeGroupSopService.updateChatSopStatus(taskId, emplId));
+        }
+    }
+
+
+    /**
+     * 用于支持H5页面的名称和关键字检索
+     *
+     * @param word 过滤字符
+     * @return 结果
+     */
+    @GetMapping(path = "/filter")
+    public TableDataInfo filter(@RequestParam("taskNameOrKeys") String word) {
+        startPage();
+        List<WeKeywordGroupTask> taskList = keywordToGroupService.filterByNameOrKeyword(word);
+        return getDataTable(taskList);
+    }
+
+}

+ 100 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCommunityKeywordGroupController.java

@@ -0,0 +1,100 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.domain.community.WeKeywordGroupTask;
+import com.linkwechat.service.IWeCommunityKeywordToGroupService;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import java.util.Arrays;
+import java.util.List;
+
+
+/**
+ * 关键词拉群
+ */
+@RestController
+@RequestMapping(value = "/communityKeywordGroup")
+public class WeCommunityKeywordGroupController extends BaseController {
+
+    @Autowired
+    private IWeCommunityKeywordToGroupService keywordToGroupService;
+
+    /**
+     * 添加新任务
+     *
+     * @param task 添加任务所需的数据
+     * @return 结果
+     */
+    @PostMapping(path = "/")
+    public AjaxResult addTask(@RequestBody @Validated WeKeywordGroupTask task) {
+        if (keywordToGroupService.isNameOccupied(task.getTaskName())) {
+            return AjaxResult.error("关键词拉群任务名称"+ task.getTaskName() +"已存在");
+        }
+        keywordToGroupService.save(task);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 根据id及更新数据对指定任务进行更新
+     */
+    @PutMapping("/{taskId}")
+    public AjaxResult updateTask(@PathVariable("taskId") Long taskId, @RequestBody @Validated WeKeywordGroupTask task) {
+        task.setTaskId(taskId);
+        WeKeywordGroupTask oldGroupTask = keywordToGroupService.getById(task.getTaskId());
+        if(null != oldGroupTask){
+            if(!oldGroupTask.getTaskName().equals(task.getTaskName())){
+                if (keywordToGroupService.isNameOccupied(task.getTaskName())) {
+                    return AjaxResult.error("关键词拉群任务名称"+ task.getTaskName() +"已存在");
+                }
+            }
+        }
+        keywordToGroupService.updateById(task);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 通过id列表批量删除任务
+     *
+     * @param ids id列表
+     * @return 结果
+     */
+    @DeleteMapping(path = "/{ids}")
+    public AjaxResult batchDeleteTask(@PathVariable("ids") Long[] ids) {
+        keywordToGroupService.removeByIds(Arrays.asList(ids));
+        return AjaxResult.success();
+    }
+
+
+
+    /**
+     * 根据过滤条件获取关键词拉群任务列表
+     */
+    @ApiOperation(value = "获取关键词拉群任务列表")
+    @GetMapping(path = "/list")
+    public TableDataInfo<List<WeKeywordGroupTask>> list(WeKeywordGroupTask task) {
+        startPage();
+        List<WeKeywordGroupTask> taskList = keywordToGroupService.getTaskList(task);
+        return getDataTable(taskList);
+    }
+
+
+    /**
+     * 根据id获取任务详情
+     *
+     * @param taskId 任务id
+     * @return 任务详情
+     */
+    @GetMapping(path = "/{taskId}")
+    public AjaxResult<WeKeywordGroupTask> getTask(@ApiParam("任务id") @PathVariable("taskId") Long taskId) {
+        return AjaxResult.success(keywordToGroupService.getTaskById(taskId));
+    }
+
+
+
+
+}

+ 153 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCommunityNewGroupController.java

@@ -0,0 +1,153 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.collection.ListUtil;
+import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.constant.HttpStatus;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.utils.file.FileUtils;
+import com.linkwechat.domain.community.WeCommunityNewGroup;
+import com.linkwechat.domain.community.WeEmpleCode;
+import com.linkwechat.domain.community.query.WeCommunityNewGroupQuery;
+import com.linkwechat.domain.community.vo.WeCommunityNewGroupVo;
+import com.linkwechat.service.IWeCommunityNewGroupService;
+import com.linkwechat.service.IWeEmpleCodeService;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+
+/**
+ * 社区运营新客自动拉群
+ */
+@RestController
+@RequestMapping(value = "/communityNewGroup")
+public class WeCommunityNewGroupController extends BaseController {
+
+    @Autowired
+    private IWeCommunityNewGroupService iWeCommunityNewGroupService;
+
+
+    @Autowired
+    private IWeEmpleCodeService iWeEmpleCodeService;
+
+
+
+    /**
+     * 新增新客自动拉群
+     */
+    @PostMapping("/")
+    public AjaxResult add(@RequestBody @Validated WeCommunityNewGroupQuery weCommunityNewGroupQuery) {
+
+        iWeCommunityNewGroupService.add(weCommunityNewGroupQuery);
+        return AjaxResult.success();
+    }
+
+
+
+    /**
+     * 单个下载
+     *z
+     * @param id       待下载员工活码
+     * @param response 响应
+     */
+    @Log(title = "员工活码下载", businessType = BusinessType.OTHER)
+    @GetMapping("/download")
+    public void download(String id, HttpServletResponse response) throws IOException {
+        WeCommunityNewGroup weCommunityNewGroup = iWeCommunityNewGroupService.getById(id);
+
+        if(null != weCommunityNewGroup){
+            WeEmpleCode empleCode = iWeEmpleCodeService.selectWeEmpleCodeById(weCommunityNewGroup.getEmplCodeId());
+            FileUtils.downloadFile(empleCode.getQrCode(), response.getOutputStream());
+        }
+    }
+
+
+    /**
+     * 员工活码批量下载
+     *
+     * @param ids      新客自动拉群ids
+     * @param response 输出
+     */
+    @Log(title = "员工活码批量下载", businessType = BusinessType.OTHER)
+    @GetMapping("/downloadBatch")
+    public void downloadBatch(Long[] ids, HttpServletResponse response) throws IOException {
+        List<WeCommunityNewGroup> weCommunityNewGroups = iWeCommunityNewGroupService.listByIds(ListUtil.toList(ids));
+
+        if(CollectionUtil.isNotEmpty(weCommunityNewGroups)){
+
+            List<FileUtils.FileEntity> fileList=new ArrayList<>();
+
+            weCommunityNewGroups.stream().forEach(k->{
+                WeEmpleCode weEmpleCode = iWeEmpleCodeService.getById(k.getEmplCodeId());
+                fileList.add(
+                        FileUtils.FileEntity.builder()
+                                .fileName(weEmpleCode.getScenario())
+                                .url(weEmpleCode.getQrCode())
+                                .suffix(".jpg")
+                                .build()
+                );
+            });
+            FileUtils.batchDownloadFile(fileList, response.getOutputStream());
+
+        }
+    }
+
+    /**
+     * 查询新客自动拉群列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo<List<WeCommunityNewGroupVo>> list(WeCommunityNewGroup weCommunityNewGroup) {
+        startPage();
+        List<WeCommunityNewGroupVo> communityNewGroupVos = iWeCommunityNewGroupService.selectWeCommunityNewGroupList(weCommunityNewGroup);
+        return getDataTable(communityNewGroupVos);
+    }
+
+
+    /**
+     * 获取新客自动拉群详细信息
+     */
+    @GetMapping(value = "/{id}")
+    public AjaxResult<WeCommunityNewGroupVo> getInfo(@PathVariable("id") String id) {
+        List<WeCommunityNewGroupVo> communityNewGroupVos = iWeCommunityNewGroupService.selectWeCommunityNewGroupList(WeCommunityNewGroup.builder()
+                        .id(Long.valueOf(id))
+                .build());
+
+        return AjaxResult.success(communityNewGroupVos.stream().findFirst().get());
+    }
+
+    /**
+     * 修改新客自动拉群
+     */
+    @Log(title = "新客自动拉群", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable("id") String id,@RequestBody @Validated WeCommunityNewGroupQuery weCommunityNewGroupQuery) {
+        weCommunityNewGroupQuery.setId(Long.valueOf(id));
+        iWeCommunityNewGroupService.updateWeCommunityNewGroup(weCommunityNewGroupQuery);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 删除新客自动拉群
+     */
+    @Log(title = "新客自动拉群", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        iWeCommunityNewGroupService.removeByIds(ListUtil.toList(ids));
+
+        return AjaxResult.success();
+    }
+
+}

+ 123 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCommunityPresTagGroupController.java

@@ -0,0 +1,123 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.constant.HttpStatus;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.domain.taggroup.WePresTagGroupTask;
+import com.linkwechat.domain.taggroup.WePresTagGroupTaskStat;
+import com.linkwechat.domain.taggroup.vo.WePresTagGroupTaskVo;
+import com.linkwechat.domain.taggroup.vo.WePresTagTaskListVo;
+import com.linkwechat.service.IWePresTagGroupTaskService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+import java.util.List;
+
+@Api(tags = "老客标签建群接口")
+@RestController
+@RequestMapping(value = "/communityPresTagGroup")
+public class WeCommunityPresTagGroupController extends BaseController {
+
+    @Autowired
+    private IWePresTagGroupTaskService taskService;
+
+
+    /**
+     * 获取老客标签建群列表数据
+     */
+    //  @PreAuthorize("@ss.hasPermi('wecom:communitytagGroup:list')")
+    @GetMapping(path = "/list")
+    @ApiOperation(value = "获取老客标签建群任务分页数据", httpMethod = "GET")
+    public TableDataInfo<List<WePresTagTaskListVo>> getPage(
+            @RequestParam(value = "taskName", required = false) String taskName,
+            @RequestParam(value = "sendType", required = false) Integer sendType,
+            @RequestParam(value = "createBy", required = false) String createBy,
+            @RequestParam(value = "beginTime", required = false) String beginTime,
+            @RequestParam(value = "endTime", required = false) String endTime) {
+        startPage();
+        List<WePresTagTaskListVo> result = taskService.selectTaskList(taskName, sendType, createBy, beginTime, endTime);
+        return getDataTable(result);
+    }
+
+    /**
+     * 新建老客标签建群任务
+     */
+    //   @PreAuthorize("@ss.hasPermi('wecom:communitytagGroup:add')")
+    @PostMapping
+    @ApiOperation(value = "新建老客标签建群任务", httpMethod = "POST")
+    public AjaxResult add(@RequestBody @Validated WePresTagGroupTask task) {
+        // 检测任务名是否可用
+        if (taskService.isNameOccupied(task)) {
+            return AjaxResult.error("任务名已存在");
+        }
+         taskService.add(task);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 根据获取任务详细信息
+     */
+    //  @PreAuthorize("@ss.hasPermi('wecom:communitytagGroup:query')")
+    @GetMapping(path = "/{id}")
+    @ApiOperation(value = "根据获取任务详细信息", httpMethod = "GET")
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
+        WePresTagGroupTaskVo taskVo = taskService.getTaskById(id);
+        if (StringUtils.isNull(taskVo)) {
+            return AjaxResult.error(HttpStatus.NOT_FOUND, "群活码不存在");
+        }
+        return AjaxResult.success(taskVo);
+    }
+
+    /**
+     * 更新任务信息
+     */
+    //    @PreAuthorize("@ss.hasPermi('wecom:communitytagGroup:edit')")
+    @PutMapping(path = "/{id}")
+    @ApiOperation(value = "更新任务信息", httpMethod = "PUT")
+    public AjaxResult update(@PathVariable("id") Long id, @RequestBody @Validated WePresTagGroupTask task) {
+
+        try {
+            // 保存新任务
+            task.setTaskId(id);
+            task.setUpdateTime(new Date());
+            taskService.updateTaskAndSendMsg(task);
+        } catch (Exception e) {
+            return AjaxResult.error(e.getMessage());
+        }
+
+        return AjaxResult.success();
+    }
+
+    /**
+     * 批量删除老客标签建群任务
+     */
+    //   @PreAuthorize("@ss.hasPermi('wecom:communitytagGroup:remove')")
+    @DeleteMapping(path = "/{ids}")
+    @ApiOperation(value = "批量删除老客标签建群任务", httpMethod = "DELETE")
+    public AjaxResult batchRemove(@PathVariable("ids") Long[] ids) {
+        return AjaxResult.success(taskService.batchRemoveTaskByIds(ids));
+    }
+
+    /**
+     * 根据老客标签建群id及过滤条件,获取其统计信息
+     */
+    @GetMapping(path = "/stat/{id}")
+    public TableDataInfo<List<WePresTagGroupTaskStat>> getStatInfo(
+            @PathVariable("id") Long id,
+            @RequestParam(value = "customerName", required = false) String customerName,
+            @RequestParam(value = "isInGroup", required = false) Integer isInGroup,
+            @RequestParam(value = "isSent", required = false) Integer isSent
+    ) {
+        WePresTagGroupTask task = taskService.getById(id);
+        startPage();
+        List<WePresTagGroupTaskStat> stats = taskService.getTaskStat(id, customerName, isInGroup, isSent,
+                task.getSendType());
+        return getDataTable(stats);
+    }
+}

+ 49 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeConfigController.java

@@ -0,0 +1,49 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.config.LinkWeChatConfig;
+import com.linkwechat.common.config.WeSideBarConfig;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.utils.SecurityUtils;
+import com.linkwechat.domain.WeSideBarVo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.text.MessageFormat;
+
+@RestController
+@RequestMapping(value = "/config")
+public class WeConfigController {
+
+    @Autowired
+    LinkWeChatConfig linkWeChatConfig;
+
+
+    /**
+     * 获取侧边栏相关配置
+     * @return
+     */
+    @GetMapping("/findWeSideBar")
+    public AjaxResult<WeSideBarVo> findWeSideBar(){
+
+        WeSideBarConfig weSideBarConfig = linkWeChatConfig.getWeSideBarConfig();
+
+        return AjaxResult.success(
+                WeSideBarVo.builder()
+                        .materialName(weSideBarConfig.getMaterial().getMaterialName())
+                        .materialUrl(weSideBarConfig.getMaterial().getMaterialUrl())
+                        .wordGroupName(weSideBarConfig.getWordGroup().getWordGroupName())
+                        .wordGroupUrl(weSideBarConfig.getWordGroup().getWordGroupUrl())
+                        .customerPortraitName(weSideBarConfig.getCustomerPortrait().getCustomerPortraitName())
+                        .customerPortraitUrl(weSideBarConfig.getCustomerPortrait().getCustomerPortraitUrl())
+                        .redEnvelopesName(weSideBarConfig.getRedEnvelopes().getRedEnvelopesName())
+                        .redEnvelopesUrl(weSideBarConfig.getRedEnvelopes().getRedEnvelopesUrl())
+                        .build()
+        );
+
+
+    }
+
+
+}

+ 55 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeContentActionController.java

@@ -0,0 +1,55 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.domain.material.query.WeContentSendRecordQuery;
+import com.linkwechat.service.IWeContentSendRecordService;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+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;
+import javax.annotation.Resource;
+
+
+/**
+ * 素材发送和查看行为
+ *
+ * @author WangYX
+ * @version 1.0.0
+ * @date 2022/10/27 10:34
+ */
+@Slf4j
+@RestController
+@RequestMapping("/material/action")
+public class WeContentActionController {
+
+
+
+    @Resource
+    private IWeContentSendRecordService weContentSendRecordService;
+
+    @ApiOperation("保存单个素材发送详情")
+    @PostMapping("/addSend")
+    public AjaxResult addSend(@RequestBody WeContentSendRecordQuery weContentSendRecordQuery) {
+        log.info("保存单个素材发送详情:素材Id{},发送者:{},素材类型:{}",
+                weContentSendRecordQuery.getContentId(),
+                weContentSendRecordQuery.getSendById(),
+                weContentSendRecordQuery.getResourceType());
+        weContentSendRecordService.singleSend(weContentSendRecordQuery);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation("保存一键发送详情")
+    @PostMapping("/addAllSend")
+    public AjaxResult addAllSend(@RequestBody WeContentSendRecordQuery weContentSendRecordQuery) {
+        log.info("保存一键发送发送详情:会话Id{},发送者:{},素材类型:{}",
+                weContentSendRecordQuery.getTalkId(),
+                weContentSendRecordQuery.getSendById(),
+                weContentSendRecordQuery.getResourceType());
+        weContentSendRecordService.withOneTouchSend(weContentSendRecordQuery);
+        return AjaxResult.success();
+    }
+
+
+}

+ 161 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeContentTalkController.java

@@ -0,0 +1,161 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.domain.material.dto.WeContentSendViewDto;
+import com.linkwechat.domain.material.entity.*;
+import com.linkwechat.domain.material.query.ContentDetailQuery;
+import com.linkwechat.domain.material.vo.WeMaterialAnalyseVo;
+import com.linkwechat.domain.material.vo.talk.WeContentTalkVo;
+import com.linkwechat.service.*;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.util.Arrays;
+import java.util.List;
+
+
+@RestController
+@RequestMapping("/content/talk")
+public class WeContentTalkController extends BaseController {
+
+    @Resource
+    private IWeContentTalkService weContentTalkService;
+
+    @Resource
+    private IWeMaterialService iWeMaterialService;
+
+    @Resource
+    private IWeContentSendRecordService weContentSendRecordService;
+
+    @Resource
+    private IWeCategoryService weCategoryService;
+
+
+
+
+
+    @PostMapping
+    @ApiOperation("保存话术")
+    public AjaxResult saveContentTalk(@RequestBody WeContentTalk weContentTalk) {
+        weContentTalkService.saveContentTalk(weContentTalk);
+        return AjaxResult.success("保存成功");
+    }
+
+    @GetMapping("/selectPage")
+    @ApiOperation("分页查询")
+    public TableDataInfo selectContentTalkPage(WeContentTalk weContentTalk) {
+
+        //查询查看和发送次数
+        //2企业话术 3客服话术
+        Integer resourceType = weContentTalk.getTalkType() == 0 ? 2 : 3;
+        List<WeContentSendViewDto> weContentSendViewDtoList = weContentSendRecordService.getSendViewDataTotal(resourceType);
+
+        //查询话术列表
+        startPage();
+        List<WeContentTalkVo> weContentTalkVoList = weContentTalkService.selectWeContentVoList(weContentTalk);
+
+        for (WeContentTalkVo weContentTalkVo : weContentTalkVoList) {
+            String materialIds = weContentTalkVo.getMaterialIds();
+            if (ObjectUtil.isNotEmpty(materialIds)) {
+                List<String> list = Arrays.asList(materialIds.split(","));
+                weContentTalkVo.setMaterialNum(list.size());
+                for (WeContentSendViewDto weContentSendViewDto : weContentSendViewDtoList) {
+//                    Long contentId = weContentSendViewDto.getContentId();
+                    if (weContentTalkVo.getId().equals(weContentSendViewDto.getTalkId())) {
+                    //if (list.contains(contentId.toString()) && weContentTalkVo.getId().equals(weContentSendViewDto.getTalkId())) {
+
+                        Integer sendTotalNum = weContentTalkVo.getSendTotalNum();
+                        Integer viewTotalNum = weContentTalkVo.getViewTotalNum();
+                        Integer viewByTotalNum = weContentTalkVo.getViewByTotalNum();
+
+                        weContentTalkVo.setSendTotalNum((int) (sendTotalNum + weContentSendViewDto.getSendTotalNum()));
+                        weContentTalkVo.setViewTotalNum((int) (viewTotalNum + weContentSendViewDto.getViewTotalNum()));
+                        weContentTalkVo.setViewByTotalNum((int) (viewByTotalNum + weContentSendViewDto.getViewByTotalNum()));
+                    }
+                }
+            }
+        }
+        return getDataTable(weContentTalkVoList);
+    }
+
+    @ApiOperation("批量删除")
+    @DeleteMapping("/del/{talkIds}")
+    public AjaxResult del(@PathVariable List<Long> talkIds) {
+        weContentTalkService.del(talkIds);
+        return AjaxResult.success();
+    }
+
+    @GetMapping("/detail/{talkId}")
+    @ApiOperation("话术详情")
+    public AjaxResult getDetailByTalkId(@PathVariable Long talkId) {
+        WeContentTalk weContentTalk = weContentTalkService.getOne(new LambdaQueryWrapper<WeContentTalk>().eq(WeContentTalk::getId, talkId).eq(WeContentTalk::getDelFlag, 0));
+        List<WeMaterial> materials = iWeMaterialService.getWeMaterialListByTalkId(talkId);
+        Long categoryId = weContentTalk.getCategoryId();
+        if (categoryId == 1L) {
+            weContentTalk.setCategoryName("默认分组");
+        } else {
+            WeCategory weCategory = weCategoryService.getById(categoryId);
+            if (ObjectUtil.isNotEmpty(weCategory)) {
+                weContentTalk.setCategoryName(weCategory.getName());
+            }
+        }
+        weContentTalk.setWeMaterialList(materials);
+        return AjaxResult.success(weContentTalk);
+    }
+
+    @GetMapping("/detail/getTalkMaterial")
+    @ApiOperation("话术素材明细")
+    public AjaxResult getTalkMaterial(ContentDetailQuery contentDetailQuery) {
+        List<WeMaterialAnalyseVo> weMaterialAnalyseVos = iWeMaterialService.selectMaterialsByTalkId(contentDetailQuery);
+//        //TODO 代码优化
+//        //获取话术关联的素材
+//        LambdaQueryWrapper<WeTalkMaterial> queryWrapper = new LambdaQueryWrapper<>();
+//        queryWrapper.eq(WeTalkMaterial::getTalkId, contentDetailQuery.getTalkId());
+//        List<WeTalkMaterial> list = weTalkMaterialService.list(queryWrapper);
+//
+//        List<WeMaterialAnalyseVo> weMaterialAnalyseVos = new ArrayList<>();
+//
+//        if (list != null && list.size() > 0) {
+//            //遍历列表
+//            for (WeTalkMaterial weTalkMaterial : list) {
+//                QueryWrapper<WeMaterial> weMaterialQueryWrapper = new QueryWrapper<>();
+//                weMaterialQueryWrapper.lambda().select(WeMaterial::getId,WeMaterial::getMaterialName,WeMaterial::getMaterialUrl,WeMaterial::getMediaType);
+//                weMaterialQueryWrapper.lambda().eq(WeMaterial::getId, weTalkMaterial.getMaterialId());
+//                WeMaterial weMaterial = weMaterialService.getOne(weMaterialQueryWrapper);
+//                WeMaterialAnalyseVo weMaterialAnalyseVo = BeanUtil.copyProperties(weMaterial, WeMaterialAnalyseVo.class);
+//
+//                LambdaQueryWrapper<WeContentSendRecord> sendRecordLambdaQueryWrapper = new LambdaQueryWrapper<>();
+//                sendRecordLambdaQueryWrapper.eq(WeContentSendRecord::getTalkId, contentDetailQuery.getTalkId());
+//                sendRecordLambdaQueryWrapper.eq(WeContentSendRecord::getContentId, weTalkMaterial.getMaterialId());
+//                sendRecordLambdaQueryWrapper.apply(StringUtils.isNotBlank(contentDetailQuery.getBeginTime()), "DATE_FORMAT(send_time,'%Y-%m-%d')>=DATE_FORMAT('" + contentDetailQuery.getBeginTime() + "','%Y-%m-%d')");
+//                sendRecordLambdaQueryWrapper.apply(StringUtils.isNotBlank(contentDetailQuery.getEndTime()), "DATE_FORMAT(send_time,'%Y-%m-%d')<=DATE_FORMAT('" + contentDetailQuery.getEndTime() + "','%Y-%m-%d')");
+//                int count = weContentSendRecordService.count(sendRecordLambdaQueryWrapper);
+//                //发送总次数
+//                weMaterialAnalyseVo.setSendTotalNum(count);
+//
+//                LambdaQueryWrapper<WeContentViewRecord> viewRecordLambdaQueryWrapper = new LambdaQueryWrapper<>();
+//                viewRecordLambdaQueryWrapper.eq(WeContentViewRecord::getTalkId, contentDetailQuery.getTalkId());
+//                viewRecordLambdaQueryWrapper.eq(WeContentViewRecord::getContentId, weTalkMaterial.getMaterialId());
+//                viewRecordLambdaQueryWrapper.apply(StringUtils.isNotBlank(contentDetailQuery.getBeginTime()), "DATE_FORMAT(view_time,'%Y-%m-%d')>=DATE_FORMAT('" + contentDetailQuery.getBeginTime() + "','%Y-%m-%d')");
+//                viewRecordLambdaQueryWrapper.apply(StringUtils.isNotBlank(contentDetailQuery.getEndTime()), "DATE_FORMAT(view_time,'%Y-%m-%d')<=DATE_FORMAT('" + contentDetailQuery.getEndTime() + "','%Y-%m-%d')");
+//                List<WeContentViewRecord> viewRecords = weContentViewRecordService.list(viewRecordLambdaQueryWrapper);
+//                if (viewRecords != null && viewRecords.size() > 0) {
+//                    //查看总次数
+//                    weMaterialAnalyseVo.setViewTotalNum(viewRecords.size());
+//                    //查看总人数
+//                    Set<String> collect = viewRecords.stream().map(o -> o.getViewOpenid()).collect(Collectors.toSet());
+//                    weMaterialAnalyseVo.setViewByTotalNum(collect.size());
+//                } else {
+//                    weMaterialAnalyseVo.setViewTotalNum(0);
+//                    weMaterialAnalyseVo.setViewByTotalNum(0);
+//                }
+//                weMaterialAnalyseVos.add(weMaterialAnalyseVo);
+//            }
+//        }
+        return AjaxResult.success(weMaterialAnalyseVos);
+    }
+}

+ 82 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCorpAccountController.java

@@ -0,0 +1,82 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.config.LinkWeChatConfig;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.utils.SecurityUtils;
+import com.linkwechat.domain.WeCorpAccount;
+import com.linkwechat.fegin.QwCorpClient;
+import com.linkwechat.service.IWeCorpAccountService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+
+@RestController
+@RequestMapping(value = "/corp")
+public class WeCorpAccountController extends BaseController {
+
+    @Autowired
+    private IWeCorpAccountService iWeCorpAccountService;
+
+    @Autowired
+    private LinkWeChatConfig linkWeChatConfig;
+
+    @Autowired
+    private QwCorpClient qwCorpClient;
+
+    /**
+     * 获取当前租户信息
+     *
+     * @return
+     */
+    @GetMapping("/findCurrentCorpAccount")
+    public AjaxResult<WeCorpAccount> findCurrentCorpAccount() {
+        WeCorpAccount corpAccount = iWeCorpAccountService.getCorpAccountByCorpId(null);
+        return AjaxResult.success(corpAccount);
+    }
+
+
+    /**
+     * 新增或更新企业配置相关
+     *
+     * @param weCorpAccount
+     * @return
+     */
+    @PostMapping("/addOrUpdate")
+    public AjaxResult addOrUpdate(@RequestBody WeCorpAccount weCorpAccount) {
+        if(linkWeChatConfig.isDemoEnviron()){
+            return AjaxResult.error("当前为演示环境,无法修改配置");
+
+        }
+        if(iWeCorpAccountService.saveOrUpdate(weCorpAccount)){
+            qwCorpClient.removeAllWeAccessToken(weCorpAccount.getCorpId());
+        };
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 客户流失通知开关
+     *
+     * @param status
+     * @return
+     */
+    @PutMapping("/startCustomerChurnNoticeSwitch/{status}")
+    public AjaxResult startCustomerChurnNoticeSwitch(@PathVariable String status) {
+        iWeCorpAccountService.startCustomerChurnNoticeSwitch(status);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 客户流失通知开关查询
+     *
+     * @return
+     */
+    @GetMapping("/getCustomerChurnNoticeSwitch")
+    public AjaxResult getCustomerChurnNoticeSwitch() {
+        return AjaxResult.success("操作成功",iWeCorpAccountService.getCustomerChurnNoticeSwitch());
+    }
+
+
+}

+ 311 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCustomerController.java

@@ -0,0 +1,311 @@
+package com.linkwechat.controller;
+
+
+import cn.hutool.core.collection.ListUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.annotation.PhoneEncryptMethod;
+import com.linkwechat.common.constant.Constants;
+import com.linkwechat.common.constant.SynchRecordConstants;
+import com.linkwechat.common.constant.WeConstans;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.core.page.TableSupport;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.exception.CustomException;
+import com.linkwechat.common.utils.ServletUtils;
+import com.linkwechat.common.utils.poi.LwExcelUtil;
+import com.linkwechat.domain.WeCustomer;
+import com.linkwechat.domain.WeCustomerSeas;
+import com.linkwechat.domain.customer.WeBacthMakeCustomerTag;
+import com.linkwechat.domain.customer.WeMakeCustomerTag;
+import com.linkwechat.domain.customer.query.WeCustomersQuery;
+import com.linkwechat.domain.customer.query.WeOnTheJobCustomerQuery;
+import com.linkwechat.domain.customer.vo.WeCustomerDetailInfoVo;
+import com.linkwechat.domain.customer.vo.WeCustomersVo;
+import com.linkwechat.service.IWeCustomerService;
+import com.linkwechat.service.IWeSynchRecordService;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 企业微信客户Controller
+ *
+ * @author ruoyi
+ * @date 2020-09-13
+ */
+@Slf4j
+@RestController
+@RequestMapping("/customer")
+public class WeCustomerController extends BaseController {
+
+
+    @Autowired
+    private IWeCustomerService weCustomerService;
+
+    @Autowired
+    private IWeSynchRecordService iWeSynchRecordService;
+
+
+    /**
+     * 查询企业微信客户列表(分页),索引优化客户列表,未使用默认分页
+     */
+    @GetMapping("/findWeCustomerList")
+    public TableDataInfo findWeCustomerList(WeCustomersQuery weCustomersQuery)
+    {
+        weCustomersQuery.setDelFlag(null == weCustomersQuery.getDelFlag() ? Constants.COMMON_STATE : weCustomersQuery.getDelFlag());
+        List<WeCustomersVo> list = weCustomerService.findWeCustomerList(weCustomersQuery, TableSupport.buildPageRequest());
+        TableDataInfo dataTable = getDataTable(list);
+        //设置总条数
+        dataTable.setTotal(
+                weCustomerService.countWeCustomerList(weCustomersQuery)
+        );
+        dataTable.setLastSyncTime(
+                iWeSynchRecordService.findUpdateLatestTime(SynchRecordConstants.SYNCH_CUSTOMER)
+        );//最近同步时间
+        dataTable.setNoRepeatCustomerTotal(
+                weCustomerService.noRepeatCountCustomer(weCustomersQuery)
+        );//去重客户数
+
+        return dataTable;
+    }
+
+    /**
+     * 导出企业客户
+     */
+    @GetMapping("/exprotWeCustomerList")
+    public void exprotWeCustomerList(WeCustomersQuery weCustomersQuery)
+    {
+        weCustomersQuery.setDelFlag(null == weCustomersQuery.getDelFlag() ? Constants.COMMON_STATE : weCustomersQuery.getDelFlag());
+        List<WeCustomersVo> list = weCustomerService.findWeCustomerList(weCustomersQuery, null);
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeCustomersVo .class, list,"企业客户"
+        );
+
+    }
+
+
+    /**
+     * 移动应用客户列表(分页)
+     * @param weCustomersQuery
+     * @return
+     */
+    @GetMapping("/findWeCustomerListByApp")
+    public TableDataInfo<List<WeCustomersVo>> findWeCustomerListByApp(WeCustomersQuery weCustomersQuery){
+
+        return weCustomerService.findWeCustomerListByApp(
+                weCustomersQuery,TableSupport.buildPageRequest()
+        );
+    }
+
+
+
+
+    /**
+     * 查询企业微信客户列表(不分页)
+     */
+    @GetMapping("/findAllWeCustomerList")
+    public AjaxResult findAllWeCustomerList(WeCustomersQuery weCustomersQuery)
+    {
+        weCustomersQuery.setDelFlag(null == weCustomersQuery.getDelFlag() ? Constants.COMMON_STATE : weCustomersQuery.getDelFlag());
+        return AjaxResult.success(
+                weCustomerService.findWeCustomerList(weCustomersQuery,null)
+        );
+    }
+
+
+    /**
+     * 客户同步接口
+     * @return
+     */
+    @Log(title = "企业微信客户同步接口", businessType = BusinessType.DELETE)
+    @GetMapping("/synchWeCustomer")
+    public AjaxResult synchWeCustomer() {
+        try {
+            weCustomerService.synchWeCustomer();
+        }catch (CustomException e){
+            log.error("客户同步失败:ex:{}",e.getMessage(),e);
+            return AjaxResult.error(e.getMessage());
+        }
+
+
+        return AjaxResult.success(WeConstans.SYNCH_TIP);
+
+    }
+
+
+    /**
+     * 客户打标签
+     * @param weMakeCustomerTag
+     * @return
+     */
+    @Log(title = "客户打标签", businessType = BusinessType.UPDATE)
+    @PostMapping("/makeLabel")
+    public AjaxResult makeLabel(@RequestBody WeMakeCustomerTag weMakeCustomerTag){
+
+        weCustomerService.makeLabel(weMakeCustomerTag);
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 在职继承
+     * @param weOnTheJobCustomerQuery
+     * @return
+     */
+    @Log(title="在职继承",businessType = BusinessType.UPDATE)
+    @PostMapping("/jobExtends")
+    public AjaxResult jobExtends(@RequestBody WeOnTheJobCustomerQuery weOnTheJobCustomerQuery){
+
+        try {
+            weCustomerService.allocateOnTheJobCustomer(
+                    weOnTheJobCustomerQuery
+            );
+        }catch (Exception e){
+            e.printStackTrace();
+            return AjaxResult.error(e.getMessage());
+        }
+
+
+        return AjaxResult.success();
+    }
+
+
+    /**********************************************************************************
+     **************************************客户详情*************************************
+     *********************************************************************************/
+
+
+    /**
+     *  客户详情基础(基础信息+社交关系)
+     * @param externalUserid
+     * @param userId
+     * @param
+     * @return
+     */
+    @GetMapping("/findWeCustomerBaseInfo")
+    public AjaxResult<WeCustomerDetailInfoVo> findWeCustomerBaseInfo(String externalUserid, String userId, @RequestParam(defaultValue = "0") Integer delFlag){
+
+        return AjaxResult.success(
+                weCustomerService.findWeCustomerDetail(externalUserid,userId,null)
+        );
+    }
+
+
+
+    /**
+     * 客户画像汇总
+     * @param externalUserid
+     * @return
+     */
+    @GetMapping("/findWeCustomerInfoSummary")
+    public AjaxResult<WeCustomerDetailInfoVo> findWeCustomerInfoSummary(String externalUserid,@RequestParam(defaultValue = "0") Integer delFlag){
+
+
+        return AjaxResult.success(
+                weCustomerService.findWeCustomerInfoSummary(externalUserid,null,null)
+        );
+    }
+
+
+
+    /**
+     * 单个跟进人客户
+     * @param externalUserid
+     * @param weUserId
+     * @return
+     */
+    @GetMapping("/findWeCustomerInfoByUserId")
+    public AjaxResult findWeCustomerInfoByUserId(String externalUserid,String weUserId,@RequestParam(defaultValue = "0") Integer delFlag){
+
+
+
+        return AjaxResult.success(
+                weCustomerService.findWeCustomerInfoByUserId(externalUserid,weUserId,null)
+        );
+    }
+
+
+
+//    /**
+//     * 跟进记录
+//     * @param externalUserid
+//     * @param userId
+//     * @return
+//     */
+//    @GetMapping("/followUpRecord")
+//    public TableDataInfo followUpRecord(String externalUserid,String userId){
+//
+//        startPage();
+//
+//        return getDataTable( iWeCustomerTrackRecordService.list(new LambdaQueryWrapper<WeCustomerTrackRecord>()
+//                .eq(WeCustomerTrackRecord::getExternalUserid,externalUserid)
+//                .eq(WeCustomerTrackRecord::getWeUserId,userId)));
+//    }
+
+
+//    /**
+//     * 客户轨迹
+//     * @param externalUserid
+//     * @param weUserId
+//     * @param trajectoryType 轨迹类型:1:信息动态;2:社交动态;3:活动规则;4:待办动态
+//     * @return
+//     */
+//    @GetMapping("/findTrajectory")
+//    public AjaxResult<WeCustomerTrajectory> findTrajectory(String externalUserid,String weUserId,Integer trajectoryType){
+//
+//        List<WeCustomerTrajectory> weCustomerTrajectories = iWeCustomerTrajectoryService
+//                .list(new LambdaQueryWrapper<WeCustomerTrajectory>()
+//                        .eq(StringUtils.isNotEmpty(externalUserid),WeCustomerTrajectory::getExternalUseridOrChatid, externalUserid)
+//                        .eq(StringUtils.isNotEmpty(weUserId),WeCustomerTrajectory::getWeUserId, weUserId)
+//                        .eq(trajectoryType !=null,WeCustomerTrajectory::getTrajectoryType,trajectoryType)
+//                );
+//
+//        return AjaxResult.success(weCustomerTrajectories);
+//    }
+
+
+    @ApiOperation(value = "通过条件校验客户数据", httpMethod = "POST")
+    @PostMapping("/checkByCondition")
+    public AjaxResult checkByCondition(@RequestBody WeCustomersQuery query){
+        log.info("checkByCondition 接口入参:query:{}", JSONObject.toJSONString(query));
+        List<WeCustomer> customerList = weCustomerService.getCustomerListByCondition(query);
+        Set<String> externalUserIdSet = Optional.ofNullable(customerList).orElseGet(ArrayList::new)
+                .stream().map(WeCustomer::getExternalUserid).collect(Collectors.toSet());
+        Set<String> userNum = Optional.ofNullable(customerList).orElseGet(ArrayList::new)
+                .stream().map(WeCustomer::getAddUserId).collect(Collectors.toSet());
+        JSONObject result = new JSONObject();
+        result.put("customerNum",externalUserIdSet.size());
+        result.put("userNum",userNum.size());
+        return AjaxResult.success(result);
+    }
+
+
+    /**
+     * 批量添加或删除客户标签
+     * @param makeCustomerTags
+     * @return
+     */
+    @Log(title = "批量添加或删除客户标签", businessType = BusinessType.UPDATE)
+    @PostMapping("/batchMakeLabel")
+    public AjaxResult batchMakeLabel(@RequestBody WeBacthMakeCustomerTag makeCustomerTags){
+
+        weCustomerService.batchMakeLabel(makeCustomerTags);
+
+        return AjaxResult.success();
+    }
+
+
+
+
+
+}

+ 289 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCustomerPortraitController.java

@@ -0,0 +1,289 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.collection.ListUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.constant.Constants;
+import com.linkwechat.common.constant.WeConstans;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.utils.SnowFlakeUtil;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.domain.*;
+import com.linkwechat.domain.customer.WeMakeCustomerTag;
+import com.linkwechat.domain.customer.vo.WeCustomerAddGroupVo;
+import com.linkwechat.domain.customer.vo.WeCustomerPortraitVo;
+import com.linkwechat.service.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * @description: 客户画像相关controller
+ * @author: HaoN
+ * @create: 2021-03-03 15:10
+ **/
+@RestController
+@RequestMapping("/portrait")
+public class WeCustomerPortraitController extends BaseController{
+
+
+    @Autowired
+    private IWeCustomerService weCustomerService;
+
+    @Autowired
+    private IWeTagService iWeTagService;
+
+    @Autowired
+    private IWeTagGroupService iWeTagGroupService;
+
+    @Autowired
+    private IWeGroupService iWeGroupService;
+
+    @Autowired
+    private IWeCustomerTrajectoryService iWeCustomerTrajectoryService;
+
+    @Autowired
+    private  IWeMomentsService iWeMomentsService;
+
+
+    @Autowired
+    private IWeSysFieldTemplateService iWeSysFieldTemplateService;
+
+
+
+
+
+    /**
+     * 根据客户id和当前企业员工id获取客户详细信息
+     * @param externalUserid
+     * @param userId
+     * @return
+     */
+    @GetMapping(value = "/findWeCustomerInfo")
+    public AjaxResult<WeCustomerPortraitVo> findWeCustomerInfo(String externalUserid, String userId) throws Exception {
+
+        return AjaxResult.success(
+                weCustomerService.findCustomerByOperUseridAndCustomerId(externalUserid,userId)
+        );
+    }
+
+
+    /**
+     * 客户画像资料更新
+     * @param weCustomerPortrait
+     * @return
+     */
+    @PostMapping(value = "/updateWeCustomerInfo")
+    public AjaxResult updateWeCustomerInfo(@RequestBody WeCustomerPortraitVo weCustomerPortrait){
+
+
+
+        weCustomerService.updateWeCustomerPortrait(weCustomerPortrait);
+
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 获取当前系统所有可用标签
+     * @param userId 员工id
+     * @return
+     */
+    @GetMapping(value = "/findAllTags")
+    public AjaxResult findAllTags(Integer groupTagType,String userId){
+
+        if(groupTagType.equals(new Integer(1))){//企业标签
+            return AjaxResult.success(
+                    iWeTagGroupService.selectWeTagGroupList(
+                            WeTagGroup.builder()
+                                    .groupTagType(1)
+                                    .build()
+                    )
+            );
+        }
+        return AjaxResult.success(
+                iWeTagService.list(
+                        new LambdaQueryWrapper<WeTag>()
+                                .eq(WeTag::getDelFlag,new Integer(0))
+                                .eq(WeTag::getOwner,userId)
+                )
+
+        );
+
+    }
+
+
+
+    /**
+     * 客户画像个人标签库新增
+     * @param weTagGroup
+     * @return
+     */
+    @PostMapping("/addOrUpdatePersonTags")
+    public AjaxResult addOrUpdatePersonTags(@RequestBody WeTagGroup weTagGroup){
+        List<WeTag> weTags = weTagGroup.getWeTags();
+        if(CollectionUtil.isNotEmpty(weTags)){
+            weTags.stream().forEach(k->{
+                k.setId(SnowFlakeUtil.nextId());
+                k.setTagId(k.getId().toString());
+                k.setGroupId(weTagGroup.getGroupId());
+                k.setTagType(new Integer(3));
+            });
+            iWeTagService.saveOrUpdateBatch(weTags);
+        }
+        return AjaxResult.success();
+    }
+
+    /**
+     * 个人标签删除
+     * @param ids
+     * @return
+     */
+    @DeleteMapping("/deletePersonTag/{ids}")
+    public AjaxResult deletePersonTag(@PathVariable String[] ids){
+        iWeTagService.removeByIds(CollectionUtil.newArrayList(ids));
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 更新客户画像标签
+     * @param weMakeCustomerTag
+     * @return
+     */
+    @PostMapping(value = "/updateWeCustomerPorTraitTag")
+    public AjaxResult updateWeCustomerPorTraitTag(@RequestBody WeMakeCustomerTag weMakeCustomerTag){
+
+
+        weCustomerService.makeLabel(weMakeCustomerTag);
+
+        return AjaxResult.success();
+    }
+
+
+
+    /**
+     * 查看客户添加的员工
+     * @param externalUserid
+     * @return
+     */
+    @GetMapping(value = "/findAddaddEmployes/{externalUserid}")
+    public AjaxResult findaddEmployes(@PathVariable String externalUserid){
+        return AjaxResult.success(
+                weCustomerService.findWeUserByCustomerId(externalUserid)
+        );
+    }
+
+    /**
+     * 获取用户添加的群
+     * @param externalUserid
+     * @param userId
+     * @return
+     */
+    @GetMapping(value = "/findAddGroupNum")
+    public AjaxResult<WeCustomerAddGroupVo> findAddGroupNum(String externalUserid, String userId){
+
+        return AjaxResult.success(
+                iWeGroupService.findWeGroupByCustomer(userId,externalUserid)
+        );
+    }
+
+
+    /**
+     * 获取轨迹信息
+     * @param trajectoryType
+     * @return
+     */
+    @GetMapping(value = "/findTrajectory")
+    public TableDataInfo findTrajectory(String userId, String externalUserid, Integer trajectoryType){
+        startPage();
+        LambdaQueryWrapper<WeCustomerTrajectory> ne = new LambdaQueryWrapper<WeCustomerTrajectory>()
+                .eq(WeCustomerTrajectory::getWeUserId,userId)
+                .eq(WeCustomerTrajectory::getExternalUseridOrChatid,externalUserid)
+                .orderByDesc(WeCustomerTrajectory::getCreateTime);
+        if(trajectoryType != null){
+            ne.eq(WeCustomerTrajectory::getTrajectoryType, trajectoryType);
+        }
+        return getDataTable(
+                iWeCustomerTrajectoryService.list(ne)
+        );
+
+    }
+
+
+    /**
+     *编辑跟进动态
+     * @param trajectory
+     * @return
+     */
+    @PostMapping(value = "/addOrEditWaitHandle")
+    public AjaxResult addOrEditWaitHandle(@RequestBody WeCustomerTrackRecord trajectory){
+
+
+        weCustomerService.addOrEditWaitHandle(trajectory);
+
+        return AjaxResult.success();
+    }
+
+
+
+    /**
+     * 个人朋友圈互动数据同步
+     * @param userId
+     * @return
+     */
+    @GetMapping("/synchMomentsInteracte/{userId}")
+    public AjaxResult synchMomentsInteracte(@PathVariable String userId){
+
+        iWeMomentsService.synchMomentsInteracte(CollectionUtil.newArrayList(userId));
+
+        return AjaxResult.success(WeConstans.SYNCH_TIP);
+    }
+
+    /**
+     * 获取指定客户拥有的表单字段以及填充的值
+     * @param weUserId
+     * @param externalUserId
+     * @return
+     */
+    @GetMapping("/findSysFieldTemplate")
+    public AjaxResult<List<WeSysFieldTemplate>> findSysFieldTemplate(String weUserId, String externalUserId){
+
+        List<WeSysFieldTemplate> weSysFieldTemplates =   iWeSysFieldTemplateService.findLists(new WeSysFieldTemplate());
+
+        for (Iterator iter = weSysFieldTemplates.iterator(); iter.hasNext();) {
+
+            WeSysFieldTemplate weSysFieldTemplate = (WeSysFieldTemplate) iter.next();
+            if(StringUtils.isNotEmpty(weSysFieldTemplate.getVisualTagIds())){
+                WeCustomer weCustomer = weCustomerService.getOne(new LambdaQueryWrapper<WeCustomer>()
+                        .eq(WeCustomer::getExternalUserid, externalUserId)
+                        .eq(WeCustomer::getAddUserId, weUserId));
+
+                if(null != weCustomer && StringUtils.isNotEmpty(weCustomer.getTagIds())){
+                    if(!ListUtil.toList(weCustomer
+                            .getTagIds().split(",")).contains(weSysFieldTemplate.getVisualTagIds())){
+
+                        iter.remove();
+                    };
+
+
+                }
+
+            }
+        }
+
+        return AjaxResult.success(
+                weSysFieldTemplates
+        );
+
+    }
+
+
+}

+ 273 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCustomerSeasController.java

@@ -0,0 +1,273 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.collection.ListUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.annotation.PhoneEncryptMethod;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.domain.entity.SysUser;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.exception.wecom.WeComException;
+import com.linkwechat.common.utils.*;
+import com.linkwechat.common.utils.poi.ExcelUtil;
+import com.linkwechat.common.utils.poi.LwExcelUtil;
+import com.linkwechat.domain.WeCustomerSeas;
+import com.linkwechat.domain.live.WeLive;
+import com.linkwechat.domain.wecom.vo.customer.seas.CustomerSeasRecordVo;
+import com.linkwechat.service.IWeCustomerSeasService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+
+/**
+ * 客户公海
+ */
+@RestController
+@RequestMapping(value = "/seas")
+public class WeCustomerSeasController extends BaseController {
+    @Autowired
+    private IWeCustomerSeasService iWeCustomerSeasService;
+
+
+    /**
+     * 客户公海模版下载
+     * @return
+     */
+    @GetMapping("/importTemplate")
+    public void importTemplate()
+    {
+
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeCustomerSeas.class, ListUtil.toList(new WeCustomerSeas()),"客户公海"
+        );
+
+    }
+
+
+
+
+    /**
+     * 导入公海数据
+     * @param file
+     * @param weCustomerSea
+     * @return
+     */
+    @PostMapping("/importData")
+    public AjaxResult  importData(MultipartFile file, WeCustomerSeas weCustomerSea) throws Exception {
+        ExcelUtil<WeCustomerSeas> util = new ExcelUtil<WeCustomerSeas>(WeCustomerSeas.class);
+        List<WeCustomerSeas> weCustomerSeas = util.importExcel(file.getInputStream());
+        String tip=new String("成功导入{0}条,去重复{1}条");
+        if(CollectionUtil.isNotEmpty(weCustomerSeas)){
+
+            if(StringUtils.isEmpty(weCustomerSea.getAddUserName())&&StringUtils.isEmpty(weCustomerSea.getAddUserId())){
+                return AjaxResult.error("分配人不可为空!");
+            }
+            //过滤不符合规范的手机号
+            List<WeCustomerSeas> noRuleWeCustomerSeas
+                    = weCustomerSeas.stream().filter(s -> StringUtils.isMobile(s.getPhone())).collect(Collectors.toList());
+
+            if(CollectionUtil.isEmpty(noRuleWeCustomerSeas)){
+                return AjaxResult.error("请传入合法手机号!");
+            }
+
+            //过滤字段为空的数据
+            List<WeCustomerSeas> deduplicationSeas = noRuleWeCustomerSeas.stream().filter(s -> StringUtils.isNotEmpty(s.getCustomerName())
+                    || StringUtils.isNotEmpty(s.getPhone())).collect(Collectors.toList());
+            if(CollectionUtil.isEmpty(deduplicationSeas)){
+                return AjaxResult.error("导入用户数据不能为空!");
+            }
+            //根据手机号去重(去除excel中重复的号码)
+            List<WeCustomerSeas> deduplicationSeasNoRepeat=deduplicationSeas.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() ->
+                    new TreeSet<>(Comparator.comparing(WeCustomerSeas :: getPhone))), ArrayList::new));
+
+
+            //根据手机号去除数据库与excel中重复号码
+            List<WeCustomerSeas> dbExist = iWeCustomerSeasService.list(new LambdaQueryWrapper<WeCustomerSeas>()
+                    .in(WeCustomerSeas::getPhone, deduplicationSeasNoRepeat.stream()
+                            .map(WeCustomerSeas::getPhone).collect(Collectors.toList())));
+            if(CollectionUtil.isNotEmpty(dbExist)){
+                List<WeCustomerSeas> noRepetWeCustomerSeas
+                        = deduplicationSeasNoRepeat.stream().filter(item ->
+                        !dbExist.stream().map(e->e.getPhone()).collect(Collectors.toList())
+                                .contains(item.getPhone())).collect(Collectors.toList());
+                deduplicationSeasNoRepeat.clear();
+                deduplicationSeasNoRepeat.addAll(
+                        noRepetWeCustomerSeas
+                );
+            }
+
+
+            if(CollectionUtil.isNotEmpty(deduplicationSeasNoRepeat)){
+
+                Long tabaleExcelId= SnowFlakeUtil.nextId();
+
+                List<String> userIds = ListUtil.toList(weCustomerSea.getAddUserId().split(","));
+                List<String> userNames = ListUtil.toList(weCustomerSea.getAddUserName().split(","));
+
+                IntStream.range(0,deduplicationSeasNoRepeat.size()).forEach(i->{
+                    WeCustomerSeas k = deduplicationSeasNoRepeat.get(i);
+                    if(Objects.nonNull(k)){
+                        k.setTagIds(weCustomerSea.getTagIds());
+                        k.setTagNames(weCustomerSea.getTagNames());
+                        k.setAddUserId( userIds.get(Math.floorMod(i, userIds.size())));
+                        k.setAddUserName(userNames.get(Math.floorMod(i, userNames.size())));
+                        k.setTableExcelName(file.getOriginalFilename());
+                        k.setTableExcelId(tabaleExcelId);
+                    }
+                });
+                if(iWeCustomerSeasService.saveBatch(deduplicationSeasNoRepeat)){
+                    //发送提醒
+                    deduplicationSeasNoRepeat.stream().collect(Collectors.groupingBy(WeCustomerSeas::getAddUserId)).forEach((k,v)->{
+
+                        iWeCustomerSeasService.remidUser(ListUtil.toList(k),v.size());
+
+
+                    });
+
+                }
+
+                tip = MessageFormat.format(tip, new Object[]{new Integer(deduplicationSeasNoRepeat.size()).toString(),
+                        new Integer( weCustomerSeas.size()-deduplicationSeasNoRepeat.size()).toString()});
+
+            }else{
+                tip = MessageFormat.format(tip, new Object[] { "0",weCustomerSeas.size() });
+            }
+
+
+            ;
+        }else{
+            return AjaxResult.error("导入用户数据不能为空!");
+        }
+
+        return AjaxResult.success(tip);
+    }
+
+
+
+    /**
+     * 获取公海客户列表
+     * @param weCustomerSea
+     * @return
+     */
+    @GetMapping("list")
+    public TableDataInfo<WeCustomerSeas> list(WeCustomerSeas weCustomerSea){
+
+        startPage();
+
+        return getDataTable(
+                iWeCustomerSeasService.findWeCustomerSeas(weCustomerSea)
+        );
+    }
+
+
+    /**
+     * 删除公海客户
+     * @param ids
+     * @return
+     */
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        iWeCustomerSeasService.removeByIds(
+                ListUtil.toList(ids)
+        );
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 提醒员工
+     * @param ids
+     * @return
+     */
+    @PostMapping("/remidUser/{ids}")
+    public AjaxResult remidUser(@PathVariable Long[] ids){
+
+        List<WeCustomerSeas> weCustomerSeas
+                = iWeCustomerSeasService.listByIds(ListUtil.toList(ids));
+
+        if(CollectionUtil.isNotEmpty(weCustomerSeas)){
+            weCustomerSeas.stream().collect(Collectors.groupingBy(WeCustomerSeas::getAddUserId)).forEach((k,v)->{
+                iWeCustomerSeasService.remidUser(ListUtil.toList(k),v.size());
+            });
+
+        }
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 详情头部统计
+     * @return
+     */
+    @GetMapping("/countCustomerSeas")
+    public AjaxResult countCustomerSeas(){
+
+        return AjaxResult.success(
+                iWeCustomerSeasService.countCustomerSeas()
+        );
+    }
+
+
+    /**
+     * 导入记录或员工添加统计
+     * @param groupByType 1:获取导入记录数据 2:员工添加统计
+     * @return
+     */
+    @GetMapping("/findSeasRecord")
+    public TableDataInfo findSeasRecord(@RequestParam(defaultValue = "1") Integer groupByType){
+        startPage();
+        List<CustomerSeasRecordVo> seasRecord = iWeCustomerSeasService.findSeasRecord(groupByType);
+
+        return getDataTable(seasRecord);
+    }
+
+
+    /**
+     * 设置公海客户状态
+     * @return
+     */
+    @PostMapping("/setState")
+    public AjaxResult setState(@RequestBody WeCustomerSeas weCustomerSea){
+        if(StringUtils.isEmpty(weCustomerSea.getPhone())){
+            return AjaxResult.error("手机号不可为空");
+        }
+        iWeCustomerSeasService.update(WeCustomerSeas.builder().addState(new Integer(3)).build()
+                ,new LambdaQueryWrapper<WeCustomerSeas>().eq(WeCustomerSeas::getPhone,weCustomerSea.getPhone()));
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 获取指定员工所拥有的状态(移动端)
+     * @param addState
+     * @return
+     */
+    @GetMapping("/findEmployeeCustomer")
+    public TableDataInfo findEmployeeCustomer(Integer addState){
+        SysUser sysUser = SecurityUtils.getLoginUser().getSysUser();
+        String userId = sysUser.getWeUserId();
+        if (StringUtils.isBlank(userId)) {
+            throw new WeComException("token中获取的we_user_id为空!");
+        }
+        startPage();
+        List<WeCustomerSeas> weCustomerSeas = iWeCustomerSeasService.list(new LambdaQueryWrapper<WeCustomerSeas>()
+                .eq(StringUtils.isNotEmpty(userId),WeCustomerSeas::getAddUserId, userId)
+                .eq(addState != null,WeCustomerSeas::getAddState, addState));
+
+        return getDataTable(weCustomerSeas);
+    }
+
+
+
+
+}

+ 45 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeCustomerTrackRecordController.java

@@ -0,0 +1,45 @@
+package com.linkwechat.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.domain.WeCustomerTrackRecord;
+import com.linkwechat.service.IWeCustomerTrackRecordService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import java.util.List;
+
+/**
+ * 跟进记录相关
+ */
+@RestController
+@RequestMapping("/trackRecord")
+public class WeCustomerTrackRecordController extends BaseController {
+
+    @Autowired
+    private IWeCustomerTrackRecordService iWeCustomerTrackRecordService;
+
+    /**
+     * 跟进记录
+     * @param externalUserid
+     * @param weUserId
+     * @return
+     */
+    @GetMapping("/followUpRecord")
+    public TableDataInfo followUpRecord(String externalUserid, String weUserId){
+
+        startPage();
+        List<WeCustomerTrackRecord> trackRecords = iWeCustomerTrackRecordService.list(new LambdaQueryWrapper<WeCustomerTrackRecord>()
+                .eq(StringUtils.isNotEmpty(externalUserid),WeCustomerTrackRecord::getExternalUserid, externalUserid)
+                .eq(StringUtils.isNotEmpty(weUserId),WeCustomerTrackRecord::getWeUserId, weUserId));
+
+        return getDataTable(trackRecords);
+
+    }
+
+
+
+}

+ 274 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeFissionController.java

@@ -0,0 +1,274 @@
+package com.linkwechat.controller;
+
+
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.exception.wecom.WeComException;
+import com.linkwechat.common.utils.SecurityUtils;
+import com.linkwechat.common.utils.ServletUtils;
+import com.linkwechat.common.utils.poi.LwExcelUtil;
+import com.linkwechat.domain.fission.WeFission;
+import com.linkwechat.domain.fission.vo.*;
+import com.linkwechat.service.IWeFissionService;
+import com.linkwechat.service.IWeMaterialService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import java.util.Arrays;
+import java.util.List;
+
+
+/**
+ * 裂变相关
+ */
+@RestController
+@RequestMapping("/fission")
+public class WeFissionController  extends BaseController {
+
+    @Autowired
+    IWeFissionService iWeFissionService;
+
+    @Autowired
+    IWeMaterialService iWeMaterialService;
+
+
+    /**
+     * 获取裂变列表
+     * @param weFission
+     * @return
+     */
+    @GetMapping("/list")
+    public TableDataInfo<List<WeFission>> list(WeFission weFission){
+        startPage();
+
+        return getDataTable(
+                iWeFissionService.findWeFissions(weFission)
+        );
+
+    }
+
+
+    /**
+     * 创建裂变
+     * @param weFission
+     * @return
+     */
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody WeFission weFission){
+
+        return AjaxResult.success(iWeFissionService.buildWeFission2(weFission));
+    }
+
+
+    /**
+     * 更新裂变
+     * @param weFission
+     * @return
+     */
+    @PutMapping("/update")
+    public AjaxResult update(@RequestBody WeFission weFission){
+
+//        iWeFissionService.updateById(weFission);
+        if(weFission.getId()==null){
+            throw new WeComException("唯一主键不可为空!");
+        }
+        return AjaxResult.success(iWeFissionService.buildWeFission2(weFission));
+    }
+
+
+    /**
+     * 获取任务信息
+     * @param id
+     * @return
+     */
+    @GetMapping("/getWeFissionDetail/{id}")
+    public AjaxResult<WeFission> getWeFissionDetail(@PathVariable Long id){
+
+        WeFission weFission = iWeFissionService.getById(id);
+
+        if(null != weFission){
+            weFission.setWematerial(
+                    iWeMaterialService.getById(weFission.getPosterId())
+            );
+        }
+        return AjaxResult.success(
+                weFission
+        );
+
+    }
+
+
+
+    /**
+     * 通过id批量删除裂变
+     *
+     * @param ids id列表
+     * @return 结果
+     */
+    @DeleteMapping(path = "/{ids}")
+    public AjaxResult batchDeleteFission(@PathVariable("ids") Long[] ids) {
+        iWeFissionService.removeByIds(Arrays.asList(ids));
+        return AjaxResult.success();
+    }
+
+
+    /*****************************************
+     ***************统计相关*******************
+     ****************************************/
+
+    /**
+     * 裂变头部汇总统计
+     * @param fissionId
+     * @return
+     */
+    @GetMapping("/findWeFissionTab/{fissionId}")
+    public AjaxResult<WeFissionTabVo> findWeFissionTab(@PathVariable Long fissionId){
+
+
+        return AjaxResult.success(
+                iWeFissionService.findWeFissionTab(fissionId)
+        );
+
+    }
+
+
+    /**
+     * 数据趋势
+     * @param weFission
+     * @return
+     */
+    @GetMapping("/findWeFissionTrend")
+    public AjaxResult<List<WeFissionTrendVo>> findWeFissionTrend(WeFission weFission){
+
+        startPage();
+
+        return AjaxResult.success(
+                iWeFissionService.findWeFissionTrend(weFission)
+        );
+
+    }
+
+    /**
+     * 数据报表
+     * @param weFission
+     * @return
+     */
+    @GetMapping("/findWeFissionDataReport")
+    public TableDataInfo<List<WeFissionDataReportVo>>  findWeFissionDataReport(WeFission weFission){
+
+        startPage();
+
+        return getDataTable(
+                iWeFissionService.findWeFissionDataReport(weFission)
+        );
+
+    }
+
+
+    /**
+     * 导出数据报表
+     * @return
+     */
+    @GetMapping("/exportWeFissionDataReport")
+    public void exportWeFissionDataReport(){
+
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeFissionDataReportVo.class, iWeFissionService.findWeFissionDataReport(new WeFission()),"数据报表"
+        );
+
+    }
+
+
+
+
+
+    /**
+     * 裂变明细(群裂变)
+     * @param fissionId
+     * @param customerName 客户名
+     * @param weUserId 员工id
+     * @param chatId 群id
+     * @return
+     */
+    @GetMapping("/findWeGroupFissionDetail")
+    public TableDataInfo<List<WeGroupFissionDetailVo>> findWeGroupFissionDetail(String fissionId,String customerName, String weUserId,String chatId){
+        startPage();
+
+        return getDataTable(
+                iWeFissionService.findWeGroupFissionDetail(fissionId,customerName,weUserId,chatId)
+        );
+    }
+
+
+    /**
+     * 裂变明细(任务宝)
+     * @param fissionId
+     * @param customerName 客户名
+     * @param weUserId 员工id
+     * @return
+     */
+    @GetMapping("/findWeTaskFissionDetail")
+    public TableDataInfo<List<WeTaskFissionDetailVo>> findWeTaskFissionDetail(String fissionId,String customerName, String weUserId){
+        startPage();
+        // 空的,代表不是员工邀请的,而是直接进入活动链接的
+        return getDataTable(
+                iWeFissionService.findWeTaskFissionDetail(fissionId,customerName,weUserId)
+        );
+    }
+
+
+    /**
+     * 导出群裂变明细(群裂变)
+     * @return
+     */
+    @GetMapping("/exportWeGroupFissionDetail")
+    public void exportWeGroupFissionDetail(String fissionId){
+
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeGroupFissionDetailVo.class, iWeFissionService.findWeGroupFissionDetail(fissionId,null,null,null),"群裂变明细"
+        );
+
+    }
+
+
+    /**
+     * 导出群裂变明细(任务宝)
+     * @return
+     */
+    @GetMapping("/exportWeTaskFissionDetail")
+    public void exportWeTaskFissionDetail(String fissionId){
+
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeTaskFissionDetailVo.class, iWeFissionService.findWeTaskFissionDetail(fissionId,null,null),"任务宝明细"
+        );
+
+    }
+
+
+
+
+
+
+    /**
+     * 裂变明细sub
+     * @param fissionInviterRecordId
+     * @return
+     */
+    @GetMapping("/findWeFissionDetailSub")
+    public TableDataInfo<List<WeFissionDetailSubVo>> findWeFissionDetailSub(Long fissionInviterRecordId){
+        startPage();
+
+        return getDataTable(
+                iWeFissionService.findWeFissionDetailSub(fissionInviterRecordId)
+        );
+
+    }
+
+
+
+
+
+
+
+
+}

+ 83 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeFormSurveyAnswerController.java

@@ -0,0 +1,83 @@
+package com.linkwechat.controller;
+
+
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.domain.WeFormSurveyAnswer;
+import com.linkwechat.domain.form.query.WeAddFormSurveyAnswerQuery;
+import com.linkwechat.domain.form.query.WeFormSurveyAnswerQuery;
+import com.linkwechat.service.IWeFormSurveyAnswerService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+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;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 智能表单用户表Controller
+ *
+ * @author HXD
+ * @date 2022-06-08 09.32
+ */
+@RestController
+@RequestMapping("/form/answer/")
+@Api(tags = "智能表单用户接口")
+@Slf4j
+public class WeFormSurveyAnswerController extends BaseController {
+
+    @Autowired
+    private IWeFormSurveyAnswerService weFormSurveyAnswerService;
+
+    /**
+     * 新增用户填写信息
+     */
+    @PostMapping("/add")
+    @ApiOperation("新增用户填写信息")
+    public AjaxResult addAnswer(@RequestBody @Validated WeAddFormSurveyAnswerQuery query) {
+        weFormSurveyAnswerService.addAnswer(query);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 查询用户填写信息列表
+     */
+    @PostMapping("/getAnswerList")
+    @ApiOperation("查询用户填写信息列表")
+    public TableDataInfo<WeFormSurveyAnswer> getAnswerList(@RequestBody @Validated WeFormSurveyAnswerQuery query) {
+        startPage();
+        List<WeFormSurveyAnswer> tSurveyAnswerList = weFormSurveyAnswerService.getAnswerList(query);
+        return getDataTable(tSurveyAnswerList);
+    }
+
+    /**
+     * 判断用户是否已经填写表单
+     * <p>
+     * 通过用户的IP地址来判断用户是否已经填写
+     *
+     * @param query
+     * @return {@link AjaxResult}
+     * @author WangYX
+     * @date 2022/10/13 16:18
+     */
+    @PostMapping("/isCompleteSurvey")
+    @ApiOperation("判断用户是否已经填写表单")
+    public AjaxResult isCompleteSurvey(@RequestBody @Validated WeFormSurveyAnswerQuery query) {
+        Integer count = weFormSurveyAnswerService.isCompleteSurvey(query);
+        if (Objects.nonNull(count) && count > 0) {
+            return AjaxResult.error("该用户已填写表单");
+        }
+        return AjaxResult.success("该用户未填写表单");
+    }
+
+
+}

+ 101 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeFormSurveyCatalogueController.java

@@ -0,0 +1,101 @@
+package com.linkwechat.controller;
+
+
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.domain.WeFormSurveyCatalogue;
+import com.linkwechat.domain.form.query.WeAddFormSurveyCatalogueQuery;
+import com.linkwechat.domain.form.query.WeFormSurveyCatalogueQuery;
+import com.linkwechat.service.IWeFormSurveyCatalogueService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 智能表单基本目录表Controller
+ *
+ * @author HXD
+ * @date 2022-05-27 10:08
+ */
+@RestController
+@RequestMapping("/form/survey")
+@Api(tags = "智能表单接口")
+@Slf4j
+public class WeFormSurveyCatalogueController extends BaseController {
+
+    @Autowired
+    private IWeFormSurveyCatalogueService weFormSurveyCatalogueService;
+
+
+    @PostMapping(value = "/add")
+    @ApiOperation(value = "新增表单基本信息", httpMethod = "POST")
+    public AjaxResult add(@RequestBody @Validated WeAddFormSurveyCatalogueQuery query) {
+        return AjaxResult.success(weFormSurveyCatalogueService.add(query));
+    }
+
+    /**
+     * 删除表单
+     */
+    @DeleteMapping(value = "/delete/{ids}")
+    @ApiOperation(value = "删除表单", httpMethod = "DELETE")
+    public AjaxResult deleteSurvey(@PathVariable("ids") List<Long> ids) {
+        weFormSurveyCatalogueService.deleteSurvey(ids);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 更新表单
+     */
+    @PutMapping(value = "/update")
+    @ApiOperation(value = "更新表单", httpMethod = "PUT")
+    public AjaxResult updateSurvey(@RequestBody @Validated WeAddFormSurveyCatalogueQuery query) {
+        weFormSurveyCatalogueService.updateSurvey(query);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 查询表单详情
+     */
+    @GetMapping(value = "/getInfo/{id}")
+    @ApiOperation(value = "查询表单详情", httpMethod = "GET")
+    public AjaxResult<WeFormSurveyCatalogue> getInfo(@PathVariable("id") Long id) {
+        WeFormSurveyCatalogue info = weFormSurveyCatalogueService.getInfo(id);
+        return AjaxResult.success(info);
+    }
+
+    /**
+     * 查询表单列表
+     */
+    @GetMapping(value = "/list")
+    @ApiOperation(value = "查询表单列表", httpMethod = "GET")
+    public TableDataInfo<WeFormSurveyCatalogue> getList(WeFormSurveyCatalogueQuery query) {
+        startPage();
+        List<WeFormSurveyCatalogue> list = weFormSurveyCatalogueService.getList(query);
+        return getDataTable(list);
+    }
+
+    /**
+     * 更新表单状态(
+     * 1.启用 2.暂停 3.结束)
+     */
+    @PutMapping(value = "/updateStatus")
+    @ApiOperation(value = "更新表单状态", httpMethod = "PUT")
+    public AjaxResult updateStatus(@RequestBody WeFormSurveyCatalogueQuery query) {
+        weFormSurveyCatalogueService.updateStatus(query);
+        return AjaxResult.success();
+    }
+
+
+    @ApiOperation(value = "删除表单分组", httpMethod = "GET")
+    @GetMapping("/deleteGroup")
+    public AjaxResult deleteFormSurveyGroup(@RequestParam("groupId") Long groupId) {
+        weFormSurveyCatalogueService.deleteFormSurveyGroup(groupId);
+        return AjaxResult.success();
+    }
+}

+ 62 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeFormSurveyCategoryController.java

@@ -0,0 +1,62 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.domain.material.entity.WeCategory;
+import com.linkwechat.service.IWeFormSurveyCategoryService;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+/**
+ * 智能表单分组
+ *
+ * @author WangYX
+ * @version 1.0.0
+ * @date 2022/10/13 9:33
+ */
+@RestController
+@RequestMapping("/form/category")
+public class WeFormSurveyCategoryController {
+
+    @Resource
+    private IWeFormSurveyCategoryService weFormSurveyCategoryService;
+
+    /**
+     * 添加类目
+     */
+    @Log(title = "添加类目", businessType = BusinessType.INSERT)
+    @PostMapping
+    @ApiOperation("添加类目")
+    public AjaxResult add(@Validated @RequestBody WeCategory category) {
+        weFormSurveyCategoryService.insertWeCategory(category);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 更新目录
+     */
+    @Log(title = "更新目录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    @ApiOperation("更新目录")
+    public AjaxResult edit(@Validated @RequestBody WeCategory category) {
+        weFormSurveyCategoryService.updateWeCategory(category);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 删除类目
+     */
+    @Log(title = "删除类目", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    @ApiOperation("删除类目")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        weFormSurveyCategoryService.deleteWeCategoryById(ids);
+        return AjaxResult.success();
+    }
+
+
+}

+ 675 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeFormSurveyStatisticsController.java

@@ -0,0 +1,675 @@
+package com.linkwechat.controller;
+
+
+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.hutool.core.util.ObjectUtil;
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.constant.SiteStasConstants;
+import com.linkwechat.common.constant.SiteStasConstants;
+import com.linkwechat.common.constant.SiteStatsConstants;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.domain.vo.SysAreaVo;
+import com.linkwechat.common.utils.ServletUtils;
+import com.linkwechat.common.utils.SnowFlakeUtil;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.common.utils.poi.ExcelUtil;
+import com.linkwechat.domain.*;
+import com.linkwechat.domain.form.query.WeFormSiteStasQuery;
+import com.linkwechat.domain.form.query.WeFormSurveyRadioQuery;
+import com.linkwechat.domain.form.query.WeFormSurveyStatisticQuery;
+import com.linkwechat.domain.form.vo.WeFormSurveyAnswerVO;
+import com.linkwechat.domain.form.vo.WeFormSurveyStatisticsVO;
+import com.linkwechat.fegin.QwSysAreaClient;
+import com.linkwechat.service.*;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 智能表单统计Controller
+ *
+ * @author HXD
+ * @date 2022-06-08 09.32
+ */
+@RestController
+@RequestMapping("/form/statistic")
+@Api(tags = "智能表单统计接口")
+@Slf4j
+public class WeFormSurveyStatisticsController extends BaseController {
+
+    @Autowired
+    private IWeFormSurveyStatisticsService weFormSurveyStatisticsService;
+
+    @Autowired
+    private IWeFormSurveyRadioService weFormSurveyRadioService;
+
+    @Autowired
+    private IWeFormSurveyAnswerService weFormSurveyAnswerService;
+
+    @Autowired
+    private IWeCustomerService weCustomerService;
+
+    @Autowired
+    private IWeFormSurveyCatalogueService weFormSurveyCatalogueService;
+
+    @Resource
+    private QwSysAreaClient qwSysAreaClient;
+
+    @Resource
+    private RedisTemplate redisTemplate;
+    @Resource
+    private IWeFormSurveySiteStasService weFormSurveySiteStasService;
+
+    /**
+     * 新增访问记录
+     */
+    @PostMapping("/add")
+    @ApiOperation(value = "新增访问记录", httpMethod = "POST")
+    public AjaxResult addStatistics(@RequestBody @Validated WeFormSurveyStatistics query) {
+        weFormSurveyStatisticsService.addStatistics(query);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 查询基本表单统计信息
+     */
+    @GetMapping("/getStatistics")
+    @ApiOperation(value = "查询基本表单统计信息", httpMethod = "GET")
+    public AjaxResult getStatistics(WeFormSurveyStatistics query) {
+        List<WeFormSurveyStatistics> Statistics = weFormSurveyStatisticsService.getStatistics(query);
+        return AjaxResult.success(Statistics);
+    }
+
+
+    @PostMapping("/lineChart")
+    @ApiOperation("折线图")
+    public AjaxResult lineChart(@RequestBody @Validated WeFormSurveyStatisticQuery query) {
+        String type = query.getType();
+        startPage();
+        Date startTime = null;
+        Date endTime = null;
+        Map<String, Object> legendData = new HashMap<>();
+        String[] legendArray = new String[]{"总访问量", "总访问用户", "有效收集量"};
+        legendData.put("data", legendArray);
+        Map<String, Map<String, Object>> legendDataMap = new HashMap<>();
+        legendDataMap.put("legend", legendData);
+        Map<String, Object> xAxisData = new HashMap<>();
+        if (!type.equals("customization")) {
+            if (type.equals("week")) {
+                startTime = DateUtil.offsetWeek(new Date(), -1);
+            } else if (type.equals("month")) {
+                startTime = DateUtil.offsetMonth(new Date(), -1);
+            }
+            endTime = new Date();
+        } else {
+            startTime = query.getStartDate();
+            endTime = query.getEndDate();
+        }
+        List<DateTime> timeList = DateUtil.rangeToList(startTime, endTime, DateField.DAY_OF_YEAR);
+        String[] xAxisArray = new String[timeList.size()];
+        //使用for循环得到数组
+        int[] totalVisitsArray = new int[timeList.size()];
+        int[] totalUserArray = new int[timeList.size()];
+        int[] collectionVolumeArray = new int[timeList.size()];
+        String[] collectionRateArray = new String[timeList.size()];
+        int[] averageTimeArray = new int[timeList.size()];
+        for (int i = 0; i < timeList.size(); i++) {
+            xAxisArray[i] = timeList.get(i).toDateStr();
+            WeFormSurveyStatisticQuery surveyStatistics = new WeFormSurveyStatisticQuery();
+            surveyStatistics.setStartDate(DateUtil.parseDate(timeList.get(i).toDateStr()));
+            surveyStatistics.setEndDate(DateUtil.parseDate(timeList.get(i).toDateStr()));
+            surveyStatistics.setDataSource(query.getDataSource());
+            surveyStatistics.setBelongId(query.getBelongId());
+
+            List<WeFormSurveyStatistics> statistics = weFormSurveyStatisticsService.dataList(surveyStatistics);
+            if (CollectionUtil.isNotEmpty(statistics)) {
+                for (WeFormSurveyStatistics list : statistics) {
+                    Integer totalVisits = list.getTotalVisits();
+                    Integer totalUser = list.getTotalUser();
+                    Integer collectionVolume = list.getCollectionVolume();
+                    String collectionRate = list.getCollectionRate();
+                    Integer averageTime = list.getAverageTime();
+                    totalVisitsArray[i] = totalVisits;
+                    totalUserArray[i] = totalUser;
+                    collectionVolumeArray[i] = collectionVolume;
+                    collectionRateArray[i] = collectionRate;
+                    averageTimeArray[i] = averageTime;
+                }
+            } else {
+                totalVisitsArray[i] = 0;
+                totalUserArray[i] = 0;
+                collectionVolumeArray[i] = 0;
+                collectionRateArray[i] = "0";
+                averageTimeArray[i] = 0;
+            }
+        }
+        Map<String, Object> totalVisitsData = new HashMap<>();
+        Map<String, Object> totalUserData = new HashMap<>();
+        Map<String, Object> collectionVolumeData = new HashMap<>();
+        Map<String, Object> collectionRateData = new HashMap<>();
+        Map<String, Object> averageTimeData = new HashMap<>();
+        Map<String, Object> seriesMap = new HashMap<>();
+        totalVisitsData.put("data", totalVisitsArray);
+        totalVisitsData.put("name", "总访问量");
+        totalUserData.put("data", totalUserArray);
+        totalUserData.put("name", "总访问用户");
+        collectionVolumeData.put("data", collectionVolumeArray);
+        collectionVolumeData.put("name", "有效收集量");
+        collectionRateData.put("data", collectionRateArray);
+        collectionRateData.put("name", "收集率");
+        averageTimeData.put("data", averageTimeArray);
+        averageTimeData.put("name", "平均完成时间");
+        xAxisData.put("data", xAxisArray);
+        Map<String, Map<String, Object>> xAxisDataMap = new HashMap<>();
+        xAxisDataMap.put("xAxis", xAxisData);
+        List list = new ArrayList();
+        list.add(totalVisitsData);
+        list.add(totalUserData);
+        list.add(collectionVolumeData);
+        list.add(collectionRateData);
+        list.add(averageTimeData);
+        seriesMap.put("series", list);
+        Map map = new HashMap();
+        map.put("legend", legendData);
+        map.put("xAxis", xAxisData);
+        map.put("series", list);
+        return AjaxResult.success(map);
+    }
+
+
+    @PostMapping("/insertPieValue")
+    @ApiOperation("保存饼图数据")
+    public AjaxResult insertPieValue(@RequestBody @Validated WeFormSurveyStatisticQuery query) {
+        WeFormSurveyRadio tQuRadio = new WeFormSurveyRadio();
+        List pieList = JSONObject.parseObject(query.getAnswer(), List.class);
+        for (int i = 0; i < pieList.size(); i++) {
+            Map newMap = (Map) pieList.get(i);
+            String formCodeId = newMap.get("formCodeId").toString();
+            if (formCodeId.equals("6") || formCodeId.equals("7")
+                    || formCodeId.equals("8") || formCodeId.equals("9")) {
+                String defaultValue = newMap.get("defaultValue").toString();
+                String label = newMap.get("label").toString();
+                String formId = newMap.get("formId").toString();
+                if (!formCodeId.equals("9")) {
+                    String options = newMap.get("options").toString();
+                    tQuRadio.setOptions(options);
+                }
+                String questionNumber = newMap.get("questionNumber").toString();
+                tQuRadio.setId(SnowFlakeUtil.nextId());
+                tQuRadio.setFormCodeId(formCodeId);
+                tQuRadio.setLabel(label);
+                tQuRadio.setFormId(formId);
+                tQuRadio.setDefaultValue(defaultValue);
+                tQuRadio.setQuestionNumber(questionNumber);
+                if (StringUtils.isNotBlank(query.getDataSource())) {
+                    tQuRadio.setDataSource(query.getDataSource());
+                }
+                weFormSurveyRadioService.save(tQuRadio);
+            }
+        }
+        return AjaxResult.success();
+    }
+
+    @PostMapping("/pieChart")
+    @ApiOperation(value = "饼图", httpMethod = "POST")
+    public AjaxResult pieChart(@RequestBody WeFormSurveyRadioQuery query) {
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        startPage();
+        query.setLabel("pie");
+        List<WeFormSurveyRadio> radioList = weFormSurveyRadioService.selectNumber(query);
+        for (WeFormSurveyRadio number : radioList) {
+            String questionNumber = number.getQuestionNumber();
+            String options = number.getOptions();
+            String label = number.getLabel();
+            WeFormSurveyRadioQuery newQuery = new WeFormSurveyRadioQuery();
+            newQuery.setQuestionNumber(questionNumber);
+            newQuery.setFormId(query.getFormId());
+            newQuery.setLabel("pie");
+            if (StringUtils.isNotBlank(query.getDataSource())) {
+                newQuery.setDataSource(query.getDataSource());
+            }
+            List<WeFormSurveyRadio> tQuRadios = weFormSurveyRadioService.selectDefaultValue(newQuery);
+            Map<String, Object> result = new HashMap<>();
+            List<Map<String, Object>> list = new ArrayList();
+            if (tQuRadios.size() > 0) {
+                for (WeFormSurveyRadio newList : tQuRadios) {
+                    Map<String, Object> map = new HashMap<>();
+                    String defaultValue = newList.getDefaultValue();
+                    WeFormSurveyRadioQuery newTQuRadio = new WeFormSurveyRadioQuery();
+                    newTQuRadio.setFormId(query.getFormId());
+                    if (StringUtils.isNotBlank(query.getDataSource())) {
+                        newTQuRadio.setDataSource(query.getDataSource());
+                    }
+                    newTQuRadio.setDefaultValue(defaultValue);
+                    newTQuRadio.setQuestionNumber(questionNumber);
+                    newTQuRadio.setLabel("pie");
+                    Integer value = weFormSurveyRadioService.countDefaultValue(newTQuRadio);
+                    map.put("value", value);
+                    map.put("name", defaultValue);
+                    list.add(map);
+                }
+                result.put("questionNumber", questionNumber);
+                result.put("options", options);
+                result.put("label", label);
+                result.put("data", list);
+            }
+            resultList.add(result);
+        }
+        return AjaxResult.success(resultList);
+    }
+
+
+    @PostMapping("/customer")
+    @ApiOperation("用户统计列表")
+    public AjaxResult customer(@RequestBody @Validated WeFormSurveyStatisticQuery query) {
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        List<WeFormSurveyAnswer> total = weFormSurveyAnswerService.selectCustomerList(query);
+        startPage();
+        List<WeFormSurveyAnswer> customerList = weFormSurveyAnswerService.selectCustomerList(query);
+        for (WeFormSurveyAnswer list : customerList) {
+            Map<String, Object> map = new HashMap<>();
+            String unionId = list.getUnionId();
+            List<WeCustomer> weCustomers = weCustomerService.list(new LambdaQueryWrapper<WeCustomer>().eq(WeCustomer::getUnionid, unionId).eq(WeCustomer::getDelFlag, 0));
+            if (weCustomers.size() > 0) {
+                map.put("isOfficeCustomer", true);
+            } else {
+                map.put("isOfficeCustomer", false);
+            }
+            map.put("data", list);
+            map.put("total", total.size());
+            resultList.add(map);
+        }
+        if (customerList.size() > 0) {
+            return AjaxResult.success(resultList);
+        }
+        return AjaxResult.success("数据为空", resultList);
+    }
+
+    @ApiOperation(value = "数据概览列表", httpMethod = "POST")
+    @PostMapping("/dataList")
+    public AjaxResult dataList(@RequestBody @Validated WeFormSurveyStatisticQuery query) {
+        String type = query.getType();
+        int count = 0;
+        if (StringUtils.isNotBlank(type) && type.equals("week")) {
+            query.setStartDate(DateUtil.offsetWeek(new Date(), -1));
+            query.setEndDate(DateUtil.date());
+        }
+        if (StringUtils.isNotBlank(type) && type.equals("month")) {
+            query.setStartDate(DateUtil.offsetMonth(new Date(), -1));
+            query.setEndDate(DateUtil.date());
+        }
+        startPage();
+        List<WeFormSurveyStatistics> tSurveyList = weFormSurveyStatisticsService.dataList(query);
+        List<WeFormSurveyStatisticsVO> result = new ArrayList<>();
+        for (WeFormSurveyStatistics weFormSurveyStatistics : tSurveyList) {
+            WeFormSurveyStatisticsVO weFormSurveyStatisticsVO = new WeFormSurveyStatisticsVO();
+            weFormSurveyStatisticsVO.setCreateTime(DateUtil.offsetDay(weFormSurveyStatistics.getCreateTime(), -1));
+            weFormSurveyStatisticsVO.setTotalVisits(weFormSurveyStatistics.getTotalVisits());
+            weFormSurveyStatisticsVO.setTotalUser(weFormSurveyStatistics.getTotalUser());
+            weFormSurveyStatisticsVO.setCollectionRate(weFormSurveyStatistics.getCollectionRate());
+            weFormSurveyStatisticsVO.setCollectionVolume(weFormSurveyStatistics.getCollectionVolume());
+            weFormSurveyStatisticsVO.setAverageTime(weFormSurveyStatistics.getAverageTime());
+            result.add(weFormSurveyStatisticsVO);
+        }
+
+        count = tSurveyList.size();
+        Map<String, Object> resultMap = new HashMap<>();
+        resultMap.put("result", result);
+        resultMap.put("total", count);
+        if (tSurveyList.size() == 0) {
+            return AjaxResult.success("该时间段内数据为空!", resultMap);
+        }
+        return AjaxResult.success(resultMap);
+    }
+
+
+    /**
+     * 省级联动
+     */
+    @Log(title = "省级联动")
+    @PostMapping("/areaStatistic")
+    @ApiOperation("省级联动")
+    public AjaxResult areaStatistic(WeFormSurveyRadioQuery query) {
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        query.setLabel("area");
+        List<WeFormSurveyRadio> selectNumber = weFormSurveyRadioService.selectNumber(query);
+        for (WeFormSurveyRadio number : selectNumber) {
+            Map<String, Object> result = new HashMap<>();
+            List<Map<String, Object>> list = new ArrayList<>();
+            String questionNumber = number.getQuestionNumber();
+            String label = number.getLabel();
+            List<SysAreaVo> areaDataList = qwSysAreaClient.getChildListById(query.getParentCode()).getData();
+            for (SysAreaVo areaData : areaDataList) {
+                Map<String, Object> map = new HashMap<>();
+                String name = areaData.getName();
+                query.setCode(areaData.getId());
+                query.setQuestionNumber(questionNumber);
+                int value = weFormSurveyRadioService.selectCountArea(query);
+                map.put("name", name);
+                map.put("value", value);
+                list.add(map);
+            }
+            result.put("questionNumber", questionNumber);
+            result.put("label", label);
+            result.put("data", list);
+            resultList.add(result);
+        }
+        return AjaxResult.success(resultList);
+    }
+
+    /**
+     * 站点统计
+     *
+     * @return
+     */
+    @PostMapping("/siteStas")
+    @ApiOperation("站点统计")
+    public AjaxResult siteStas(@RequestBody @Validated WeFormSiteStasQuery weFormSiteStasQuery) {
+
+        Long belongId = weFormSiteStasQuery.getBelongId();
+        String dataSource = weFormSiteStasQuery.getDataSource();
+
+        WeFormSurveyCatalogue weFormSurveyCatalogue = weFormSurveyCatalogueService.getWeFormSurveyCatalogueById(belongId);
+        String channelsPath = weFormSurveyCatalogue.getChannelsPath();
+        String channelsName = weFormSurveyCatalogue.getChannelsName();
+        String[] paths = channelsPath.split(",");
+        String[] names = channelsName.split(",");
+
+        for (int i = 0; i < paths.length; i++) {
+            if (paths[i].equals(dataSource)) {
+                dataSource = names[i];
+                break;
+            }
+        }
+
+        //PV
+        String pvKey = StringUtils.format(SiteStasConstants.PREFIX_KEY_PV, belongId, dataSource);
+        redisTemplate.opsForValue().increment(pvKey);
+        //IP
+        String ipKey = StringUtils.format(SiteStasConstants.PREFIX_KEY_IP, belongId, dataSource);
+        log.info("请求的IP地址:{}", weFormSiteStasQuery.getIpAddr());
+        redisTemplate.opsForSet().add(ipKey, weFormSiteStasQuery.getIpAddr());
+
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 统计数据导出
+     */
+    @ApiOperation("导出用户统计")
+    @Log(title = "导出用户统计")
+    @PostMapping("/user/export")
+    public AjaxResult userExport(@RequestBody @Validated WeFormSurveyStatisticQuery query) {
+        Date startTime = null;
+        Date endTime = null;
+        String type = query.getType();
+        if (!type.equals("customization")) {
+            if (type.equals("week")) {
+                startTime = DateUtil.offsetWeek(new Date(), -1);
+            } else if (type.equals("month")) {
+                startTime = DateUtil.offsetMonth(new Date(), -1);
+            }
+            endTime = new Date();
+        } else {
+            startTime = query.getStartDate();
+            endTime = query.getEndDate();
+        }
+        query.setStartDate(startTime);
+        query.setEndDate(endTime);
+        QueryWrapper<WeFormSurveyAnswer> queryWrapper = new QueryWrapper<>();
+        queryWrapper.lambda().eq(WeFormSurveyAnswer::getBelongId, query.getBelongId());
+        queryWrapper.lambda().eq(WeFormSurveyAnswer::getDataSource, query.getDataSource());
+        queryWrapper.apply("DATE_FORMAT(CREATE_TIME, '%Y-%m-%d' ) >= '" + DateUtil.formatDate(query.getStartDate()) + "'");
+        queryWrapper.apply("DATE_FORMAT(CREATE_TIME, '%Y-%m-%d' ) <= '" + DateUtil.formatDate(query.getEndDate()) + "'");
+        List<WeFormSurveyAnswer> total = weFormSurveyAnswerService.list(queryWrapper);
+
+        List<WeFormSurveyAnswerVO> list = new ArrayList<>();
+        for (WeFormSurveyAnswer weFormSurveyAnswer : total) {
+            WeFormSurveyAnswerVO weFormSurveyAnswerVO = new WeFormSurveyAnswerVO();
+            weFormSurveyAnswerVO.setCreateTime(weFormSurveyAnswer.getCreateTime());
+            weFormSurveyAnswerVO.setName(weFormSurveyAnswer.getName());
+            weFormSurveyAnswerVO.setDataSource(weFormSurveyAnswer.getDataSource());
+            weFormSurveyAnswerVO.setMobile(weFormSurveyAnswer.getMobile());
+            weFormSurveyAnswerVO.setOpenId(weFormSurveyAnswer.getOpenId());
+            weFormSurveyAnswerVO.setUnionId(weFormSurveyAnswer.getUnionId());
+            weFormSurveyAnswerVO.setIsCorpUser("否");
+            list.add(weFormSurveyAnswerVO);
+        }
+        ExcelUtil<WeFormSurveyAnswerVO> util = new ExcelUtil<>(WeFormSurveyAnswerVO.class);
+        return util.exportExcel(list, "user");
+    }
+
+
+    /**
+     * 统计数据导出
+     */
+    @ApiOperation("统计数据导出")
+    @Log(title = "统计数据导出")
+    @PostMapping("/data/export")
+    public AjaxResult dataExport(@RequestBody @Validated WeFormSurveyStatisticQuery query) {
+        String type = query.getType();
+        if (StringUtils.isNotBlank(type) && type.equals("week")) {
+            query.setStartDate(DateUtil.offsetWeek(new Date(), -1));
+            query.setEndDate(DateUtil.date());
+        }
+        if (StringUtils.isNotBlank(type) && type.equals("month")) {
+            query.setStartDate(DateUtil.offsetMonth(new Date(), -1));
+            query.setEndDate(DateUtil.date());
+        }
+        List<WeFormSurveyStatistics> tSurveyList = weFormSurveyStatisticsService.dataList(query);
+
+        List<WeFormSurveyStatisticsVO> list = new ArrayList<>();
+        if (tSurveyList != null && tSurveyList.size() > 0) {
+            for (WeFormSurveyStatistics weFormSurveyStatistics : tSurveyList) {
+                WeFormSurveyStatisticsVO weFormSurveyStatisticsVO = new WeFormSurveyStatisticsVO();
+                weFormSurveyStatisticsVO.setCreateTime(DateUtil.offsetDay(weFormSurveyStatistics.getCreateTime(), -1));
+                weFormSurveyStatisticsVO.setTotalVisits(weFormSurveyStatistics.getTotalVisits());
+                weFormSurveyStatisticsVO.setTotalUser(weFormSurveyStatistics.getTotalUser());
+                weFormSurveyStatisticsVO.setCollectionRate(weFormSurveyStatistics.getCollectionRate());
+                weFormSurveyStatisticsVO.setCollectionVolume(weFormSurveyStatistics.getCollectionVolume());
+                weFormSurveyStatisticsVO.setAverageTime(weFormSurveyStatistics.getAverageTime());
+                list.add(weFormSurveyStatisticsVO);
+            }
+        }
+        ExcelUtil<WeFormSurveyStatisticsVO> util = new ExcelUtil<>(WeFormSurveyStatisticsVO.class);
+        return util.exportExcel(list, "data");
+    }
+
+    /**
+     * 问卷答案数据导出
+     *
+     * @param query
+     * @throws IOException
+     */
+    @ApiOperation("问卷答案数据导出")
+    @Log(title = "问卷答案数据导出")
+    @GetMapping("/answer/export")
+    public void answerExport(WeFormSurveyStatisticQuery query) throws IOException {
+        //时间类型处理
+        String type = query.getType();
+        if (StringUtils.isNotBlank(type) && type.equals("week")) {
+            query.setStartDate(DateUtil.offsetWeek(new Date(), -1));
+            query.setEndDate(DateUtil.date());
+        }
+        if (StringUtils.isNotBlank(type) && type.equals("month")) {
+            query.setStartDate(DateUtil.offsetMonth(new Date(), -1));
+            query.setEndDate(DateUtil.date());
+        }
+
+        //表单信息
+        WeFormSurveyCatalogue weFormSurveyCatalogue = weFormSurveyCatalogueService.getWeFormSurveyCatalogueById(query.getBelongId());
+
+        //导出数据的头部
+        List<List<String>> head = new ArrayList<>();
+        if (ObjectUtil.isNotNull(weFormSurveyCatalogue)) {
+            //导出数据的头部
+            List<String> head0 = new ArrayList<>();
+            head0.add("日期");
+            head.add(head0);
+
+            String styles = (String) weFormSurveyCatalogue.getStyles();
+            JSONArray jsonArray = JSON.parseArray(styles).getJSONArray(0);
+            for (int i = 0; i < jsonArray.size(); i++) {
+                JSONObject jsonObject = jsonArray.getJSONObject(i);
+
+                List<String> header = new ArrayList<>();
+                header.add(jsonObject.getString("label"));
+                head.add(header);
+            }
+        }
+
+        //查询列表
+        QueryWrapper<WeFormSurveyAnswer> queryWrapper = new QueryWrapper<>();
+        queryWrapper.lambda().eq(WeFormSurveyAnswer::getBelongId, query.getBelongId());
+        queryWrapper.lambda().eq(WeFormSurveyAnswer::getDataSource, query.getDataSource());
+        queryWrapper.apply("DATE_FORMAT(CREATE_TIME, '%Y-%m-%d' ) >= '" + DateUtil.formatDate(query.getStartDate()) + "'");
+        queryWrapper.apply("DATE_FORMAT(CREATE_TIME, '%Y-%m-%d' ) <= '" + DateUtil.formatDate(query.getEndDate()) + "'");
+        List<WeFormSurveyAnswer> list = weFormSurveyAnswerService.list(queryWrapper);
+
+        //导出的数据
+        List<List<Object>> exportList = new ArrayList<>();
+        //填充数据
+        if (list != null && list.size() > 0) {
+            for (WeFormSurveyAnswer weFormSurveyAnswer : list) {
+                List<Object> item = new ArrayList<>();
+                item.add(DateUtil.format(weFormSurveyAnswer.getCreateTime(), "yyyy-MM-dd"));
+
+                //表单数据
+                String answer = weFormSurveyAnswer.getAnswer();
+                JSONArray jsonArray = JSON.parseArray(answer);
+                //根据问题编号,将表单分组
+                Map<String, List<Object>> answerList = jsonArray.stream().collect(Collectors.groupingBy(o -> JSON.parseObject(o.toString()).getString("questionNumber")));
+                //遍历问题
+                answerList.forEach((k, v) -> {
+                    if (v.size() > 1) {
+                        //多选框的处理
+                        Object o = v.get(0);
+                        JSONObject jsonObject = JSON.parseObject(o.toString());
+                        String options = jsonObject.getString("options");
+                        String[] split = options.split(",");
+                        StringBuffer defaultValue = new StringBuffer();
+                        for (int i = 0; i < v.size(); i++) {
+                            JSONObject jsonObject1 = JSON.parseObject(v.get(i).toString());
+                            if (i == 0) {
+                                defaultValue.append(split[jsonObject1.getInteger("defaultValue")]);
+                            } else {
+                                defaultValue.append("," + split[jsonObject1.getInteger("defaultValue")]);
+                            }
+                        }
+                        item.add(defaultValue.toString());
+                    } else {
+                        Object o = v.get(0);
+                        JSONObject jsonObject = JSON.parseObject(o.toString());
+
+                        //省市联动
+                        String cascader = "el-cascader";
+                        //日期
+                        String date = "el-date-picker";
+
+                        String tag = jsonObject.getString("tag");
+                        if (tag.equals(cascader)) {
+                            String defaultValue = jsonObject.getString("defaultValue");
+                            if (defaultValue.contains("[") || defaultValue.contains("]")) {
+                                //级联选择
+                                item.add(defaultValue);
+                            } else {
+                                //省市联动处理
+                                String[] split = defaultValue.split(",");
+                                StringBuffer value = new StringBuffer();
+                                for (int i = 0; i < split.length; i++) {
+                                    AjaxResult<SysAreaVo> area = qwSysAreaClient.getAreaById(Integer.valueOf(split[i]));
+                                    if (area != null && area.getCode() == 200 && area.getData() != null) {
+                                        SysAreaVo data = area.getData();
+                                        if (i == 0) {
+                                            value.append(data.getName());
+                                        } else {
+                                            value.append("-" + data.getName());
+                                        }
+                                    }
+                                }
+                                item.add(value.toString());
+                            }
+                        } else if (tag.equals(date)) {
+                            //日期处理
+                            String defaultValue = jsonObject.getString("defaultValue");
+                            DateTime parse = DateUtil.parse(defaultValue);
+                            String format = DateUtil.format(parse, "yyyy-MM-dd");
+                            item.add(format);
+                        } else {
+                            item.add(jsonObject.getString("defaultValue"));
+                        }
+                    }
+                });
+                exportList.add(item);
+            }
+        }
+        HttpServletResponse response = ServletUtils.getResponse();
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode("智能表单答案数据报表", "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+        EasyExcel.write(response.getOutputStream()).head(head).sheet("智能表单答案").doWrite(exportList);
+    }
+
+
+    /**
+     * 表单站点统计初始化
+     *
+     * @author WangYX
+     * @date 2022/10/14 10:59
+     */
+    @PostConstruct
+    public void siteStasInit() {
+        log.info("表单站点统计初始化=>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
+        //所有的表单
+        List<WeFormSurveyCatalogue> weFormSurveyCatalogues = weFormSurveyCatalogueService.getListIgnoreTenantId();
+        //上次的站点统计的数据
+        List<WeFormSurveySiteStas> list = weFormSurveySiteStasService.list();
+        Map<Long, Integer> collect = list.stream().collect(Collectors.toMap(WeFormSurveySiteStas::getBelongId, WeFormSurveySiteStas::getTotalVisits,(key1,key2) -> key1));
+
+        if (weFormSurveyCatalogues != null && weFormSurveyCatalogues.size() > 0) {
+            for (WeFormSurveyCatalogue weFormSurveyCatalogue : weFormSurveyCatalogues) {
+                String channelsName = weFormSurveyCatalogue.getChannelsName();
+                if (StringUtils.isNotBlank(channelsName)) {
+                    String[] split = channelsName.split(",");
+                    for (String channelName : split) {
+                        //PV
+                        String pvKey = StringUtils.format(SiteStasConstants.PREFIX_KEY_PV, weFormSurveyCatalogue.getId(), channelName);
+                        if (!redisTemplate.hasKey(pvKey)) {
+                            redisTemplate.opsForValue().set(pvKey, collect.get(weFormSurveyCatalogue.getId()) != null ? collect.get(weFormSurveyCatalogue.getId()) : 0);
+                        }
+                        //IP
+                        String ipKey = StringUtils.format(SiteStasConstants.PREFIX_KEY_IP, weFormSurveyCatalogue.getId(), channelName);
+                        if (!redisTemplate.hasKey(ipKey)) {
+                            redisTemplate.opsForSet().add(ipKey, "");
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 151 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeGroupChatController.java

@@ -0,0 +1,151 @@
+package com.linkwechat.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.constant.SynchRecordConstants;
+import com.linkwechat.common.constant.WeConstans;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.domain.entity.SysUser;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.utils.SecurityUtils;
+import com.linkwechat.domain.WeCustomerTrajectory;
+import com.linkwechat.domain.WeGroupMember;
+import com.linkwechat.domain.groupchat.query.WeGroupChatQuery;
+import com.linkwechat.domain.groupchat.query.WeMakeGroupTagQuery;
+import com.linkwechat.domain.groupchat.vo.LinkGroupChatListVo;
+import com.linkwechat.service.*;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author danmo
+ * @description 客户群管理
+ * @date 2021/11/12 18:22
+ **/
+
+@RestController
+@RequestMapping("groupchat")
+@Api(tags = "客户群管理")
+public class WeGroupChatController extends BaseController {
+
+    @Autowired
+    private IWeGroupService weGroupService;
+
+    @Autowired
+    private IWeGroupMemberService weGroupMemberService;
+
+    @Autowired
+    private IWeGroupTagRelService weGroupTagRelService;
+
+    @Autowired
+    private IWeSynchRecordService iWeSynchRecordService;
+
+
+    @Autowired
+    private IWeCustomerTrajectoryService iWeCustomerTrajectoryService;
+
+
+    @Autowired
+    private IWeCustomerService iWeCustomerService;
+
+    /**
+     * 客户群列表
+     * @param query
+     * @return
+     */
+    @GetMapping("/page/list")
+    public TableDataInfo<LinkGroupChatListVo> getPageList(WeGroupChatQuery query) {
+        startPage();
+        TableDataInfo dataTable = getDataTable(weGroupService.getPageList(query));
+
+        dataTable.setLastSyncTime(
+                iWeSynchRecordService.findUpdateLatestTime(SynchRecordConstants.SYNCH_CUSTOMER_GROUP)
+        );//最近同步时间
+
+        return dataTable;
+    }
+
+
+    /**
+     * 应用客户群列表
+     * @param query
+     * @return
+     */
+    @GetMapping("/page/listByApp")
+    public TableDataInfo<LinkGroupChatListVo> getPageListByApp(WeGroupChatQuery query) {
+        startPage();
+
+        SysUser sysUser
+                = iWeCustomerService.findCurrentSysUserInfo(SecurityUtils.getUserId());
+
+        if(!query.isDataScope()){//个人数据
+            query.setUserIds(sysUser.getWeUserId());
+            return getDataTable(weGroupService.selectWeGroupListByApp(query));
+        }
+
+        //全部数据(根据设定角色的数据权限范围来)
+        return getDataTable(weGroupService.getPageList(query));
+    }
+
+
+    /**
+     * 客户群详情
+     * @param chatId
+     * @return
+     */
+    @GetMapping("/get/{chatId}")
+    public AjaxResult<LinkGroupChatListVo> getInfo(@PathVariable("chatId") String chatId) {
+        return AjaxResult.success(weGroupService.getInfo(chatId));
+    }
+
+
+    /**
+     * 客户群成员列表
+     * @param weGroupMember
+     * @return
+     */
+    @GetMapping({"/member/page/list"})
+    public TableDataInfo<List<WeGroupMember>> pageList(WeGroupMember weGroupMember) {
+        startPage();
+        List<WeGroupMember> list = this.weGroupMemberService.getPageList(weGroupMember);
+        return getDataTable(list);
+    }
+
+    /**
+     * 同步客户群
+     * @return
+     */
+    @GetMapping("/synch")
+    public AjaxResult synchWeGroup() {
+        weGroupService.synchWeGroup();
+        return AjaxResult.success(WeConstans.SYNCH_TIP);
+    }
+
+    /**
+     * 编辑群标签
+     * @return
+     */
+    @PostMapping("/makeGroupTag")
+    public AjaxResult makeGroupTag(@RequestBody WeMakeGroupTagQuery query){
+        weGroupTagRelService.makeGroupTag(query);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 获取指定群相关轨迹
+     * @param chatId
+     * @return
+     */
+    @GetMapping("/findGroupTrajectory/{chatId}")
+    public TableDataInfo<WeCustomerTrajectory> findGroupTrajectory(@PathVariable String chatId){
+        startPage();
+        return getDataTable(
+                iWeCustomerTrajectoryService.list(new LambdaQueryWrapper<WeCustomerTrajectory>()
+                        .eq(WeCustomerTrajectory::getExternalUseridOrChatid,chatId))
+        );
+    }
+}

+ 189 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeGroupCodeController.java

@@ -0,0 +1,189 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.exception.CustomException;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.common.utils.file.FileUtils;
+import com.linkwechat.domain.groupcode.entity.WeGroupCode;
+import com.linkwechat.domain.groupcode.vo.WeGroupChatInfoVo;
+import com.linkwechat.domain.groupcode.vo.WeGroupCodeCountTrendVo;
+import com.linkwechat.service.IWeGroupCodeService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * 客户群活码Controller
+ *
+ * @author ruoyi
+ * @date 2020-10-07
+ */
+@Slf4j
+@RestController
+@RequestMapping("/groupCode")
+public class WeGroupCodeController extends BaseController {
+
+    @Autowired
+    private IWeGroupCodeService groupCodeService;
+
+
+
+    /**
+     * 查询客户群活码列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo<WeGroupCode> list(WeGroupCode weGroupCode) {
+        startPage();
+        List<WeGroupCode> list = groupCodeService.selectWeGroupCodeList(weGroupCode);
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 获取客群详情
+     * @param groupCodeId
+     * @return
+     */
+    @GetMapping("/findWeGroupCodeById/{groupCodeId}")
+    public AjaxResult<WeGroupCode> findWeGroupCodeById(@PathVariable Long groupCodeId){
+
+        return AjaxResult.success(
+                groupCodeService.findWeGroupCodeById(groupCodeId)
+        );
+    }
+
+
+    /**
+     * 获取群活码详情
+     * @param id
+     * @return
+     */
+    @GetMapping("/getActualCode/{id}")
+    public AjaxResult<WeGroupCode> getWeGroupCode(@PathVariable String id){
+        return AjaxResult.success(
+                groupCodeService.getById(id)
+        );
+    }
+
+    /**
+     * 批量下载群活码
+     */
+    @GetMapping("/downloadBatch")
+    public void downloadBatch(String ids, HttpServletResponse response) {
+
+        // 构建文件信息列表
+        List<FileUtils.FileEntity> fileList = Arrays.stream(Optional.ofNullable(ids).orElse("").split(","))
+                .filter(StringUtils::isNotEmpty).map(id -> {
+                    WeGroupCode code = groupCodeService.getById(id);
+                    return FileUtils.FileEntity.builder()
+                            .fileName(code.getActivityName())
+                            .url(code.getCodeUrl())
+                            .suffix(".jpg")
+                            .build();
+                }).collect(Collectors.toList());
+        try {
+            FileUtils.batchDownloadFile(fileList, response.getOutputStream());
+        } catch (IOException e) {
+            log.error("群活码批量下载失败:"+e.getMessage());
+            throw new CustomException("群活码批量下载失败");
+        }
+    }
+
+    /**
+     * 下载群活码
+     */
+    @GetMapping("/download")
+    public void download(String id, HttpServletResponse response) {
+        WeGroupCode weGroupCode = groupCodeService.getById(Long.valueOf(id));
+        try {
+            FileUtils.downloadFile(weGroupCode.getCodeUrl(), response.getOutputStream());
+        } catch (IOException e) {
+            log.error("群活码下载失败:"+e.getMessage());
+            throw new CustomException("群活码下载失败");
+        }
+    }
+
+
+    /**
+     * 新增客户群活码
+     */
+    @Log(title = "客户群活码", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult<WeGroupCode> add(@Validated @RequestBody WeGroupCode weGroupCode) {
+
+        groupCodeService.insertWeGroupCode(weGroupCode);
+        return AjaxResult.success(
+                weGroupCode
+        );
+    }
+
+    /**
+     * 修改客户群活码
+     */
+    @Log(title = "客户群活码", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult<WeGroupCode> edit(@Validated @RequestBody WeGroupCode weGroupCode) {
+
+
+        return AjaxResult.success(
+                groupCodeService
+                        .updateWeGroupCode(weGroupCode)
+        );
+    }
+
+    /**
+     * 删除客户群活码
+     */
+    @Log(title = "客户群活码", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult batchRemove(@PathVariable Long[] ids) {
+        groupCodeService.batchRemoveByIds(Arrays.asList(ids));
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 获取指定活码下,群相关信息
+     * @return
+     */
+    @GetMapping("/findWeGroupChatInfos/{groupId}")
+    public AjaxResult<WeGroupChatInfoVo> findWeGroupChatInfoVos(@PathVariable Long groupId){
+        return AjaxResult.success(
+                groupCodeService.findWeGroupChatInfoVos(groupId)
+        );
+    }
+
+
+    /**
+     *  获取指定活码加群退群指定时间段内相关数据
+     * @param state
+     * @param beginTime
+     * @param endTime
+     * @return
+     * @throws ParseException
+     */
+    @GetMapping("/findWeGroupCodeCountTrend")
+    public AjaxResult<WeGroupCodeCountTrendVo> findWeGroupCodeCountTrend(String state, String beginTime, String endTime) throws ParseException {
+
+        return AjaxResult.success(
+                groupCodeService.findWeGroupCodeCountTrend(state,beginTime,endTime)
+        );
+    }
+
+
+
+
+}

+ 113 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeGroupMessageTemplateController.java

@@ -0,0 +1,113 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.domain.WeGroupMessageSendResult;
+import com.linkwechat.domain.WeGroupMessageTask;
+import com.linkwechat.domain.WeGroupMessageTemplate;
+import com.linkwechat.domain.groupmsg.query.WeAddGroupMessageQuery;
+import com.linkwechat.domain.groupmsg.vo.WeGroupMessageDetailVo;
+import com.linkwechat.service.IWeGroupMessageTemplateService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 群发消息模板Controller
+ *
+ * @author ruoyi
+ * @date 2021-10-27
+ */
+@Api(tags = "群发消息管理")
+@RestController
+@RequestMapping("/groupmsg/template")
+public class WeGroupMessageTemplateController extends BaseController {
+
+    @Autowired
+    private IWeGroupMessageTemplateService iWeGroupMessageTemplateService;
+
+    /**
+     * 查询群发消息模板列表
+     */
+    ////@PreAuthorize("@ss.hasPermi('linkwechat:template:list')")
+    @ApiOperation(value = "查询群发消息模板列表", httpMethod = "GET")
+    @GetMapping("/list")
+    public TableDataInfo<WeGroupMessageTemplate> list(WeGroupMessageTemplate weGroupMessageTemplate) {
+        startPage();
+        List<WeGroupMessageTemplate> list = iWeGroupMessageTemplateService.queryList(weGroupMessageTemplate);
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 获取群发消息模板详细信息
+     */
+    ////@PreAuthorize("@ss.hasPermi('linkwechat:template:query')")
+    @ApiOperation(value = "获取群发消息模板详细信息", httpMethod = "GET")
+    @GetMapping(value = "/{id}")
+    public AjaxResult<WeGroupMessageDetailVo> getGroupMsgTemplateDetail(@PathVariable("id") Long id) {
+        return AjaxResult.success(iWeGroupMessageTemplateService.getGroupMsgTemplateDetail(id));
+    }
+
+    /**
+     * 新增群发消息模板
+     */
+    ////@PreAuthorize("@ss.hasPermi('linkwechat:template:add')")
+    @ApiOperation(value = "新增群发消息模板", httpMethod = "POST")
+    @PostMapping("/add")
+    public AjaxResult addGroupMsgTemplate(@RequestBody WeAddGroupMessageQuery query){
+        iWeGroupMessageTemplateService.addGroupMsgTemplate(query);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 同步消息发送结果
+     */
+    ////@PreAuthorize("@ss.hasPermi('linkwechat:template:remove')")
+    @ApiOperation(value = "同步消息发送结果", httpMethod = "GET")
+    @GetMapping("/sync/{ids}")
+    public AjaxResult sync(@PathVariable Long[] ids) {
+        iWeGroupMessageTemplateService.syncGroupMsgSendResultByIds(Arrays.asList(ids));
+        return AjaxResult.success();
+    }
+
+    /**
+     * 取消定时发送
+     */
+    ////@PreAuthorize("@ss.hasPermi('linkwechat:template:remove')")
+    @ApiOperation(value = "取消定时发送", httpMethod = "GET")
+    @GetMapping("/cancel/{ids}")
+    public AjaxResult cancel(@PathVariable Long[] ids) {
+        iWeGroupMessageTemplateService.cancelByIds(Arrays.asList(ids));
+        return AjaxResult.success();
+    }
+
+    /**
+     * 群发成员发送任务列表
+     */
+    ////@PreAuthorize("@ss.hasPermi('linkwechat:template:remove')")
+    @ApiOperation(value = "群发成员发送任务列表", httpMethod = "GET")
+    @GetMapping("/task/list")
+    public TableDataInfo<WeGroupMessageTask> groupMsgTaskList(WeGroupMessageTask task) {
+        startPage();
+        List<WeGroupMessageTask> messageTaskList = iWeGroupMessageTemplateService.groupMsgTaskList(task);
+        return getDataTable(messageTaskList);
+    }
+
+    /**
+     * 群发成员发送任务列表
+     */
+    ////@PreAuthorize("@ss.hasPermi('linkwechat:template:remove')")
+    @ApiOperation(value = "群发成员发送任务列表", httpMethod = "GET")
+    @GetMapping("/send/result/list")
+    public TableDataInfo<WeGroupMessageSendResult> groupMsgSendResultList(WeGroupMessageSendResult sendResult) {
+        startPage();
+        List<WeGroupMessageSendResult> sendResultList = iWeGroupMessageTemplateService.groupMsgSendResultList(sendResult);
+        return getDataTable(sendResultList);
+    }
+}

+ 94 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeGroupRobotController.java

@@ -0,0 +1,94 @@
+package com.linkwechat.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.domain.WeGroupRobotInfo;
+import com.linkwechat.domain.WeGroupRobotMsg;
+import com.linkwechat.domain.robot.query.WeRobotAddQuery;
+import com.linkwechat.domain.robot.query.WeRobotMsgAddQuery;
+import com.linkwechat.domain.robot.query.WeRobotMsgListQuery;
+import com.linkwechat.domain.robot.query.WeRobotQuery;
+import com.linkwechat.service.IWeGroupRobotInfoService;
+import com.linkwechat.service.IWeGroupRobotMsgService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author danmo
+ * @description 群机器人管理
+ * @date 2022/11/03 18:22
+ **/
+@Slf4j
+@RestController
+@RequestMapping(value = "group/robot")
+@Api(tags = "群机器人管理")
+public class WeGroupRobotController extends BaseController {
+
+    @Autowired
+    private IWeGroupRobotInfoService weGroupRobotInfoService;
+
+    @Autowired
+    private IWeGroupRobotMsgService weGroupRobotMsgService;
+
+
+    @ApiOperation(value = "新增群机器人", httpMethod = "POST")
+    @PostMapping("/add")
+    public AjaxResult addRobot(@RequestBody @Validated WeRobotAddQuery query) {
+        log.info("新增群机器人入参query:{}", JSONObject.toJSONString(query));
+        Long id = weGroupRobotInfoService.addGroupRobot(query);
+        return AjaxResult.success(id);
+    }
+
+    @ApiOperation(value = "编辑群机器人", httpMethod = "PUT")
+    @PutMapping("/update/{id}")
+    public AjaxResult updateRobot(@PathVariable("id") Long id, @Validated @RequestBody WeRobotAddQuery query) {
+        query.setId(id);
+        log.info("编辑群机器人入参query:{}", JSONObject.toJSONString(query));
+        weGroupRobotInfoService.updateRobot(query);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "获取机器人列表", httpMethod = "GET")
+    @GetMapping("/list")
+    public TableDataInfo<WeGroupRobotInfo> getList(WeRobotQuery query) {
+        log.info("获取机器人列表接口");
+        super.startPage();
+        List<WeGroupRobotInfo> list = weGroupRobotInfoService.getList(query);
+        return getDataTable(list);
+    }
+
+
+    @ApiOperation(value = "删除机器人", httpMethod = "DELETE")
+    @DeleteMapping("/delete/{id}")
+    public AjaxResult deleteGroupRobot(@PathVariable("id") Long id) {
+        log.info("删除机器人入参query:{}", id);
+        weGroupRobotInfoService.deleteGroupRobot(id);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "获取机器人历史消息列表", httpMethod = "GET")
+    @GetMapping("/msg/list")
+    public TableDataInfo<WeGroupRobotMsg> getMsgList(WeRobotMsgListQuery query) {
+        log.info("获取机器人历史消息列表入参query:{}", JSONObject.toJSONString(query));
+        super.startPage();
+        List<WeGroupRobotMsg> list = weGroupRobotMsgService.getMsgList(query);
+        return getDataTable(list);
+    }
+
+    @ApiOperation(value = "发送群机器人消息", httpMethod = "POST")
+    @PostMapping("/msg/add")
+    public AjaxResult addRobotMsg(@Validated @RequestBody WeRobotMsgAddQuery query) {
+        log.info("发送群机器人消息入参query:{}", JSONObject.toJSONString(query));
+        weGroupRobotMsgService.addRobotMsg(query);
+        return AjaxResult.success();
+    }
+
+}

+ 138 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeIndexController.java

@@ -0,0 +1,138 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.io.resource.ResourceUtil;
+import cn.hutool.core.net.Ipv4Util;
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.nacos.common.utils.IPUtil;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.annotation.ShortLinkView;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.redis.RedisService;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.exception.wecom.WeComException;
+import com.linkwechat.common.utils.Base62NumUtil;
+import com.linkwechat.common.utils.ServletUtils;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.common.utils.ip.IpUtils;
+import com.linkwechat.domain.index.vo.WeIndexVo;
+import com.linkwechat.domain.operation.vo.WeCustomerAnalysisVo;
+import com.linkwechat.domain.operation.vo.WeGroupAnalysisVo;
+import com.linkwechat.domain.shortlink.vo.WeShortLinkVo;
+import com.linkwechat.service.*;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+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.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.ParseException;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 首页相关
+ */
+@Api(tags = "首页管理")
+@Slf4j
+@RestController
+public class WeIndexController {
+    @Autowired
+    private IWeCustomerService iWeCustomerService;
+
+    @Autowired
+    private IWeCorpAccountService iWeCorpAccountService;
+
+    @Autowired
+    private IWeOperationCenterService weOperationCenterService;
+
+    @Autowired
+    private IWeSynchRecordService iWeSynchRecordService;
+
+    @Autowired
+    private IWeShortLinkService weShortLinkService;
+
+
+    @Autowired
+    private RedisService redisService;
+
+    /**
+     * 系统首页相关基础数据获取
+     * @return
+     * @throws ParseException
+     */
+    @GetMapping("/getWeIndex")
+    public AjaxResult<WeIndexVo> getWeIndex() throws ParseException {
+        WeIndexVo weIndexVo=new WeIndexVo();
+        weIndexVo.setCurrentEdition("标准版");
+        weIndexVo.setUserNumbers(iWeCustomerService.findAllSysUser().size());
+
+        WeCustomerAnalysisVo customerAnalysis = weOperationCenterService.getCustomerAnalysis();
+        if(null != customerAnalysis){
+            weIndexVo.setCustomerTotalNumber(
+                    customerAnalysis.getTotalCnt()
+            );
+        }
+
+        WeGroupAnalysisVo groupAnalysis = weOperationCenterService.getGroupAnalysis();
+        if(null != groupAnalysis){
+            weIndexVo.setGroupTotalNumber(
+                    groupAnalysis.getTotalCnt()
+            );
+            weIndexVo.setGroupMemberTotalNumber(
+                    groupAnalysis.getMemberTotalCnt()
+            );
+        }
+
+        weIndexVo.setSynchTime(DateUtil.date());
+
+        return AjaxResult.success(weIndexVo);
+    }
+
+    @ShortLinkView(prefix = "t:")
+    @ApiOperation(value = "短链换取长链", httpMethod = "GET")
+    @GetMapping("/t/{shortUrl}")
+    public void getShort2LongUrl(HttpServletRequest request, HttpServletResponse resp, @PathVariable("shortUrl") String shortUrl) throws IOException {
+        log.info("短链换取长链 shortUrl:{}",shortUrl);
+        if(StringUtils.isEmpty(shortUrl)){
+            return;
+        }
+        String ipAddr = IpUtils.getIpAddr(request);
+        String key = "we:t:shortUrl:"+ipAddr + ":" +shortUrl;
+
+        JSONObject short2LongUrl = new JSONObject();
+        //判断键是否存在
+        Boolean hasKey = redisService.hasKey(key);
+        if(hasKey){
+            short2LongUrl = (JSONObject) redisService.getCacheObject(key);
+        }else {
+            //尝试加锁
+            Boolean lock = redisService.tryLock(key, "lock", 2L);
+            if(lock){
+                short2LongUrl = weShortLinkService.getShort2LongUrl(shortUrl);
+                if(StringUtils.isNotEmpty(short2LongUrl.getString("errorMsg"))){
+                    redisService.setCacheObject(key,short2LongUrl,5, TimeUnit.MINUTES);
+                }else {
+                    redisService.setCacheObject(key,short2LongUrl,1, TimeUnit.DAYS);
+                }
+                //释放锁
+                redisService.unLock(key, "lock");
+            }else {
+                throw new WeComException("操作过于频繁,请稍后再试");
+            }
+        }
+        String result = ResourceUtil.readUtf8Str("templates/jump.html");
+        resp.setHeader("Content-Type", "text/html; charset=utf-8");
+        resp.setStatus(HttpServletResponse.SC_OK);
+        PrintWriter writer = resp.getWriter();
+        writer.write(StringUtils.format(result,short2LongUrl.toJSONString()).toCharArray());
+        writer.close();
+    }
+}

+ 122 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeLeaveUserController.java

@@ -0,0 +1,122 @@
+package com.linkwechat.controller;
+
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.constant.SynchRecordConstants;
+import com.linkwechat.common.constant.WeConstans;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.CorpUserEnum;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.domain.*;
+import com.linkwechat.service.IWeLeaveUserService;
+import com.linkwechat.service.IWeSynchRecordService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import java.util.List;
+
+
+/**
+ * 离职继承相关员工
+ *
+ * @author ruoyi
+ * @date 2020-08-31
+ */
+@RestController
+@RequestMapping("/leaveUser")
+public class WeLeaveUserController extends BaseController {
+
+    @Autowired
+    private IWeLeaveUserService iWeLeaveUserService;
+
+    @Autowired
+    private IWeSynchRecordService iWeSynchRecordService;
+
+    /**
+     * 离职员工列表
+     *
+     * @param weLeaveUser
+     * @return
+     */
+    @GetMapping({"/leaveUserList"})
+    public TableDataInfo<List<SysLeaveUser>> leaveUserList(SysLeaveUser weLeaveUser) {
+        startPage();
+        List<SysLeaveUser> sysLeaveUsers = iWeLeaveUserService.list(new LambdaQueryWrapper<SysLeaveUser>()
+                        .eq(weLeaveUser.getIsAllocate()!=null,SysLeaveUser::getIsAllocate,weLeaveUser.getIsAllocate())
+                .like(StringUtils.isNotEmpty(weLeaveUser.getUserName()),SysLeaveUser::getUserName,weLeaveUser.getUserName())
+                .apply(StringUtils.isNotEmpty(weLeaveUser.getBeginTime())&&StringUtils.isNotEmpty(weLeaveUser.getEndTime()),
+                        "date_format(dimission_time,'%Y-%m-%d') BETWEEN '"+
+                                weLeaveUser.getBeginTime()
+                                +"' AND '"+
+                                weLeaveUser.getEndTime()+"'")
+                .orderByDesc(SysLeaveUser::getDimissionTime));
+        TableDataInfo dataTable = getDataTable(sysLeaveUsers);
+        dataTable.setLastSyncTime(
+                iWeSynchRecordService.findUpdateLatestTime(SynchRecordConstants.SYNCH_LEAVE_USER)
+        );//最近同步时间
+
+        return dataTable;
+    }
+
+
+
+
+    /**
+     * 离职分配
+     *
+     * @param weLeaveUserInfoAllocate
+     * @return
+     */
+    @PutMapping({"/allocateLeaveUserAboutData"})
+    public AjaxResult allocateLeaveUserAboutData(@RequestBody WeLeaveUserInfoAllocate weLeaveUserInfoAllocate) {
+
+        iWeLeaveUserService.allocateLeaveUserAboutData(weLeaveUserInfoAllocate);
+
+        return AjaxResult.success("离职分配成功");
+    }
+
+
+    /**
+     * 获取历史分配记录的成员
+     *
+     * @param weAllocateCustomers
+     * @return
+     */
+    @GetMapping({"/getAllocateCustomers"})
+    public TableDataInfo<List<WeAllocateCustomer>> getAllocateCustomers(WeAllocateCustomer weAllocateCustomers) {
+        startPage();
+        List<WeAllocateCustomer> list = iWeLeaveUserService.getAllocateCustomers(weAllocateCustomers);
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 获取历史分配记录的群
+     *
+     * @param weAllocateGroups
+     * @return
+     */
+    @GetMapping({"/getAllocateGroups"})
+    public TableDataInfo<List<WeAllocateGroups>> getAllocateGroups(WeAllocateGroups weAllocateGroups) {
+        startPage();
+        List<WeAllocateGroups> list = iWeLeaveUserService.getAllocateGroups(weAllocateGroups);
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 离职员工相关同步
+     * @return
+     */
+    @GetMapping("/synchLeaveUser")
+    public AjaxResult synchLeaveUser(){
+
+        iWeLeaveUserService.synchLeaveSysUser();
+
+        return AjaxResult.success();
+    }
+
+
+
+}

+ 307 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeLiveController.java

@@ -0,0 +1,307 @@
+package com.linkwechat.controller;
+
+
+import com.alibaba.excel.EasyExcel;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.utils.ServletUtils;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.common.utils.poi.LwExcelUtil;
+import com.linkwechat.domain.kf.vo.WeKfRecordListVo;
+import com.linkwechat.domain.live.WeLive;
+import com.linkwechat.domain.live.WeLiveWatchUser;
+import com.linkwechat.domain.live.vo.WeLinveUserVo;
+import com.linkwechat.fegin.QwSysUserClient;
+import com.linkwechat.service.IWeLiveService;
+import com.linkwechat.service.IWeLiveTipService;
+import com.linkwechat.service.IWeLiveWatchUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.List;
+
+
+/**
+ * 直播相关
+ */
+@RestController
+@RequestMapping("/live")
+public class WeLiveController extends BaseController {
+
+    @Autowired
+    IWeLiveService iWeLiveService;
+
+    @Autowired
+    IWeLiveTipService iWeLiveTipService;
+
+    @Autowired
+    IWeLiveWatchUserService iWeLiveWatchUserService;
+
+    @Autowired
+    QwSysUserClient qwSysUserClient;
+
+
+    /**
+     * 新增或更新直播
+     * @param weLive
+     * @return
+     */
+    @PostMapping("/addOrUpdate")
+    public AjaxResult  addOrUpdate(@RequestBody WeLive weLive){
+        iWeLiveService.addOrUpdate(weLive);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 获取直播列表
+     * @param weLive
+     * @return
+     */
+    @GetMapping("/findWelives")
+    public TableDataInfo findWelives(WeLive weLive){
+
+        startPage();
+        return getDataTable(iWeLiveService.findLives(weLive));
+    }
+
+
+    /**
+     * 获取直播详情
+     * @return
+     */
+    @GetMapping("/findLiveDetail/{weLiveId}")
+    public AjaxResult findLiveDetail(@PathVariable String weLiveId){
+
+        return AjaxResult.success(
+                iWeLiveService.findLiveDetail(weLiveId)
+        );
+
+    }
+
+
+    /**
+     * 通过id列表删除
+     *
+     * @param ids id列表
+     * @return 结果
+     */
+    @DeleteMapping(path = "/{ids}")
+    public AjaxResult batchDeleteLiwes(@PathVariable("ids") Long[] ids) {
+        iWeLiveService.removeLives(Arrays.asList(ids));
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 取消直播
+     * @param live
+     * @return
+     */
+    @PostMapping("/cancelLive")
+    public AjaxResult cancelLive(@RequestBody WeLive live){
+        iWeLiveService.cancleLive(live);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 同步直播
+     * @return
+     */
+    @GetMapping("/synchLive")
+    public AjaxResult synchLive(){
+
+        iWeLiveService.synchLive();
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 导出直播
+     * @return
+     */
+    @GetMapping("/exportWeLives")
+    public void exportWeLives(){
+
+
+
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeLive.class, iWeLiveService.findLives(new WeLive()),"直播"
+        );
+
+    }
+
+
+    /**
+     *  分享统计-tab(客户或客群维度) (客户或客群维度 1:客户 2:客群)
+     * @param liveId
+     * @return
+     */
+    @GetMapping("/findWeLiveTaskCustomerDetailTab")
+    public AjaxResult findWeLiveTaskCustomerDetailTab(String liveId){
+        WeLive weLive = iWeLiveService.getById(liveId);
+        if(null != weLive){
+            return AjaxResult.success(
+                    iWeLiveTipService.findWeLiveTaskExecuteUserDetailTab(liveId,weLive.getTargetType())
+            );
+        }
+
+        return AjaxResult.success();
+    }
+    /**
+     *  分享统计(员工维度)-tab
+     * @param liveId
+     * @return
+     */
+    @GetMapping("findWeLiveTaskUserDetailTab")
+    public AjaxResult findWeLiveTaskUserDetailTab(String liveId){
+
+        WeLive weLive = iWeLiveService.getById(liveId);
+        if(null != weLive){
+            return AjaxResult.success(
+                    iWeLiveTipService.findWeLiveTaskUserDetailTab(liveId,weLive.getTargetType())
+            );
+        }
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     *  分享统计-员工详情(客户或客群)
+     * @param userName 员工名称
+     * @param liveId 直播id
+     * @param sendTargetType 目标类型  发送目标1:客户;2:客群
+     * @return
+     */
+    @GetMapping("/findWeLiveTaskUserDetail")
+    public TableDataInfo findWeLiveTaskUserDetail(String userName, String liveId,@RequestParam(defaultValue = "1") Integer sendTargetType){
+          startPage();
+
+          return getDataTable(
+                  iWeLiveTipService.findWeLiveTaskUserDetail(userName, liveId, sendTargetType)
+          );
+
+    }
+
+
+    /**
+     * 分享统计-客户详情(客户或客群)
+     * @param sendTargetType 目标类型  发送目标1:客户;2:客群
+     * @param userName 员工名称
+     * @param liveId 直播id
+     * @param sendState 直播状态(0:预约中;1:直播中;2:已结束;3:已过期;4:已取消)
+     * @return
+     */
+    @GetMapping("/findWeLiveTaskCustomerDetail")
+    public TableDataInfo findWeLiveTaskCustomerDetail(Integer sendTargetType,String userName,String liveId,Integer sendState){
+        startPage();
+
+        return getDataTable(
+                iWeLiveTipService.findWeLiveTaskCustomerDetail(sendTargetType,userName,liveId,sendState)
+        );
+    }
+
+
+    /**
+     * 获取观众列表
+     * @param weLiveWatchUser
+     * @return
+     */
+    @GetMapping("/weLiveWatchUser")
+    public TableDataInfo  findVisitorList(WeLiveWatchUser weLiveWatchUser){
+        startPage();
+        return getDataTable(
+                iWeLiveWatchUserService.list(
+                        new LambdaQueryWrapper<WeLiveWatchUser>()
+                                .eq(weLiveWatchUser.getLiveId() !=null,WeLiveWatchUser::getLiveId,weLiveWatchUser.getLiveId())
+                                .eq(weLiveWatchUser.getWatchUserType()!=null,WeLiveWatchUser::getWatchUserType,weLiveWatchUser.getWatchUserType())
+
+                )
+        );
+    }
+
+    /**
+     * 获取直播凭证
+     * @param livingId
+     * @param openId
+     * @return
+     */
+    @GetMapping("/getLivingCode")
+    public AjaxResult  getLivingCode(String livingId,String openId){
+        WeLinveUserVo weLinveUserVo=new WeLinveUserVo();
+
+        WeLive weLive = iWeLiveService.getById(livingId);
+
+        if(null != weLive){
+            weLinveUserVo.setLiveCode(iWeLiveService.getLivingCode(weLive.getLivingId(), openId));
+        }
+
+
+        return AjaxResult.success(
+                weLinveUserVo
+        );
+    }
+
+    /**
+     * 导出观看成员
+     * @param watchUserType
+     * @return
+     */
+    @GetMapping("/exprotWeLiveWatchUser")
+    public AjaxResult exprotWeLiveWatchUser(Integer watchUserType,String liveId){
+
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeLiveWatchUser.class,
+                iWeLiveWatchUserService.list(
+                        new LambdaQueryWrapper<WeLiveWatchUser>()
+                                .eq(StringUtils.isNotEmpty(liveId),WeLiveWatchUser::getLiveId,liveId)
+                                .eq(watchUserType != null, WeLiveWatchUser::getWatchUserType, watchUserType)
+
+                )
+                ,"观看成员"
+        );
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 同步发送结果
+     * @param id
+     * @return
+     */
+    @GetMapping("/synchExecuteResult/{id}")
+    public AjaxResult synchExecuteResult(@PathVariable String id){
+        iWeLiveService.synchExecuteResult(id);
+
+        return AjaxResult.success();
+
+    }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}

+ 151 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeLxQrCodeController.java

@@ -0,0 +1,151 @@
+package com.linkwechat.controller;
+
+import com.alibaba.excel.EasyExcel;
+import com.github.pagehelper.PageInfo;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.utils.ServletUtils;
+import com.linkwechat.domain.qr.query.WeLxQrAddQuery;
+import com.linkwechat.domain.qr.query.WeLxQrCodeListQuery;
+import com.linkwechat.domain.qr.query.WeLxQrCodeQuery;
+import com.linkwechat.domain.qr.vo.*;
+import com.linkwechat.service.IWeLxQrCodeService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.List;
+
+/**
+ * @author danmo
+ * @description 拉新活码管理
+ * @date 2023/03/06 18:22
+ **/
+
+@RestController
+@RequestMapping(value = "lxqr")
+@Api(tags = "拉新活码管理")
+public class WeLxQrCodeController extends BaseController {
+
+    @Autowired
+    private IWeLxQrCodeService weLxQrCodeService;
+
+    @ApiOperation(value = "新增拉新活码", httpMethod = "POST")
+    @PostMapping("/add")
+    public AjaxResult<WeLxQrAddVo> addQrCode(@RequestBody @Validated WeLxQrAddQuery query) {
+        Long id = weLxQrCodeService.addQrCode(query);
+        WeLxQrAddVo lxQrCode = weLxQrCodeService.generateQrCode(id,query.getQrType());
+        return AjaxResult.success(lxQrCode);
+    }
+
+    @ApiOperation(value = "修改活码", httpMethod = "POST")
+    @PutMapping("/update/{id}")
+    public AjaxResult updateQrCode(@PathVariable("id") Long id, @RequestBody @Validated WeLxQrAddQuery query) {
+        query.setQrId(id);
+        weLxQrCodeService.updateQrCode(query);
+        return AjaxResult.success();
+    }
+
+
+    @ApiOperation(value = "删除拉新活码", httpMethod = "DELETE")
+    @DeleteMapping("/del/{ids}")
+    public AjaxResult<WeQrCodeDetailVo> delQrCode(@PathVariable("ids") List<Long> ids) {
+        weLxQrCodeService.delQrCode(ids);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "获取拉新活码详情", httpMethod = "GET")
+    @GetMapping("/get/{id}")
+    public AjaxResult<WeLxQrCodeDetailVo> getQrDetail(@PathVariable("id") Long id) {
+        WeLxQrCodeDetailVo qrDetail = weLxQrCodeService.getQrDetail(id);
+        return AjaxResult.success(qrDetail);
+    }
+
+    @ApiOperation(value = "获取活码列表", httpMethod = "GET")
+    @GetMapping("/list")
+    public TableDataInfo<WeLxQrCodeListVo> getQrCodeList(WeLxQrCodeListQuery query) {
+        startPage();
+        PageInfo<WeLxQrCodeListVo> qrCodeList = weLxQrCodeService.getQrCodeList(query);
+        return getDataTable(qrCodeList);
+    }
+
+
+    @ApiOperation(value = "获取活码折线统计", httpMethod = "GET")
+    @GetMapping("/line/statistics")
+    public AjaxResult<WeLxQrCodeLineVo> getWeQrCodeLineStatistics(WeLxQrCodeListQuery query) {
+        weLxQrCodeService.statisticsParamsCheck(query);
+        WeLxQrCodeLineVo weQrCodeScanCount = weLxQrCodeService.getWeQrCodeLineStatistics(query);
+        return AjaxResult.success(weQrCodeScanCount);
+    }
+
+    @ApiOperation(value = "获取活码列表统计", httpMethod = "GET")
+    @GetMapping("/list/statistics")
+    public TableDataInfo<WeLxQrCodeSheetVo> getWeQrCodeListStatistics(WeLxQrCodeListQuery query) {
+        weLxQrCodeService.statisticsParamsCheck(query);
+        startPage();
+        List<WeLxQrCodeSheetVo> list = weLxQrCodeService.getWeQrCodeListStatistics(query);
+        return getDataTable(list);
+    }
+
+    @ApiOperation(value = "导出活码列表统计", httpMethod = "GET")
+    @GetMapping("/list/statistics/export")
+    public void qrCodeListStatisticsExport(WeLxQrCodeListQuery query) throws IOException {
+        List<WeLxQrCodeSheetVo> list = weLxQrCodeService.getWeQrCodeListStatistics(query);
+        HttpServletResponse response = ServletUtils.getResponse();
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode("活码列表统计", "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+        EasyExcel.write(response.getOutputStream(), WeLxQrCodeSheetVo.class).sheet("活码列表统计").doWrite(list);
+    }
+
+
+    @ApiOperation(value = "领取总数统计", httpMethod = "GET")
+    @GetMapping("/receive/statistics")
+    public AjaxResult<WeLxQrCodeReceiveVo> receiveStatistics(WeLxQrCodeQuery query) {
+        WeLxQrCodeReceiveVo receiveStatistics = weLxQrCodeService.receiveStatistics(query);
+        return AjaxResult.success(receiveStatistics);
+    }
+
+    @ApiOperation(value = "领取红包个数统计(折线图)", httpMethod = "GET")
+    @GetMapping("/receive/line/num")
+    public AjaxResult<WeLxQrCodeReceiveLineVo> receiveLineNum(WeLxQrCodeQuery query) {
+        WeLxQrCodeReceiveLineVo lineNum = weLxQrCodeService.receiveLineNum(query);
+        return AjaxResult.success(lineNum);
+    }
+
+    @ApiOperation(value = "领取红包金额统计(折线图)", httpMethod = "GET")
+    @GetMapping("/receive/line/amount")
+    public AjaxResult<WeLxQrCodeReceiveLineVo> receiveLineAmount(WeLxQrCodeQuery query) {
+        WeLxQrCodeReceiveLineVo lineNum = weLxQrCodeService.receiveLineAmount(query);
+        return AjaxResult.success(lineNum);
+    }
+
+    @ApiOperation(value = "领取红包列表统计(表格)", httpMethod = "GET")
+    @GetMapping("/receive/list/statistics")
+    public TableDataInfo<List<WeLxQrCodeReceiveListVo>> receiveListStatistics(WeLxQrCodeQuery query) {
+        startPage();
+        List<WeLxQrCodeReceiveListVo> list = weLxQrCodeService.receiveListStatistics(query);
+        return getDataTable(list);
+    }
+
+    @ApiOperation(value = "领取红包列表统计(表格)", httpMethod = "GET")
+    @GetMapping("/receive/list/statistics/export")
+    public void receiveListStatisticsExport(WeLxQrCodeQuery query) throws IOException {
+        List<WeLxQrCodeReceiveListVo> list = weLxQrCodeService.receiveListStatistics(query);
+        HttpServletResponse response = ServletUtils.getResponse();
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode("领取红包列表统计", "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+        EasyExcel.write(response.getOutputStream(), WeLxQrCodeReceiveListVo.class).sheet("领取红包列表统计").doWrite(list);
+    }
+
+
+}

+ 414 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeMaterialController.java

@@ -0,0 +1,414 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.linkwechat.common.annotation.DataColumn;
+import com.linkwechat.common.annotation.DataScope;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.domain.BaseEntity;
+import com.linkwechat.common.core.domain.FileEntity;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.enums.MediaType;
+import com.linkwechat.common.exception.CustomException;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.common.utils.bean.BeanUtils;
+import com.linkwechat.domain.WeCustomer;
+import com.linkwechat.domain.material.ao.PurePoster;
+import com.linkwechat.domain.material.ao.WePoster;
+import com.linkwechat.domain.material.ao.WePosterFontAO;
+import com.linkwechat.domain.material.dto.ResetCategoryDto;
+import com.linkwechat.domain.material.dto.TemporaryMaterialDto;
+import com.linkwechat.domain.material.entity.WeMaterial;
+import com.linkwechat.domain.material.query.ContentDetailQuery;
+import com.linkwechat.domain.material.query.LinkMediaQuery;
+import com.linkwechat.domain.material.vo.*;
+import com.linkwechat.domain.wecom.vo.media.WeMediaVo;
+import com.linkwechat.service.IWeCustomerService;
+import com.linkwechat.service.IWeMaterialService;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 素材管理/海报管理/海报字体管理
+ *
+ * @author leejoker
+ * @date 2022/3/29 22:50
+ */
+@RestController
+public class WeMaterialController extends BaseController {
+    @Resource
+    private IWeMaterialService materialService;
+
+    @Resource
+    private IWeCustomerService weCustomerService;
+
+
+    @GetMapping("/material/list")
+    @ApiOperation("查询素材列表")
+    public TableDataInfo list(LinkMediaQuery query) {
+        startPage();
+        List<WeMaterialNewVo> weMaterialNewVos = materialService.selectListByLkQuery(query);
+        return getDataTable(weMaterialNewVos);
+    }
+
+
+    @GetMapping("/material/{id}")
+    @ApiOperation("查询素材详细信息")
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(materialService.findWeMaterialById(id));
+    }
+
+
+    @Log(title = "添加素材信息", businessType = BusinessType.INSERT)
+    @PostMapping("/material")
+    @ApiOperation("添加素材信息")
+    public AjaxResult add(@RequestBody WeMaterial material) {
+        Integer moduleType = material.getModuleType();
+        if (moduleType.equals(1) && material.getCategoryId() == null) {
+            throw new CustomException("请先选择素材分组!");
+        }
+        return AjaxResult.success(materialService.addOrUpdate(material));
+    }
+
+
+    @Log(title = "更新素材信息", businessType = BusinessType.UPDATE)
+    @PutMapping("/material")
+    @ApiOperation("更新素材信息")
+    public AjaxResult edit(@RequestBody WeMaterial material) {
+        Integer moduleType = material.getModuleType();
+        if (moduleType.equals(1) &&material.getCategoryId() == null) {
+            throw new CustomException("请先选择素材分组!");
+        }
+        materialService.addOrUpdate(material);
+        return AjaxResult.success();
+    }
+
+
+    @Log(title = "删除素材信息", businessType = BusinessType.DELETE)
+    @DeleteMapping("/material/{ids}")
+    @ApiOperation("删除素材信息")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        materialService.deleteWeMaterialByIds(ids);
+        return AjaxResult.success();
+    }
+
+
+    @Log(title = "上传素材信息", businessType = BusinessType.OTHER)
+    @PostMapping("/material/upload")
+    @ApiOperation("上传素材信息")
+    public AjaxResult upload(@RequestParam(value = "file") MultipartFile file,
+                             @RequestParam(value = "mediaType") String mediaType) {
+        WeMaterialFileVo weMaterialFileVo = materialService.uploadWeMaterialFile(file, mediaType);
+        return AjaxResult.success(weMaterialFileVo);
+    }
+
+
+    @Log(title = "更换分组", businessType = BusinessType.OTHER)
+    @PutMapping("/material/resetCategory")
+    @ApiOperation("更换分组")
+    public AjaxResult resetCategory(@RequestBody ResetCategoryDto resetCategoryDto) {
+        materialService.resetCategory(resetCategoryDto.getCategoryId(), resetCategoryDto.getMaterials());
+        return AjaxResult.success();
+    }
+
+    @Log(title = "获取素材media_id", businessType = BusinessType.OTHER)
+    @GetMapping("/material/temporaryMaterialMediaId")
+    @ApiOperation("H5端发送获取素材media_id")
+    public AjaxResult temporaryMaterialMediaId(String url, String type, String name) {
+        WeMediaVo weMediaDto = materialService.uploadTemporaryMaterial(url, type,
+                name + "." + url.substring(url.lastIndexOf(".") + 1, url.length()));
+        return AjaxResult.success(weMediaDto);
+    }
+
+
+    @Log(title = "获取素材media_id", businessType = BusinessType.OTHER)
+    @PostMapping("/material/temporaryMaterialMediaIdForWeb")
+    @ApiOperation("web端发送获取素材media_id")
+    public AjaxResult temporaryMaterialMediaIdForWeb(@RequestBody TemporaryMaterialDto temporaryMaterialDto) {
+        WeMediaVo weMediaDto = materialService.uploadTemporaryMaterial(temporaryMaterialDto.getUrl(),
+                MediaType.of(temporaryMaterialDto.getType()).get().getMediaType(), temporaryMaterialDto.getName());
+        return AjaxResult.success(weMediaDto);
+    }
+
+
+    @Log(title = "上传素材图片", businessType = BusinessType.OTHER)
+    @PostMapping("/material/uploadimg")
+    @ApiOperation("上传素材图片")
+    public AjaxResult<WeMediaVo> uploadImg(MultipartFile file) {
+        WeMediaVo weMediaVo = materialService.uploadImg(file);
+        return AjaxResult.success(weMediaVo);
+    }
+
+
+
+    /**
+     * 纯创建海报,数据未写入到素材中心
+     *
+     * @param purePoster
+     * @return
+     */
+    @PostMapping(value = "/pure/poster/insert")
+    @ApiOperation("纯创建海报")
+    private AjaxResult<FileEntity> createPoster(@RequestBody PurePoster purePoster) {
+        FileEntity poster = materialService.createPoster(purePoster);
+        return AjaxResult.success(poster);
+    }
+
+    @PostMapping(value = "/poster/insert")
+    @ApiOperation("创建海报")
+    @Transactional(rollbackFor = RuntimeException.class)
+    public AjaxResult insert(@RequestBody WePoster poster) throws Exception{
+        String materialName = poster.getMaterialName();
+        if (materialName != null && materialName.length() > 60) {
+            throw new CustomException("海报标题不可超过60个字符!");
+        }
+        String digest = poster.getDigest();
+        if (digest != null && digest.length() > 100) {
+            throw new CustomException("海报描述不可超过100个字符!");
+        }
+        WeMaterial material = materialService.builderSimpleImg(poster);
+        material.setMediaType(MediaType.POSTER.getType());
+        material.setModuleType(poster.getModuleType());
+        boolean b = materialService.saveOrUpdate(material);
+        WeMaterialVo weMaterialVo = new WeMaterialVo();
+        BeanUtils.copyProperties(material, weMaterialVo);
+        return AjaxResult.success(weMaterialVo);
+    }
+
+    @PutMapping(value = "/poster/update")
+    @ApiOperation("修改海报")
+    @Transactional(rollbackFor = RuntimeException.class)
+    public AjaxResult update(@RequestBody WePoster poster) throws Exception {
+        if (poster.getId() == null) {
+            return AjaxResult.error("id为空");
+        }
+        String materialName = poster.getMaterialName();
+        if (materialName != null && materialName.length() > 60) {
+            throw new CustomException("海报标题不可超过60个字符!");
+        }
+        String digest = poster.getDigest();
+        if (digest != null && digest.length() > 100) {
+            throw new CustomException("海报描述不可超过100个字符!");
+        }
+        poster.setMediaType(null);
+
+        WeMaterial material = materialService.builderSimpleImg(poster);
+        materialService.saveOrUpdate(material);
+        return AjaxResult.success(material);
+    }
+
+
+    @GetMapping(value = "/poster/entity/{id}")
+    @ApiOperation("查询海报详情")
+    public AjaxResult entity(@PathVariable Long id) {
+        WeMaterial material = materialService.getById(id);
+        WePosterVo vo = BeanUtil.copyProperties(material, WePosterVo.class);
+        if (StringUtils.isNotBlank(material.getMaterialName())) {
+            vo.setTitle(material.getMaterialName());
+        }
+        vo.setSampleImgPath(material.getMaterialUrl());
+        vo.setBackgroundImgPath(material.getBackgroundImgUrl());
+        return AjaxResult.success(vo);
+    }
+
+    @GetMapping(value = "/poster/page")
+    @ApiOperation("分页查询海报")
+    @DataScope(type = "2", value = @DataColumn(alias = "we_material", name = "create_by_id", userid = "user_id"))
+    public AjaxResult page(Long categoryId, String name) {
+        startPage();
+        List<WeMaterial> materials = materialService.lambdaQuery()
+                .eq(WeMaterial::getMediaType, MediaType.POSTER.getType()).eq(WeMaterial::getDelFlag, 0)
+                .eq(categoryId != null, WeMaterial::getCategoryId, categoryId)
+                .like(com.linkwechat.common.utils.StringUtils.isNotBlank(name), WeMaterial::getMaterialName, name)
+                .orderByDesc(WeMaterial::getCreateTime).list();
+        List<WePosterVo> posterList = materials.stream().map(m -> {
+            WePosterVo vo = BeanUtil.copyProperties(m, WePosterVo.class);
+            vo.setTitle(m.getMaterialName());
+            vo.setSampleImgPath(m.getMaterialUrl());
+            vo.setBackgroundImgPath(m.getBackgroundImgUrl());
+            return vo;
+        }).collect(Collectors.toList());
+        return AjaxResult.success(getDataTable(posterList));
+    }
+
+    @DeleteMapping(value = "/poster/delete/{id}")
+    @ApiOperation(value = "删除海报")
+    @Transactional(rollbackFor = RuntimeException.class)
+    public AjaxResult deletePoster(@PathVariable Long id) {
+        materialService.update(
+                Wrappers.lambdaUpdate(WeMaterial.class).set(WeMaterial::getDelFlag, 1).eq(WeMaterial::getId, id));
+        return AjaxResult.success("删除成功");
+    }
+
+    @PostMapping(value = "/posterFont")
+    @ApiOperation("创建海报字体")
+    @Transactional(rollbackFor = RuntimeException.class)
+    public AjaxResult insertPosterFont(@RequestBody WePosterFontAO posterFont) {
+        posterFont.setMediaType(MediaType.POSTER_FONT.getType());
+        WeMaterial material = BeanUtil.copyProperties(posterFont, WeMaterial.class);
+        material.setMaterialName(posterFont.getFontName());
+        material.setMaterialUrl(posterFont.getFontUrl());
+        materialService.save(material);
+        return AjaxResult.success("创建成功");
+    }
+
+
+    @PutMapping(value = "/posterFont")
+    @ApiOperation("修改海报字体")
+    @Transactional(rollbackFor = RuntimeException.class)
+    public AjaxResult updatePosterFont(@RequestBody WePosterFontAO posterFont) {
+        if (posterFont.getId() == null) {
+            return AjaxResult.error("id为空");
+        }
+        posterFont.setMediaType(null);
+        WeMaterial material = BeanUtil.copyProperties(posterFont, WeMaterial.class);
+        material.setMaterialName(posterFont.getFontName());
+        material.setMaterialUrl(posterFont.getFontUrl());
+        materialService.saveOrUpdate(material);
+        return AjaxResult.success("修改成功");
+    }
+
+    @GetMapping(value = "/posterFont/posterFontList")
+    @ApiOperation("列表查询海报字体")
+    public AjaxResult selectPosterFontList(BaseEntity query) {
+        return AjaxResult.success(materialService.getFontList(query));
+    }
+
+    @GetMapping(value = "/posterFont/posterFontPage")
+    @ApiOperation("分页查询海报字体")
+    public AjaxResult selectPosterFontPage(BaseEntity query) {
+        startPage();
+        return AjaxResult.success(getDataTable(materialService.getFontList(query)));
+    }
+
+    @DeleteMapping(value = "/posterFont/{id}")
+    @ApiOperation("删除海报字体")
+    @Transactional(rollbackFor = RuntimeException.class)
+    public AjaxResult deletePosterFont(@PathVariable Long id) {
+        materialService.update(
+                Wrappers.lambdaUpdate(WeMaterial.class).set(WeMaterial::getDelFlag, 1).eq(WeMaterial::getId, id));
+        return AjaxResult.success("删除成功");
+    }
+
+    @ApiOperation("文本素材导出模板")
+    @GetMapping("/material/importTemplate")
+    public void importTemplate() throws IOException {
+        materialService.importTemplate();
+    }
+
+    /**
+     * 文本素材数据按模板导入
+     *
+     * @param weMaterial
+     * @param file
+     * @return
+     * @throws Exception
+     */
+    @PostMapping("/material/importData")
+    public AjaxResult importData(WeMaterial weMaterial, MultipartFile file) throws Exception {
+        String tip = materialService.importData(file);
+        return AjaxResult.success(tip);
+    }
+
+
+    // 统计 数据分析 共用 数据分析时 不需要传ContentId
+    @ApiOperation(value = "素材详情数据统计", httpMethod = "GET")
+    @Log(title = "素材详情数据统计", businessType = BusinessType.SELECT)
+    @GetMapping("/material/count")
+    public AjaxResult<WeContentCountVo> getWeMaterialCount(ContentDetailQuery contentDetailQuery) {
+        WeContentCountVo WeContentCountVo = materialService.getWeMaterialCount(contentDetailQuery);
+        return AjaxResult.success(WeContentCountVo);
+    }
+
+
+    @ApiOperation(value = "素材详情数据明细", httpMethod = "GET")
+    @Log(title = "素材详情数据明细", businessType = BusinessType.SELECT)
+    @GetMapping("/material/dataDetail")
+    public TableDataInfo getWeMaterialDataCount(ContentDetailQuery contentDetailQuery) {
+        startPage();
+        List<ContentDataDetailVo> weMaterialDataCount = materialService.getWeMaterialDataCount(contentDetailQuery);
+        TableDataInfo dataTable = getDataTable(weMaterialDataCount);
+        Integer detailsType = contentDetailQuery.getDetailsType();
+        if (detailsType.equals(2)) {
+            List<ContentDataDetailVo> rows = (List<ContentDataDetailVo>) dataTable.getRows();
+            if (rows != null && rows.size() > 0) {
+                Set<String> collect = rows.stream().map(o -> o.getViewByUnionid()).collect(Collectors.toSet());
+                LambdaQueryWrapper<WeCustomer> queryWrapper = new LambdaQueryWrapper<>();
+                queryWrapper.in(WeCustomer::getUnionid, collect);
+                List<WeCustomer> list = weCustomerService.list(queryWrapper);
+                if (list != null && list.size() > 0) {
+                    Map<String, WeCustomer> weCustomersMap = list.stream().collect(Collectors.toMap(WeCustomer::getUnionid, Function.identity(), (key1, key2) -> key2));
+                    for (ContentDataDetailVo row : rows) {
+                        WeCustomer weCustomer = weCustomersMap.get(row.getViewByUnionid());
+                        if (ObjectUtil.isNotNull(weCustomer)) {
+                            row.setIsCustomer(1);
+                            String viewBy = row.getViewBy();
+                            if (StrUtil.isBlank(viewBy)) {
+                                row.setViewBy(weCustomer.getCustomerName());
+                            }
+                            String viewAvatar = row.getViewAvatar();
+                            if (StrUtil.isBlank(viewAvatar)) {
+                                if (StrUtil.isNotBlank(weCustomer.getAvatar())) {
+                                    row.setViewAvatar(weCustomer.getAvatar());
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return getDataTable(weMaterialDataCount);
+    }
+
+
+    @ApiOperation(value = "素材数据分析Top5", httpMethod = "GET")
+    @Log(title = "素材数据分析Top5", businessType = BusinessType.SELECT)
+    @GetMapping("/material/analyseTop")
+    public AjaxResult getWeMaterialAnalyseTop(ContentDetailQuery contentDetailQuery) {
+        List<WeMaterialAnalyseVo> list = materialService.getWeMaterialAnalyseTop(contentDetailQuery);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取素材类型
+     *
+     * @param
+     * @return {@link AjaxResult}
+     * @author WangYX
+     * @date 2022/10/21 14:24
+     */
+    @ApiOperation(value = "获取素材类型", httpMethod = "GET")
+    @GetMapping("/material/media/type")
+    public AjaxResult getMaterialMediaType() {
+
+        //素材类型:需要参考 CategoryMediaType类中的定义
+        //由于枚举类定义了很多不属于素材中的类型,所以需要把用到的素材类型挑选出来
+        List<MaterialMediaTypeVO> result = new ArrayList<>();
+        result.add(MaterialMediaTypeVO.builder().type(4).name("文本").sort(1).build());
+        result.add(MaterialMediaTypeVO.builder().type(0).name("图片").sort(2).build());
+        result.add(MaterialMediaTypeVO.builder().type(9).name("图文").sort(3).build());
+        result.add(MaterialMediaTypeVO.builder().type(11).name("小程序").sort(4).build());
+        result.add(MaterialMediaTypeVO.builder().type(12).name("文章").sort(5).build());
+        result.add(MaterialMediaTypeVO.builder().type(2).name("视频").sort(6).build());
+        result.add(MaterialMediaTypeVO.builder().type(3).name("文件").sort(7).build());
+        result.add(MaterialMediaTypeVO.builder().type(5).name("海报").sort(8).build());
+        result = result.stream().sorted(Comparator.comparing(MaterialMediaTypeVO::getSort)).collect(Collectors.toList());
+        return AjaxResult.success(result);
+    }
+
+
+}

+ 104 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeMomentsController.java

@@ -0,0 +1,104 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.linkwechat.common.constant.SynchRecordConstants;
+import com.linkwechat.common.constant.WeConstans;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.exception.CustomException;
+import com.linkwechat.domain.WeMoments;
+import com.linkwechat.service.IWeMomentsService;
+import com.linkwechat.service.IWeSynchRecordService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 朋友圈相关
+ */
+@RestController
+@RequestMapping("/moments")
+public class WeMomentsController extends BaseController {
+
+
+    @Resource
+    private IWeMomentsService iWeMomentsService;
+
+    @Resource
+    private IWeSynchRecordService iWeSynchRecordService;
+
+
+    /**
+     * 获取朋友圈列表
+     *
+     * @param weMoments
+     * @return
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(WeMoments weMoments) {
+        startPage();
+        List<WeMoments> moments = iWeMomentsService.findMoments(weMoments);
+        TableDataInfo dataTable = getDataTable(moments);
+        //最近同步时间
+        dataTable.setLastSyncTime(iWeSynchRecordService.findUpdateLatestTime(weMoments.getType()
+                .equals(0) ? SynchRecordConstants.SYNCH_CUSTOMER_ENTERPRISE_MOMENTS : SynchRecordConstants.SYNCH_CUSTOMER_PERSON_MOMENTS));
+        return dataTable;
+    }
+
+
+    /**
+     * 获取朋友圈详情
+     *
+     * @param momentId
+     * @return
+     */
+    @GetMapping("/findMomentsDetail/{momentId}")
+    public AjaxResult findMomentsDetail(@PathVariable String momentId) {
+        return AjaxResult.success(iWeMomentsService.findMomentsDetail(momentId));
+    }
+
+    /**
+     * 新增或者编辑朋友圈
+     *
+     * @return
+     */
+    @PostMapping("/addOrUpdate")
+    public AjaxResult addOrUpdate(@RequestBody WeMoments weMoments){
+        iWeMomentsService.addOrUpdateMoments(weMoments);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 朋友圈同步
+     *
+     * @param filterType 0:企业朋友圈;1:个人朋友圈
+     * @return
+     */
+    @GetMapping("/synchMoments")
+    public AjaxResult synchMoments(@RequestParam(defaultValue = "0") Integer filterType) {
+
+        if (Objects.equals(0,filterType)) {
+            iWeMomentsService.synchEnterpriseMoments(filterType);
+        } else if (Objects.equals(1,filterType)) {
+            iWeMomentsService.synchPersonMoments(filterType);
+        }
+        return AjaxResult.success(WeConstans.SYNCH_TIP);
+    }
+
+
+    /**
+     * 个人朋友圈互动数据同步
+     *
+     * @param userIds
+     * @return
+     */
+    @GetMapping("/synchMomentsInteracte/{userIds}")
+    public AjaxResult synchMomentsInteracte(@PathVariable String[] userIds) {
+        iWeMomentsService.synchMomentsInteracte(CollectionUtil.newArrayList(userIds));
+        return AjaxResult.success(WeConstans.SYNCH_TIP);
+    }
+}

+ 105 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeMsgTlpController.java

@@ -0,0 +1,105 @@
+package com.linkwechat.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.domain.WeMsgTlp;
+import com.linkwechat.domain.material.entity.WeContentTalk;
+import com.linkwechat.domain.material.entity.WeMaterial;
+import com.linkwechat.domain.msgtlp.dto.WeMsgTlpDto;
+import com.linkwechat.domain.msgtlp.query.WeMsgTlpAddQuery;
+import com.linkwechat.domain.msgtlp.query.WeMsgTlpQuery;
+import com.linkwechat.domain.msgtlp.vo.WeMsgTlpVo;
+import com.linkwechat.service.IWeMsgTlpService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author sxw
+ * @description 欢迎语管理
+ * @date 2022/3/26 10:29
+ **/
+@RestController
+@RequestMapping(value = "tlp")
+@Api(tags = "欢迎语管理")
+public class WeMsgTlpController extends BaseController {
+
+    @Autowired
+    private IWeMsgTlpService weMsgTlpService;
+
+    @ApiOperation(value = "新增欢迎语模板", httpMethod = "POST")
+    @Log(title = "欢迎语管理", businessType = BusinessType.OTHER)
+    @PostMapping("/addOrUpdate")
+    public AjaxResult addOrUpdate(@RequestBody @Validated WeMsgTlpDto weMsgTlpDto) {
+        weMsgTlpService.addOrUpdate(weMsgTlpDto);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "新增欢迎语模板", httpMethod = "POST")
+    @Log(title = "欢迎语管理", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult addMsgTlp(@RequestBody @Validated WeMsgTlp weMsgTlp) {
+        weMsgTlpService.addMsgTlp(weMsgTlp);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "修改欢迎语模板", httpMethod = "PUT")
+    @Log(title = "欢迎语管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/update")
+    public AjaxResult updateMsgTlp(@RequestBody @Validated WeMsgTlpAddQuery query) {
+        weMsgTlpService.updateMsgTlp(query);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "删除欢迎语模板", httpMethod = "DELETE")
+    @Log(title = "欢迎语管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/del/{ids}")
+    public AjaxResult delMsgTlp(@PathVariable("ids") List<Long> ids) {
+        weMsgTlpService.delMsgTlp(ids);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "获取欢迎语模板列表", httpMethod = "GET")
+    @Log(title = "欢迎语管理", businessType = BusinessType.SELECT)
+    @GetMapping("/list")
+    public TableDataInfo getList(WeMsgTlpQuery query) {
+        startPage();
+        List<WeMsgTlpVo> weMsgTlpVoList = weMsgTlpService.getList(query);
+        return getDataTable(weMsgTlpVoList);
+    }
+
+    @ApiOperation(value = "预览", httpMethod = "GET")
+    @Log(title = "预览", businessType = BusinessType.SELECT)
+    @GetMapping("/preview/{tlpId}")
+    public AjaxResult preview(@PathVariable Long tlpId) {
+        WeMsgTlp weMsgTlp = weMsgTlpService.getOne(new LambdaQueryWrapper<WeMsgTlp>().eq(WeMsgTlp::getId, tlpId).eq(WeMsgTlp::getDelFlag, 0));
+        List<WeMaterial> weMaterialList = weMsgTlpService.preview(tlpId);
+        weMsgTlp.setWeMaterialList(weMaterialList);
+        return AjaxResult.success(weMsgTlp);
+    }
+
+    @ApiOperation(value = "批量分组", httpMethod = "GET")
+    @Log(title = "批量分组", businessType = BusinessType.SELECT)
+    @GetMapping("/updateCategory")
+    public AjaxResult updateCategory(WeMsgTlpQuery query) {
+        weMsgTlpService.updateCategory(query);
+        return AjaxResult.success();
+    }
+
+
+    @ApiOperation(value = "获取欢迎语模板详情", httpMethod = "GET")
+    @Log(title = "欢迎语管理", businessType = BusinessType.SELECT)
+    @GetMapping("/get/{id}")
+    public AjaxResult<WeMsgTlpVo> getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(weMsgTlpService.getInfo(id));
+    }
+}

+ 380 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeOperationCenterController.java

@@ -0,0 +1,380 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.domain.BaseEntity;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.utils.ServletUtils;
+import com.linkwechat.common.utils.poi.ExcelUtil;
+import com.linkwechat.common.utils.poi.LwExcelUtil;
+import com.linkwechat.domain.operation.query.WeOperationCustomerQuery;
+import com.linkwechat.domain.operation.query.WeOperationGroupQuery;
+import com.linkwechat.domain.operation.vo.*;
+import com.linkwechat.service.IWeOperationCenterService;
+import com.linkwechat.service.IWeQrCodeService;
+import com.linkwechat.service.IWeSynchRecordService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * @author danmo
+ * @description 运营中心
+ * @date 2022/1/9 16:57
+ **/
+@Api(tags = "运营中心管理")
+@Slf4j
+@RestController
+@RequestMapping("operation")
+public class WeOperationCenterController extends BaseController {
+
+    @Autowired
+    private IWeOperationCenterService weOperationCenterService;
+
+    /**
+     *  客户数据分析
+     * @return
+     */
+    @GetMapping("/customer/getAnalysis")
+    public AjaxResult<WeCustomerAnalysisVo> getCustomerAnalysis() {
+        return AjaxResult.success(weOperationCenterService.getCustomerAnalysis());
+    }
+
+    /**
+     * 客户数据-客户总数
+     * @param query
+     * @return
+     */
+    @GetMapping("/customer/getTotalCnt")
+    public AjaxResult<List<WeCustomerTotalCntVo>> getCustomerTotalCnt(WeOperationCustomerQuery query) {
+        return AjaxResult.success(weOperationCenterService.getCustomerTotalCnt(query));
+    }
+
+
+    /**
+     *  客户数据-实时数据
+     * @param query
+     * @return
+     */
+    @GetMapping("/customer/getRealCnt")
+    public AjaxResult<List<WeCustomerRealCntVo>> getCustomerRealCnt(WeOperationCustomerQuery query) {
+        return AjaxResult.success(weOperationCenterService.getCustomerRealCnt(query));
+    }
+
+
+    /**
+     *  客户数据-员工客户排行
+     * @param query
+     * @return
+     */
+    @GetMapping("/customer/getRankCnt")
+    public AjaxResult<List<WeUserCustomerRankVo>> getCustomerRank(WeOperationCustomerQuery query) {
+        return AjaxResult.success(weOperationCenterService.getCustomerRank(query));
+    }
+
+
+    /**
+     *  客户数据-实时数据(分页)
+     * @param query
+     * @return
+     */
+    @GetMapping("/customer/getCustomerRealPageCnt")
+    public TableDataInfo<WeCustomerRealCntVo> getCustomerRealPageCnt(WeOperationCustomerQuery query) {
+        startPage();
+        return getDataTable(weOperationCenterService.getCustomerRealCntPage(query));
+    }
+
+
+    /**
+     *  客户数据-实时数据-导出
+     * @param query
+     * @return
+     */
+    @GetMapping("/customer/real/export")
+    public void customerRealExport(WeOperationCustomerQuery query) {
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeCustomerRealCntVo.class, weOperationCenterService.getCustomerRealCnt(query),"客户实时数据"
+        );
+    }
+
+
+    /**
+     *  客群数据分析
+     * @return
+     */
+    @GetMapping("/group/getAnalysis")
+    public AjaxResult<WeGroupAnalysisVo> getGroupAnalysis() {
+        return AjaxResult.success(weOperationCenterService.getGroupAnalysis());
+    }
+
+
+    /**
+     * 客群数据-客群总数
+     * @param query
+     * @return
+     */
+    @GetMapping("/group/getTotalCnt")
+    public AjaxResult<List<WeGroupTotalCntVo>> getGroupTotalCnt(WeOperationGroupQuery query) {
+        return AjaxResult.success(weOperationCenterService.getGroupTotalCnt(query));
+    }
+
+
+    /**
+     *  客群数据-客群成员总数
+     * @param query
+     * @return
+     */
+    @GetMapping("/group/member/getTotalCnt")
+    public AjaxResult<List<WeGroupTotalCntVo>> getGroupMemberTotalCnt(WeOperationGroupQuery query) {
+        return AjaxResult.success(weOperationCenterService.getGroupMemberTotalCnt(query));
+    }
+
+
+    /**
+     * 客群数据-客群实时数据
+     * @param query
+     * @return
+     */
+    @GetMapping("/group/getRealCnt")
+    public AjaxResult<List<WeGroupRealCntVo>> getGroupRealCnt(WeOperationGroupQuery query) {
+        return AjaxResult.success(weOperationCenterService.getGroupRealCnt(query));
+    }
+
+
+    /**
+     *  客群数据-客群实时数据(分页)
+     * @param query
+     * @return
+     */
+    @GetMapping("/group/getGroupRealPageCnt")
+    public TableDataInfo<WeGroupRealCntVo> getGroupRealPageCnt(WeOperationGroupQuery query) {
+        startPage();
+        return getDataTable(weOperationCenterService.getGroupRealCnt(query));
+    }
+
+
+    /**
+     * 客群数据-客群成员实时数据
+     * @param query
+     * @return
+     */
+    @GetMapping("/group/member/getRealCnt")
+    public AjaxResult<List<WeGroupMemberRealCntVo>> getGroupMemberRealCnt(WeOperationGroupQuery query) {
+        return AjaxResult.success(weOperationCenterService.getGroupMemberRealCnt(query));
+    }
+
+
+    /**
+     * 客群数据-客群成员实时数据(分页)
+     * @param query
+     * @return
+     */
+    @GetMapping("/group/member/getGroupMemberRealPageCnt")
+    public TableDataInfo<WeGroupMemberRealCntVo> getGroupMemberRealPageCnt(WeOperationGroupQuery query) {
+        startPage();
+//        return getDataTable(weOperationCenterService.getGroupMemberRealCnt(query));
+        return getDataTable(weOperationCenterService.selectGroupMemberBrokenLine(query));
+    }
+
+
+    /**
+     * 客群数据-客群实时数据-导出
+     * @param query
+     * @return
+     */
+    @GetMapping("/group/real/export")
+    public void groupRealExport(WeOperationGroupQuery query) {
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeGroupRealCntVo.class, weOperationCenterService.getGroupRealCnt(query),"客群实时数据"
+        );
+    }
+
+
+
+
+    /**
+     * 客群数据-客群成员实时数据-导出
+     * @param query
+     * @return
+     */
+    @GetMapping("/group/member/real/export")
+    public void groupMemberRealExport(WeOperationGroupQuery query) {
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeGroupMemberRealCntVo.class, weOperationCenterService.getGroupMemberRealCnt(query),"客群成员实时数据"
+        );
+    }
+
+
+    /**
+     * 会话分析-客户联系总数
+     * @return
+     */
+    @GetMapping("/session/customer/getAnalysis")
+    public AjaxResult<WeSessionCustomerAnalysisVo> getCustomerSessionAnalysis() {
+        return AjaxResult.success(weOperationCenterService.getCustomerSessionAnalysis());
+    }
+
+
+    /**
+     * 会话分析-客户会话总数
+     * @param query
+     * @return
+     */
+    @GetMapping("/session/customer/getTotalCnt")
+    public AjaxResult<List<WeSessionCustomerTotalCntVo>> getCustomerSessionTotalCnt(WeOperationCustomerQuery query) {
+        return AjaxResult.success(weOperationCenterService.getCustomerSessionTotalCnt(query));
+    }
+
+
+    /**
+     * 会话分析-客户会话总数(分页)
+     * @param query
+     * @return
+     */
+    @GetMapping("/session/customer/getTotalPageCnt")
+    public TableDataInfo<WeSessionCustomerTotalCntVo> getCustomerSessionTotalPageCnt(WeOperationCustomerQuery query) {
+        startPage();
+        return getDataTable(weOperationCenterService.getCustomerSessionTotalCnt(query));
+    }
+
+
+    /**
+     * 会话分析-员工单聊总数
+     * @param query
+     * @return
+     */
+    @GetMapping("/session/customer/getUserChatRank")
+    public AjaxResult<List<WeSessionUserChatRankVo>> getUserChatRank(WeOperationCustomerQuery query) {
+        return AjaxResult.success(weOperationCenterService.getUserChatRank(query));
+    }
+
+
+    /**
+     * 会话分析-员工平均恢复时长
+     * @param query
+     * @return
+     */
+    @GetMapping("/session/customer/getUserAvgReplyTimeRank")
+    public AjaxResult<List<WeSessionUserAvgReplyTimeRankVo>> getUserAvgReplyTimeRank(WeOperationCustomerQuery query) {
+        return AjaxResult.success(weOperationCenterService.getUserAvgReplyTimeRank(query));
+    }
+
+
+
+    /**
+     * 客群数据-会话总数-导出
+     * @param query
+     * @return
+     */
+    @GetMapping("/session/customer/total/export")
+    public void customerSessionTotalExport(WeOperationCustomerQuery query) {
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeSessionCustomerTotalCntVo.class,weOperationCenterService.getCustomerSessionTotalCnt(query),"会话总数"
+        );
+    }
+
+
+    /**
+     * 会话分析-客群联系总数
+     * @return
+     */
+    @GetMapping("/session/group/getAnalysis")
+    public AjaxResult<WeSessionGroupAnalysisVo> getGroupSessionAnalysis() {
+        return AjaxResult.success(weOperationCenterService.getGroupSessionAnalysis());
+    }
+
+
+    /**
+     * 会话分析-群聊会话总数
+     * @param query
+     * @return
+     */
+    @GetMapping("/session/group/getTotalCnt")
+    public AjaxResult<List<WeSessionGroupTotalCntVo>> getGroupSessionTotalCnt(WeOperationGroupQuery query) {
+        return AjaxResult.success(weOperationCenterService.getGroupSessionTotalCnt(query));
+    }
+
+
+    /**
+     * 会话分析-群聊会话总数(分页)
+     * @param query
+     * @return
+     */
+    @GetMapping("/session/group/getTotalPageCnt")
+    public TableDataInfo<WeSessionGroupTotalCntVo> getGroupSessionTotalPageCnt(WeOperationGroupQuery query) {
+        startPage();
+        return getDataTable(weOperationCenterService.getGroupSessionTotalCnt(query));
+    }
+
+    /**
+     * 会话分析-群聊会话总数-导出
+     * @param query
+     * @return
+     */
+    @GetMapping("/session/group/total/export")
+    public AjaxResult getGroupSessionTotalExport(WeOperationGroupQuery query) {
+        List<WeSessionGroupTotalCntVo> groupSessionTotalCnt = weOperationCenterService.getGroupSessionTotalCnt(query);
+        ExcelUtil<WeSessionGroupTotalCntVo> util = new ExcelUtil<WeSessionGroupTotalCntVo>(WeSessionGroupTotalCntVo.class);
+        return util.exportExcel(groupSessionTotalCnt, "会话总数");
+    }
+
+    /**
+     * 会话存档数据分析
+     * @return
+     */
+    @GetMapping("/session/archive/getAnalysis")
+    public AjaxResult<WeSessionArchiveAnalysisVo> getSessionArchiveAnalysis() {
+        return AjaxResult.success(weOperationCenterService.getSessionArchiveAnalysis());
+    }
+
+    /**
+     * 会话存档同意明细
+     * @param query
+     * @return
+     */
+    @GetMapping("/session/archive/details")
+    public TableDataInfo<WeSessionArchiveDetailVo> getSessionArchiveDetails(BaseEntity query) {
+        startPage();
+        return getDataTable(weOperationCenterService.getSessionArchiveDetails(query));
+    }
+
+
+    /**
+     * 自建应用获取客户分析数据
+     * @param dataScope 个人数据:false 全部数据(相对于角色定义的数据权限):true
+     * @return
+     */
+    @GetMapping("/getCustomerAnalysisForApp")
+    public AjaxResult<WeCustomerAnalysisVo> getCustomerAnalysisForApp(@RequestParam(defaultValue = "false") boolean dataScope){
+
+
+        return AjaxResult.success(
+                weOperationCenterService.getCustomerAnalysisForApp(dataScope)
+        );
+
+    }
+
+
+    /**
+     * 自建应用获取群分析数据
+     * @param dataScope 个人数据:false 全部数据(相对于角色定义的数据权限):true
+     * @return
+     */
+    @GetMapping("/getGroupAnalysisByApp")
+    public AjaxResult<WeGroupAnalysisVo> getGroupAnalysisByApp(@RequestParam(defaultValue = "false") boolean dataScope){
+
+        return AjaxResult.success(
+                weOperationCenterService.getGroupAnalysisByApp(dataScope)
+        );
+    }
+
+
+
+}

+ 526 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeProductAnalyzeController.java

@@ -0,0 +1,526 @@
+package com.linkwechat.controller;
+
+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.hutool.core.util.ObjectUtil;
+import com.alibaba.excel.EasyExcel;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.constant.ProductOrderConstants;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.utils.ServletUtils;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.domain.WeProductDayStatistics;
+import com.linkwechat.domain.WeProductStatistics;
+import com.linkwechat.domain.product.analyze.vo.WeProductAnalyzeStatisticsVo;
+import com.linkwechat.domain.product.analyze.vo.WeProductOrderDataReportVo;
+import com.linkwechat.domain.product.analyze.vo.WeProductOrderTop5Vo;
+import com.linkwechat.domain.product.order.query.WeProductOrderQuery;
+import com.linkwechat.domain.product.order.vo.WeProductOrderWareVo;
+import com.linkwechat.domain.product.product.query.WeProductLineChartQuery;
+import com.linkwechat.domain.product.product.vo.WeUserOrderTop5Vo;
+import com.linkwechat.domain.product.refund.vo.WeProductOrderRefundVo;
+import com.linkwechat.service.IWeProductDayStatisticsService;
+import com.linkwechat.service.IWeProductOrderService;
+import com.linkwechat.service.IWeProductStatisticsService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.net.URLEncoder;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.DoubleStream;
+
+/**
+ * 商品分析
+ *
+ * @author WangYX
+ * @version 1.0.0
+ * @date 2022/11/22 15:44
+ */
+@Slf4j
+@Api(tags = "商品分析")
+@RestController
+@RequestMapping("/product/analyze")
+public class WeProductAnalyzeController extends BaseController {
+
+    @Resource
+    private IWeProductStatisticsService weProductStatisticsService;
+    @Resource
+    private IWeProductDayStatisticsService weProductDayStatisticsService;
+    @Resource
+    private RedisTemplate redisTemplate;
+    @Resource
+    private IWeProductOrderService weProductOrderService;
+
+    @ApiOperation(value = "商品分析统计", httpMethod = "GET")
+    @Log(title = "商品分析统计", businessType = BusinessType.SELECT)
+    @GetMapping("/statistics")
+    public AjaxResult<WeProductAnalyzeStatisticsVo> statistics() {
+        //返回数据
+        WeProductAnalyzeStatisticsVo weProductAnalyzeStatisticsVo = new WeProductAnalyzeStatisticsVo();
+        //设置默认值
+        //总的数据
+        weProductAnalyzeStatisticsVo.setOrderNum(0);
+        weProductAnalyzeStatisticsVo.setTotalFee(BigDecimal.ZERO);
+        weProductAnalyzeStatisticsVo.setRefundFee(BigDecimal.ZERO);
+        weProductAnalyzeStatisticsVo.setNetIncome(BigDecimal.ZERO);
+        //今天的数据
+        weProductAnalyzeStatisticsVo.setTodayOrderNum(0);
+        weProductAnalyzeStatisticsVo.setTodayTotalFee(BigDecimal.ZERO);
+        weProductAnalyzeStatisticsVo.setTodayRefundFee(BigDecimal.ZERO);
+        weProductAnalyzeStatisticsVo.setTodayNetIncome(BigDecimal.ZERO);
+        //相较于昨天的数据
+        weProductAnalyzeStatisticsVo.setOrderNumComparedToYes(0);
+        weProductAnalyzeStatisticsVo.setTotalFeeComparedToYes(BigDecimal.ZERO);
+        weProductAnalyzeStatisticsVo.setRefundFeeComparedToYes(BigDecimal.ZERO);
+        weProductAnalyzeStatisticsVo.setNetIncomeComparedToYes(BigDecimal.ZERO);
+
+        //从数据库中取出=>总的统计数据
+        LambdaQueryWrapper<WeProductStatistics> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(WeProductStatistics::getDelFlag, 0);
+        List<WeProductStatistics> list = weProductStatisticsService.list(queryWrapper);
+        if (list != null && list.size() > 0) {
+            WeProductStatistics weProductStatistics = list.get(0);
+            weProductAnalyzeStatisticsVo.setOrderNum(weProductStatistics.getOrderTotalNum());
+            weProductAnalyzeStatisticsVo.setTotalFee(new BigDecimal(weProductStatistics.getOrderTotalFee()));
+            weProductAnalyzeStatisticsVo.setRefundFee(new BigDecimal(weProductStatistics.getRefundTotalFee()));
+            weProductAnalyzeStatisticsVo.setNetIncome(new BigDecimal(weProductStatistics.getNetIncome()));
+        }
+
+        //从Redis中取出今天的数据
+        //今天订单总数
+        Integer todayOrderNum = (Integer) redisTemplate.opsForValue().get(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_NUMBER);
+        //今天订单总额:分
+        String todayTotalFeeStr = (String) redisTemplate.opsForValue().get(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_TOTAL_FEE);
+        //今天退款总额:分
+        String todayRefundFeeStr = (String) redisTemplate.opsForValue().get(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_REFUND_FEE);
+
+        todayOrderNum = todayOrderNum == null ? 0 : todayOrderNum;
+        todayTotalFeeStr = StringUtils.isNotBlank(todayTotalFeeStr) ? todayTotalFeeStr : "0";
+        todayRefundFeeStr = StringUtils.isNotBlank(todayRefundFeeStr) ? todayRefundFeeStr : "0";
+
+        BigDecimal tempBigDecimal = BigDecimal.valueOf(100L);
+        //设置今天订单总数
+        weProductAnalyzeStatisticsVo.setTodayOrderNum(todayOrderNum);
+        weProductAnalyzeStatisticsVo.setOrderNum(weProductAnalyzeStatisticsVo.getOrderNum() + todayOrderNum);
+        //设置今天订单总额
+        BigDecimal todayTotalFee = new BigDecimal(todayTotalFeeStr);
+        weProductAnalyzeStatisticsVo.setTodayTotalFee(todayTotalFee.divide(tempBigDecimal).setScale(2, BigDecimal.ROUND_HALF_UP));
+        weProductAnalyzeStatisticsVo.setTotalFee(weProductAnalyzeStatisticsVo.getTotalFee().add(todayTotalFee).divide(tempBigDecimal).setScale(2, BigDecimal.ROUND_HALF_UP));
+        //设置今天退款总额
+        BigDecimal todayRefundFee = new BigDecimal(todayRefundFeeStr);
+        weProductAnalyzeStatisticsVo.setTodayRefundFee(todayRefundFee.divide(tempBigDecimal).setScale(2, BigDecimal.ROUND_HALF_UP));
+        weProductAnalyzeStatisticsVo.setRefundFee(weProductAnalyzeStatisticsVo.getRefundFee().add(todayRefundFee).divide(tempBigDecimal).setScale(2, BigDecimal.ROUND_HALF_UP));
+        //设置今天净收入
+        BigDecimal subtract = todayTotalFee.subtract(todayRefundFee);
+        weProductAnalyzeStatisticsVo.setTodayNetIncome(subtract.divide(tempBigDecimal).setScale(2, BigDecimal.ROUND_HALF_UP));
+        weProductAnalyzeStatisticsVo.setNetIncome(weProductAnalyzeStatisticsVo.getNetIncome().add(subtract).divide(tempBigDecimal).setScale(2, BigDecimal.ROUND_HALF_UP));
+
+        //昨天的统计数据
+        DateTime dateTime = DateUtil.offsetDay(new Date(), -1);
+        LambdaQueryWrapper<WeProductDayStatistics> query = new LambdaQueryWrapper<>();
+        query.eq(WeProductDayStatistics::getDelFlag, 0);
+        query.apply("date_format(create_time,'yyyy-MM-dd') = date_format('" + dateTime + "','yyyy-MM-dd')");
+        List<WeProductDayStatistics> yesterdayDatas = weProductDayStatisticsService.list(query);
+        if(CollectionUtil.isNotEmpty(yesterdayDatas)){
+            WeProductDayStatistics yesterdayData = yesterdayDatas.stream().findFirst().get();
+            if (ObjectUtil.isEmpty(yesterdayData)) {
+                yesterdayData = new WeProductDayStatistics();
+                yesterdayData.setDayOrderTotalNum(0);
+                yesterdayData.setDayRefundTotalFee("0");
+                yesterdayData.setDayOrderTotalFee("0");
+                yesterdayData.setDayNetIncome("0");
+            }
+            weProductAnalyzeStatisticsVo.setOrderNumComparedToYes(weProductAnalyzeStatisticsVo.getTodayOrderNum() - yesterdayData.getDayOrderTotalNum());
+            weProductAnalyzeStatisticsVo.setTotalFeeComparedToYes(weProductAnalyzeStatisticsVo.getTodayTotalFee().subtract(new BigDecimal(yesterdayData.getDayOrderTotalFee()).divide(tempBigDecimal).setScale(2, BigDecimal.ROUND_HALF_UP)));
+            weProductAnalyzeStatisticsVo.setRefundFeeComparedToYes(weProductAnalyzeStatisticsVo.getTodayRefundFee().subtract(new BigDecimal(yesterdayData.getDayRefundTotalFee()).divide(tempBigDecimal).setScale(2, BigDecimal.ROUND_HALF_UP)));
+            weProductAnalyzeStatisticsVo.setNetIncomeComparedToYes(weProductAnalyzeStatisticsVo.getTodayNetIncome().subtract(new BigDecimal(yesterdayData.getDayNetIncome()).divide(tempBigDecimal).setScale(2, BigDecimal.ROUND_HALF_UP)));
+        }
+
+
+
+        return AjaxResult.success(weProductAnalyzeStatisticsVo);
+    }
+
+    @ApiOperation(value = "商品分析折线图", httpMethod = "POST")
+    @Log(title = "商品分析折线图", businessType = BusinessType.SELECT)
+    @PostMapping("/lineChart")
+    public AjaxResult lineChart(@RequestBody WeProductLineChartQuery query) {
+        String type = query.getType();
+        Date startTime = null;
+        Date endTime = null;
+        if (!type.equals("customization")) {
+            if (type.equals("week")) {
+                startTime = DateUtil.offsetWeek(new Date(), -1);
+            } else if (type.equals("month")) {
+                startTime = DateUtil.offsetMonth(new Date(), -1);
+            }
+            endTime = new Date();
+        } else {
+            startTime = DateUtil.parse(query.getStartDate(), "yyyy-MM-dd");
+            endTime = DateUtil.parse(query.getEndDate(), "yyyy-MM-dd");
+        }
+
+        List<DateTime> timeList = DateUtil.rangeToList(startTime, endTime, DateField.DAY_OF_YEAR);
+
+        //折线图X轴数据
+        String[] xAxisArray = new String[timeList.size()];
+        //订单总额(元)
+        String[] totalFeeArray = new String[timeList.size()];
+        //退款总额(元)
+        String[] refundFeeArray = new String[timeList.size()];
+        //净收入
+        String[] netIncomeArray = new String[timeList.size()];
+
+        //查询该时间段之内的数据
+        LambdaQueryWrapper<WeProductDayStatistics> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.between(WeProductDayStatistics::getCreateTime, startTime, endTime);
+        List<WeProductDayStatistics> list = weProductDayStatisticsService.list(queryWrapper);
+
+        for (int i = 0; i < timeList.size(); i++) {
+            String dateStr = timeList.get(i).toDateStr();
+            xAxisArray[i] = dateStr;
+            Optional<WeProductDayStatistics> first = list.stream().filter(o -> DateUtil.format(o.getCreateTime(), "yyyy-MM-dd").equals(dateStr)).findFirst();
+            if (first.isPresent()) {
+                WeProductDayStatistics weProductDayStatistics = first.get();
+                BigDecimal totalFee = new BigDecimal(weProductDayStatistics.getDayOrderTotalFee()).divide(BigDecimal.valueOf(100)).setScale(2, BigDecimal.ROUND_HALF_UP);
+                BigDecimal refundFee = new BigDecimal(weProductDayStatistics.getDayRefundTotalFee()).divide(BigDecimal.valueOf(100)).setScale(2, BigDecimal.ROUND_HALF_UP);
+                totalFeeArray[i] = totalFee.toString();
+                refundFeeArray[i] = refundFee.toString();
+                netIncomeArray[i] = totalFee.subtract(totalFee).toString();
+            } else {
+                totalFeeArray[i] = "0";
+                refundFeeArray[i] = "0";
+                netIncomeArray[i] = "0";
+            }
+        }
+
+        Map<String, Object> totalFeeData = new HashMap<>();
+        totalFeeData.put("data", totalFeeArray);
+        totalFeeData.put("name", "订单金额(元)");
+
+        Map<String, Object> refundFeeData = new HashMap<>();
+        refundFeeData.put("data", refundFeeArray);
+        refundFeeData.put("name", "退款金额(元)");
+
+        Map<String, Object> netIncomeData = new HashMap<>();
+        netIncomeData.put("data", netIncomeArray);
+        netIncomeData.put("name", "净收入(元)");
+
+        Map<String, Object> xAxisData = new HashMap<>();
+        xAxisData.put("data", xAxisArray);
+        Map<String, Map<String, Object>> xAxisDataMap = new HashMap<>();
+        xAxisDataMap.put("xAxis", xAxisData);
+
+        List series = new ArrayList();
+        series.add(totalFeeData);
+        series.add(refundFeeData);
+        series.add(netIncomeData);
+
+        Map<String, Object> seriesMap = new HashMap<>();
+        seriesMap.put("series", list);
+
+        Map<String, Object> legendData = new HashMap<>();
+        String[] legendArray = new String[]{"订单金额(元)", "退款金额(元)", "净收入(元)"};
+        legendData.put("data", legendArray);
+
+        Map map = new HashMap();
+        map.put("legend", legendData);
+        map.put("xAxis", xAxisData);
+        map.put("series", series);
+        return AjaxResult.success(map);
+    }
+
+    @ApiOperation(value = "商品订单Top5", httpMethod = "POST")
+    @Log(title = "商品订单Top5", businessType = BusinessType.SELECT)
+    @PostMapping("/top5")
+    public AjaxResult<List<WeProductOrderTop5Vo>> productOrderTop5(@RequestBody WeProductLineChartQuery query) {
+
+        //设置时间
+        String type = query.getType();
+        Date startTime = null;
+        Date endTime = null;
+        if (!type.equals("customization")) {
+            if (type.equals("week")) {
+                startTime = DateUtil.offsetWeek(new Date(), -1);
+            } else if (type.equals("month")) {
+                startTime = DateUtil.offsetMonth(new Date(), -1);
+            }
+            endTime = new Date();
+        } else {
+            startTime = DateUtil.parse(query.getStartDate(), "yyyy-MM-dd");
+            endTime = DateUtil.parse(query.getEndDate(), "yyyy-MM-dd");
+        }
+
+        //获取时间段内的数据
+        WeProductOrderQuery weProductOrderQuery = new WeProductOrderQuery();
+        weProductOrderQuery.setBeginTime(DateUtil.format(startTime, "yyyy-MM-dd"));
+        weProductOrderQuery.setEndTime(DateUtil.format(endTime, "yyyy-MM-dd"));
+        List<WeProductOrderWareVo> list = weProductOrderService.list(weProductOrderQuery);
+        Map<Long, List<WeProductOrderWareVo>> collect = list.stream().collect(Collectors.groupingBy(WeProductOrderWareVo::getProductId));
+
+        //返回数据
+        List<WeProductOrderTop5Vo> result = new ArrayList<>();
+
+        //遍历数据
+        Iterator<Map.Entry<Long, List<WeProductOrderWareVo>>> iterator = collect.entrySet().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry<Long, List<WeProductOrderWareVo>> next = iterator.next();
+
+            WeProductOrderTop5Vo weProductOrderTop5Vo = new WeProductOrderTop5Vo();
+            weProductOrderTop5Vo.setProductId(next.getKey());
+
+            List<WeProductOrderWareVo> value = next.getValue();
+            weProductOrderTop5Vo.setProductDesc(value.get(0).getDescribe());
+            weProductOrderTop5Vo.setPicture(value.get(0).getPicture());
+            weProductOrderTop5Vo.setOrderNum(value.size());
+            double totalFee = value.stream().flatMapToDouble(o -> DoubleStream.of(Double.valueOf(o.getTotalFee()))).sum();
+            final BigDecimal totalFeeDec = BigDecimal.valueOf(totalFee).divide(BigDecimal.valueOf(100L).setScale(2, BigDecimal.ROUND_HALF_UP));
+            weProductOrderTop5Vo.setTotalFee(totalFeeDec);
+
+            List<WeProductOrderRefundVo> refundVos = value.stream().flatMap(o -> o.getRefunds().stream()).collect(Collectors.toList());
+            double refundFee = refundVos.stream().filter(o -> o.getRefundState().equals(2)).flatMapToDouble(o -> DoubleStream.of(Double.valueOf(o.getRefundFee()))).sum();
+            BigDecimal refundFeeDec = BigDecimal.valueOf(refundFee).divide(BigDecimal.valueOf(100L).setScale(2, BigDecimal.ROUND_HALF_UP));
+            weProductOrderTop5Vo.setNetIncome(totalFeeDec.subtract(refundFeeDec));
+            result.add(weProductOrderTop5Vo);
+        }
+        result.sort(Comparator.comparing(WeProductOrderTop5Vo::getTotalFee).reversed());
+        if (result.size() > 5) {
+            result = result.subList(0, 5);
+        }
+        return AjaxResult.success(result);
+    }
+
+    @ApiOperation(value = "员工订单Top5", httpMethod = "POST")
+    @Log(title = "员工订单Top5", businessType = BusinessType.SELECT)
+    @PostMapping("/user/top5")
+    public AjaxResult<List<WeUserOrderTop5Vo>> userOrderTop5(@RequestBody WeProductLineChartQuery query) {
+        //设置时间
+        String type = query.getType();
+        Date startTime = null;
+        Date endTime = null;
+        if (!type.equals("customization")) {
+            if (type.equals("week")) {
+                startTime = DateUtil.offsetWeek(new Date(), -1);
+            } else if (type.equals("month")) {
+                startTime = DateUtil.offsetMonth(new Date(), -1);
+            }
+            endTime = new Date();
+        } else {
+            startTime = DateUtil.parse(query.getStartDate(), "yyyy-MM-dd");
+            endTime = DateUtil.parse(query.getEndDate(), "yyyy-MM-dd");
+        }
+
+        //返回结果
+        List<WeUserOrderTop5Vo> result = new ArrayList<>();
+
+        //获取时间段内的数据
+        WeProductOrderQuery weProductOrderQuery = new WeProductOrderQuery();
+        weProductOrderQuery.setBeginTime(DateUtil.format(startTime, "yyyy-MM-dd"));
+        weProductOrderQuery.setEndTime(DateUtil.format(endTime, "yyyy-MM-dd"));
+        List<WeProductOrderWareVo> list = weProductOrderService.list(weProductOrderQuery);
+        if (list != null && list.size() > 0) {
+            Map<String, List<WeProductOrderWareVo>> collect = list.stream().collect(Collectors.groupingBy(WeProductOrderWareVo::getWeUserId));
+            Iterator<Map.Entry<String, List<WeProductOrderWareVo>>> iterator = collect.entrySet().iterator();
+            while (iterator.hasNext()) {
+                Map.Entry<String, List<WeProductOrderWareVo>> next = iterator.next();
+                //构建返回结果
+                WeUserOrderTop5Vo weUserOrderTop5Vo = new WeUserOrderTop5Vo();
+                weUserOrderTop5Vo.setWeUserId(next.getKey());
+                weUserOrderTop5Vo.setWeUserName(next.getValue().get(0).getWeUserName());
+                weUserOrderTop5Vo.setOrderNum(next.getValue().size());
+                double sum = next.getValue().stream().flatMapToDouble(o -> DoubleStream.of(Double.valueOf(o.getTotalFee()))).sum();
+                weUserOrderTop5Vo.setTotalFee(BigDecimal.valueOf(sum).toString());
+                result.add(weUserOrderTop5Vo);
+            }
+        }
+        result.sort(Comparator.comparing(WeUserOrderTop5Vo::getTotalFee).reversed());
+        if (result.size() > 5) {
+            result.subList(0, 5);
+        }
+        return AjaxResult.success(result);
+    }
+
+    @ApiOperation(value = "数据报表-分页", httpMethod = "GET")
+    @Log(title = "数据报表-分页", businessType = BusinessType.SELECT)
+    @GetMapping("/data/report")
+    public TableDataInfo<WeProductOrderDataReportVo> dataReport(WeProductLineChartQuery query) {
+        startPage();
+        List<WeProductOrderDataReportVo> result = getProductOrderDataReportVos(query);
+        return getDataTable(result);
+    }
+
+    /**
+     * 获取数据报表
+     *
+     * @author WangYX
+     * @date 2022/11/23 11:11
+     * @version 1.0.0
+     */
+    private List<WeProductOrderDataReportVo> getProductOrderDataReportVos(WeProductLineChartQuery query) {
+        //设置时间
+        String type = query.getType();
+        Date startTime = null;
+        Date endTime = null;
+        if (!type.equals("customization")) {
+            if (type.equals("week")) {
+                startTime = DateUtil.offsetWeek(new Date(), -1);
+            } else if (type.equals("month")) {
+                startTime = DateUtil.offsetMonth(new Date(), -1);
+            }
+            endTime = new Date();
+        } else {
+            startTime = DateUtil.parse(query.getStartDate(), "yyyy-MM-dd");
+            endTime = DateUtil.parse(query.getEndDate(), "yyyy-MM-dd");
+        }
+        LambdaQueryWrapper<WeProductDayStatistics> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.between(WeProductDayStatistics::getCreateTime, startTime, endTime);
+        List<WeProductDayStatistics> list = weProductDayStatisticsService.list(queryWrapper);
+
+        BigDecimal tempBigDecimal = BigDecimal.valueOf(100L);
+        //返回结果
+        List<WeProductOrderDataReportVo> result = new ArrayList<>();
+        if (list != null && list.size() > 0) {
+            list.sort(Comparator.comparing(WeProductDayStatistics::getCreateTime).reversed());
+            for (WeProductDayStatistics weProductDayStatistics : list) {
+                WeProductOrderDataReportVo weProductOrderDataReportVo = new WeProductOrderDataReportVo();
+                String format = DateUtil.format(weProductDayStatistics.getCreateTime(), "yyyy-MM-dd");
+                weProductOrderDataReportVo.setDateStr(format);
+                weProductOrderDataReportVo.setTotalFee(new BigDecimal(weProductDayStatistics.getDayOrderTotalFee()).divide(tempBigDecimal).setScale(2, BigDecimal.ROUND_HALF_UP));
+                weProductOrderDataReportVo.setRefundFee(new BigDecimal(weProductDayStatistics.getDayRefundTotalFee()).divide(tempBigDecimal).setScale(2, BigDecimal.ROUND_HALF_UP));
+                weProductOrderDataReportVo.setNetIncome(weProductOrderDataReportVo.getTotalFee().subtract(weProductOrderDataReportVo.getRefundFee()));
+                weProductOrderDataReportVo.setOrderNum(weProductDayStatistics.getDayOrderTotalNum());
+                result.add(weProductOrderDataReportVo);
+            }
+        }
+        return result;
+    }
+
+    @ApiOperation(value = "数据报表导出", httpMethod = "POST")
+    @Log(title = "数据报表导出", businessType = BusinessType.SELECT)
+    @PostMapping("/data/report/export")
+    public void dataReportExport(@RequestBody WeProductLineChartQuery query) throws IOException {
+        List<WeProductOrderDataReportVo> result = getProductOrderDataReportVos(query);
+        HttpServletResponse response = ServletUtils.getResponse();
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode("商品订单数据报表", "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+        EasyExcel.write(response.getOutputStream(), WeProductOrderDataReportVo.class).sheet("商品订单数据报表").doWrite(result);
+
+
+    }
+
+
+    /**
+     * 初始化当天商品订单数据
+     * 项目启动成功之后执行
+     *
+     * @author WangYX
+     * @date 2022/11/22 16:59
+     * @version 1.0.0
+     */
+    @PostConstruct
+    public void initProductOrderData() {
+        log.info("初始化当天商品订单数据=>>>>>>>>>>>>>>>>>>>>>>");
+        LambdaQueryWrapper<WeProductDayStatistics> query = new LambdaQueryWrapper<>();
+        query.select(WeProductDayStatistics::getId, WeProductDayStatistics::getDayOrderTotalNum, WeProductDayStatistics::getDayOrderTotalFee, WeProductDayStatistics::getDayRefundTotalFee);
+        query.eq(WeProductDayStatistics::getDelFlag, 0);
+        query.apply("date_format(create_time,'%Y-%m-%d') = date_format(now(),'%Y-%m-%d')");
+        WeProductDayStatistics weProductDayStatistics = weProductDayStatisticsService.getOne(query);
+        if (ObjectUtil.isNotEmpty(weProductDayStatistics)) {
+            //今天订单总数
+            redisTemplate.opsForValue().set(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_NUMBER, weProductDayStatistics.getDayOrderTotalNum());
+            //今天订单总额:分
+            redisTemplate.opsForValue().set(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_TOTAL_FEE, weProductDayStatistics.getDayOrderTotalFee());
+            //今天退款总额:分
+            redisTemplate.opsForValue().set(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_REFUND_FEE, weProductDayStatistics.getDayRefundTotalFee());
+        } else {
+            //今天订单总数
+            Boolean orderNumber = redisTemplate.hasKey(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_NUMBER);
+            if (!orderNumber) {
+                redisTemplate.opsForValue().set(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_NUMBER, 0);
+            }
+            //今天订单总额:分
+            Boolean totalFee = redisTemplate.hasKey(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_TOTAL_FEE);
+            if (!totalFee) {
+                redisTemplate.opsForValue().set(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_TOTAL_FEE, "0");
+            }
+            //今天退款总额:分
+            Boolean refundFee = redisTemplate.hasKey(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_REFUND_FEE);
+            if (!refundFee) {
+                redisTemplate.opsForValue().set(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_REFUND_FEE, "0");
+            }
+        }
+    }
+
+    /**
+     * 保存当天商品订单数据
+     * 项目停止之前执行
+     *
+     * @author WangYX
+     * @date 2022/11/22 17:02
+     * @version 1.0.0
+     */
+    @PreDestroy
+    public void destroyProductOrderData() {
+        log.info("保存当天商品订单数据");
+        //今天订单总数
+        Integer todayOrderNum = (Integer) redisTemplate.opsForValue().get(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_NUMBER);
+        //今天订单总额:分
+        String todayTotalFeeStr = (String) redisTemplate.opsForValue().get(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_TOTAL_FEE);
+        //今天退款总额:分
+        String todayRefundFeeStr = (String) redisTemplate.opsForValue().get(ProductOrderConstants.PRODUCT_ANALYZE_ORDER_REFUND_FEE);
+
+        todayOrderNum = todayOrderNum == null ? 0 : todayOrderNum;
+        todayTotalFeeStr = StringUtils.isBlank(todayTotalFeeStr) ? "0" : todayTotalFeeStr;
+        todayRefundFeeStr = StringUtils.isBlank(todayRefundFeeStr) ? "0" : todayRefundFeeStr;
+
+        LambdaQueryWrapper<WeProductDayStatistics> query = new LambdaQueryWrapper<>();
+        query.eq(WeProductDayStatistics::getDelFlag, 0);
+        query.apply("date_format(create_time,'%Y-%m-%d') = date_format(now(),'%Y-%m-%d')");
+        WeProductDayStatistics weProductDayStatistics = weProductDayStatisticsService.getOne(query);
+        if (ObjectUtil.isNotEmpty(weProductDayStatistics)) {
+            //修改数据
+            weProductDayStatistics.setDayOrderTotalNum(weProductDayStatistics.getDayOrderTotalNum() + todayOrderNum);
+
+            BigDecimal dayTotalFee = new BigDecimal(weProductDayStatistics.getDayOrderTotalFee()).add(new BigDecimal(todayTotalFeeStr));
+            weProductDayStatistics.setDayOrderTotalFee(dayTotalFee.toString());
+
+            BigDecimal dayRefundFee = new BigDecimal(weProductDayStatistics.getDayRefundTotalFee()).add(new BigDecimal(todayRefundFeeStr));
+            weProductDayStatistics.setDayRefundTotalFee(dayRefundFee.toString());
+
+            BigDecimal subtract = new BigDecimal(weProductDayStatistics.getDayOrderTotalFee()).subtract(new BigDecimal(weProductDayStatistics.getDayRefundTotalFee()));
+            weProductDayStatistics.setDayNetIncome(subtract.toString());
+            weProductDayStatistics.setUpdateTime(LocalDateTime.now());
+            weProductDayStatisticsService.updateById(weProductDayStatistics);
+        } else {
+            //添加新数据
+            weProductDayStatistics = new WeProductDayStatistics();
+            weProductDayStatistics.setDayOrderTotalNum(todayOrderNum);
+            weProductDayStatistics.setDayOrderTotalFee(todayTotalFeeStr);
+            weProductDayStatistics.setDayRefundTotalFee(todayRefundFeeStr);
+            BigDecimal subtract = new BigDecimal(weProductDayStatistics.getDayOrderTotalFee()).subtract(new BigDecimal(weProductDayStatistics.getDayRefundTotalFee()));
+            weProductDayStatistics.setDayNetIncome(subtract.toString());
+            weProductDayStatistics.setCreateTime(LocalDateTime.now());
+            weProductDayStatistics.setUpdateTime(LocalDateTime.now());
+            weProductDayStatisticsService.save(weProductDayStatistics);
+        }
+    }
+}

+ 125 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeProductController.java

@@ -0,0 +1,125 @@
+package com.linkwechat.controller;
+
+import com.google.protobuf.ServiceException;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.domain.product.product.query.WeAddProductQuery;
+import com.linkwechat.domain.product.product.query.WeProductLineChartQuery;
+import com.linkwechat.domain.product.product.query.WeProductQuery;
+import com.linkwechat.domain.product.product.vo.WeProductListVo;
+import com.linkwechat.domain.product.product.vo.WeProductStatisticsVo;
+import com.linkwechat.domain.product.product.vo.WeProductVo;
+import com.linkwechat.domain.product.product.vo.WeUserOrderTop5Vo;
+import com.linkwechat.service.IWeProductService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author danmo
+ * @description 商品图册管理
+ * @date 2022/9/12 18:22
+ **/
+
+@RestController
+@RequestMapping(value = "product")
+@Api(tags = "商品图册管理")
+public class WeProductController extends BaseController {
+
+    @Autowired
+    private IWeProductService weProductService;
+
+    @ApiOperation(value = "新增商品", httpMethod = "POST")
+    @Log(title = "新增商品", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult addProduct(@RequestBody @Validated WeAddProductQuery query) throws ServiceException {
+        weProductService.addProduct(query);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "修改商品", httpMethod = "PUT")
+    @Log(title = "修改商品", businessType = BusinessType.UPDATE)
+    @PutMapping("/update/{id}")
+    public AjaxResult updateProduct(@PathVariable("id") Long id, @RequestBody @Validated WeAddProductQuery query) throws ServiceException {
+        weProductService.updateProduct(id, query);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "删除商品", httpMethod = "DELETE")
+    @Log(title = "删除商品", businessType = BusinessType.UPDATE)
+    @DeleteMapping("/delete/{ids}")
+    public AjaxResult delProduct(@PathVariable("ids") List<Long> ids) {
+        weProductService.delProduct(ids);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "商品详情", httpMethod = "GET")
+    @Log(title = "商品详情", businessType = BusinessType.SELECT)
+    @GetMapping("/get/{id}")
+    public AjaxResult<WeProductVo> getProduct(@PathVariable("id") Long id) {
+        WeProductVo product = weProductService.getProduct(id);
+        return AjaxResult.success(product);
+    }
+
+    @ApiOperation(value = "商品列表", httpMethod = "GET")
+    @Log(title = "商品列表", businessType = BusinessType.SELECT)
+    @GetMapping("/page/list")
+    public TableDataInfo<WeProductListVo> productPageList(WeProductQuery query) {
+        startPage();
+        List<WeProductListVo> list = weProductService.productList(query);
+        list.forEach(o -> {
+            o.setPrice(new BigDecimal(o.getPrice()).divide(BigDecimal.valueOf(100L)).toString());
+            if (!o.getOrderTotalAmount().equals("0.0")) {
+                BigDecimal bigDecimal = new BigDecimal(o.getOrderTotalAmount());
+                bigDecimal = bigDecimal.divide(BigDecimal.valueOf(100L)).setScale(2, BigDecimal.ROUND_HALF_UP);
+                o.setOrderTotalAmount(bigDecimal.toString());
+            }
+        });
+        return getDataTable(list);
+    }
+
+    @ApiOperation(value = "商品列表同步", httpMethod = "GET")
+    @Log(title = "商品列表同步", businessType = BusinessType.OTHER)
+    @GetMapping("/list/sync")
+    public AjaxResult syncProductList() {
+        weProductService.syncProductList();
+        return AjaxResult.success();
+    }
+
+
+    @ApiOperation(value = "商品详情统计", httpMethod = "GET")
+    @Log(title = "商品详情统计", businessType = BusinessType.SELECT)
+    @GetMapping("/statistics/{id}")
+    public AjaxResult statistics(@PathVariable("id") Long id) {
+        WeProductStatisticsVo statistics = weProductService.statistics(id);
+        return AjaxResult.success(statistics);
+    }
+
+
+    @ApiOperation(value = "商品详情折线图", httpMethod = "POST")
+    @Log(title = "商品详情折线图", businessType = BusinessType.SELECT)
+    @PostMapping("/lineChart")
+    public AjaxResult lineChart(@RequestBody WeProductLineChartQuery query) {
+        Map<String, Object> map = weProductService.lineChart(query);
+        return AjaxResult.success(map);
+    }
+
+    @ApiOperation(value = "员工订单Top5", httpMethod = "GET")
+    @Log(title = "员工订单Top5", businessType = BusinessType.SELECT)
+    @GetMapping("/top5/{id}")
+    public AjaxResult<List<WeUserOrderTop5Vo>> top5(@PathVariable("id") Long id) {
+        List<WeUserOrderTop5Vo> weUserOrderTop5Vos = weProductService.userOrderTop5(id);
+        return AjaxResult.success(weUserOrderTop5Vos);
+    }
+
+
+}

+ 174 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeProductOrderController.java

@@ -0,0 +1,174 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUnit;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.excel.EasyExcel;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.enums.ProductOrderStateEnum;
+import com.linkwechat.common.enums.ProductRefundOrderStateEnum;
+import com.linkwechat.common.exception.ServiceException;
+import com.linkwechat.common.utils.ServletUtils;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.domain.WeProductOrder;
+import com.linkwechat.domain.product.order.query.WeProductOrderQuery;
+import com.linkwechat.domain.product.order.vo.WeProductOrderPayInfoVo;
+import com.linkwechat.domain.product.order.vo.WeProductOrderVo;
+import com.linkwechat.domain.product.order.vo.WeProductOrderWareVo;
+import com.linkwechat.domain.product.refund.vo.WeProductOrderRefundVo;
+import com.linkwechat.service.IWeProductOrderService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 商品订单
+ *
+ * @author WangYX
+ * @version 1.0.0
+ * @date 2022/11/21 18:22
+ */
+@RestController
+@RequestMapping(value = "product/order")
+@Api(tags = "商品订单")
+public class WeProductOrderController extends BaseController {
+
+    @Resource
+    private IWeProductOrderService weProductOrderService;
+
+    @ApiOperation(value = "商品订单列表", httpMethod = "GET")
+    @Log(title = "商品订单列表", businessType = BusinessType.SELECT)
+    @GetMapping("/page/list")
+    public TableDataInfo<WeProductOrderVo> list(WeProductOrderQuery query) {
+        startPage();
+        List<WeProductOrderWareVo> list = weProductOrderService.list(query);
+        List<WeProductOrderVo> result = new ArrayList<>();
+        if (list != null && list.size() > 0) {
+            for (WeProductOrderWareVo weProductOrderWareVo : list) {
+                WeProductOrderVo weProductOrderVo = BeanUtil.copyProperties(weProductOrderWareVo, WeProductOrderVo.class);
+                //订单状态
+                weProductOrderVo.setOrderStateStr(ProductOrderStateEnum.of(weProductOrderVo.getOrderState()).getMsg());
+                //订单金额
+                BigDecimal totalFee = new BigDecimal(weProductOrderVo.getTotalFee()).divide(BigDecimal.valueOf(100L)).setScale(2, BigDecimal.ROUND_HALF_UP);
+                weProductOrderVo.setTotalFee(totalFee.toString());
+                List<WeProductOrderRefundVo> refunds = weProductOrderWareVo.getRefunds();
+                if (refunds != null && refunds.size() > 0) {
+                    refunds.sort(Comparator.comparing(WeProductOrderRefundVo::getRefundTime).reversed());
+                    WeProductOrderRefundVo weProductOrderRefundVo = refunds.get(0);
+                    //退款订单金额
+                    BigDecimal refundFee = new BigDecimal(weProductOrderRefundVo.getRefundFee()).divide(BigDecimal.valueOf(100L)).setScale(2, BigDecimal.ROUND_HALF_UP);
+                    weProductOrderVo.setRefundFee(refundFee.toString());
+                    //退款订单状态
+                    weProductOrderVo.setRefundStateStr(ProductRefundOrderStateEnum.of(weProductOrderRefundVo.getRefundState()).getMsg());
+                }
+                //客户类型
+                weProductOrderVo.setExternalTypeStr(weProductOrderVo.getExternalType() == 1 ? "微信" : "企业微信");
+                result.add(weProductOrderVo);
+            }
+        }
+        return getDataTable(result);
+    }
+
+    @ApiOperation(value = "查询订单交易状态", httpMethod = "GET")
+    @Log(title = "订单交易状态", businessType = BusinessType.SELECT)
+    @GetMapping("/trading/status/{orderNo}")
+    public AjaxResult<WeProductOrderPayInfoVo> orderTradingStatus(@PathVariable("orderNo") String orderNo) {
+        LambdaQueryWrapper<WeProductOrder> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.select(WeProductOrder::getPayTime, WeProductOrder::getOrderNo, WeProductOrder::getMchNo);
+        queryWrapper.eq(WeProductOrder::getOrderNo, orderNo);
+        queryWrapper.eq(WeProductOrder::getDelFlag, 0);
+        WeProductOrder weProductOrder = weProductOrderService.getOne(queryWrapper);
+        WeProductOrderPayInfoVo weProductOrderPayInfoVo = null;
+        if (ObjectUtil.isNotEmpty(weProductOrder)) {
+            weProductOrderPayInfoVo = BeanUtil.copyProperties(weProductOrder, WeProductOrderPayInfoVo.class);
+        }
+        return AjaxResult.success(weProductOrderPayInfoVo);
+    }
+
+    /**
+     * 导出Excel,仅导出查询范围内的数据,最多支持导出一年的数据
+     *
+     * @param query
+     * @return {@link AjaxResult}
+     * @author WangYX
+     * @date 2022/11/22 10:16
+     */
+    @ApiOperation(value = "导出Excel", httpMethod = "GET")
+    @Log(title = "导出Excel", businessType = BusinessType.SELECT)
+    @GetMapping("/export")
+    public void export(WeProductOrderQuery query) throws IOException {
+        if (StringUtils.isNotBlank(query.getBeginTime()) && StringUtils.isNotBlank(query.getEndTime())) {
+            //判断时间间隔
+            long between = DateUtil.between(DateUtil.parse(query.getBeginTime()), DateUtil.parse(query.getEndTime()), DateUnit.DAY);
+            long yearOfDays = 365L;
+            if (between > yearOfDays) {
+                throw new ServiceException("最多支持导出一年的数据");
+            }
+        } else {
+            //默认导出一个月的数据
+            DateTime dateTime = DateUtil.offsetMonth(new Date(), -1);
+            query.setBeginTime(DateUtil.format(dateTime, "yyyy-MM-dd"));
+            query.setEndTime(DateUtil.format(new Date(), "yyyy-MM-dd"));
+        }
+        List<WeProductOrderWareVo> list = weProductOrderService.list(query);
+        List<WeProductOrderVo> result = new ArrayList<>();
+        if (list != null && list.size() > 0) {
+            for (WeProductOrderWareVo weProductOrderWareVo : list) {
+                WeProductOrderVo weProductOrderVo = BeanUtil.copyProperties(weProductOrderWareVo, WeProductOrderVo.class);
+                //订单状态
+                weProductOrderVo.setOrderStateStr(ProductOrderStateEnum.of(weProductOrderVo.getOrderState()).getMsg());
+                //订单金额
+                BigDecimal totalFee = new BigDecimal(weProductOrderVo.getTotalFee()).divide(BigDecimal.valueOf(100L)).setScale(2, BigDecimal.ROUND_HALF_UP);
+                weProductOrderVo.setTotalFee(totalFee.toString());
+                List<WeProductOrderRefundVo> refunds = weProductOrderWareVo.getRefunds();
+                if (refunds != null && refunds.size() > 0) {
+                    refunds.sort(Comparator.comparing(WeProductOrderRefundVo::getRefundTime).reversed());
+                    WeProductOrderRefundVo weProductOrderRefundVo = refunds.get(0);
+                    //退款订单金额
+                    BigDecimal refundFee = new BigDecimal(weProductOrderRefundVo.getRefundFee()).divide(BigDecimal.valueOf(100L)).setScale(2, BigDecimal.ROUND_HALF_UP);
+                    weProductOrderVo.setRefundFee(refundFee.toString());
+                    //退款订单状态
+                    weProductOrderVo.setRefundStateStr(ProductRefundOrderStateEnum.of(weProductOrderRefundVo.getRefundState()).getMsg());
+                }
+                //客户类型
+                weProductOrderVo.setExternalTypeStr(weProductOrderVo.getExternalType() == 1 ? "微信" : "企业微信");
+                result.add(weProductOrderVo);
+            }
+        }
+        HttpServletResponse response = ServletUtils.getResponse();
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode("商品订单", "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+        EasyExcel.write(response.getOutputStream(), WeProductOrderVo.class).sheet("商品订单").doWrite(result);
+    }
+
+    @ApiOperation(value = "同步订单", httpMethod = "GET")
+    @Log(title = "同步订单", businessType = BusinessType.SELECT)
+    @GetMapping("/orderSync")
+    public AjaxResult orderSync() {
+        weProductOrderService.orderSync();
+        return AjaxResult.success();
+    }
+
+}

+ 63 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeProductOrderRefundController.java

@@ -0,0 +1,63 @@
+package com.linkwechat.controller;
+
+
+import cn.hutool.core.bean.BeanUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.enums.ProductRefundOrderStateEnum;
+import com.linkwechat.domain.WeProductOrderRefund;
+import com.linkwechat.domain.product.refund.vo.WeProductOrderRefundVo;
+import com.linkwechat.service.IWeProductOrderRefundService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <p>
+ * 商品订单退款表 前端控制器
+ * </p>
+ *
+ * @author WangYX
+ * @since 2022-11-22
+ */
+@Api(tags = "商品订单退款")
+@RestController
+@RequestMapping("/product/refund")
+public class WeProductOrderRefundController extends BaseController {
+
+    @Resource
+    private IWeProductOrderRefundService weProductOrderRefundService;
+
+    @ApiOperation(value = "查询退款订单交易状态", httpMethod = "GET")
+    @Log(title = "退款订单交易状态", businessType = BusinessType.SELECT)
+    @GetMapping("/refund/status/{orderNo}")
+    public AjaxResult<List<WeProductOrderRefundVo>> refundStatus(@PathVariable("orderNo") String orderNo) {
+        LambdaQueryWrapper<WeProductOrderRefund> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(WeProductOrderRefund::getOrderNo, orderNo);
+        queryWrapper.eq(WeProductOrderRefund::getDelFlag, 0);
+        List<WeProductOrderRefund> list = weProductOrderRefundService.list(queryWrapper);
+        List<WeProductOrderRefundVo> weProductOrderRefundVos = new ArrayList<>();
+        if (list != null && list.size() > 0) {
+            for (WeProductOrderRefund one : list) {
+                WeProductOrderRefundVo weProductOrderRefundVo = BeanUtil.copyProperties(one, WeProductOrderRefundVo.class);
+                BigDecimal bigDecimal = new BigDecimal(weProductOrderRefundVo.getRefundFee()).divide(BigDecimal.valueOf(100L)).setScale(2, BigDecimal.ROUND_HALF_UP);
+                weProductOrderRefundVo.setRefundFee(bigDecimal.toString());
+                weProductOrderRefundVo.setRefundStateStr(ProductRefundOrderStateEnum.of(weProductOrderRefundVo.getRefundState()).getMsg());
+                weProductOrderRefundVos.add(weProductOrderRefundVo);
+            }
+        }
+        return AjaxResult.success(weProductOrderRefundVos);
+    }
+}
+

+ 154 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeQrCodeController.java

@@ -0,0 +1,154 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.github.pagehelper.PageInfo;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.exception.CustomException;
+import com.linkwechat.common.exception.wecom.WeComException;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.common.utils.file.FileUtils;
+import com.linkwechat.domain.qr.query.WeQrAddQuery;
+import com.linkwechat.domain.qr.query.WeQrCodeListQuery;
+import com.linkwechat.domain.qr.vo.WeQrCodeDetailVo;
+import com.linkwechat.domain.qr.vo.WeQrCodeScanCountVo;
+import com.linkwechat.domain.qr.vo.WeQrScopeUserVo;
+import com.linkwechat.domain.qr.vo.WeQrScopeVo;
+import com.linkwechat.service.IWeQrCodeService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author danmo
+ * @description 活码管理
+ * @date 2021/11/12 18:22
+ **/
+
+@RestController
+@RequestMapping(value = "qr")
+@Api(tags = "活码管理")
+public class WeQrCodeController extends BaseController {
+
+    @Autowired
+    private IWeQrCodeService weQrCodeService;
+
+    @ApiOperation(value = "新增活码", httpMethod = "POST")
+    @Log(title = "活码管理", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult addQrCode(@RequestBody @Validated WeQrAddQuery weQrAddQuery) {
+        weQrCodeService.addQrCode(weQrAddQuery);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "修改活码", httpMethod = "POST")
+    @Log(title = "活码管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/update")
+    public AjaxResult updateQrCode(@RequestBody @Validated WeQrAddQuery weQrAddQuery) {
+        if(Objects.isNull(weQrAddQuery.getQrId())){
+            throw new WeComException("活码ID不能为空!");
+        }
+        weQrCodeService.updateQrCode(weQrAddQuery);
+        return AjaxResult.success();
+    }
+
+
+    @ApiOperation(value = "获取活码详情", httpMethod = "GET")
+    @Log(title = "活码管理", businessType = BusinessType.SELECT)
+    @GetMapping("/get/{id}")
+    public AjaxResult<WeQrCodeDetailVo> getQrDetail(@PathVariable("id") Long Id) {
+        WeQrCodeDetailVo qrDetail = weQrCodeService.getQrDetail(Id);
+        return AjaxResult.success(qrDetail);
+    }
+
+    @ApiOperation(value = "获取活码列表", httpMethod = "GET")
+    @Log(title = "活码管理", businessType = BusinessType.SELECT)
+    @GetMapping("/list")
+    public TableDataInfo<WeQrCodeDetailVo> getQrCodeList(WeQrCodeListQuery qrCodeListQuery) {
+        startPage(true, false);
+        PageInfo<WeQrCodeDetailVo> qrCodeList = weQrCodeService.getQrCodeList(qrCodeListQuery);
+        return getDataTable(qrCodeList);
+    }
+
+    @ApiOperation(value = "删除活码", httpMethod = "DELETE")
+    @Log(title = "活码管理", businessType = BusinessType.SELECT)
+    @DeleteMapping("/del/{ids}")
+    public AjaxResult<WeQrCodeDetailVo> delQrCode(@PathVariable("ids") List<Long> ids) {
+        weQrCodeService.delQrCode(ids);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "获取活码统计", httpMethod = "GET")
+    @Log(title = "活码管理", businessType = BusinessType.SELECT)
+    @GetMapping("/scan/count")
+    public AjaxResult<WeQrCodeScanCountVo> getWeQrCodeScanCount(WeQrCodeListQuery qrCodeListQuery) {
+        WeQrCodeScanCountVo weQrCodeScanCount = weQrCodeService.getWeQrCodeScanCount(qrCodeListQuery);
+        return AjaxResult.success(weQrCodeScanCount);
+    }
+
+    /**
+     * 员工活码批量下载
+     *
+     * @param ids      员工活码ids
+     * @param request  请求
+     * @param response 输出
+     * @throws Exception
+     */
+    @ApiOperation(value = "员工活码批量下载", httpMethod = "GET")
+    @Log(title = "活码管理", businessType = BusinessType.OTHER)
+    @GetMapping("/batch/download")
+    public void batchDownload(@RequestParam("ids") List<Long> ids, HttpServletRequest request, HttpServletResponse response) throws IOException {
+        List<WeQrCodeDetailVo> qrCodeList = weQrCodeService.getQrDetailByQrIds(ids);
+        if (CollectionUtil.isNotEmpty(qrCodeList)) {
+            List< FileUtils.FileEntity> fileList = qrCodeList.stream().map(item -> {
+                List<WeQrScopeUserVo> userVoList = item.getQrUserInfos().stream().map(WeQrScopeVo::getWeQrUserList).flatMap(Collection::stream).collect(Collectors.toList());
+                String fileName = userVoList.stream().map(WeQrScopeUserVo::getUserName).collect(Collectors.joining(","));
+
+                return  FileUtils.FileEntity.builder()
+                        .fileName( fileName + "-" + item.getName())
+                        .url(item.getQrCode())
+                        .suffix(".jpg")
+                        .build();
+            }).collect(Collectors.toList());
+            FileUtils.batchDownloadFile(fileList, response.getOutputStream());
+        }
+    }
+
+    /**
+     * 员工活码下载
+     *
+     * @param id       员工活码id
+     * @param request  请求
+     * @param response 输出
+     * @throws Exception
+     */
+    @ApiOperation(value = "员工活码下载", httpMethod = "GET")
+    @Log(title = "活码管理", businessType = BusinessType.OTHER)
+    @GetMapping("/download")
+    public void download(@RequestParam("id") Long id, HttpServletRequest request, HttpServletResponse response) throws IOException {
+        WeQrCodeDetailVo qrDetail = weQrCodeService.getQrDetail(id);
+        if (StringUtils.isEmpty(qrDetail.getQrCode())) {
+            throw new CustomException("活码不存在");
+        } else {
+            FileUtils.downloadFile(qrDetail.getQrCode(), response.getOutputStream());
+        }
+    }
+
+    @ApiOperation(value = "删除活码分组", httpMethod = "GET")
+    @GetMapping("/deleteGroup")
+    public AjaxResult deleteQrGroup(@RequestParam("groupId") Long groupId){
+        weQrCodeService.deleteQrGroup(groupId);
+        return AjaxResult.success();
+    }
+}

+ 571 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeRedEnvelopesController.java

@@ -0,0 +1,571 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.constant.HttpStatus;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.RedEnvelopesType;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.common.utils.poi.ExcelUtil;
+import com.linkwechat.domain.WeCorpAccount;
+import com.linkwechat.domain.envelopes.WeRedEnvelopes;
+import com.linkwechat.domain.envelopes.WeRedEnvelopesLimit;
+import com.linkwechat.domain.envelopes.WeRedEnvelopesRecord;
+import com.linkwechat.domain.envelopes.WeUserRedEnvelopsLimit;
+import com.linkwechat.domain.envelopes.query.H5RedEnvelopesParmQuery;
+import com.linkwechat.domain.envelopes.query.WeRedEnvelopeListQuery;
+import com.linkwechat.domain.envelopes.vo.RedEnvelopesBaseInfoVo;
+import com.linkwechat.domain.envelopes.vo.WeCutomerRedEnvelopesVo;
+import com.linkwechat.domain.envelopes.vo.WeGroupRedEnvelopesVo;
+import com.linkwechat.domain.storecode.entity.WeStoreCode;
+import com.linkwechat.service.IWeCorpAccountService;
+import com.linkwechat.service.IWeRedEnvelopesService;
+import com.linkwechat.service.IWeUserRedEnvelopsLimitService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * 红包相关接口
+ */
+@Api(tags = "红包接口管理")
+@RestController
+@RequestMapping(value = "/RedEnvelopes")
+public class WeRedEnvelopesController extends BaseController {
+
+    @Autowired
+    private IWeRedEnvelopesService iWeRedEnvelopesService;
+
+
+    @Autowired
+    private IWeUserRedEnvelopsLimitService iWeUserRedEnvelopsLimitService;
+
+
+    @Autowired
+    private IWeCorpAccountService iWeCorpAccountService;
+
+    /**********************************************************
+     *******************************企业红包start***************
+     **********************************************************/
+
+    /**
+     * 红包限额获取
+     * @return
+     */
+    @GetMapping("/findLimitRedEnvelopes")
+    public AjaxResult<WeRedEnvelopesLimit> findLimitRedEnvelopes(){
+        return AjaxResult.success(
+                iWeRedEnvelopesService.findLimitRedEnvelopes()
+        );
+    }
+
+
+    /**
+     * 设置红包限额
+     * @param envelopesLimit
+     * @return
+     */
+    @PostMapping("/limitRedEnvelopes")
+    public AjaxResult limitRedEnvelopes(@RequestBody WeRedEnvelopesLimit envelopesLimit){
+        iWeRedEnvelopesService.limitRedEnvelopes(envelopesLimit);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 新建或编辑红包
+     * @return
+     */
+    @PostMapping("/addorUpdateRedEnvelopes")
+    public AjaxResult addorUpdateRedEnvelopes(@RequestBody WeRedEnvelopes weRedEnvelopes){
+        iWeRedEnvelopesService.saveOrUpdate(weRedEnvelopes);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 获取企业红包列表
+     * @param weRedEnvelope
+     * @return
+     */
+    @GetMapping("/redEnvelopes")
+    public TableDataInfo redEnvelopes(WeRedEnvelopes weRedEnvelope){
+        startPage();
+        return getDataTable(
+                iWeRedEnvelopesService.list(new LambdaQueryWrapper<WeRedEnvelopes>()
+                        .eq(weRedEnvelope.getStatus() != null,WeRedEnvelopes::getStatus,weRedEnvelope.getStatus())
+                        .eq(weRedEnvelope.getSceneType() !=null,WeRedEnvelopes::getSceneType,weRedEnvelope.getSceneType())
+                        .eq(WeRedEnvelopes::getRedEnvelopesType, RedEnvelopesType.COMPANY_RED_ENVELOPES.getType())
+                        .orderByDesc(WeRedEnvelopes::getCreateTime)
+                )
+        );
+    }
+
+    /**
+     * 启用或者停用红包 0:启用;1:停用
+     * @param weRedEnvelope
+     * @return
+     */
+    @PutMapping("/startOrStopRedEnvelope")
+    public AjaxResult startOrStopRedEnvelope(@RequestBody WeRedEnvelopes weRedEnvelope){
+
+        iWeRedEnvelopesService.updateById(weRedEnvelope);
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 删除红包
+     * @param ids
+     * @return
+     */
+    @DeleteMapping("/deleteRedEnvelop/{ids}")
+    public AjaxResult deleteRedEnvelop(@PathVariable String[] ids){
+        iWeRedEnvelopesService.removeByIds(ListUtil.toList(ids));
+        return AjaxResult.success();
+    }
+
+    /**********************************************************
+     *******************************企业红包end*****************
+     **********************************************************/
+
+
+    /**********************************************************
+     *******************************员工限额start***************
+     **********************************************************/
+
+    /**
+     * 单个员工限额新增或编辑
+     * @param redEnvelopsLimit
+     * @return
+     */
+    @PostMapping("/addOrUpdateUserRedEnvelopsLimit")
+    public AjaxResult addOrUpdateUserRedEnvelopsLimit(@RequestBody WeUserRedEnvelopsLimit redEnvelopsLimit){
+        StringBuilder sb=new StringBuilder();
+        String weUserId = redEnvelopsLimit.getWeUserId();
+        if(StringUtils.isNotEmpty(weUserId)){
+            WeRedEnvelopesLimit limitRedEnvelopes = iWeRedEnvelopesService.findLimitRedEnvelopes();
+            List<WeUserRedEnvelopsLimit> redEnvelopsLimits=new ArrayList<>();
+            String[] split = weUserId.split(",");
+            List<String> list = Arrays.asList(split);
+            int singleCustomerReceiveMoney = limitRedEnvelopes.getSingleCustomerReceiveMoney();
+            Integer customerReceiveMoney = redEnvelopsLimit.getSingleCustomerReceiveMoney();
+            if(singleCustomerReceiveMoney < customerReceiveMoney * list.size()){
+                return AjaxResult.error("员工红包金额不能超过企业红包限额");
+            }
+
+            List<WeUserRedEnvelopsLimit> weUserRedEnvelopsLimitList= iWeUserRedEnvelopsLimitService.list(
+                    new LambdaQueryWrapper<WeUserRedEnvelopsLimit>().in(WeUserRedEnvelopsLimit::getWeUserId, list));
+
+            if(ObjectUtil.isNotEmpty(weUserRedEnvelopsLimitList)){
+                return AjaxResult.error("员工不允许有重复");
+            }
+            list.forEach(k->{
+                WeUserRedEnvelopsLimit copyRedEnvelops=new WeUserRedEnvelopsLimit();
+                BeanUtil.copyProperties(redEnvelopsLimit,copyRedEnvelops,true);
+                copyRedEnvelops.setWeUserId(k);
+                redEnvelopsLimits.add(copyRedEnvelops);
+
+            });
+            iWeUserRedEnvelopsLimitService.saveOrUpdateBatch(redEnvelopsLimits);
+        }
+
+        return AjaxResult.success(sb.toString());
+    }
+
+
+    /**
+     * 批量更新或编辑员工限额
+     * @param redEnvelopsLimit
+     * @return
+     */
+    @PostMapping("/batchUpdateUserRedEnvelopsLimit")
+    public AjaxResult batchUpdateUserRedEnvelopsLimit(@RequestBody WeUserRedEnvelopsLimit redEnvelopsLimit){
+        if(StringUtils.isNotEmpty(redEnvelopsLimit.getIds())){
+            List<WeUserRedEnvelopsLimit> redEnvelopsLimits=new ArrayList<>();
+            Arrays.asList(redEnvelopsLimit.getIds().split(",")).stream().forEach(k->{
+                redEnvelopsLimits.add(
+                        WeUserRedEnvelopsLimit.builder()
+                                .id(Long.parseLong(k))
+                                .singleCustomerReceiveMoney(redEnvelopsLimit.getSingleCustomerReceiveMoney())
+                                .singleCustomerReceiveNum(redEnvelopsLimit.getSingleCustomerReceiveNum())
+                                .build()
+                );
+            });
+
+            iWeUserRedEnvelopsLimitService.updateBatchById(redEnvelopsLimits);
+        }
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 员工限额列表
+     * @param userId
+     * @return
+     */
+    @GetMapping("/findLimitUserRedEnvelops")
+    public TableDataInfo findLimitUserRedEnvelops(String userId){
+        startPage();
+        return getDataTable(
+                iWeUserRedEnvelopsLimitService.findLimitUserRedEnvelops(userId)
+        );
+    }
+
+
+    /**
+     * 删除员工限额
+     * @param ids
+     * @return
+     */
+    @DeleteMapping("/deleteLimitUserRedEnvelops/{ids}")
+    public AjaxResult deleteLimitUserRedEnvelops(@PathVariable String[] ids){
+        iWeUserRedEnvelopsLimitService.removeByIds(ListUtil.toList(ids));
+        return AjaxResult.success();
+    }
+
+    /**********************************************************
+     *******************************员工限额end*****************
+     **********************************************************/
+
+    /**********************************************************
+     *******************************发放记录start***************
+     **********************************************************/
+    /**
+     * 发放记录-发放客户
+     * @param redEnvelopesRecord
+     * @return
+     */
+    @GetMapping("/findRedEnveForUser")
+    public TableDataInfo findRedEnveForUser(WeRedEnvelopesRecord redEnvelopesRecord){
+        startPage();
+        return getDataTable(
+                iWeRedEnvelopesService.findRedEnveForUser(redEnvelopesRecord)
+        );
+    }
+
+
+    /**
+     * 发放记录-发放客户导出
+     * @param redEnvelopesRecord
+     * @return
+     */
+    @GetMapping("/exportRedEnveForUser")
+    public AjaxResult exportRedEnveForUser(WeRedEnvelopesRecord redEnvelopesRecord){
+        List<WeCutomerRedEnvelopesVo> redEnvelopesVos=iWeRedEnvelopesService.findRedEnveForUser(redEnvelopesRecord);
+        ExcelUtil<WeCutomerRedEnvelopesVo> util = new ExcelUtil<>(WeCutomerRedEnvelopesVo.class);
+        return util.exportExcel(redEnvelopesVos, UUID.randomUUID().toString());
+    }
+
+
+    /**
+     *  发放记录-发放客户群
+     * @param redEnvelopesRecord
+     * @return
+     */
+    @GetMapping("/findRedEnveForGroup")
+    public TableDataInfo findRedEnveForGroup(WeRedEnvelopesRecord redEnvelopesRecord){
+        startPage();
+        return getDataTable(
+                iWeRedEnvelopesService.findRedEnveForGroup(redEnvelopesRecord)
+        );
+
+    }
+
+    /**
+     *  发放记录-发放客户群导出
+     * @param redEnvelopesRecord
+     * @return
+     */
+    @GetMapping("/exportRedEnveForGroup")
+    public AjaxResult exportRedEnveForGroup(WeRedEnvelopesRecord redEnvelopesRecord){
+        List<WeGroupRedEnvelopesVo> redEnvelopesVos=iWeRedEnvelopesService.findRedEnveForGroup(redEnvelopesRecord);
+        ExcelUtil<WeGroupRedEnvelopesVo> util = new ExcelUtil<WeGroupRedEnvelopesVo>(WeGroupRedEnvelopesVo.class);
+        return util.exportExcel(redEnvelopesVos, "红包发放客户群");
+    }
+
+
+    /**
+     * 获取群成员领取红包列表
+     * @param chatId
+     * @return
+     */
+    @GetMapping("/findRedEnveForGroupUser")
+    public TableDataInfo findRedEnveForGroupUser(String chatId,String orderNo){
+        startPage();
+
+        return getDataTable(
+                iWeRedEnvelopesService.findRedEnveForGroupUser(chatId,orderNo)
+        );
+    }
+    /**********************************************************
+     *******************************发放记录end*****************
+     **********************************************************/
+
+
+
+    /**********************************************************
+     *******************************支出统计start***************
+     **********************************************************/
+    /**
+     * 支出统计头部
+     * @return
+     */
+    @GetMapping("/countTab")
+    public AjaxResult countTab(){
+
+        return AjaxResult.success(
+                iWeRedEnvelopesService.countTab()
+        );
+    }
+
+
+    /**
+     * 支出统计-支出趋势
+     * @param beginTime
+     * @param endTime
+     * @return
+     */
+    @GetMapping("/countLineChart")
+    public AjaxResult countLineChart(String beginTime,String endTime){
+
+        return AjaxResult.success(
+                iWeRedEnvelopesService.countLineChart(beginTime,endTime)
+        );
+    }
+
+
+    /**
+     * 支出统计-支出记录
+     * @return
+     */
+    @GetMapping("/findRecordGroupByUserId")
+    public TableDataInfo findRecordGroupByUserId(String beginTime,String endTime){
+        startPage();
+
+        return getDataTable(
+                iWeRedEnvelopesService.findRecordGroupByUserId(beginTime,endTime)
+        );
+    }
+
+
+    /**********************************************************
+     *******************************支出统计end*****************
+     **********************************************************/
+
+
+    /**********************************************************
+     *******************************移动端start*****************
+     **********************************************************/
+
+
+    /**
+     * 为客户发送个人红包
+     * @return
+     */
+    @PostMapping("/sendReEnvelopesToCustomer")
+    public AjaxResult sendPersonReEnvelopesToCustomer(@RequestBody H5RedEnvelopesParmQuery parmDto)  {
+        return AjaxResult.success(
+                iWeRedEnvelopesService.createCustomerRedEnvelopesOrder(parmDto.getRedenvelopesId(),parmDto.getRedEnvelopeAmount(),
+                        parmDto.getRedEnvelopeName(),1,
+                        parmDto.getSendUserId(),1, parmDto.getExternalUserid())
+        );
+    }
+
+
+    /**
+     *  为客户发送企业红包
+     * @param parmDto
+     * @return
+     */
+    @PostMapping("/sendCompanyEnvelopesToCustomer")
+    public AjaxResult sendCompanyEnvelopesToCustomer(@RequestBody H5RedEnvelopesParmQuery parmDto){
+
+        WeRedEnvelopes weRedEnvelopes
+                = iWeRedEnvelopesService.getById(parmDto.getRedenvelopesId());
+        if(null == weRedEnvelopes){
+            return AjaxResult.error(HttpStatus.NOT_ORTHER_IMPLEMENTED,"当前企业红包不存在");
+        }
+
+        String returnMsg = iWeRedEnvelopesService.checkRedEnvelopesTplLimit();
+
+        if(StringUtils.isNotEmpty(returnMsg)){
+            return AjaxResult.error(HttpStatus.NOT_ORTHER_IMPLEMENTED,returnMsg);
+        }
+
+
+        return AjaxResult.success(
+                iWeRedEnvelopesService.createCustomerRedEnvelopesOrder(parmDto.getRedenvelopesId(),weRedEnvelopes.getMoney(),
+                        weRedEnvelopes.getName(),1,
+                        parmDto.getSendUserId(),2, parmDto.getExternalUserid())
+        );
+    }
+
+
+    /**
+     *  为客户群发送个人红包
+     * @param parmDto
+     * @return
+     */
+    @PostMapping("/sendPersonReEnvelopesToGroup")
+    public AjaxResult sendPersonReEnvelopesToGroup(@RequestBody H5RedEnvelopesParmQuery parmDto){
+
+        return AjaxResult.success(
+
+                iWeRedEnvelopesService.createGroupRedEnvelopesOrder(parmDto.getRedenvelopesId(),
+                        parmDto.getRedEnvelopeAmount(),parmDto.getRedEnvelopeName(),
+                        parmDto.getRedEnvelopeNum(),parmDto.getChatId(),parmDto.getSendUserId(),parmDto.getRedEnvelopesType(),1
+                )
+        );
+    }
+
+
+    /**
+     *  为客户群发送企业红包
+     * @param parmDto
+     * @return
+     */
+    @PostMapping("/sendCompanyEnvelopesToGroup")
+    public AjaxResult sendCompanyEnvelopesToGroup(@RequestBody H5RedEnvelopesParmQuery parmDto){
+
+        WeRedEnvelopes weRedEnvelopes
+                = iWeRedEnvelopesService.getById(parmDto.getRedenvelopesId());
+        if(null == weRedEnvelopes){
+            return AjaxResult.error(HttpStatus.NOT_ORTHER_IMPLEMENTED,"当前企业红包不存在");
+        }
+
+        String returnMsg = iWeRedEnvelopesService.checkRedEnvelopesTplLimit();
+
+        if(StringUtils.isNotEmpty(returnMsg)){
+            return AjaxResult.error(HttpStatus.NOT_ORTHER_IMPLEMENTED,returnMsg);
+        }
+
+        return AjaxResult.success(
+                iWeRedEnvelopesService.createGroupRedEnvelopesOrder(parmDto.getRedenvelopesId(),
+                        weRedEnvelopes.getMoney(),weRedEnvelopes.getName(),parmDto.getRedEnvelopeNum(),parmDto.getChatId(),parmDto.getSendUserId(),parmDto.getRedEnvelopesType(),2
+                )
+        );
+
+    }
+
+
+    /**
+     * 获取企业红包模版
+     * @param sceneType 1:客户 2:客群
+     * @return
+     */
+    @GetMapping("/findCompanyRedEnvelopes")
+    public AjaxResult findCompanyRedEnvelopes(Integer  sceneType){
+        return AjaxResult.success(
+                iWeRedEnvelopesService.list(
+                        new LambdaQueryWrapper<WeRedEnvelopes>()
+                                .eq(WeRedEnvelopes::getStatus,0)
+                                .in(WeRedEnvelopes::getSceneType,ListUtil.toList(sceneType,3))
+                )
+        );
+    }
+
+
+
+
+
+
+    /**
+     * 红包领取
+     * @param orderNo
+     * @param openId
+     * @param receiveName
+     * @param avatar
+     * @return
+     * @throws Exception
+     */
+    @GetMapping("/receiveRedEnvelopes")
+    public AjaxResult receiveRedEnvelopes(String orderNo,String openId,String receiveName,String avatar) throws Exception {
+
+        String returnMsg = iWeRedEnvelopesService.customerReceiveRedEnvelopes(orderNo, openId,receiveName,avatar);
+
+        if(StringUtils.isNotEmpty(returnMsg)){
+            return AjaxResult.error(HttpStatus.NOT_IMPLEMENTED,returnMsg);
+        }
+        return AjaxResult.success(
+                returnMsg
+        );
+    }
+
+
+    /**
+     * 红包领取列表
+     * @param orderNo
+     * @param openId
+     * @param chatId
+     * @return
+     */
+    @GetMapping("/receiveRedEnvelopesLists")
+    public AjaxResult receiveRedEnvelopesLists(String orderNo,String openId,String chatId){
+
+        return AjaxResult.success(
+                iWeRedEnvelopesService.detailDto(orderNo,openId,chatId)
+        );
+    }
+
+    /**
+     * 获取红包基础信息
+     * @return
+     */
+    @GetMapping("/findRedEnvelopesInfo")
+    public AjaxResult<RedEnvelopesBaseInfoVo> findRedEnvelopesInfo(){
+        RedEnvelopesBaseInfoVo baseInfo=RedEnvelopesBaseInfoVo.builder().build();
+        WeCorpAccount validWeCorpAccount = iWeCorpAccountService.getCorpAccountByCorpId(null);
+        if(validWeCorpAccount !=null){
+            baseInfo.setLogo(validWeCorpAccount.getLogoUrl());
+            baseInfo.setCorpName(validWeCorpAccount.getCompanyName());
+//            baseInfo.setWeAppId(validWeCorpAccount.getWxAppId());
+//            baseInfo.setWeAppSecret(baseInfo.getWeAppSecret());
+        }
+        return AjaxResult.success(baseInfo);
+    }
+
+
+    /**
+     * 校验客户是否领取红包
+     * @param orderNo 订单id
+     * @param openId 客户id
+     * @return
+     */
+    @GetMapping("/checkRecevieRedEnvelopes")
+    public AjaxResult checkRecevieRedEnvelopes(String orderNo,String openId){
+
+        return AjaxResult.success(
+                iWeRedEnvelopesService.checkRecevieRedEnvelopes(orderNo,openId)
+        );
+
+    }
+
+
+    /**********************************************************
+     *******************************移动端end*******************
+     **********************************************************/
+
+
+    @ApiOperation(value = "红包列表查询",httpMethod = "GET")
+    @GetMapping("/list")
+    public TableDataInfo<List<WeRedEnvelopes>> getList(WeRedEnvelopeListQuery query){
+        super.startPage();
+        List<WeRedEnvelopes> list =  iWeRedEnvelopesService.getList(query);
+        return getDataTable(list);
+    }
+
+}

+ 120 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeSensitiveActController.java

@@ -0,0 +1,120 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.constant.HttpStatus;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.utils.poi.ExcelUtil;
+import com.linkwechat.domain.WeSensitiveAct;
+import com.linkwechat.domain.WeSensitiveActHit;
+import com.linkwechat.service.IWeSensitiveActHitService;
+import com.linkwechat.service.IWeSensitiveActService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 敏感行为管理接口
+ *
+ * @author leejoker <1056650571@qq.com>
+ * @version 1.0
+ * @date 2021/1/12 18:07
+ */
+@Api(tags = "敏感行为管理接口")
+@RestController
+@RequestMapping("/sensitive/act")
+public class WeSensitiveActController extends BaseController {
+    @Autowired
+    private IWeSensitiveActService weSensitiveActService;
+
+    @Autowired
+    private IWeSensitiveActHitService weSensitiveActHitService;
+
+    /**
+     * 查询敏感行为列表
+     */
+    @ApiOperation(value = "查询敏感行为列表",httpMethod = "GET")
+    @GetMapping("/list")
+    public TableDataInfo<List<WeSensitiveAct>> list(WeSensitiveAct weSensitiveAct) {
+        startPage();
+        List<WeSensitiveAct> list = weSensitiveActService.selectWeSensitiveActList(weSensitiveAct);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取敏感行为详细信息
+     */
+    @ApiOperation(value = "获取敏感行为详细信息",httpMethod = "GET")
+    @GetMapping(value = "/{id}")
+    public AjaxResult<WeSensitiveAct> getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(weSensitiveActService.selectWeSensitiveActById(id));
+    }
+
+    /**
+     * 新增敏感行为设置
+     */
+    @ApiOperation(value = "新增敏感行为设置",httpMethod = "POST")
+    @Log(title = "新增敏感行为", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Valid @RequestBody WeSensitiveAct weSensitiveAct) {
+        return weSensitiveActService.insertWeSensitiveAct(weSensitiveAct) ? AjaxResult.success() : AjaxResult.error();
+    }
+
+    /**
+     * 修改敏感行为
+     */
+    @ApiOperation(value = "修改敏感行为",httpMethod = "PUT")
+    @Log(title = "修改敏感行为", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody WeSensitiveAct weSensitiveAct) {
+        Long id = weSensitiveAct.getId();
+        WeSensitiveAct originData = weSensitiveActService.selectWeSensitiveActById(id);
+        if (originData == null) {
+            return AjaxResult.error(HttpStatus.NOT_FOUND, "数据不存在");
+        }
+        return weSensitiveActService.updateWeSensitiveAct(weSensitiveAct) ? AjaxResult.success() : AjaxResult.error();
+    }
+
+    /**
+     * 删除敏感行为
+     */
+    @ApiOperation(value = "删除敏感行为",httpMethod = "DELETE")
+    @Log(title = "删除敏感行为", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable("ids") String ids) {
+        String[] id = ids.split(",");
+        Long[] idArray = new Long[id.length];
+        Arrays.stream(id).map(Long::parseLong).collect(Collectors.toList()).toArray(idArray);
+        return weSensitiveActService.deleteWeSensitiveActByIds(idArray) ? AjaxResult.success() : AjaxResult.error();
+    }
+
+    /**
+     * 敏感行为命中查询
+     */
+    @ApiOperation(value = "敏感行为命中查询(这个接口有问题)",httpMethod = "GET")
+    @GetMapping("/hit/list")
+    public TableDataInfo<List<WeSensitiveActHit>> hitList() {
+        startPage();
+        List<WeSensitiveActHit> list = weSensitiveActHitService.list();
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出敏感行为记录
+     */
+    @ApiOperation(value = "导出敏感行为记录",httpMethod = "POST")
+    @PostMapping("/hit/export")
+    public AjaxResult export() {
+        List<WeSensitiveActHit> list = weSensitiveActHitService.list();
+        ExcelUtil<WeSensitiveActHit> util = new ExcelUtil<>(WeSensitiveActHit.class);
+        return util.exportExcel(list, "敏感行为记录");
+    }
+}

+ 105 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeSensitiveController.java

@@ -0,0 +1,105 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.constant.HttpStatus;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.domain.WeSensitive;
+import com.linkwechat.domain.msgaudit.query.WeSensitiveHitQuery;
+import com.linkwechat.domain.msgaudit.vo.WeChatContactSensitiveMsgVo;
+import com.linkwechat.service.IWeSensitiveService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 敏感词Controller
+ *
+ * @author ruoyi
+ * @date 2020-12-29
+ */
+@RestController
+@RequestMapping("/sensitive")
+@Api(tags = "敏感词管理")
+public class WeSensitiveController extends BaseController {
+    @Autowired
+    private IWeSensitiveService weSensitiveService;
+
+    /**
+     * 查询敏感词设置列表
+     */
+    @GetMapping("/list")
+    @ApiOperation(value = "查询敏感词列表",httpMethod = "GET")
+    public TableDataInfo<List<WeSensitive>> list(WeSensitive weSensitive) {
+        startPage();
+        List<WeSensitive> list = weSensitiveService.selectWeSensitiveList(weSensitive);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取敏感词设置详细信息
+     */
+    @GetMapping(value = "/{id}")
+    @ApiOperation(value = "查询敏感词详情",httpMethod = "GET")
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(weSensitiveService.selectWeSensitiveById(id));
+    }
+
+    /**
+     * 新增敏感词设置
+     */
+    @Log(title = "敏感词设置", businessType = BusinessType.INSERT)
+    @PostMapping
+    @ApiOperation(value = "添加敏感词",httpMethod = "POST")
+    public AjaxResult add(@Valid @RequestBody WeSensitive weSensitive) {
+        weSensitiveService.insertWeSensitive(weSensitive);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 修改敏感词设置
+     */
+    @Log(title = "敏感词设置", businessType = BusinessType.UPDATE)
+    @PutMapping
+    @ApiOperation(value = "修改敏感词",httpMethod = "PUT")
+    public AjaxResult edit(@Valid @RequestBody WeSensitive weSensitive) {
+        WeSensitive originData = weSensitiveService.selectWeSensitiveById(weSensitive.getId());
+        if (originData == null) {
+            return AjaxResult.error(HttpStatus.NOT_FOUND, "数据不存在");
+        }
+        weSensitiveService.updateWeSensitive(weSensitive);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 删除敏感词设置
+     */
+    @Log(title = "敏感词设置", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    @ApiOperation(value = "删除敏感词",httpMethod = "DELETE")
+    public AjaxResult remove(@PathVariable("ids") String ids) {
+        String[] id = ids.split(",");
+        Long[] idArray = new Long[id.length];
+        Arrays.stream(id).map(Long::parseLong).collect(Collectors.toList()).toArray(idArray);
+        weSensitiveService.destroyWeSensitiveByIds(idArray);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 敏感词命中查询
+     */
+    @GetMapping("/hit/list")
+    @ApiOperation(value = "敏感词命中查询",httpMethod = "GET")
+    public TableDataInfo<WeChatContactSensitiveMsgVo> hitList(WeSensitiveHitQuery query) {
+        startPage();
+        return getDataTable(weSensitiveService.getHitSensitiveList(query));
+    }
+}

+ 131 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeShortLinkController.java

@@ -0,0 +1,131 @@
+package com.linkwechat.controller;
+
+import com.github.pagehelper.PageInfo;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.constant.WeConstans;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.core.redis.RedisService;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.exception.wecom.WeComException;
+import com.linkwechat.common.utils.Base62NumUtil;
+import com.linkwechat.domain.shortlink.query.WeShortLinkAddQuery;
+import com.linkwechat.domain.shortlink.query.WeShortLinkQuery;
+import com.linkwechat.domain.shortlink.query.WeShortLinkStatisticQuery;
+import com.linkwechat.domain.shortlink.vo.WeShortLinkAddVo;
+import com.linkwechat.domain.shortlink.vo.WeShortLinkListVo;
+import com.linkwechat.domain.shortlink.vo.WeShortLinkStatisticsVo;
+import com.linkwechat.domain.shortlink.vo.WeShortLinkVo;
+import com.linkwechat.service.IWeShortLinkService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author danmo
+ * @description 短链管理
+ * @date 2022/12/18 18:22
+ **/
+
+@RestController
+@RequestMapping(value = "/short/link")
+@Api(tags = "短链管理")
+public class WeShortLinkController extends BaseController {
+
+    @Autowired
+    private IWeShortLinkService weShortLinkService;
+
+    @Autowired
+    private RedisService redisService;
+
+    @ApiOperation(value = "新增短链", httpMethod = "POST")
+    @Log(title = "新增短链", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult<WeShortLinkAddVo> addShortLink(@RequestBody @Validated WeShortLinkAddQuery query) {
+        WeShortLinkAddVo result = weShortLinkService.addShortLink(query);
+        return AjaxResult.success(result);
+    }
+
+
+    @ApiOperation(value = "修改短链", httpMethod = "PUT")
+    @Log(title = "修改短链", businessType = BusinessType.UPDATE)
+    @PutMapping("/update/{id}")
+    public AjaxResult<WeShortLinkAddVo> updateShortLink(@PathVariable("id") Long id, @RequestBody @Validated WeShortLinkAddQuery query) {
+        query.setId(id);
+        WeShortLinkAddVo result = weShortLinkService.updateShortLink(query);
+        return AjaxResult.success(result);
+    }
+
+
+    @ApiOperation(value = "删除短链", httpMethod = "DELETE")
+    @Log(title = "删除短链", businessType = BusinessType.DELETE)
+    @DeleteMapping("/delete/{ids}")
+    public AjaxResult deleteShortLink(@PathVariable("ids") List<Long> ids) {
+        weShortLinkService.deleteShortLink(ids);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation(value = "通过ID获取短链详情", httpMethod = "GET")
+    @Log(title = "短链详情", businessType = BusinessType.SELECT)
+    @GetMapping("/get/{id}")
+    public AjaxResult<WeShortLinkVo> getShortLinkInfo(@PathVariable("id") Long id) {
+        WeShortLinkVo result = weShortLinkService.getShortLinkInfo(id);
+        return AjaxResult.success(result);
+    }
+
+    @ApiOperation(value = "通过短链接获取短链详情", httpMethod = "GET")
+    @Log(title = "短链详情", businessType = BusinessType.SELECT)
+    @GetMapping("/getByShort/{shortUrl}")
+    public AjaxResult<WeShortLinkVo> getShortLinkInfo(@PathVariable("shortUrl") String shortUrl) {
+        long id = Base62NumUtil.decode(shortUrl);
+        WeShortLinkVo result = weShortLinkService.getShortLinkInfo(id);
+        redisService.increment(WeConstans.WE_SHORT_LINK_KEY  + WeConstans.OPEN_APPLET + shortUrl);
+        return AjaxResult.success(result);
+    }
+
+
+    @ApiOperation(value = "短链列表", httpMethod = "GET")
+    @Log(title = "短链列表", businessType = BusinessType.SELECT)
+    @GetMapping("/list")
+    public TableDataInfo<PageInfo<WeShortLinkListVo>> getShortLinkList(WeShortLinkQuery query) {
+        startPage();
+        PageInfo<WeShortLinkListVo> shortLinkList = weShortLinkService.getShortLinkList(query);
+        return getDataTable(shortLinkList);
+    }
+
+
+    @ApiOperation(value = "短链数据统计(传ID查单个)", httpMethod = "GET")
+    @Log(title = "短链数据统计", businessType = BusinessType.SELECT)
+    @GetMapping("/data/statistics")
+    public AjaxResult<WeShortLinkStatisticsVo> getDataStatistics(WeShortLinkStatisticQuery query) {
+        if(Objects.isNull(query)){
+            throw new WeComException("入参不能为空");
+        }
+        WeShortLinkStatisticsVo result = weShortLinkService.getDataStatistics(query);
+        return AjaxResult.success(result);
+    }
+
+    @ApiOperation(value = "短链折线统计", httpMethod = "GET")
+    @Log(title = "短链折线统计", businessType = BusinessType.SELECT)
+    @GetMapping("/line/statistics")
+    public AjaxResult<WeShortLinkStatisticsVo> getLineStatistics(WeShortLinkStatisticQuery query) {
+        if(Objects.isNull(query)){
+            throw new WeComException("入参不能为空");
+        }
+        if(Objects.isNull(query.getId())){
+            throw new WeComException("ID不能为空");
+        }
+        if(Objects.isNull(query.getBeginTime())){
+            throw new WeComException("开始时间不能为空");
+        }
+        WeShortLinkStatisticsVo result = weShortLinkService.getLineStatistics(query);
+        return AjaxResult.success(result);
+    }
+
+}

+ 540 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeStoreCodeController.java

@@ -0,0 +1,540 @@
+package com.linkwechat.controller;
+
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.collection.ListUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.annotation.Log;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.domain.entity.SysDictData;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.BusinessType;
+import com.linkwechat.common.utils.DateUtils;
+import com.linkwechat.common.utils.MapUtils;
+import com.linkwechat.common.utils.ServletUtils;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.common.utils.file.FileUtils;
+import com.linkwechat.common.utils.poi.ExcelUtil;
+import com.linkwechat.common.utils.poi.LwExcelUtil;
+import com.linkwechat.domain.WeCustomerSeas;
+import com.linkwechat.domain.groupcode.entity.WeGroupCode;
+import com.linkwechat.domain.storecode.entity.WeStoreCode;
+import com.linkwechat.domain.storecode.entity.WeStoreCodeConfig;
+import com.linkwechat.domain.qr.WeQrAttachments;
+import com.linkwechat.domain.storecode.entity.WeStoreCodeCount;
+import com.linkwechat.domain.storecode.vo.WeStoreCodesVo;
+import com.linkwechat.domain.storecode.vo.datareport.WeStoreGroupReportVo;
+import com.linkwechat.domain.storecode.vo.datareport.WeStoreShopGuideReportVo;
+import com.linkwechat.domain.storecode.vo.drum.WeStoreGroupDrumVo;
+import com.linkwechat.domain.storecode.vo.drum.WeStoreShopGuideDrumVo;
+import com.linkwechat.domain.storecode.vo.tab.WeStoreGroupTabVo;
+import com.linkwechat.domain.storecode.vo.tab.WeStoreShopGuideTabVo;
+import com.linkwechat.domain.storecode.vo.tab.WeStoreTabVo;
+import com.linkwechat.domain.storecode.vo.trend.WeStoreGroupTrendVo;
+import com.linkwechat.domain.storecode.vo.trend.WeStoreShopGuideTrendVo;
+import com.linkwechat.service.IWeQrAttachmentsService;
+import com.linkwechat.service.IWeStoreCodeConfigService;
+import com.linkwechat.service.IWeStoreCodeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+
+/**
+ * 门店活码
+ */
+@RestController
+@RequestMapping("/storeCode")
+public class WeStoreCodeController extends BaseController {
+
+    @Autowired
+    private IWeStoreCodeConfigService iWeStoreCodeConfigService;
+
+
+    @Autowired
+    private IWeStoreCodeService iWeStoreCodeService;
+
+    @Autowired
+    private IWeQrAttachmentsService attachmentsService;
+
+    @Autowired
+    private MapUtils mapUtils;
+
+
+    /**
+     *  获取门店导购码或门店群活码
+     * @param storeCodeType
+     * @return
+     */
+    @GetMapping("/config/{storeCodeType}")
+    public AjaxResult<WeStoreCodeConfig> getStoreCodeConfig(@PathVariable Integer storeCodeType){
+
+        WeStoreCodeConfig storeCodeConfig = iWeStoreCodeConfigService.getOne(new LambdaQueryWrapper<WeStoreCodeConfig>()
+                .eq(WeStoreCodeConfig::getStoreCodeType, storeCodeType));
+        if(null != storeCodeConfig){
+            storeCodeConfig.setWeQrAttachments(
+                    attachmentsService.list(new LambdaQueryWrapper<WeQrAttachments>()
+                            .eq(WeQrAttachments::getQrId, storeCodeConfig.getId())
+                            .eq(WeQrAttachments::getBusinessType,2))
+            );
+        }
+
+        return AjaxResult.success(
+                storeCodeConfig
+        );
+    }
+
+
+    /**
+     * 下载导购或门前群活码
+     * 门店码类型(1:门店导购码;2:门店群活码)
+     */
+    @Log(title = "群活码下载", businessType = BusinessType.OTHER)
+    @GetMapping("/downloadStoreCodeConfigUrl")
+    public void downloadStoreCodeConfigUrl(Integer storeCodeType, HttpServletResponse response) {
+        WeStoreCodeConfig storeCodeConfig = iWeStoreCodeConfigService.getOne(new LambdaQueryWrapper<WeStoreCodeConfig>()
+                .eq(WeStoreCodeConfig::getStoreCodeType, storeCodeType));
+        try {
+            if(storeCodeConfig != null && StringUtils.isNotEmpty(storeCodeConfig.getStoreCodeConfigQr())){
+                FileUtils.downloadFile(storeCodeConfig.getStoreCodeConfigQr(), response.getOutputStream());
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+
+    /**
+     * 新增或边际门店活码或群活码
+     * @param storeCodeConfig
+     * @return
+     */
+    @PostMapping("/config/createOrUpdate")
+    public AjaxResult createOrUpdateConfig(@RequestBody WeStoreCodeConfig storeCodeConfig) throws IOException {
+
+        iWeStoreCodeConfigService.createOrUpdate(storeCodeConfig);
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 新建或编辑门店
+     * @param weStoreCode
+     * @return
+     */
+    @PostMapping("/code/createOrUpdateStoreCode")
+    public AjaxResult createOrUpdateStoreCode(@RequestBody WeStoreCode weStoreCode){
+
+        iWeStoreCodeService.createOrUpdateStoreCode(weStoreCode);
+
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 获取门店列表
+     * @param weStoreCode
+     * @return
+     */
+    @GetMapping("/storeCodes")
+    public TableDataInfo storeCodes(WeStoreCode weStoreCode){
+        startPage();
+        return getDataTable(
+             iWeStoreCodeService.storeCodes(weStoreCode)
+        );
+    }
+
+    /**
+     * 跟进门店id获取门店详情
+     * @param storeId
+     * @return
+     */
+    @GetMapping("/getWeStoreCodeById/{storeId}")
+    public AjaxResult<WeStoreCode> getWeStoreCodeById(@PathVariable Long storeId){
+        return AjaxResult.success(
+                iWeStoreCodeService.getById(storeId)
+        );
+    }
+
+    /**
+     * 通过id列表批量删除任务
+     *
+     * @param ids id列表
+     * @return 结果
+     */
+    @DeleteMapping(path = "/{ids}")
+    public AjaxResult batchDeleteStoreCode(@PathVariable("ids") Long[] ids) {
+        iWeStoreCodeService.removeByIds(Arrays.asList(ids));
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 批量启用或停用门店
+     * @param ids
+     * @param weStoreCode
+     * @return
+     */
+    @PutMapping("/batchStartOrStop/{ids}")
+    public AjaxResult batchStartOrStop(@PathVariable("ids") Long[] ids,@RequestBody WeStoreCode weStoreCode){
+
+        iWeStoreCodeService.update(WeStoreCode.builder()
+                        .storeState(weStoreCode.getStoreState())
+                .build(), new LambdaQueryWrapper<WeStoreCode>()
+                .in(WeStoreCode::getId,Arrays.asList(ids)));
+
+        return AjaxResult.success();
+
+    }
+
+
+    /***************************************************************
+     **************************统计相关tab相关 start*******************
+     ****************************************************************/
+
+
+
+    /**
+     * 门店导购码统计-头部tab
+     * @return
+     */
+    @GetMapping("/countWeStoreShopGuideTab")
+    public AjaxResult<WeStoreShopGuideTabVo> countWeStoreShopGuideTab(){
+
+        return AjaxResult.success(
+                iWeStoreCodeService.countWeStoreShopGuideTab()
+        );
+    }
+
+
+    /**
+     * 门店码统计-头部tab
+     * @param storeCodeId
+     * @return
+     */
+    @GetMapping("/countWeStoreTab/{storeCodeId}")
+    public AjaxResult<WeStoreTabVo> countWeStoreTab(@PathVariable Long storeCodeId){
+
+
+        return AjaxResult.success(
+                iWeStoreCodeService.countWeStoreTab(storeCodeId)
+        );
+    }
+
+
+    /**
+     *  门店群活码统计-头部tab
+     * @return
+     */
+    @GetMapping("/countWeStoreGroupTab")
+    public AjaxResult<WeStoreGroupTabVo> countWeStoreGroupTab(){
+
+        return AjaxResult.success(
+                iWeStoreCodeService.countWeStoreGroupTab()
+        );
+    }
+
+
+
+    /***************************************************************
+     **************************统计相关tab相关 end********************
+     ****************************************************************/
+
+
+
+
+    /***************************************************************
+     **************************统计相关趋势图相关 start****************
+     ****************************************************************/
+
+    /**
+     * 门店导购码趋势图
+     * @param weStoreCode
+     * @return
+     */
+    @GetMapping("/countStoreShopGuideTrend")
+    public AjaxResult<List<WeStoreShopGuideTrendVo>> countStoreShopGuideTrend(WeStoreCode weStoreCode){
+
+
+        return AjaxResult.success(
+                iWeStoreCodeService.countStoreShopGuideTrend(weStoreCode)
+        );
+    }
+
+
+    /**
+     * 门店群码趋势图
+     * @param weStoreCode
+     * @return
+     */
+    @GetMapping("/countStoreGroupTrend")
+    public AjaxResult<List<WeStoreGroupTrendVo>> countStoreGroupTrend(WeStoreCode weStoreCode){
+
+
+        return AjaxResult.success(
+                iWeStoreCodeService.countStoreGroupTrend(weStoreCode)
+        );
+    }
+
+    /***************************************************************
+     **************************统计相关趋势图相关 end******************
+     ****************************************************************/
+
+
+
+
+    /***************************************************************
+     **************************统计相关Top 10相关 start****************
+     ****************************************************************/
+
+
+    /**
+     *  门店导购码统计-门店新增客户top10
+     * @param beginTime
+     * @param endTime
+     * @return
+     */
+    @GetMapping("/countStoreShopGuideDrum")
+    public AjaxResult<List<WeStoreShopGuideDrumVo>> countStoreShopGuideDrum(String beginTime,String endTime){
+
+        return AjaxResult.success(
+                iWeStoreCodeService.countStoreShopGuideDrum(beginTime,endTime)
+        );
+    }
+
+
+    /**
+     *  门店群码统计-门店客群新增客户top10
+     * @param beginTime
+     * @param endTime
+     * @return
+     */
+    @GetMapping("/countStoreShopGroupDrum")
+    public AjaxResult<List<WeStoreGroupDrumVo>> countStoreShopGroupDrum(String beginTime,String endTime){
+
+        return AjaxResult.success(
+                iWeStoreCodeService.countStoreShopGroupDrum(beginTime,endTime)
+        );
+    }
+
+
+
+
+
+    /***************************************************************
+     **************************统计相关Top 10相关 end****************
+     ****************************************************************/
+
+
+    /***************************************************************
+     **************************统计数据报表相关 start****************
+     ****************************************************************/
+
+    /**
+     * 门店导购统计-数据报表
+     * @return
+     */
+    @GetMapping("/countShopGuideReport")
+    public TableDataInfo<WeStoreShopGuideReportVo> countShopGuideReport(WeStoreCode weStoreCode){
+           startPage();
+           return getDataTable(
+                   iWeStoreCodeService.countShopGuideReport(weStoreCode)
+           );
+    }
+
+    /**
+     *  门店导购统计-数据报表导出
+     * @param weStoreCode
+     * @return
+     */
+    @GetMapping("/exportCountShopGuideReport")
+    public AjaxResult exportCountShopGuideReport(WeStoreCode weStoreCode){
+        List<WeStoreShopGuideReportVo> weStoreShopGuideReportVos
+                = iWeStoreCodeService.countShopGuideReport(weStoreCode);
+        ExcelUtil<WeStoreShopGuideReportVo> util = new ExcelUtil<WeStoreShopGuideReportVo>(WeStoreShopGuideReportVo.class);
+        return util.exportExcel(weStoreShopGuideReportVos, DateUtils.dateTimeNow(DateUtils.YYYY_MM_DD)+"_门店导购统计");
+
+    }
+
+
+    /**
+     *  门店群码统计-数据报表
+     * @param weStoreCode
+     * @return
+     */
+    @GetMapping("/countStoreGroupReport")
+    public TableDataInfo<WeStoreGroupReportVo> countStoreGroupReport(WeStoreCode weStoreCode){
+        startPage();
+        return getDataTable(
+                iWeStoreCodeService.countStoreGroupReport(weStoreCode)
+        );
+    }
+
+
+    /**
+     * 门店群码统计-数据报表导出
+     * @param weStoreCode
+     * @return
+     */
+    @GetMapping("/exportCountStoreGroupReport")
+    public AjaxResult exportCountStoreGroupReport(WeStoreCode weStoreCode){
+        List<WeStoreGroupReportVo> weStoreGroupReportVos
+                = iWeStoreCodeService.countStoreGroupReport(weStoreCode);
+        ExcelUtil<WeStoreGroupReportVo> util = new ExcelUtil<WeStoreGroupReportVo>(WeStoreGroupReportVo.class);
+        return util.exportExcel(weStoreGroupReportVos, DateUtils.dateTimeNow(DateUtils.YYYY_MM_DD)+"_门店群码统计");
+
+    }
+
+
+    /***************************************************************
+     **************************统计数据报表相关 end********************
+     ****************************************************************/
+
+
+
+    /**
+     * 门店模版下载
+     * @return
+     */
+    @GetMapping("/importTemplate")
+    public void importTemplate()
+    {
+
+        LwExcelUtil.exprotForWeb(
+                ServletUtils.getResponse(), WeStoreCode.class, ListUtil.toList(new WeStoreCode()),DateUtils.dateTimeNow(DateUtils.YYYY_MM_DD)+"_门店模版"
+        );
+
+    }
+
+
+
+    /**
+     * 导入门店
+     * @param file
+     * @return
+     */
+    @PostMapping("/importData")
+    public AjaxResult  importData(MultipartFile file) throws Exception {
+        ExcelUtil<WeStoreCode> util = new ExcelUtil<WeStoreCode>(WeStoreCode.class);
+        List<WeStoreCode> weStoreCode = util.importExcel(file.getInputStream());
+        String tip=new String("成功导入{0}条");
+        if(CollectionUtil.isNotEmpty(weStoreCode)){
+            //过滤字段为空的数据
+            List<WeStoreCode> deduplicationWeStoreCode = weStoreCode.stream().filter(s -> StringUtils.isNotEmpty(s.getStoreName())
+                    && StringUtils.isNotEmpty(s.getArea()) && StringUtils.isNotEmpty(s.getAddress()) ).collect(Collectors.toList());
+            if(CollectionUtil.isEmpty(deduplicationWeStoreCode)){
+                return AjaxResult.error("导入用户数据不能为空!");
+            }
+
+            //根据门店名称去重(去除excel中重复的号码)
+            List<WeStoreCode> deduplicationSeasNoRepeat=deduplicationWeStoreCode.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() ->
+                    new TreeSet<>(Comparator.comparing(WeStoreCode :: getStoreName))), ArrayList::new));
+
+            //根据手机号去除数据库与excel中重复号码
+            List<WeStoreCode> dbExist = iWeStoreCodeService.list(new LambdaQueryWrapper<WeStoreCode>()
+                    .in(WeStoreCode::getStoreName, deduplicationSeasNoRepeat.stream()
+                            .map(WeStoreCode::getStoreName).collect(Collectors.toList())));
+            if(CollectionUtil.isNotEmpty(dbExist)){
+                List<WeStoreCode> noRepetWeCustomerSeas
+                        = deduplicationSeasNoRepeat.stream().filter(item ->
+                        !dbExist.stream().map(e->e.getStoreName()).collect(Collectors.toList())
+                                .contains(item.getStoreName())).collect(Collectors.toList());
+                deduplicationSeasNoRepeat.clear();
+                deduplicationSeasNoRepeat.addAll(
+                        noRepetWeCustomerSeas
+                );
+            }
+
+            if(CollectionUtil.isNotEmpty(deduplicationSeasNoRepeat)){
+                deduplicationSeasNoRepeat.stream().forEach(k->{
+                    Map<String, String> lMap
+                            = mapUtils.addressTolongitudea(k.getArea() + k.getAddress());
+                    k.setLatitude(lMap.get(MapUtils.lat));
+                    k.setLongitude(lMap.get(MapUtils.lng));
+                });
+//                List<WeStoreCode> weStoreCodeList = iWeStoreCodeService.getBaseMapper().selectList(new LambdaQueryWrapper<WeStoreCode>()
+//                        .eq(WeStoreCode::getDelFlag, 0));
+//                List<String> lst = weStoreCodeList.stream().map(WeStoreCode::getStoreName).collect(Collectors.toList());
+                deduplicationSeasNoRepeat = deduplicationSeasNoRepeat.stream().filter(item -> item.getStoreName().length()<30).collect(Collectors.toList());
+                if(iWeStoreCodeService.saveBatch(deduplicationSeasNoRepeat)){
+                    tip = MessageFormat.format(tip, new Object[]{new Integer(deduplicationSeasNoRepeat.size()).toString()});
+                }
+
+            }else{
+                tip = MessageFormat.format(tip, new Object[] { "0"});
+            }
+
+        }else{
+            return AjaxResult.error("导入用户数据不能为空!");
+        }
+        return AjaxResult.success(
+                tip
+        );
+    }
+
+
+    /**
+     * 获取附件门店
+     * @param storeCodeType 门店码类型(1:门店导购码;2:门店群活码)
+     * @param unionid 微信unionid
+     * @param longitude 经度
+     * @param latitude 纬度
+     * @param area 区域
+     * @return
+     */
+    @GetMapping("/findStoreCode")
+    public AjaxResult<WeStoreCodesVo> findStoreCode(Integer storeCodeType, String unionid, String longitude, String latitude, String area){
+
+        return AjaxResult.success(
+                iWeStoreCodeService.findStoreCode(storeCodeType,unionid,longitude,latitude,area)
+        );
+    }
+
+
+    /**
+     * 获取门店对应的配置相关
+     * @param storeCodeType
+     * @return
+     */
+    @GetMapping("/findWeStoreCodeConfig")
+    public AjaxResult<WeStoreCodeConfig> findWeStoreCodeConfig(Integer storeCodeType){
+
+
+        return AjaxResult.success(
+                iWeStoreCodeConfigService.getWeStoreCodeConfig(storeCodeType)
+        );
+    }
+
+
+    /**
+     * 记录用户扫码行为
+     * @return
+     */
+    @PostMapping("/countUserBehavior")
+    public AjaxResult countUserBehavior(@RequestBody WeStoreCodeCount weStoreCodeCount){
+        iWeStoreCodeService.countUserBehavior(weStoreCodeCount);
+        return AjaxResult.success();
+    }
+
+
+
+
+
+
+
+
+
+
+
+
+
+}

+ 100 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeStrackStageController.java

@@ -0,0 +1,100 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.utils.MapUtils;
+import com.linkwechat.domain.WeCustomer;
+import com.linkwechat.domain.WeStrackStage;
+import com.linkwechat.service.IWeCustomerService;
+import com.linkwechat.service.IWeStrackStageService;
+import org.apache.ibatis.util.MapUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import java.util.List;
+
+
+/**
+ * 商机阶段
+ */
+@RestController
+@RequestMapping("/strackStage")
+public class WeStrackStageController extends BaseController {
+
+    @Autowired
+    private IWeStrackStageService iWeStrackStageService;
+
+    @Autowired
+    private IWeCustomerService iWeCustomerService;
+
+
+    /**
+     * 列表
+     * @return
+     */
+    @GetMapping("/findWeStrackStage")
+    public AjaxResult<List<WeStrackStage>> findWeStrackStage(){
+
+        return AjaxResult.success(
+                iWeStrackStageService.list(new LambdaQueryWrapper<WeStrackStage>()
+                        .orderByAsc(WeStrackStage::getSort))
+        );
+    }
+
+    /**
+     * 新增
+     * @param weStrackStage
+     * @return
+     */
+    @PostMapping("/add")
+    public AjaxResult<WeStrackStage> add(@RequestBody WeStrackStage weStrackStage){
+
+        iWeStrackStageService.add(weStrackStage);
+        return AjaxResult.success(weStrackStage);
+    }
+
+
+    /**
+     * 更新
+     * @param weStrackStages
+     * @return
+     */
+    @PutMapping("/batchUpdate")
+    public AjaxResult batchUpdate(@RequestBody List<WeStrackStage> weStrackStages){
+
+        iWeStrackStageService.updateBatchById(weStrackStages);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 通过id删除
+     *
+     * @param id
+     * @return 结果
+     */
+    @DeleteMapping(path = "/delete")
+    public AjaxResult delete(String id,Integer growStageVal) {
+
+        iWeStrackStageService.remove(id,growStageVal);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 获取指定商阶状态下的客户数量
+     * @param stageVal
+     * @return
+     */
+    @GetMapping("/getStrackStageCustomerNum/{stageVal}")
+    public AjaxResult getStrackStageCustomerNum(@PathVariable Integer stageVal){
+
+
+        return AjaxResult.success(
+                MapUtil.entry("customerNumbwe",iWeCustomerService.count(new LambdaQueryWrapper<WeCustomer>()
+                        .eq(WeCustomer::getTrackState,stageVal)))
+        );
+
+    }
+}

+ 67 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeStrategicCrowdController.java

@@ -0,0 +1,67 @@
+package com.linkwechat.controller;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.github.pagehelper.PageInfo;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.strategiccrowd.CorpAddStateEnum;
+import com.linkwechat.common.exception.wecom.WeComException;
+import com.linkwechat.common.utils.spring.SpringUtils;
+import com.linkwechat.domain.WeStrategicCrowd;
+import com.linkwechat.domain.customer.vo.WeCustomersVo;
+import com.linkwechat.domain.material.entity.WeMaterial;
+import com.linkwechat.domain.strategic.crowd.query.WeAddStrategicCrowdQuery;
+import com.linkwechat.domain.strategic.crowd.query.WeCorpStateTagSourceQuery;
+import com.linkwechat.domain.strategic.crowd.query.WeStrategicCrowdListQuery;
+import com.linkwechat.domain.strategic.crowd.query.WeStrategicCrowdQuery;
+import com.linkwechat.domain.strategic.crowd.vo.WeStrategicCrowdAnalyzelVo;
+import com.linkwechat.domain.strategic.crowd.vo.WeStrategicCrowdDetailVo;
+import com.linkwechat.service.IWeStrategicCrowdService;
+import com.linkwechat.service.IWeStrategicCrowdStateTagService;
+import com.linkwechat.service.impl.WeCategoryServiceImpl;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author danmo
+ * @description 策略人群
+ * @date 2022/07/05 18:22
+ **/
+
+@RestController
+@RequestMapping(value = "strategic/crowd")
+@Api(tags = "策略人群")
+public class WeStrategicCrowdController extends BaseController {
+
+    @Autowired
+    private IWeStrategicCrowdService weStrategicCrowdService;
+
+
+    @ApiOperation(value = "获取策略下拉选项", httpMethod = "POST")
+    @PostMapping("/getDownOptions")
+    public AjaxResult<Map<String, Object>> getDownOptions(@RequestBody List<String> enumNames) {
+        return AjaxResult.success(weStrategicCrowdService.getDownOptions(enumNames));
+    }
+
+    @ApiOperation(value = "获取策略渠道来源", httpMethod = "GET")
+    @GetMapping("/getCorpStateTagSource")
+    public TableDataInfo getCorpStateTagSource(WeCorpStateTagSourceQuery query) {
+        CorpAddStateEnum corpAddStateEnum = CorpAddStateEnum.parseEnum(query.getCode());
+        if(corpAddStateEnum == null){
+            throw new WeComException("无效CODE");
+        }
+        String method = corpAddStateEnum.getMethod();
+        startPage();
+        List<Map<String,Object>> stateTagSourceList = SpringUtils.getBean(method, IWeStrategicCrowdStateTagService.class).getStateTagSourceList(query);
+        return getDataTable(stateTagSourceList);
+    }
+
+
+}

+ 94 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeSysFieldTemplateController.java

@@ -0,0 +1,94 @@
+package com.linkwechat.controller;
+
+
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.domain.WeSysFieldTemplate;
+import com.linkwechat.service.IWeSysFieldTemplateService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import java.util.Arrays;
+import java.util.List;
+
+
+/**
+ * 客户画像自定义模版
+ */
+@RestController
+@RequestMapping(value = "field")
+public class WeSysFieldTemplateController {
+
+
+    @Autowired
+    IWeSysFieldTemplateService iWeSysFieldTemplateService;
+
+
+    /**
+     * 获取列表
+     * @param weSysFieldTemplate
+     * @return
+     */
+    @GetMapping("/list")
+    public AjaxResult<List<WeSysFieldTemplate>> list(WeSysFieldTemplate weSysFieldTemplate){
+
+        return AjaxResult.success(
+                iWeSysFieldTemplateService.findLists(weSysFieldTemplate)
+        );
+    }
+
+
+    /**
+     * 新增
+     * @param sysFieldTemplate
+     * @return
+     */
+    @PostMapping("/addSysField")
+    public AjaxResult  addSysField(@RequestBody WeSysFieldTemplate sysFieldTemplate){
+        iWeSysFieldTemplateService.addSysField(sysFieldTemplate);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 更新
+     * @param sysFieldTemplate
+     * @return
+     */
+    @PutMapping("/updateSysField")
+    public AjaxResult updateSysField(@RequestBody WeSysFieldTemplate sysFieldTemplate){
+        iWeSysFieldTemplateService.updateSysField(sysFieldTemplate);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 批量更新
+     * @param sysFieldTemplates
+     * @return
+     */
+    @PostMapping("/batchUpdateSysField")
+    public AjaxResult batchUpdateSysField(@RequestBody List<WeSysFieldTemplate> sysFieldTemplates){
+        iWeSysFieldTemplateService.updateBatchById(sysFieldTemplates);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 通过id列表批量删除识客码
+     *
+     * @param ids id列表
+     * @return 结果
+     */
+    @DeleteMapping(path = "/{ids}")
+    public AjaxResult batchDeleteSysField(@PathVariable("ids") Long[] ids) {
+        iWeSysFieldTemplateService.removeByIds(Arrays.asList(ids));
+        return AjaxResult.success();
+    }
+
+
+
+
+
+
+
+
+}

+ 164 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeTagController.java

@@ -0,0 +1,164 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.constant.HttpStatus;
+import com.linkwechat.common.constant.SynchRecordConstants;
+import com.linkwechat.common.constant.WeConstans;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.core.domain.model.LoginUser;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.exception.CustomException;
+import com.linkwechat.common.utils.SecurityUtils;
+import com.linkwechat.domain.WeTag;
+import com.linkwechat.domain.WeTagGroup;
+import com.linkwechat.service.IWeSynchRecordService;
+import com.linkwechat.service.IWeTagGroupService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import java.util.List;
+
+
+/**
+ * 标签
+ */
+@RestController
+@RequestMapping("/tag")
+public class WeTagController extends BaseController {
+
+    @Autowired
+    private IWeTagGroupService weTagGroupService;
+
+    @Autowired
+    private IWeSynchRecordService iWeSynchRecordService;
+
+    /**
+     * 查询标签组列表(分页)
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(WeTagGroup weTagGroup)
+    {
+        startPage();
+
+        TableDataInfo dataTable = getDataTable(weTagGroupService.selectWeTagGroupList(weTagGroup));
+
+        dataTable.setLastSyncTime(
+                iWeSynchRecordService.findUpdateLatestTime(SynchRecordConstants.SYNCH_CUSTOMER_TAG)
+        );//最近同步时间
+
+        return dataTable;
+    }
+
+
+
+    /**
+     *  查询标签组列表(不分页)
+     * @param weTagGroup
+     * @return
+     */
+    @GetMapping("/allList")
+    public AjaxResult<List<WeTagGroup>> allList(WeTagGroup weTagGroup)
+    {
+        return AjaxResult.success(
+                weTagGroupService.selectWeTagGroupList(weTagGroup)
+        );
+    }
+
+    /**
+     * 新增标签组
+     */
+    @PostMapping
+    public AjaxResult add(@RequestBody WeTagGroup weTagGroup)
+    {
+
+        //校验标签组名称与标签名称是否相同
+        if(StrUtil.isNotBlank(weTagGroup.getGroupName())){
+            //检验当前标签组名称是否重复
+            if(weTagGroupService.count(new LambdaQueryWrapper<WeTagGroup>()
+                    .eq(WeTagGroup::getGroupName,weTagGroup.getGroupName())
+                    .eq(WeTagGroup::getGroupTagType,weTagGroup.getGroupTagType()))>0){
+                return AjaxResult.error(
+                        "标签组名称不可重复"
+                );
+            }
+
+            List<WeTag> weTags = weTagGroup.getWeTags();
+            if(CollectionUtil.isNotEmpty(weTags)){
+                if(weTags.stream().filter(m -> m.getName().equals(weTagGroup.getGroupName())).findAny().isPresent()){
+                    return   AjaxResult.error("标签组名称与标签名不可重复");
+                }
+
+            }
+        }
+        weTagGroupService.insertWeTagGroup(weTagGroup);
+
+        return AjaxResult.success();
+    }
+
+    /**
+     * 修改标签组
+     */
+    @PutMapping
+    public AjaxResult edit(@RequestBody WeTagGroup weTagGroup)
+    {
+        //校验标签组名称与标签名称是否相同
+        if(StrUtil.isNotBlank(weTagGroup.getGroupName())){
+            if(!weTagGroupService.getById(weTagGroup.getId()).getGroupName()
+                    .equals(weTagGroup.getGroupName())){
+                //检验当前标签组名称是否重复
+                if(weTagGroupService.count(new LambdaQueryWrapper<WeTagGroup>()
+                        .eq(WeTagGroup::getGroupName,weTagGroup.getGroupName())
+                        .eq(WeTagGroup::getGroupTagType,weTagGroup.getGroupTagType()))>0){
+                    return AjaxResult.error(
+                            "标签组名称不可重复"
+                    );
+                }
+            }
+
+            List<WeTag> weTags = weTagGroup.getWeTags();
+            if(CollectionUtil.isNotEmpty(weTags)){
+                if(weTags.stream().filter(m -> m.getName().equals(weTagGroup.getGroupName())).findAny().isPresent()){
+                    return   AjaxResult.error("标签组名称与标签名不可重复");
+                }
+
+            }
+        }
+
+        weTagGroupService.updateWeTagGroup(weTagGroup);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 删除标签组
+     */
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable String[] ids)
+    {
+        weTagGroupService.deleteWeTagGroupByIds(ids);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 同步标签
+     * @return
+     */
+    @GetMapping("/synchWeTags")
+    public AjaxResult synchWeTags(){
+
+        try {
+            weTagGroupService.synchWeTags();
+        }catch (CustomException e){
+            return AjaxResult.error(e.getMessage());
+        }
+
+
+        return  AjaxResult.success(WeConstans.SYNCH_TIP);
+    }
+
+
+
+
+}

+ 104 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeTalkController.java

@@ -0,0 +1,104 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.constant.Constants;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.domain.material.entity.WeContentTalk;
+import com.linkwechat.domain.material.entity.WeMaterial;
+import com.linkwechat.domain.material.entity.WeTalkMaterial;
+import com.linkwechat.domain.material.query.WeTalkQuery;
+import com.linkwechat.domain.material.vo.WeMaterialVo;
+import com.linkwechat.domain.material.vo.talk.WeTalkVO;
+import com.linkwechat.service.IWeContentTalkService;
+import com.linkwechat.service.IWeMaterialService;
+import com.linkwechat.service.IWeTalkMaterialService;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 话术中心
+ *
+ * @author WangYX
+ * @version 1.0.0
+ * @date 2022/10/27 16:37
+ */
+@RestController
+@RequestMapping("/talk")
+public class WeTalkController extends BaseController {
+
+    @Resource
+    private IWeContentTalkService weContentTalkService;
+    @Resource
+    private IWeTalkMaterialService weTalkMaterialService;
+    @Resource
+    private IWeMaterialService weMaterialService;
+
+    /**
+     * 话术列表
+     *
+     * @return
+     */
+    @ApiOperation("话术列表")
+    @GetMapping("/list")
+    private TableDataInfo list(WeTalkQuery weTalkQuery) {
+        startPage();
+        //获取话术列表信息
+        LambdaQueryWrapper<WeContentTalk> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(weTalkQuery.getCategoryId() != null, WeContentTalk::getCategoryId, weTalkQuery.getCategoryId());
+        queryWrapper.eq(weTalkQuery.getTalkType() != null, WeContentTalk::getTalkType, weTalkQuery.getTalkType());
+        queryWrapper.eq(WeContentTalk::getDelFlag, Constants.COMMON_STATE);
+        queryWrapper.like(StringUtils.isNotBlank(weTalkQuery.getTalkTitle()), WeContentTalk::getTalkTitle, weTalkQuery.getTalkTitle());
+        queryWrapper.orderByDesc(WeContentTalk::getCreateTime);
+        List<WeContentTalk> list = weContentTalkService.list(queryWrapper);
+
+        TableDataInfo dataTable = getDataTable(list);
+
+        List<WeTalkVO> result = new ArrayList<>();
+        //获取话术包含的素材
+        if (list != null && list.size() > 0) {
+            for (WeContentTalk weContentTalk : list) {
+                //话术基本信息
+                WeTalkVO weTalkVO = new WeTalkVO();
+                weTalkVO.setId(weContentTalk.getId());
+                weTalkVO.setCategoryId(weContentTalk.getCategoryId());
+                weTalkVO.setTalkType(weContentTalk.getTalkType());
+                weTalkVO.setTalkTitle(weContentTalk.getTalkTitle());
+
+                //话术素材关联
+                LambdaQueryWrapper<WeTalkMaterial> query = new LambdaQueryWrapper();
+                query.eq(WeTalkMaterial::getTalkId, weContentTalk.getId());
+                query.orderByAsc(WeTalkMaterial::getSort);
+                List<WeTalkMaterial> weTalkMaterials = weTalkMaterialService.list(query);
+
+                if (weTalkMaterials != null && weTalkMaterials.size() > 0) {
+                    //获取素材
+                    List<Long> collect = weTalkMaterials.stream().map(o -> o.getMaterialId()).collect(Collectors.toList());
+                    List<WeMaterial> weMaterials = weMaterialService.listByIds(collect);
+                    if (weMaterials != null && weMaterials.size() > 0) {
+                        List<WeMaterialVo> weMaterialVos = new ArrayList<>();
+                        weMaterials.forEach(o -> {
+                            WeMaterialVo weMaterialVo = BeanUtil.copyProperties(o, WeMaterialVo.class);
+                            weMaterialVos.add(weMaterialVo);
+                        });
+                        weTalkVO.setMaterials(weMaterialVos);
+                    }
+                }
+                result.add(weTalkVO);
+            }
+        }
+
+        dataTable.setRows(result);
+        return dataTable;
+    }
+
+}

+ 63 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeTicketController.java

@@ -0,0 +1,63 @@
+package com.linkwechat.controller;
+
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.domain.AjaxResult;
+import com.linkwechat.common.utils.SecurityUtils;
+import com.linkwechat.common.utils.wecom.TicketUtils;
+import com.linkwechat.domain.wecom.query.WeBaseQuery;
+import com.linkwechat.fegin.QwTicketClient;
+import io.swagger.annotations.Api;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * @author danmo
+ * @description h5 ticket校验接口
+ * @date 2021/1/6 11:23
+ **/
+@Api(tags = " h5 ticket校验接口")
+@Slf4j
+@RequestMapping(value = "ticket")
+@RestController
+public class WeTicketController extends BaseController {
+
+    @Resource
+    private QwTicketClient qwTicketClient;
+
+
+
+
+    /**
+     * 获取企业的jsapi_ticket
+     *
+     * @param url JS接口页面的完整URL
+     * @return
+     */
+    @GetMapping("/getAppTicket")
+    public AjaxResult getAppTicket(String url) {
+        WeBaseQuery query = new WeBaseQuery();
+        query.setCorpid(SecurityUtils.getLoginUser().getCorpId());
+        String ticket = qwTicketClient.getCorpTicket(query).getData();
+        return AjaxResult.success(TicketUtils.getSignatureMap(ticket,url));
+    }
+
+    /**
+     * 获取应用的jsapi_ticket
+     *
+     * @param url JS接口页面的完整URL
+     * @return
+     */
+    @GetMapping("/getAgentTicket")
+    public AjaxResult getAgentTicket(String url) {
+        WeBaseQuery query = new WeBaseQuery();
+        query.setCorpid(SecurityUtils.getCorpId());
+        String ticket = qwTicketClient.getAgentTicket(query).getData();
+        return AjaxResult.success(TicketUtils.getSignatureMap(ticket,url));
+    }
+
+}

+ 65 - 0
linkwe-api/src/main/java/com/linkwechat/controller/WeTrajectoryController.java

@@ -0,0 +1,65 @@
+package com.linkwechat.controller;
+
+import cn.hutool.core.collection.ListUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.linkwechat.common.core.controller.BaseController;
+import com.linkwechat.common.core.page.TableDataInfo;
+import com.linkwechat.common.enums.TrajectoryType;
+import com.linkwechat.common.utils.StringUtils;
+import com.linkwechat.domain.WeCustomerTrajectory;
+import com.linkwechat.service.IWeCustomerTrajectoryService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RestController
+@RequestMapping("/trajectory")
+public class WeTrajectoryController extends BaseController {
+
+    @Autowired
+    IWeCustomerTrajectoryService weCustomerTrajectoryService;
+
+    /**
+     * 获取企业动态
+     * @param trajectoryType
+     * @param dataScope 个人数据:false 全部数据(相对于角色定义的数据权限):true
+     * @return
+     */
+    @GetMapping("/findCompanyDynamics")
+    public TableDataInfo<WeCustomerTrajectory> findCompanyDynamics(Integer trajectoryType,@RequestParam(defaultValue = "true") boolean dataScope){
+        startPage();
+        return getDataTable(
+                dataScope?weCustomerTrajectoryService.findAllTrajectory(
+                        WeCustomerTrajectory.builder().trajectoryType(trajectoryType).build()
+                ):weCustomerTrajectoryService.findPersonTrajectory(
+                        WeCustomerTrajectory.builder().trajectoryType(trajectoryType).build()
+                )
+        );
+    }
+
+
+    /**
+     * 获取客户轨迹
+     * @return
+     */
+    @GetMapping("/findTrajectory")
+    public TableDataInfo<WeCustomerTrajectory> findTrajectory(String externalUserid,String weUserId,Integer trajectoryType){
+        startPage();
+        return getDataTable(
+                weCustomerTrajectoryService.list(
+                        new LambdaQueryWrapper<WeCustomerTrajectory>()
+                                .eq(StringUtils.isNotEmpty(externalUserid),WeCustomerTrajectory::getExternalUseridOrChatid,externalUserid)
+                                .eq(StringUtils.isNotEmpty(weUserId),WeCustomerTrajectory::getWeUserId,weUserId)
+                                .eq(trajectoryType != null,WeCustomerTrajectory::getTrajectoryType,trajectoryType)
+                                .orderByDesc(WeCustomerTrajectory::getCreateTime)
+                )
+        );
+    }
+
+
+}

+ 56 - 0
linkwe-api/src/main/java/com/linkwechat/interceptor/RequestContextInterceptor.java

@@ -0,0 +1,56 @@
+package com.linkwechat.interceptor;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.linkwechat.common.constant.SecurityConstants;
+import com.linkwechat.common.context.SecurityContextHolder;
+import com.linkwechat.common.core.domain.model.LoginUser;
+import com.linkwechat.common.core.domain.model.WxLoginUser;
+import com.linkwechat.common.exception.wecom.WeComException;
+import com.linkwechat.common.utils.ServletUtils;
+import com.linkwechat.common.utils.StringUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Objects;
+
+/**
+ * 自定义请求头拦截器,将Header数据封装到线程变量中方便获取
+ *
+ * @author xueyi
+ */
+@Component
+public class RequestContextInterceptor implements HandlerInterceptor {
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        if (!(handler instanceof HandlerMethod)) {
+            return true;
+        }
+
+        SecurityContextHolder.setCorpId(ServletUtils.getHeader(request, SecurityConstants.CORP_ID));
+        SecurityContextHolder.setCorpName(ServletUtils.getHeader(request, SecurityConstants.CORP_NAME));
+        SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.USER_ID));
+        SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.USER_NAME));
+        SecurityContextHolder.setUserType(ServletUtils.getHeader(request, SecurityConstants.USER_TYPE));
+        SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY));
+        String loginType = ServletUtils.getHeader(request, SecurityConstants.LOGIN_TYPE);
+        if(StringUtils.isNotEmpty(loginType) && ObjectUtil.notEqual("LinkWeChatAPI",loginType)){
+           throw new WeComException("token不合法");
+        }
+        String loginUserStr = ServletUtils.getHeader(request, SecurityConstants.LOGIN_USER);
+        if(StringUtils.isNotEmpty(loginUserStr)){
+            SecurityContextHolder.set(SecurityConstants.Details.LOGIN_USER.getCode(), JSONObject.parseObject(loginUserStr, LoginUser.class));
+        }
+
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
+            throws Exception {
+        SecurityContextHolder.remove();
+    }
+}

+ 10 - 0
linkwe-api/src/main/resources/banner.txt

@@ -0,0 +1,10 @@
+${AnsiColor.GREEN}
+  _     _       _   __        __    ____ _           _      _          _
+ | |   (_)_ __ | | _\ \      / /__ / ___| |__   __ _| |_   / \   _ __ (_)
+ | |   | | '_ \| |/ /\ \ /\ / / _ \ |   | '_ \ / _` | __| / _ \ | '_ \| |
+ | |___| | | | |   <  \ V  V /  __/ |___| | | | (_| | |_ / ___ \| |_) | |
+ |_____|_|_| |_|_|\_\  \_/\_/ \___|\____|_| |_|\__,_|\__/_/   \_\ .__/|_|
+                                                                |_|
+ ${AnsiColor.WHITE}
+
+ Spring Boot Version: ${spring-boot.version}

+ 206 - 0
linkwe-api/src/main/resources/logback-spring.xml

@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <!-- 日志存放路径 -->
+
+    <property name="app_name" value="linkwe-api"/>
+
+    <!-- 日志输出格式 -->
+    <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS,CTT} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+    <!--  邮件 -->
+    <!-- SMTP server的地址,必需指定。如网易的SMTP服务器地址是: smtp.163.com -->
+    <property name="smtpHost" value="smtp.163.com"/><!--填入要发送邮件的smtp服务器地址-->
+    <!-- SMTP server的端口地址。默认值:25 -->
+    <property name="smtpPort" value="25"/>
+    <!-- 发送邮件账号,默认为null -->
+    <property name="username" value="*"/><!--发件人账号-->
+    <!-- 发送邮件密码,默认为null -->
+    <property name="password" value="*"/><!--发件人密码-->
+    <!-- 如果设置为true,appender将会使用SSL连接到日志服务器。默认值:false -->
+    <property name="SSL" value="true"/>
+    <!-- 指定发送到那个邮箱,可设置多个<to>属性,指定多个目的邮箱 -->
+    <property name="email_to" value="*"/>
+    <!--收件人账号多个可以逗号隔开-->
+    <!-- 指定发件人名称。如果设置成“&lt;ADMIN&gt; ”,则邮件发件人将会是“<ADMIN> ” -->
+    <property name="email_from" value="1" />
+    <!-- 指定emial的标题,它需要满足PatternLayout中的格式要求。如果设置成“Log: %logger - %msg ”,就案例来讲,则发送邮件时,标题为“【Error】: com.foo.Bar - Hello World ”。 默认值:"%logger{20} - %m". -->
+    <property name="email_subject" value="【linkwechat-api】【Error】: %logger" />
+
+    <!-- 日志根目录-->
+    <springProperty scope="context" name="log.path" source="logging.path"
+                    defaultValue="./logs/linkwe-api"/>
+
+    <!-- 控制台输出 -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+
+    <!-- 系统日志输出 -->
+    <!-- 循环文件输出(基于时间戳的分文件,是实际项目中用途最广的一种情况) -->
+    <appender name="file_debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/debug/${app_name}.debug.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <!-- rollover daily -->
+            <fileNamePattern>${log.path}/debug/${app_name}_%d{yyyy-MM-dd}.debug.%i.log</fileNamePattern>
+            <!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
+            <maxFileSize>50MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>DEBUG</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <!-- 异步输出 -->
+    <appender name="arollingfile_debug" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>1</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_debug"/>
+    </appender>
+
+    <!-- 循环文件输出(基于时间戳的分文件,是实际项目中用途最广的一种情况) -->
+    <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/info/${app_name}.info.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <!-- rollover daily -->
+            <fileNamePattern>${log.path}/info/${app_name}_%d{yyyy-MM-dd}.info.%i.log</fileNamePattern>
+            <!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
+            <maxFileSize>50MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>INFO</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <!-- 异步输出 -->
+    <appender name="arollingfile_info" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>1</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_info"/>
+    </appender>
+
+    <!-- 循环文件输出(基于时间戳的分文件,是实际项目中用途最广的一种情况) -->
+    <appender name="file_warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/warn/${app_name}.warn.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <!-- rollover daily -->
+            <fileNamePattern>${log.path}/warn/${app_name}_%d{yyyy-MM-dd}.warn.%i.log</fileNamePattern>
+            <!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
+            <maxFileSize>50MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>WARN</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <!-- 异步输出 -->
+    <appender name="arollingfile_warn" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>1</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_warn"/>
+    </appender>
+
+    <!-- 循环文件输出(基于时间戳的分文件,是实际项目中用途最广的一种情况) -->
+    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/error/${app_name}.error.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <!-- rollover daily -->
+            <fileNamePattern>${log.path}/error/${app_name}_%d{yyyy-MM-dd}.error.%i.log</fileNamePattern>
+            <!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
+            <maxFileSize>50MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <!-- 异步输出 -->
+    <appender name="arollingfile_error" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>1</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_error"/>
+    </appender>
+
+    <!-- ERROR邮件发送 -->
+    <appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender">
+        <smtpHost>${smtpHost}</smtpHost>
+        <smtpPort>${smtpPort}</smtpPort>
+        <username>${username}</username>
+        <password>${password}</password>
+       <!-- <asynchronousSending>true</asynchronousSending>-->
+        <SSL>${SSL}</SSL>
+        <!--<STARTTLS>true</STARTTLS>-->
+        <to>${email_to}</to>
+        <from>${email_from}</from>
+        <subject>${email_subject}</subject>
+        <!-- html格式-->
+        <layout class="ch.qos.logback.classic.html.HTMLLayout">
+            <Pattern>%date%level%thread%logger{0}%line%message</Pattern>
+        </layout>
+        <!-- 这里采用等级过滤器 指定等级相符才发送 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+        <cyclicBufferTracker class="ch.qos.logback.core.spi.CyclicBufferTracker">
+            <!-- 每个电子邮件只发送一个日志条目 -->
+            <bufferSize>1</bufferSize>
+        </cyclicBufferTracker>
+    </appender>
+
+
+    <!-- 系统模块日志级别控制  -->
+    <logger name="cn.linkwechat" level="info" />
+    <!-- Spring日志级别控制  -->
+    <logger name="org.springframework" level="warn" />
+
+    <!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="console" />
+        <appender-ref ref="arollingfile_debug" />
+        <appender-ref ref="arollingfile_info" />
+        <appender-ref ref="arollingfile_warn" />
+        <appender-ref ref="arollingfile_error" />
+        <!-- <appender-ref ref="alarm_error" />-->
+        <appender-ref ref="EMAIL" />
+    </root>
+
+</configuration>

Some files were not shown because too many files changed in this diff