Prechádzať zdrojové kódy

init 仓库文件初始化

18339543638 3 rokov pred
commit
97ec623517
100 zmenil súbory, kde vykonal 6438 pridanie a 0 odobranie
  1. 37 0
      .gitignore
  2. 201 0
      LICENSE
  3. 287 0
      README.md
  4. 7 0
      coffee-admin/Dockerfile
  5. 36 0
      coffee-admin/pom.xml
  6. 25 0
      coffee-admin/src/main/java/com/coffee/admin/AdminApplication.java
  7. 31 0
      coffee-admin/src/main/java/com/coffee/admin/StartUpRunner.java
  8. 38 0
      coffee-admin/src/main/java/com/coffee/admin/controller/IndexController.java
  9. 98 0
      coffee-admin/src/main/java/com/coffee/admin/controller/common/CommonController.java
  10. 87 0
      coffee-admin/src/main/java/com/coffee/admin/controller/monitor/OnlineUserController.java
  11. 78 0
      coffee-admin/src/main/java/com/coffee/admin/controller/system/SysLoginController.java
  12. 101 0
      coffee-admin/src/main/resources/application-dev.yml
  13. 0 0
      coffee-admin/src/main/resources/application-prod.yml
  14. 101 0
      coffee-admin/src/main/resources/application-test.yml
  15. 54 0
      coffee-admin/src/main/resources/application.yml
  16. 95 0
      coffee-admin/src/main/resources/logback.xml
  17. 37 0
      coffee-codegen/pom.xml
  18. 219 0
      coffee-codegen/src/main/java/com/coffee/codegen/CodeGenerator.java
  19. 119 0
      coffee-codegen/src/main/resources/templates/addDTO.java.vm
  20. 111 0
      coffee-codegen/src/main/resources/templates/controller.java.vm
  21. 120 0
      coffee-codegen/src/main/resources/templates/editDTO.java.vm
  22. 193 0
      coffee-codegen/src/main/resources/templates/entity.java.vm
  23. 20 0
      coffee-codegen/src/main/resources/templates/mapper.java.vm
  24. 39 0
      coffee-codegen/src/main/resources/templates/mapper.xml.vm
  25. 94 0
      coffee-codegen/src/main/resources/templates/queryDTO.java.vm
  26. 75 0
      coffee-codegen/src/main/resources/templates/service.java.vm
  27. 113 0
      coffee-codegen/src/main/resources/templates/serviceImpl.java.vm
  28. 24 0
      coffee-codegen/src/main/resources/templates/sql.vm
  29. 65 0
      coffee-codegen/src/main/resources/templates/vue/FormModal.vue.vm
  30. 37 0
      coffee-codegen/src/main/resources/templates/vue/api.ts.vm
  31. 65 0
      coffee-codegen/src/main/resources/templates/vue/data.ts.vm
  32. 139 0
      coffee-codegen/src/main/resources/templates/vue/index.vue.vm
  33. 96 0
      coffee-common/pom.xml
  34. 46 0
      coffee-common/src/main/java/com/coffee/common/Constants.java
  35. 16 0
      coffee-common/src/main/java/com/coffee/common/MenuConstants.java
  36. 37 0
      coffee-common/src/main/java/com/coffee/common/annotation/DataScope.java
  37. 22 0
      coffee-common/src/main/java/com/coffee/common/annotation/DataSource.java
  38. 22 0
      coffee-common/src/main/java/com/coffee/common/annotation/ExcelDict.java
  39. 18 0
      coffee-common/src/main/java/com/coffee/common/annotation/IgnoreToken.java
  40. 23 0
      coffee-common/src/main/java/com/coffee/common/annotation/Log.java
  41. 15 0
      coffee-common/src/main/java/com/coffee/common/bo/DictModel.java
  42. 77 0
      coffee-common/src/main/java/com/coffee/common/bo/LoginUser.java
  43. 40 0
      coffee-common/src/main/java/com/coffee/common/bo/SysRoleBO.java
  44. 106 0
      coffee-common/src/main/java/com/coffee/common/bo/SysUserBO.java
  45. 77 0
      coffee-common/src/main/java/com/coffee/common/config/AppConfig.java
  46. 25 0
      coffee-common/src/main/java/com/coffee/common/config/MinioConfig.java
  47. 49 0
      coffee-common/src/main/java/com/coffee/common/convert/ExcelDictConverter.java
  48. 214 0
      coffee-common/src/main/java/com/coffee/common/crud/BaseService.java
  49. 38 0
      coffee-common/src/main/java/com/coffee/common/crud/controller/BaseCrudController.java
  50. 57 0
      coffee-common/src/main/java/com/coffee/common/crud/controller/BaseCurdAuthController.java
  51. 59 0
      coffee-common/src/main/java/com/coffee/common/crud/controller/BaseDeleteController.java
  52. 60 0
      coffee-common/src/main/java/com/coffee/common/crud/controller/BaseQueryController.java
  53. 90 0
      coffee-common/src/main/java/com/coffee/common/crud/controller/BaseSaveController.java
  54. 24 0
      coffee-common/src/main/java/com/coffee/common/dto/LoginDTO.java
  55. 30 0
      coffee-common/src/main/java/com/coffee/common/entity/Entity.java
  56. 13 0
      coffee-common/src/main/java/com/coffee/common/entity/GenericEntity.java
  57. 64 0
      coffee-common/src/main/java/com/coffee/common/entity/QueryParamEntity.java
  58. 29 0
      coffee-common/src/main/java/com/coffee/common/entity/RecordCreationEntity.java
  59. 32 0
      coffee-common/src/main/java/com/coffee/common/entity/RecordModifierEntity.java
  60. 70 0
      coffee-common/src/main/java/com/coffee/common/entity/param/Term.java
  61. 129 0
      coffee-common/src/main/java/com/coffee/common/entity/param/TermType.java
  62. 31 0
      coffee-common/src/main/java/com/coffee/common/enums/BizEnum.java
  63. 38 0
      coffee-common/src/main/java/com/coffee/common/enums/DataScopeEnum.java
  64. 19 0
      coffee-common/src/main/java/com/coffee/common/enums/DataSourceTypeEnum.java
  65. 28 0
      coffee-common/src/main/java/com/coffee/common/enums/DelFlagEnum.java
  66. 20 0
      coffee-common/src/main/java/com/coffee/common/enums/FileStorageStrategyEnum.java
  67. 27 0
      coffee-common/src/main/java/com/coffee/common/enums/FrameEnum.java
  68. 34 0
      coffee-common/src/main/java/com/coffee/common/enums/GrantTypeEnum.java
  69. 27 0
      coffee-common/src/main/java/com/coffee/common/enums/KeepaliveEnum.java
  70. 28 0
      coffee-common/src/main/java/com/coffee/common/enums/LinkExternalEnum.java
  71. 28 0
      coffee-common/src/main/java/com/coffee/common/enums/LockFlagEnum.java
  72. 28 0
      coffee-common/src/main/java/com/coffee/common/enums/LogStatusEnum.java
  73. 31 0
      coffee-common/src/main/java/com/coffee/common/enums/MenuTypeEnum.java
  74. 27 0
      coffee-common/src/main/java/com/coffee/common/enums/PeriodEnum.java
  75. 28 0
      coffee-common/src/main/java/com/coffee/common/enums/PswModifiedEnum.java
  76. 32 0
      coffee-common/src/main/java/com/coffee/common/enums/SexEnum.java
  77. 26 0
      coffee-common/src/main/java/com/coffee/common/enums/StatusEnum.java
  78. 26 0
      coffee-common/src/main/java/com/coffee/common/enums/UniqueEnum.java
  79. 28 0
      coffee-common/src/main/java/com/coffee/common/enums/UserPlatformEnum.java
  80. 27 0
      coffee-common/src/main/java/com/coffee/common/enums/VisibleEnum.java
  81. 26 0
      coffee-common/src/main/java/com/coffee/common/enums/YesNoEnum.java
  82. 20 0
      coffee-common/src/main/java/com/coffee/common/exception/CustomException.java
  83. 10 0
      coffee-common/src/main/java/com/coffee/common/exception/DemoModeException.java
  84. 54 0
      coffee-common/src/main/java/com/coffee/common/redis/RedisConfig.java
  85. 582 0
      coffee-common/src/main/java/com/coffee/common/redis/RedisUtils.java
  86. 26 0
      coffee-common/src/main/java/com/coffee/common/result/IResultCode.java
  87. 95 0
      coffee-common/src/main/java/com/coffee/common/result/R.java
  88. 98 0
      coffee-common/src/main/java/com/coffee/common/result/ResultCode.java
  89. 51 0
      coffee-common/src/main/java/com/coffee/common/util/AddressUtil.java
  90. 42 0
      coffee-common/src/main/java/com/coffee/common/util/ConfigUtil.java
  91. 60 0
      coffee-common/src/main/java/com/coffee/common/util/DictUtil.java
  92. 35 0
      coffee-common/src/main/java/com/coffee/common/util/ExcelUtil.java
  93. 21 0
      coffee-common/src/main/java/com/coffee/common/util/IdUtil.java
  94. 26 0
      coffee-common/src/main/java/com/coffee/common/util/IpUtil.java
  95. 60 0
      coffee-common/src/main/java/com/coffee/common/util/MinioUtil.java
  96. 77 0
      coffee-common/src/main/java/com/coffee/common/util/SecurityUtil.java
  97. 54 0
      coffee-framework/pom.xml
  98. 59 0
      coffee-framework/src/main/java/com/coffee/framework/aspect/DataSourceAspect.java
  99. 39 0
      coffee-framework/src/main/java/com/coffee/framework/aspect/DemoModelAspect.java
  100. 146 0
      coffee-framework/src/main/java/com/coffee/framework/aspect/LogAspect.java

+ 37 - 0
.gitignore

@@ -0,0 +1,37 @@
+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/
+
+/logs/
+!/logs/
+/logs/

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 287 - 0
README.md

@@ -0,0 +1,287 @@
+<div align="center">
+<h1>Coffee Admin</h1>
+</div>
+
+<p align="center">
+    <img src="https://img.shields.io/badge/Spring%20Boot-2.5.8-blue" alt="Downloads">
+    <img src="https://img.shields.io/badge/JDK-1.8+-green.svg" alt="Build Status">
+    <img src="https://img.shields.io/badge/license-Apache%202-blue.svg" alt="Build Status">
+</p>
+
+### 简介
+
+Coffee Admin权限管理系统,基于VUE3.x、SpringBoot2.x、Sa-Token、MyBatis-Plus等技术实现的前后端分离的权限管理系统。 可用于学习参考和项目开发。
+
+### 特性
+
+- WEB容器使用了undertow,相较于tomcat,并发性更好,性能要好一些。
+- Lombok,消除冗长的java代码,更加简化。
+- Mybatis Plus,可以简化CRUD开发。
+- Mybatis Plus Generator,生成前后端代码,简化开发工作量。
+- 使用java新特性,Stream API、lambda表达式等。
+- Hutool工具集合,减少项目工具类的编写。
+- Sa-Token权限认证框架,细分到页面按钮级别。
+- EasyExcel,方便导入导出功能,自定义Convert类,实现了数据字典的转化。
+- Guava,非常方便的java工具集,提供了类似Lists.newArrayList()和Sets.newHashSet()等静态方法。
+- DataSource注解,支持多数据源切换。
+- Fastjson,方便了JSON的格式化和解析。
+- Alibaba Java Coding Guidelines插件,IDEA插件,提高代码质量。
+- MinIO,分布式文件存储。
+- 前端框架采用最新技术栈,Vue3 & Vite,打包更快更轻。
+- 前端框架采用TypeScript和Eslint,规范代码,提高项目可持续性和可维护性。
+
+### 预览
+
+
+### 项目地址
+
+
+### 项目结构
+
+```
+coffee-boot
+├── coffee-admin -- 内置功能,后台管理
+├── coffee-codegen -- 内置功能,代码生成
+├── coffee-common --内置功能,通用工具
+├── coffee-framework -- 内置功能,核心模块
+├── coffee-system -- 内置功能,系统模块
+├── coffee-oss -- 内置功能,OSS文件存储模块
+```
+
+### 核心依赖
+
+| 依赖                   | 版本          |
+| ---------------------- | ------------ |
+| Spring Boot            | 2.5.8        |
+| Sa Token               | 1.29.0       |
+| Mybatis Plus           | 3.5.1        |
+| Mybatis Plus Generator | 3.4.1        |
+| Hutool                 | 5.7.21       |
+| Guava                  | 30.1.1-jre   |
+| EasyExcel              | 3.0.5        |
+| Fastjson               | 1.2.79       |
+| Minio                  | 8.3.7        |
+
+### 内置功能
+
+```
+1、菜单管理
+2、字典管理
+3、部门管理
+4、岗位管理
+5、角色管理
+6、用户管理
+7、参数设置
+8、行政区域
+9、在线用户
+10、操作日志
+11、账户设置
+```
+### 环境安装
+
+```
+1、安装Mysql数据库,安装Redis,安装MinIO文件存储。
+2、执行./doc/db/schema.sql,创建数据库。
+3、执行./doc/db/coffee.sql,创建数据表和插入基础数据。
+```
+
+### Docker常用命令
+
+```
+1、查看镜像
+docker images
+
+2、查看所有容器
+docker ps -a
+
+3、拉取镜像
+docker pull redis:latest
+
+4、参数解释
+--name,设置运行的镜像名称
+-p,映射端口,虚拟机端口:docker端口
+-e,设置环境变量
+-v,挂载目录/文件,虚拟机目录/文件:docker目录/文件
+--privileged=true,设置特权,比如为mysql获取root权限
+-d,守护进程后台运行
+-it,启动并运行
+--restart=always,在docker服务重启后,自动重启mysql服务,也可以把docker服务作为开机启动,这样mysql就可以跟着开机启动了
+--link,设置容器别名
+```
+
+### Docker安装Redis
+
+```
+1、拉取镜像
+docker pull redis:latest
+
+2、启动并运行
+docker run -itd --name redis -p 6379:6379 redis
+```
+
+### Docker安装Mysql
+
+```
+1、拉取镜像
+docker pull mysql:5.7
+
+2、创建目录并授权
+mkdir -p /opt/docker/mysql/data /opt/docker/mysql/conf
+chmod -R 777 /opt/docker
+
+3、创建配置文件并授权
+touch /opt/docker/mysql/conf/my.cnf
+vi /opt/docker/mysql/conf/my.cnf
+chmod 777 /opt/docker/mysql/conf/my.cnf
+my.cnf内容如下:
+[client]
+port=3306
+default-character-set=utf8
+[mysql]
+default-character-set=utf8
+[mysqld]
+character_set_server=utf8
+sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
+lower_case_table_names=1
+
+4、启动并运行
+docker run -itd --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 --privileged=true -v /opt/docker/mysql/conf/my.cnf:/etc/mysql/my.cnf -v /opt/docker/mysql/data:/var/lib/mysql -v /opt/docker/mysql/logs:/var/log/mysql mysql:5.7
+```
+
+### Docker安装MinIO
+
+```
+1、拉取镜像
+docker pull minio/minio
+
+2、创建目录并授权
+mkdir -p /opt/docker/minio/data
+chmod -R 777 /opt/docker
+
+3、启动并运行
+docker run -d -p 9000:9000 -p 9001:9001 --name=minio -v /opt/docker/minio/data:/data quay.io/minio/minio server /data/data-{1...4} --console-address ":9001" --address ":9000"
+```
+
+### MinIO文件存储
+
+
+### 部署
+
+```
+一、打包命令:指定prod环境,进行打包
+    mvn clean package -DskipTests -Pprod
+二、启动
+    1、Windows环境,运行./doc/bin/run.bat
+    2、Linux环境,运行./doc/bin/linux/startup.sh
+三、注意事项
+    1、Linux执行脚本,需要先授权,chmod +x startup.sh
+    2、run.bat或startup.sh,需要和coffee-admin.jar放在同一个目录运行
+    3、指定端口,需要修改脚本,并添加server.port参数,示例:--server.port=9090
+四、服务器部署目录结构
+    /opt
+    ├── coffee
+    ├──── coffee-ui
+    ├──── coffee-boot
+    ├────── startup.sh
+    ├────── coffee-admin.jar
+```
+
+### nginx配置
+
+```
+    location / {
+        root   /opt/coffee/coffee-ui;
+        try_files $uri $uri/ @router;
+        index  index.html index.htm;
+    }
+    
+    location @router {
+        rewrite ^.*$ /index.html last;
+    }
+    
+    location ^~ /api {
+        proxy_pass  http://localhost:9090/api;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header REMOTE-HOST $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+```
+
+### 使用帮助
+
+#### 代码同步方法(示例)
+
+1、fork代码,fork from https://gitee.com/skysong/coffee-ui <br/>
+2、创建自己的分支,进行开发,feature-dev <br/>
+3、码云强制同步 <br/>
+<img src="./doc/images/doc/1.png"/>
+<br/>
+4、合并代码,merge into feature-dev <br/>
+<img src="./doc/images/doc/2.png"/>
+
+#### 代码生成(CodeGenerator)
+
+```
+    /** 按照个人需要,进行修改 */
+    public static final String AUTHOR = "Kevin";
+    public static final String PROJECT_PATH = "D:\\tempCode";
+    public static final String PACKAGE_PARENT = "com.coffee";
+    public static final String MODULE_NAME = "system";
+
+    /** 生成SQL脚本的上级菜单的ID,要开发的功能,需要放到XXX菜单下面,请找到XXX菜单的ID */
+    public static final String PARENT_MENU_ID = "1406064334403878913";
+
+    /** admin的ID,可以不用修改 */
+    public static final String CREATE_BY = "1";
+    public static final String UPDATE_BY = "1";
+
+    /** 默认菜单图标,可以不用修改,SQL脚本生成之后,在页面选择图标,进行修改即可 */
+    public static final String ICON = "ant-design:unordered-list-outlined";
+
+    // 是否导出excel
+    public static final Boolean exportExcel = false;
+
+    public static void main(String[] args) {
+        new CodeGenerator().generate(
+                "sys_example"
+        );
+    }
+```
+
+#### EasyExcel使用
+
+```
+excel标题宽度
+两个字:@ColumnWidth(10)
+四个字:@ColumnWidth(15)
+```
+
+#### Java编码规范(Java开发手册更新至嵩山版)
+
+[Java开发手册](./doc/java开发手册/阿里巴巴Java开发手册(嵩山版).pdf)
+
+#### Java规范
+
+```
+1、大道至简
+
+2、IDEA安装Alibaba Java Coding Guidelines插件,编码规约扫描,代码中不要出现警告
+
+3、尽量用@Resource,基于BeanName查找注入,少用@Autowired,基于BeanType查找注入
+
+4、尽量用Service互相注入,少用直接注入Mapper
+
+5、详细开发范例请见 com.coffee.framework.test.controller下类,Service层直接使用Service,抛弃接口类
+
+```
+
+#### 建表规范
+
+```
+1、ID主键,bigint(20),雪花算法
+
+2、审计字段,create_by、create_time、update_by、update_time
+
+3、表中字段类型,主要采用varchar,万一迁移oracle、sqlserver数据库呢,好兼容
+```

+ 7 - 0
coffee-admin/Dockerfile

@@ -0,0 +1,7 @@
+FROM anapsix/alpine-java:8_server-jre_unlimited
+
+EXPOSE 9090
+
+ADD ./target/coffee-admin.jar ./app.jar
+
+ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

+ 36 - 0
coffee-admin/pom.xml

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>coffee-boot</artifactId>
+        <groupId>com.coffee</groupId>
+        <version>1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>jar</packaging>
+    <artifactId>coffee-admin</artifactId>
+
+    <dependencies>
+        <!-- 核心模块 -->
+        <dependency>
+            <groupId>com.coffee</groupId>
+            <artifactId>coffee-framework</artifactId>
+        </dependency>
+        <!-- OSS文件存储模块 -->
+        <dependency>
+            <groupId>com.coffee</groupId>
+            <artifactId>coffee-oss</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 25 - 0
coffee-admin/src/main/java/com/coffee/admin/AdminApplication.java

@@ -0,0 +1,25 @@
+package com.coffee.admin;
+
+import com.coffee.common.config.AppConfig;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.context.annotation.Import;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+/**
+ * Admin启动类
+ *
+ * @author Kevin
+ */
+@SpringBootApplication(scanBasePackages = "com.coffee", exclude = {DataSourceAutoConfiguration.class})
+@Import(cn.hutool.extra.spring.SpringUtil.class)
+@EnableSwagger2
+public class AdminApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(AdminApplication.class, args);
+        System.out.println(String.format("============ %s API 启动成功 ============", AppConfig.getName().toUpperCase()));
+    }
+
+}

+ 31 - 0
coffee-admin/src/main/java/com/coffee/admin/StartUpRunner.java

@@ -0,0 +1,31 @@
+package com.coffee.admin;
+
+import com.coffee.system.service.ISysConfigService;
+import com.coffee.system.service.ISysDictService;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 项目初始化加载
+ *
+ * @author Kevin
+ */
+@Component
+public class StartUpRunner implements CommandLineRunner {
+
+    @Resource
+    ISysDictService sysDictService;
+
+    @Resource
+    ISysConfigService sysConfigService;
+
+    @Override
+    public void run(String... args) {
+        System.out.println("============ 服务器启动中....,开始加载数据 ============");
+        sysDictService.loadAllDictCache();
+        sysConfigService.loadAllConfig();
+    }
+
+}

+ 38 - 0
coffee-admin/src/main/java/com/coffee/admin/controller/IndexController.java

@@ -0,0 +1,38 @@
+package com.coffee.admin.controller;
+
+import cn.hutool.extra.servlet.ServletUtil;
+import lombok.SneakyThrows;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * <p>
+ * 首页 前端控制器
+ * </p>
+ *
+ * @author Kevin
+ */
+@RestController
+public class IndexController {
+
+    private static void writeIndex(HttpServletResponse response) {
+        response.setCharacterEncoding("UTF-8");
+        response.setHeader("Content-type", "text/html; charset=UTF-8");
+        ServletUtil.getWriter(response).write("<h1 style=\"text-align: center;padding: 50px 0;\">欢迎访问API</h1>");
+    }
+
+    @GetMapping("/")
+    @SneakyThrows
+    public void api(HttpServletResponse response) {
+        writeIndex(response);
+    }
+
+    @GetMapping("/index")
+    public void index(HttpServletResponse response) {
+        writeIndex(response);
+    }
+
+}

+ 98 - 0
coffee-admin/src/main/java/com/coffee/admin/controller/common/CommonController.java

@@ -0,0 +1,98 @@
+package com.coffee.admin.controller.common;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
+import com.coffee.common.config.AppConfig;
+import com.coffee.common.exception.CustomException;
+import com.coffee.common.result.R;
+import com.coffee.common.result.ResultCode;
+import com.coffee.oss.strategy.FileStorageStrategy;
+import com.coffee.oss.strategy.context.FileStorageContext;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+
+/**
+ * <p>
+ * 通用common 前端控制器
+ * </p>
+ *
+ * @author Kevin
+ */
+@RestController
+@RequestMapping("/common")
+@Slf4j
+public class CommonController {
+
+    @Resource
+    private FileStorageContext fileStorageContext;
+
+    /**
+     * 下载文件名重新编码
+     *
+     * @param response     响应对象
+     * @param realFileName 真实文件名
+     * @return
+     */
+    public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException {
+        String percentEncodedFileName = percentEncode(realFileName);
+        StringBuilder contentDispositionValue = new StringBuilder();
+        contentDispositionValue.append("attachment; filename=")
+                .append(percentEncodedFileName)
+                .append(";")
+                .append("filename*=")
+                .append("utf-8''")
+                .append(percentEncodedFileName);
+        response.setHeader("Content-disposition", contentDispositionValue.toString());
+    }
+
+    /**
+     * 百分号编码工具方法
+     *
+     * @param s 需要百分号编码的字符串
+     * @return 百分号编码后的字符串
+     */
+    public static String percentEncode(String s) throws UnsupportedEncodingException {
+        String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString());
+        return encode.replaceAll("\\+", "%20");
+    }
+
+    @PostMapping("/upload")
+    @SneakyThrows
+    public R upload(MultipartFile file, HttpServletRequest request) {
+        String bizPath = request.getParameter("bizPath");
+        if (StrUtil.isBlank(bizPath)) {
+            throw new CustomException(ResultCode.PARAM_MISS.getMessage());
+        }
+        if (Objects.isNull(file)) {
+            throw new CustomException(ResultCode.PARAM_MISS.getMessage());
+        }
+        FileStorageStrategy fileStorageStrategy = fileStorageContext.getContext();
+        String url = fileStorageStrategy.upload(file, bizPath);
+        return R.success(url);
+    }
+
+    @GetMapping("/download")
+    @SneakyThrows
+    public void download(@RequestParam String filepath, HttpServletResponse response) {
+        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+        // 文件名称转换,用户列表_uuid.xlsx -> 用户列表_1637469159968.xlsx
+        String filepathConvert = StrUtil.subBefore(filepath, "_", true)
+                + "_" + System.currentTimeMillis() + "."
+                + StrUtil.subAfter(filepath, ".", true);
+        setAttachmentResponseHeader(response, FileUtil.getName(filepathConvert));
+        FileUtil.writeToStream(AppConfig.getUploadDir() + filepath, response.getOutputStream());
+    }
+
+}

+ 87 - 0
coffee-admin/src/main/java/com/coffee/admin/controller/monitor/OnlineUserController.java

@@ -0,0 +1,87 @@
+package com.coffee.admin.controller.monitor;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.coffee.common.Constants;
+import com.coffee.common.annotation.Log;
+import com.coffee.common.bo.LoginUser;
+import com.coffee.common.redis.RedisUtils;
+import com.coffee.common.result.R;
+import com.google.common.collect.Lists;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * <p>
+ * 在线用户 前端控制器
+ * </p>
+ *
+ * @author Kevin
+ */
+@RestController
+@RequestMapping("/monitor/online")
+public class OnlineUserController {
+
+    @Resource
+    RedisUtils redisUtils;
+
+    /**
+     * 分页查询
+     */
+    @GetMapping("/page")
+    @SaCheckPermission("monitor:online:page")
+    @Log(title = "在线用户分页查询")
+    public R page(Page reqPage, String username, String userPlatform) {
+        List<String> keys = StpUtil.searchTokenValue("", -1, 0);
+        List<LoginUser> loginUserList = Lists.newArrayList();
+        for (String key : keys) {
+            // token sample, Authorization:login:token:8734da0866f4440daeea3d836d5ecf8c
+            String tokenValue = StrUtil.subAfter(key, StpUtil.getTokenName() + ":" + StpUtil.TYPE + ":token:", true);
+            LoginUser item = (LoginUser) StpUtil.getTokenSessionByToken(tokenValue).get(Constants.LOGIN_USER_KEY);
+            if (StrUtil.isNotBlank(username) && StrUtil.isNotBlank(userPlatform)) {
+                if (StrUtil.contains(item.getUsername(), username) && item.getUserPlatform().equals(userPlatform)) {
+                    loginUserList.add(item);
+                }
+            } else if (StrUtil.isNotBlank(username)) {
+                if (StrUtil.contains(item.getUsername(), username)) {
+                    loginUserList.add(item);
+                }
+            } else if (StrUtil.isNotBlank(userPlatform)) {
+                if (item.getUserPlatform().equals(userPlatform)) {
+                    loginUserList.add(item);
+                }
+            } else {
+                loginUserList.add(item);
+            }
+        }
+        // 进行排序,以登陆时间倒序
+        loginUserList = loginUserList.stream().sorted(Comparator.comparing(LoginUser::getLoginTime).reversed()).collect(Collectors.toList());
+
+        IPage<LoginUser> page = new Page<>(reqPage.getCurrent(), reqPage.getSize(), loginUserList.size());
+        if (page.getPages() > 0) {
+            int fromIndex = (int) ((page.getCurrent() - 1) * page.getSize());
+            int toIndex = page.getCurrent() == page.getPages() ? (int) page.getTotal() : (int) (fromIndex + page.getSize());
+            page.setRecords(loginUserList.subList(fromIndex, toIndex));
+        }
+        return R.success(page);
+    }
+
+    /**
+     * 强制退出
+     */
+    @PostMapping("/forceLogout")
+    @SaCheckPermission("monitor:online:forceLogout")
+    @Log(title = "在线用户强制退出")
+    public R forceLogout(@RequestParam String token) {
+        StpUtil.kickoutByTokenValue(token);
+        return R.success();
+    }
+
+}

+ 78 - 0
coffee-admin/src/main/java/com/coffee/admin/controller/system/SysLoginController.java

@@ -0,0 +1,78 @@
+package com.coffee.admin.controller.system;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.coffee.common.annotation.Log;
+import com.coffee.common.dto.LoginDTO;
+import com.coffee.common.result.R;
+import com.coffee.framework.web.service.IUserService;
+import com.coffee.system.common.vo.AccountInfoVO;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * <p>
+ * 登录 前端控制器
+ * </p>
+ *
+ * @author Kevin
+ */
+@RestController
+public class SysLoginController {
+
+    @Resource
+    IUserService userService;
+
+    @Log(title = "登录")
+    @PostMapping("/login")
+    public R login(@Validated @RequestBody LoginDTO req) {
+        String token = userService.login(req);
+        JSONObject result = new JSONObject();
+        result.put("token", token);
+        return R.success(result);
+    }
+
+    @Log(title = "获取用户信息")
+    @GetMapping("/getUserInfo")
+    public R getUserInfo() {
+        return R.success(userService.getUserInfo());
+    }
+
+    @Log(title = "获取权限集合")
+    @GetMapping("/getPermCode")
+    public R getPermCode() {
+        return R.success(userService.getPermCode());
+    }
+
+    @Log(title = "获取菜单集合")
+    @GetMapping("/getMenuList")
+    public R getMenuList() {
+        return R.success(userService.getMenuList());
+    }
+
+    @Log(title = "获取账户信息")
+    @GetMapping("/getAccountInfo")
+    public R getAccountInfo() {
+        return R.success(userService.getAccountInfo());
+    }
+
+    @Log(title = "保存用户信息")
+    @PostMapping("/saveAccountInfo")
+    public R saveAccountInfo(@Validated @RequestBody AccountInfoVO req) {
+        userService.saveAccountInfo(req);
+        return R.success();
+    }
+
+    @Log(title = "退出")
+    @PostMapping("/logout")
+    public R logout() {
+        StpUtil.logout();
+        return R.success();
+    }
+
+}

+ 101 - 0
coffee-admin/src/main/resources/application-dev.yml

@@ -0,0 +1,101 @@
+# 项目相关配置
+app:
+  # 项目名称
+  name: coffee
+  # 实例演示开关
+  demoEnabled: false
+  # 获取ip地址开关
+  addressEnabled: true
+  # 上传类型,minio;aliyun
+  uploadType: minio
+  # 上传目录
+  uploadDir: D:/${app.name}-files
+  # 缓存前缀
+  cachePrefix: ${app.name}:${profiles.active}
+
+# MinIO相关配置
+minio:
+  endpoint: http://127.0.0.1:9000
+  accessKey: minioadmin
+  secretKey: minioadmin
+  bucketName: ${app.name}-${profiles.active}-bucket
+
+# 数据源配置
+spring:
+  datasource:
+    type: com.alibaba.druid.pool.DruidDataSource
+    driverClassName: com.mysql.cj.jdbc.Driver
+    druid:
+      # 主库数据源
+      master:
+        url: jdbc:mysql://192.168.100.32:3306/nbnetpump?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&autoReconnect=true
+        username: root
+        password: 123456
+      # 从库数据源
+      slave:
+        # 从数据源开关/默认关闭
+        enabled: false
+        url:
+        username:
+        password:
+      # 初始连接数
+      initialSize: 5
+      # 最小连接池数量
+      minIdle: 10
+      # 最大连接池数量
+      maxActive: 20
+      # 配置获取连接等待超时的时间
+      maxWait: 60000
+      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+      timeBetweenEvictionRunsMillis: 60000
+      # 配置一个连接在池中最小生存的时间,单位是毫秒
+      minEvictableIdleTimeMillis: 300000
+      # 配置一个连接在池中最大生存的时间,单位是毫秒
+      maxEvictableIdleTimeMillis: 900000
+      # 配置检测连接是否有效
+      validationQuery: SELECT 1 FROM DUAL
+      testWhileIdle: true
+      testOnBorrow: false
+      testOnReturn: false
+      webStatFilter:
+        enabled: true
+      statViewServlet:
+        enabled: true
+        # 设置白名单,不填则允许所有访问
+        allow:
+        url-pattern: /druid/*
+        # 控制台管理用户名和密码
+        login-username: scott
+        login-password: tiger
+      filter:
+        stat:
+          enabled: true
+          # 慢SQL记录
+          log-slow-sql: true
+          slow-sql-millis: 1000
+          merge-sql: true
+        wall:
+          config:
+            multi-statement-allow: true
+  # redis 配置
+  redis:
+    # 地址
+    host: 192.168.100.32
+    # 端口,默认为6379
+    port: 9736
+    # 数据库索引
+    database: 6
+    # 密码
+    password: 6E6985E1F7CB40F24A\.
+    # 连接超时时间
+    timeout: 10s
+    lettuce:
+      pool:
+        # 连接池中的最小空闲连接
+        min-idle: 0
+        # 连接池中的最大空闲连接
+        max-idle: 8
+        # 连接池的最大数据库连接数
+        max-active: 8
+        # #连接池最大阻塞等待时间(使用负值表示没有限制)
+        max-wait: -1ms

+ 0 - 0
coffee-admin/src/main/resources/application-prod.yml


+ 101 - 0
coffee-admin/src/main/resources/application-test.yml

@@ -0,0 +1,101 @@
+# 项目相关配置
+app:
+  # 项目名称
+  name: coffee
+  # 实例演示开关
+  demoEnabled: false
+  # 获取ip地址开关
+  addressEnabled: true
+  # 上传类型,minio;aliyun
+  uploadType: minio
+  # 上传目录
+  uploadDir: D:/${app.name}-files
+  # 缓存前缀
+  cachePrefix: ${app.name}:${profiles.active}
+
+# MinIO相关配置
+minio:
+  endpoint: http://127.0.0.1:9000
+  accessKey: minioadmin
+  secretKey: minioadmin
+  bucketName: ${app.name}-${profiles.active}-bucket
+
+# 数据源配置
+spring:
+  datasource:
+    type: com.alibaba.druid.pool.DruidDataSource
+    driverClassName: com.mysql.cj.jdbc.Driver
+    druid:
+      # 主库数据源
+      master:
+        url: jdbc:mysql://localhost:3306/coffee-test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&autoReconnect=true
+        username: root
+        password: 123456
+      # 从库数据源
+      slave:
+        # 从数据源开关/默认关闭
+        enabled: false
+        url:
+        username:
+        password:
+      # 初始连接数
+      initialSize: 5
+      # 最小连接池数量
+      minIdle: 10
+      # 最大连接池数量
+      maxActive: 20
+      # 配置获取连接等待超时的时间
+      maxWait: 60000
+      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+      timeBetweenEvictionRunsMillis: 60000
+      # 配置一个连接在池中最小生存的时间,单位是毫秒
+      minEvictableIdleTimeMillis: 300000
+      # 配置一个连接在池中最大生存的时间,单位是毫秒
+      maxEvictableIdleTimeMillis: 900000
+      # 配置检测连接是否有效
+      validationQuery: SELECT 1 FROM DUAL
+      testWhileIdle: true
+      testOnBorrow: false
+      testOnReturn: false
+      webStatFilter:
+        enabled: true
+      statViewServlet:
+        enabled: true
+        # 设置白名单,不填则允许所有访问
+        allow:
+        url-pattern: /druid/*
+        # 控制台管理用户名和密码
+        login-username: scott
+        login-password: tiger
+      filter:
+        stat:
+          enabled: true
+          # 慢SQL记录
+          log-slow-sql: true
+          slow-sql-millis: 1000
+          merge-sql: true
+        wall:
+          config:
+            multi-statement-allow: true
+  # redis 配置
+  redis:
+    # 地址
+    host: localhost
+    # 端口,默认为6379
+    port: 6379
+    # 数据库索引
+    database: 0
+    # 密码
+    password:
+    # 连接超时时间
+    timeout: 10s
+    lettuce:
+      pool:
+        # 连接池中的最小空闲连接
+        min-idle: 0
+        # 连接池中的最大空闲连接
+        max-idle: 8
+        # 连接池的最大数据库连接数
+        max-active: 8
+        # #连接池最大阻塞等待时间(使用负值表示没有限制)
+        max-wait: -1ms

+ 54 - 0
coffee-admin/src/main/resources/application.yml

@@ -0,0 +1,54 @@
+spring:
+  application:
+    name: coffee-service
+  profiles:
+    active: @profiles.active@
+
+server:
+  port: 9090
+  servlet:
+    context-path: /api
+  undertow:
+    accesslog:
+      enabled: true
+    max-http-post-size: -1
+    buffer-size: 512
+    direct-buffers: true
+    threads:
+      io: 16
+      worker: 256
+
+# Sa-Token配置
+sa-token:
+  # token名称 (同时也是cookie名称)
+  token-name: Authorization
+  # token有效期,单位s 默认30天, -1代表永不过期
+  timeout: 2592000
+  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
+  activity-timeout: -1
+  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
+  is-concurrent: true
+  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
+  is-share: false
+  # token风格
+  token-style: simple-uuid
+  # 是否输出操作日志
+  is-log: true
+  # 是否从cookie读取token
+  is-read-cookie: false
+
+logging:
+  level:
+    com.coffee: @logging.level@
+    org.springframework: warn
+  config: classpath:logback.xml
+
+mybatis-plus:
+  mapperPackage: com.coffee.**.mapper,com.coffee.framework.test.mapper
+  mapperLocations: classpath*:mapper/**/*Mapper.xml
+  checkConfigLocation: false
+  global-config:
+    banner: false
+    enableSqlRunner: false
+    dbConfig:
+      idType: ASSIGN_ID

+ 95 - 0
coffee-admin/src/main/resources/logback.xml

@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <property name="log.path" value="./logs"/>
+    <property name="console.log.pattern"
+              value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
+    <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
+
+    <!-- 控制台输出 -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${console.log.pattern}</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+    </appender>
+
+    <!-- 控制台输出 -->
+    <appender name="sys_console" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/sys-console.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-console.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大 1天 -->
+            <maxHistory>1</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+        </filter>
+    </appender>
+
+    <!-- 系统日志输出 -->
+    <appender name="sys_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/sys-info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <appender name="sys_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/sys-error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 系统模块日志级别控制  -->
+    <logger name="com.coffee" level="info"/>
+    <!-- Spring日志级别控制  -->
+    <logger name="org.springframework" level="warn"/>
+
+    <root level="info">
+        <appender-ref ref="console"/>
+    </root>
+
+    <!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="sys_info"/>
+        <appender-ref ref="sys_error"/>
+        <appender-ref ref="sys_console"/>
+    </root>
+</configuration> 

+ 37 - 0
coffee-codegen/pom.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>coffee-boot</artifactId>
+        <groupId>com.coffee</groupId>
+        <version>1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>coffee-codegen</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-generator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity-engine-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.coffee</groupId>
+            <artifactId>coffee-common</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 219 - 0
coffee-codegen/src/main/java/com/coffee/codegen/CodeGenerator.java

@@ -0,0 +1,219 @@
+package com.coffee.codegen;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.baomidou.mybatisplus.generator.AutoGenerator;
+import com.baomidou.mybatisplus.generator.InjectionConfig;
+import com.baomidou.mybatisplus.generator.config.*;
+import com.baomidou.mybatisplus.generator.config.po.TableFill;
+import com.baomidou.mybatisplus.generator.config.po.TableInfo;
+import com.baomidou.mybatisplus.generator.config.rules.DateType;
+import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
+import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine;
+import com.coffee.common.util.IdUtil;
+import com.google.common.collect.Maps;
+
+import java.util.*;
+
+/**
+ * 代码生成工具类
+ * @author Kevin
+ */
+@SuppressWarnings("ALL")
+public class CodeGenerator {
+
+    public static final String JDBC_URL = "jdbc:mysql://localhost:3306/coffee-dev?characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai";
+    public static final String JDBC_USERNAME = "root";
+    public static final String JDBC_PASSWORD = "123456";
+    public static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";
+
+    /** 按照个人需要,进行修改 */
+    public static final String AUTHOR = "Kevin";
+    public static final String PROJECT_PATH = "D:\\tempCode";
+    public static final String PACKAGE_PARENT = "com.coffee";
+    public static final String MODULE_NAME = "system";
+
+    /** 生成SQL脚本的上级菜单的ID,要开发的功能,需要放到XXX菜单下面,请找到XXX菜单的ID */
+    public static final String PARENT_MENU_ID = "1406064334403878913";
+
+    /** admin的ID,可以不用修改 */
+    public static final String CREATE_BY = "1";
+    public static final String UPDATE_BY = "1";
+
+    /** 默认菜单图标,可以不用修改,SQL脚本生成之后,在页面选择图标,进行修改即可 */
+    public static final String ICON = "ant-design:unordered-list-outlined";
+
+    // 是否导出excel
+    public static final Boolean exportExcel = false;
+
+    public static void main(String[] args) {
+        new CodeGenerator().generate(
+                "sys_example"
+        );
+    }
+
+    private void generate(String... tableNamesInclude) {
+        // 代码生成器
+        AutoGenerator mpg = new AutoGenerator();
+
+        // 全局配置
+        GlobalConfig gc = new GlobalConfig();
+        String projectPath = PROJECT_PATH;
+        gc.setOutputDir(projectPath + "/src/main/java");
+        gc.setAuthor(AUTHOR);
+        gc.setOpen(false);
+        gc.setDateType(DateType.ONLY_DATE);
+        //默认不覆盖,如果文件存在,将不会再生成,配置true就是覆盖
+        gc.setFileOverride(true);
+        // gc.setSwagger2(true); 实体属性 Swagger2 注解
+        gc.setBaseResultMap(true);
+        // gc.setBaseColumnList(true); // XML column list
+        mpg.setGlobalConfig(gc);
+
+        // 数据源配置
+        DataSourceConfig dsc = new DataSourceConfig();
+        dsc.setUrl(JDBC_URL);
+        dsc.setDriverName(JDBC_DRIVER);
+        dsc.setUsername(JDBC_USERNAME);
+        dsc.setPassword(JDBC_PASSWORD);
+        mpg.setDataSource(dsc);
+
+        // 包配置
+        PackageConfig pc = new PackageConfig();
+        // 设置模块名,会在指定parent包下生成一个指定的模块包
+        pc.setModuleName(MODULE_NAME);
+        pc.setParent(PACKAGE_PARENT);
+        mpg.setPackageInfo(pc);
+
+        // 自定义配置
+        InjectionConfig cfg = new InjectionConfig() {
+            @Override
+            public void initMap() {
+                // to do nothing
+                Arrays.asList(tableNamesInclude).forEach(item -> {
+                    Map params = Objects.isNull(this.getMap()) ? Maps.newHashMap() : this.getMap();
+                    params.put(item +"menuId", IdUtil.getSnowflakeId());
+                    params.put(item +"parentMenuId", PARENT_MENU_ID);
+                    params.put(item +"icon", ICON);
+                    params.put(item +"pageButtonId", IdUtil.getSnowflakeId());
+                    params.put(item +"addButtonId", IdUtil.getSnowflakeId());
+                    params.put(item +"editButtonId", IdUtil.getSnowflakeId());
+                    params.put(item +"removeButtonId", IdUtil.getSnowflakeId());
+                    params.put(item +"viewButtonId", IdUtil.getSnowflakeId());
+                    params.put(item +"exportButtonId", IdUtil.getSnowflakeId());
+                    params.put(item +"createBy", CREATE_BY);
+                    params.put(item +"updateBy", UPDATE_BY);
+                    params.put(item +"exportExcel", exportExcel);
+                    this.setMap(params);
+                });
+            }
+        };
+
+        // 如果模板引擎是 freemarker
+        // String templatePath = "/templates/mapper.xml.ftl";
+        // 如果模板引擎是 velocity
+        // String templatePath = "/templates/mapper.xml.vm";
+
+        // 自定义输出配置
+        List<FileOutConfig> focList = new ArrayList<>();
+        // 自定义配置会被优先输出
+        // 调整xml生成目录
+        focList.add(new FileOutConfig("/templates/mapper.xml.vm") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/src/main/resources/mapper/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
+            }
+        });
+        // 生成自定义模板
+        focList.add(new FileOutConfig("/templates/addDTO.java.vm") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                String path = PACKAGE_PARENT.replaceAll("\\." , "/") + "/" + MODULE_NAME.replaceAll("\\." , "/");
+                return projectPath + "/src/main/java/" + path + "/common/dto/" + tableInfo.getEntityName() + "AddDTO" + StringPool.DOT_JAVA;
+            }
+        });
+        // 生成自定义模板
+        focList.add(new FileOutConfig("/templates/editDTO.java.vm") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                String path = PACKAGE_PARENT.replaceAll("\\." , "/") + "/" + MODULE_NAME.replaceAll("\\." , "/");
+                return projectPath + "/src/main/java/" + path + "/common/dto/" + tableInfo.getEntityName() + "EditDTO" + StringPool.DOT_JAVA;
+            }
+        });
+        // 生成自定义模板
+        focList.add(new FileOutConfig("/templates/queryDTO.java.vm") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                String path = PACKAGE_PARENT.replaceAll("\\." , "/") + "/" + MODULE_NAME.replaceAll("\\." , "/");
+                return projectPath + "/src/main/java/" + path + "/common/dto/" + tableInfo.getEntityName() + "QueryDTO" + StringPool.DOT_JAVA;
+            }
+        });
+        // 生成自定义模板,SQL
+        focList.add(new FileOutConfig("/templates/sql.vm") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/" + tableInfo.getName() + "_sql.sql";
+            }
+        });
+        // 生成自定义模板,VUE
+        focList.add(new FileOutConfig("/templates/vue/api.ts.vm") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/vue/" + tableInfo.getEntityPath() +"/" + tableInfo.getEntityPath() + "Api.ts";
+            }
+        });
+        // 生成自定义模板,VUE
+        focList.add(new FileOutConfig("/templates/vue/data.ts.vm") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/vue/" + tableInfo.getEntityPath() +"/" + "data.ts";
+            }
+        });
+        // 生成自定义模板,VUE
+        focList.add(new FileOutConfig("/templates/vue/FormModal.vue.vm") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/vue/" + tableInfo.getEntityPath() +"/" + "FormModal.vue";
+            }
+        });
+        // 生成自定义模板,VUE
+        focList.add(new FileOutConfig("/templates/vue/index.vue.vm") {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                return projectPath + "/vue/" + tableInfo.getEntityPath() +"/" + "index.vue";
+            }
+        });
+        cfg.setFileOutConfigList(focList);
+        mpg.setCfg(cfg);
+
+        // 配置模板
+        TemplateConfig templateConfig = new TemplateConfig();
+        templateConfig.setXml(null)
+                .setEntity("/templates/entity.java.vm")
+                .setMapper("/templates/mapper.java.vm")
+                .setController("/templates/controller.java.vm")
+                .setService("/templates/service.java.vm")
+                .setServiceImpl("/templates/serviceImpl.java.vm");
+        mpg.setTemplate(templateConfig);
+
+        // 策略配置
+        StrategyConfig strategy = new StrategyConfig();
+        strategy.setNaming(NamingStrategy.underline_to_camel);
+        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
+        strategy.setEntityLombokModel(true);
+        strategy.setRestControllerStyle(true);
+        strategy.setInclude(tableNamesInclude);
+        strategy.setChainModel(false);
+        List<TableFill> tableFillList = new ArrayList<>();
+        tableFillList.add(new TableFill("create_by" , FieldFill.INSERT));
+        tableFillList.add(new TableFill("create_time" , FieldFill.INSERT));
+        tableFillList.add(new TableFill("update_by" , FieldFill.INSERT_UPDATE));
+        tableFillList.add(new TableFill("update_time" , FieldFill.INSERT_UPDATE));
+        strategy.setTableFillList(tableFillList);
+        mpg.setStrategy(strategy);
+        mpg.setTemplateEngine(new VelocityTemplateEngine());
+        mpg.execute();
+        System.out.println(mpg.getTemplateEngine().getObjectMap(new TableInfo()));
+    }
+
+}

+ 119 - 0
coffee-codegen/src/main/resources/templates/addDTO.java.vm

@@ -0,0 +1,119 @@
+package ${package.Controller.replace(".controller", "")}.common.dto;
+
+#foreach($pkg in ${table.importPackages})
+#if(${pkg.indexOf("baomidou")}==-1)
+import ${pkg};
+#end
+#end
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+#if(${entityLombokModel})
+import lombok.Data;
+#if(${chainModel})
+import lombok.experimental.Accessors;
+#end
+#end
+
+/**
+* <p>
+* $!{table.comment}
+* </p>
+*
+* @author ${author}
+* @since ${date}
+*/
+#if(${entityLombokModel})
+@Data
+#if(${chainModel})
+@Accessors(chain = true)
+#end
+#end
+public class ${entity}AddDTO implements Serializable {
+
+#if(${entitySerialVersionUID})
+    private static final long serialVersionUID = 1L;
+#end
+## ----------BEGIN 字段循环遍历----------
+#foreach($field in ${table.fields})
+
+#if("$!field.comment" != "")
+    /**
+    * ${field.comment}
+    */
+#end
+#if(${field.keyFlag})
+## 主键
+#if(${field.keyIdentityFlag})
+#end
+## 普通字段
+#else
+## String
+#if(${field.propertyType.equals("String")})
+#if("$!field.comment" != "")
+    @NotBlank(message = "${field.comment}不能为空")
+    @Size(max = 100, message = "${field.comment}长度不能超过100个字符")
+#else
+    @NotBlank(message = "${field.propertyName}不能为空")
+    @Size(max = 100, message = "${field.propertyName}长度不能超过100个字符")
+#end
+#else
+#if("$!field.comment" != "")
+    @NotNull(message = "${field.comment}不能为空")
+#else
+    @NotNull(message = "${field.propertyName}不能为空")
+#end
+#end
+#end
+    private ${field.propertyType} ${field.propertyName};
+#end
+## ----------END 字段循环遍历----------
+
+#if(!${entityLombokModel})
+#foreach($field in ${table.fields})
+#if(${field.propertyType.equals("boolean")})
+#set($getprefix="is")
+#else
+#set($getprefix="get")
+#end
+
+    public ${field.propertyType} ${getprefix}${field.capitalName}() {
+        return ${field.propertyName};
+    }
+
+#if(${chainModel})
+    public ${entity} set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+#else
+    public void set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+#end
+        this.${field.propertyName} = ${field.propertyName};
+#if(${chainModel})
+        return this;
+#end
+    }
+#end
+## --foreach end---
+#end
+## --end of #if(!${entityLombokModel})--
+
+#if(${entityColumnConstant})
+#foreach($field in ${table.fields})
+    public static final String ${field.name.toUpperCase()} = "${field.name}";
+
+#end
+#end
+#if(!${entityLombokModel})
+    @Override
+    public String toString() {
+        return "${entity}{" +
+#foreach($field in ${table.fields})
+#if($!{foreach.index}==0)
+                "${field.propertyName}=" + ${field.propertyName} +
+#else
+                ", ${field.propertyName}=" + ${field.propertyName} +
+#end
+#end
+        "}";
+    }
+#end
+}

+ 111 - 0
coffee-codegen/src/main/resources/templates/controller.java.vm

@@ -0,0 +1,111 @@
+package ${package.Controller};
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.coffee.common.annotation.Log;
+import ${package.Controller.replace(".${package.ModuleName}.controller", "")}.common.result.R;
+import ${package.Controller.replace(".${package.ModuleName}.controller", "")}.common.utils.ExcelUtils;
+import ${package.Controller.replace(".controller", "")}.common.dto.${table.entityName}AddDTO;
+import ${package.Controller.replace(".controller", "")}.common.dto.${table.entityName}EditDTO;
+import ${package.Controller.replace(".controller", "")}.common.dto.${table.entityName}QueryDTO;
+import ${package.Entity}.${entity};
+import ${package.Service}.${table.serviceName};
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+/**
+ * <p>
+ * $!{table.comment} 前端控制器
+ * </p>
+ *
+ * @author ${author}
+ * @since ${date}
+ */
+#if(${restControllerStyle})
+@RestController
+#else
+@Controller
+#end
+@RequestMapping("#if(${package.ModuleName})/${package.ModuleName}#end/#if(${controllerMappingHyphenStyle})${controllerMappingHyphen}#else${table.entityPath}#end")
+#if(${kotlin})
+class ${table.controllerName}#if(${superControllerClass}) : ${superControllerClass}()#end
+
+#else
+#if(${superControllerClass})
+public class ${table.controllerName} extends ${superControllerClass} {
+#else
+public class ${table.controllerName} {
+#end
+
+    @Resource
+    private ${table.serviceName} ${table.entityPath}Service;
+
+    /**
+    * 分页查询
+    */
+    @GetMapping("/page")
+    @PreAuthorize("@ss.hasPermission('#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:page')")
+    @Log(title = "$!{table.comment}分页查询")
+    public R page(Page reqPage, ${table.entityName}QueryDTO req) {
+        return R.success(${table.entityPath}Service.page(reqPage, req));
+    }
+
+    /**
+    * 新增
+    */
+    @PostMapping("/add")
+    @PreAuthorize("@ss.hasPermission('#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:add')")
+    @Log(title = "$!{table.comment}新增")
+    public R add(@Validated @RequestBody ${table.entityName}AddDTO req) {
+        ${table.entityPath}Service.add(req);
+        return R.success();
+    }
+
+    /**
+    * 修改
+    */
+    @PostMapping("/edit")
+    @PreAuthorize("@ss.hasPermission('#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:edit')")
+    @Log(title = "$!{table.comment}修改")
+    public R edit(@Validated @RequestBody ${table.entityName}EditDTO req) {
+        ${table.entityPath}Service.edit(req);
+        return R.success();
+    }
+
+    /**
+    * 删除
+    */
+    @PostMapping("/remove")
+    @PreAuthorize("@ss.hasPermission('#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:remove')")
+    @Log(title = "$!{table.comment}删除")
+    public R remove(@RequestParam String ids) {
+        ${table.entityPath}Service.remove(ids);
+        return R.success();
+    }
+
+    /**
+    * 查看
+    */
+    @GetMapping("/view")
+    @PreAuthorize("@ss.hasPermission('#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:view')")
+    @Log(title = "$!{table.comment}查看")
+    public R view(@RequestParam String id) {
+        return R.success(${table.entityPath}Service.view(id));
+    }
+#if(${cfg.get("${table.name}exportExcel")})
+    /**
+    * 导出
+    */
+    @GetMapping("/export")
+    @PreAuthorize("@ss.hasPermission('#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:export')")
+    @Log(title = "$!{table.comment}导出")
+    public R export(${table.entityName}QueryDTO req) {
+        String filepath = ExcelUtils.export("$!{table.comment}列表", ${table.entityName}.class, ${table.entityPath}Service.list(req));
+        return R.success(filepath);
+    }
+#end
+}
+
+#end

+ 120 - 0
coffee-codegen/src/main/resources/templates/editDTO.java.vm

@@ -0,0 +1,120 @@
+package ${package.Controller.replace(".controller", "")}.common.dto;
+
+#foreach($pkg in ${table.importPackages})
+#if(${pkg.indexOf("baomidou")}==-1)
+import ${pkg};
+#end
+#end
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+#if(${entityLombokModel})
+import lombok.Data;
+#if(${chainModel})
+import lombok.experimental.Accessors;
+#end
+#end
+
+/**
+* <p>
+* $!{table.comment}
+* </p>
+*
+* @author ${author}
+* @since ${date}
+*/
+#if(${entityLombokModel})
+@Data
+#if(${chainModel})
+@Accessors(chain = true)
+#end
+#end
+public class ${entity}EditDTO implements Serializable {
+
+#if(${entitySerialVersionUID})
+    private static final long serialVersionUID = 1L;
+#end
+## ----------BEGIN 字段循环遍历----------
+#foreach($field in ${table.fields})
+
+#if("$!field.comment" != "")
+    /**
+    * ${field.comment}
+    */
+#end
+#if(${field.keyFlag})
+## 主键
+#if(${field.keyIdentityFlag})
+    @NotNull(message = "主键不能为空")
+#end
+## 普通字段
+#else
+## String
+#if(${field.propertyType.equals("String")})
+#if("$!field.comment" != "")
+    @NotBlank(message = "${field.comment}不能为空")
+    @Size(max = 100, message = "${field.comment}长度不能超过100个字符")
+#else
+    @NotBlank(message = "${field.propertyName}不能为空")
+    @Size(max = 100, message = "${field.propertyName}长度不能超过100个字符")
+#end
+#else
+#if("$!field.comment" != "")
+    @NotNull(message = "${field.comment}不能为空")
+#else
+    @NotNull(message = "${field.propertyName}不能为空")
+#end
+#end
+#end
+    private ${field.propertyType} ${field.propertyName};
+#end
+## ----------END 字段循环遍历----------
+
+#if(!${entityLombokModel})
+#foreach($field in ${table.fields})
+#if(${field.propertyType.equals("boolean")})
+#set($getprefix="is")
+#else
+#set($getprefix="get")
+#end
+
+    public ${field.propertyType} ${getprefix}${field.capitalName}() {
+        return ${field.propertyName};
+    }
+
+#if(${chainModel})
+    public ${entity} set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+#else
+    public void set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+#end
+        this.${field.propertyName} = ${field.propertyName};
+#if(${chainModel})
+        return this;
+#end
+    }
+#end
+## --foreach end---
+#end
+## --end of #if(!${entityLombokModel})--
+
+#if(${entityColumnConstant})
+#foreach($field in ${table.fields})
+    public static final String ${field.name.toUpperCase()} = "${field.name}";
+
+#end
+#end
+#if(!${entityLombokModel})
+    @Override
+    public String toString() {
+        return "${entity}{" +
+#foreach($field in ${table.fields})
+#if($!{foreach.index}==0)
+                "${field.propertyName}=" + ${field.propertyName} +
+#else
+                ", ${field.propertyName}=" + ${field.propertyName} +
+#end
+#end
+        "}";
+    }
+#end
+}

+ 193 - 0
coffee-codegen/src/main/resources/templates/entity.java.vm

@@ -0,0 +1,193 @@
+package ${package.Entity};
+
+#if(${cfg.get("${table.name}exportExcel")})
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.*;
+import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
+#end
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+#foreach($pkg in ${table.importPackages})
+import ${pkg};
+#end
+#if(${swagger2})
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+#end
+#if(${entityLombokModel})
+import lombok.Data;
+#if(${superEntityClass})
+import lombok.EqualsAndHashCode;
+#end
+#if(${chainModel})
+import lombok.experimental.Accessors;
+#end
+#end
+
+/**
+* <p>
+* $!{table.comment}
+* </p>
+*
+* @author ${author}
+* @since ${date}
+*/
+#if(${entityLombokModel})
+@Data
+#if(${superEntityClass})
+@EqualsAndHashCode(callSuper = true)
+#end
+#if(${chainModel})
+@Accessors(chain = true)
+#end
+#end
+#if(${cfg.get("${table.name}exportExcel")})
+@ColumnWidth(15)
+@HeadRowHeight(15)
+@ContentRowHeight(15)
+@HeadStyle
+@HeadFontStyle(fontHeightInPoints = 12)
+@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
+@ContentFontStyle(fontHeightInPoints = 10)
+@ExcelIgnoreUnannotated
+#end
+#if(${table.convert})
+@TableName("${table.name}")
+#end
+#if(${swagger2})
+@ApiModel(value="${entity}对象", description="$!{table.comment}")
+#end
+#if(${superEntityClass})
+public class ${entity} extends ${superEntityClass}#if(${activeRecord})<${entity}>#end {
+#elseif(${activeRecord})
+public class ${entity} extends Model<${entity}> {
+#else
+public class ${entity} implements Serializable {
+#end
+
+#if(${entitySerialVersionUID})
+    private static final long serialVersionUID = 1L;
+#end
+## ----------BEGIN 字段循环遍历----------
+#foreach($field in ${table.fields})
+
+#if(${field.keyFlag})
+#set($keyPropertyName=${field.propertyName})
+#end
+#if("$!field.comment" != "")
+#if(${swagger2})
+    @ApiModelProperty(value = "${field.comment}")
+#else
+    /**
+    * ${field.comment}
+    */
+#end
+#end
+#if(${field.keyFlag})
+## 主键
+#if(${field.keyIdentityFlag})
+#end
+## 普通字段
+#elseif(${field.fill})
+## -----存在字段填充设置-----
+#if(${field.convert})
+    @TableField(value = "${field.annotationColumnName}", fill = FieldFill.${field.fill})
+#else
+    @TableField(fill = FieldFill.${field.fill})
+#end
+#elseif(${field.convert})
+    @TableField("${field.annotationColumnName}")
+#end
+## 乐观锁注解
+#if(${versionFieldName}==${field.name})
+    @Version
+#end
+## 逻辑删除注解
+#if(${logicDeleteFieldName}==${field.name})
+    @TableLogic
+#end
+## Long
+#if(${field.propertyType.equals("Long")})
+    @JsonSerialize(using = ToStringSerializer.class)
+#end
+## LocalDateTime
+#if(${field.propertyType.equals("LocalDateTime")} || ${field.propertyType.equals("Date")})
+    @JsonFormat(timezone = "GMT+8" , pattern = "yyyy-MM-dd HH:mm:ss")
+#if(${cfg.get("${table.name}exportExcel")})
+    @ColumnWidth(20)
+#end
+#end
+#if("$!field.comment" != "")
+    #if(${cfg.get("${table.name}exportExcel")})
+    @ExcelProperty(value = "${field.comment}" , index = ${foreach.index})
+    #end
+#else
+    #if(${cfg.get("${table.name}exportExcel")})
+    @ExcelProperty(value = "${field.propertyName}" , index = ${foreach.index})
+    #end
+#end
+    private ${field.propertyType} ${field.propertyName};
+#end
+## ----------END 字段循环遍历----------
+
+#if(!${entityLombokModel})
+#foreach($field in ${table.fields})
+#if(${field.propertyType.equals("boolean")})
+#set($getprefix="is")
+#else
+#set($getprefix="get")
+#end
+
+    public ${field.propertyType} ${getprefix}${field.capitalName}() {
+        return ${field.propertyName};
+    }
+
+#if(${chainModel})
+    public ${entity} set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+#else
+    public void set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+#end
+        this.${field.propertyName} = ${field.propertyName};
+#if(${chainModel})
+        return this;
+#end
+    }
+#end
+## --foreach end---
+#end
+## --end of #if(!${entityLombokModel})--
+
+#if(${entityColumnConstant})
+#foreach($field in ${table.fields})
+    public static final String ${field.name.toUpperCase()} = "${field.name}";
+
+#end
+#end
+#if(${activeRecord})
+    @Override
+    protected Serializable pkVal() {
+#if(${keyPropertyName})
+        return this.${keyPropertyName};
+#else
+        return null;
+#end
+    }
+
+#end
+#if(!${entityLombokModel})
+    @Override
+    public String toString() {
+        return "${entity}{" +
+#foreach($field in ${table.fields})
+#if($!{foreach.index}==0)
+                "${field.propertyName}=" + ${field.propertyName} +
+#else
+                ", ${field.propertyName}=" + ${field.propertyName} +
+#end
+#end
+        "}";
+    }
+#end
+}

+ 20 - 0
coffee-codegen/src/main/resources/templates/mapper.java.vm

@@ -0,0 +1,20 @@
+package ${package.Mapper};
+
+import ${package.Entity}.${entity};
+import ${superMapperClassPackage};
+
+/**
+ * <p>
+ * $!{table.comment} Mapper 接口
+ * </p>
+ *
+ * @author ${author}
+ * @since ${date}
+ */
+#if(${kotlin})
+interface ${table.mapperName} : ${superMapperClass}<${entity}>
+#else
+public interface ${table.mapperName} extends ${superMapperClass}<${entity}> {
+
+}
+#end

+ 39 - 0
coffee-codegen/src/main/resources/templates/mapper.xml.vm

@@ -0,0 +1,39 @@
+<?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="${package.Mapper}.${table.mapperName}">
+
+#if(${enableCache})
+    <!-- 开启二级缓存 -->
+    <cache type="org.mybatis.caches.ehcache.LoggingEhcache"/>
+
+#end
+#if(${baseResultMap})
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="${package.Entity}.${entity}">
+#foreach($field in ${table.fields})
+#if(${field.keyFlag})##生成主键排在第一位
+        <id column="${field.name}" property="${field.propertyName}"/>
+#end
+#end
+#foreach($field in ${table.commonFields})##生成公共字段
+        <result column="${field.name}" property="${field.propertyName}"/>
+#end
+#foreach($field in ${table.fields})
+#if(!${field.keyFlag})##生成普通字段
+        <result column="${field.name}" property="${field.propertyName}"/>
+#end
+#end
+    </resultMap>
+
+#end
+#if(${baseColumnList})
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+    #foreach($field in ${table.commonFields})
+    ${field.columnName},
+    #end
+    ${table.fieldNames}
+    </sql>
+
+#end
+</mapper>

+ 94 - 0
coffee-codegen/src/main/resources/templates/queryDTO.java.vm

@@ -0,0 +1,94 @@
+package ${package.Controller.replace(".controller", "")}.common.dto;
+
+#foreach($pkg in ${table.importPackages})
+#if(${pkg.indexOf("baomidou")}==-1)
+import ${pkg};
+#end
+#end
+#if(${entityLombokModel})
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+#if(${chainModel})
+import lombok.experimental.Accessors;
+#end
+#end
+
+/**
+* <p>
+* $!{table.comment}
+* </p>
+*
+* @author ${author}
+* @since ${date}
+*/
+#if(${entityLombokModel})
+@Data
+#if(${chainModel})
+@Accessors(chain = true)
+#end
+#end
+public class ${entity}QueryDTO implements Serializable {
+
+#if(${entitySerialVersionUID})
+    private static final long serialVersionUID = 1L;
+#end
+## ----------BEGIN 字段循环遍历----------
+#foreach($field in ${table.fields})
+
+#if("$!field.comment" != "")
+    /**
+    * ${field.comment}
+    */
+#end
+    private ${field.propertyType} ${field.propertyName};
+#end
+## ----------END 字段循环遍历----------
+
+#if(!${entityLombokModel})
+#foreach($field in ${table.fields})
+#if(${field.propertyType.equals("boolean")})
+#set($getprefix="is")
+#else
+#set($getprefix="get")
+#end
+
+    public ${field.propertyType} ${getprefix}${field.capitalName}() {
+        return ${field.propertyName};
+    }
+
+#if(${chainModel})
+    public ${entity} set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+#else
+    public void set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+#end
+        this.${field.propertyName} = ${field.propertyName};
+#if(${chainModel})
+        return this;
+#end
+    }
+#end
+## --foreach end---
+#end
+## --end of #if(!${entityLombokModel})--
+
+#if(${entityColumnConstant})
+#foreach($field in ${table.fields})
+    public static final String ${field.name.toUpperCase()} = "${field.name}";
+
+#end
+#end
+#if(!${entityLombokModel})
+    @Override
+    public String toString() {
+        return "${entity}{" +
+#foreach($field in ${table.fields})
+#if($!{foreach.index}==0)
+                "${field.propertyName}=" + ${field.propertyName} +
+#else
+                ", ${field.propertyName}=" + ${field.propertyName} +
+#end
+#end
+        "}";
+    }
+#end
+}

+ 75 - 0
coffee-codegen/src/main/resources/templates/service.java.vm

@@ -0,0 +1,75 @@
+package ${package.Service};
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import ${superServiceClassPackage};
+import ${package.Controller.replace(".controller", "")}.common.dto.${entity}AddDTO;
+import ${package.Controller.replace(".controller", "")}.common.dto.${entity}EditDTO;
+import ${package.Controller.replace(".controller", "")}.common.dto.${entity}QueryDTO;
+import ${package.Entity}.${entity};
+
+import java.util.List;
+
+/**
+ * <p>
+ * $!{table.comment} 服务类
+ * </p>
+ *
+ * @author ${author}
+ * @since ${date}
+ */
+#if(${kotlin})
+interface ${table.serviceName} : ${superServiceClass}<${entity}>
+#else
+public interface ${table.serviceName} extends ${superServiceClass}<${entity}> {
+    
+    /**
+     * 分页查询
+     *
+     * @param reqPage
+     * @param req
+     * @return
+     */
+    IPage<${entity}> page(Page reqPage, ${entity}QueryDTO req);
+#if(${cfg.get("${table.name}exportExcel")})
+
+    /**
+     * 查询列表
+     *
+     * @param req
+     * @return
+     */
+    List<${entity}> list(${entity}QueryDTO req);
+#end
+
+    /**
+     * 新增
+     *
+     * @param req
+     */
+    void add(${entity}AddDTO req);
+    
+    /**
+     * 修改
+     *
+     * @param req
+     */
+    void edit(${entity}EditDTO req);
+    
+    /**
+     * 删除
+     *
+     * @param ids
+     */
+    void remove(String ids);
+    
+    /**
+     * 查看
+     *
+     * @param id
+     * @return
+     */
+    ${entity} view(String id);
+
+}
+#end

+ 113 - 0
coffee-codegen/src/main/resources/templates/serviceImpl.java.vm

@@ -0,0 +1,113 @@
+package ${package.ServiceImpl};
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import ${package.Controller.replace(".controller", "")}.common.dto.${entity}AddDTO;
+import ${package.Controller.replace(".controller", "")}.common.dto.${entity}EditDTO;
+import ${package.Controller.replace(".controller", "")}.common.dto.${entity}QueryDTO;
+import ${package.Entity}.${entity};
+import ${package.Mapper}.${table.mapperName};
+import ${package.Service}.${table.serviceName};
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * <p>
+ * $!{table.comment} 服务实现类
+ * </p>
+ *
+ * @author ${author}
+ * @since ${date}
+ */
+@Service
+#if(${kotlin})
+open class ${table.serviceImplName} : ${superServiceImplClass}<${table.mapperName}, ${entity}>(), ${table.serviceName} {
+
+}
+#else
+public class ${table.serviceImplName} extends ${superServiceImplClass}<${table.mapperName}, ${entity}> implements ${table.serviceName} {
+
+    @Override
+    public IPage<${entity}> page(Page reqPage, ${entity}QueryDTO req) {
+        LambdaQueryWrapper<${entity}> queryWrapper = Wrappers.lambdaQuery();
+## ----------BEGIN 字段循环遍历----------
+#foreach($field in ${table.fields})
+#if(${field.keyFlag})
+## 主键
+#if(${field.keyIdentityFlag})
+#end
+## 普通字段
+#else
+## String
+#if(${field.propertyType.equals("String")})
+        queryWrapper.like(StrUtil.isNotBlank(req.get${field.capitalName}()), ${entity}::get${field.capitalName}, req.get${field.capitalName}());
+#else
+        queryWrapper.eq(Objects.nonNull(req.get${field.capitalName}()), ${entity}::get${field.capitalName}, req.get${field.capitalName}());
+#end
+#end
+#end
+## ----------END 字段循环遍历----------
+        return this.page(reqPage, queryWrapper);
+    }
+#if(${cfg.get("${table.name}exportExcel")})
+    
+    @Override
+    public List<${entity}> list(${entity}QueryDTO req) {
+        LambdaQueryWrapper<${entity}> queryWrapper = Wrappers.lambdaQuery();
+## ----------BEGIN 字段循环遍历----------
+#foreach($field in ${table.fields})
+#if(${field.keyFlag})
+## 主键
+#if(${field.keyIdentityFlag})
+#end
+## 普通字段
+#else
+## String
+#if(${field.propertyType.equals("String")})
+        queryWrapper.like(StrUtil.isNotBlank(req.get${field.capitalName}()), ${entity}::get${field.capitalName}, req.get${field.capitalName}());
+#else
+        queryWrapper.eq(Objects.nonNull(req.get${field.capitalName}()), ${entity}::get${field.capitalName}, req.get${field.capitalName}());
+#end
+#end
+#end
+## ----------END 字段循环遍历----------
+        return this.list(queryWrapper);
+    }
+#end
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void add(${entity}AddDTO req) {
+        ${entity} entity = BeanUtil.copyProperties(req, ${entity}.class);
+        this.save(entity);
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void edit(${entity}EditDTO req) {
+        ${entity} entity = BeanUtil.copyProperties(req, ${entity}.class);
+        this.updateById(entity);
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void remove(String ids) {
+        this.removeByIds(Arrays.asList(ids.split(",")));
+    }
+    
+    @Override
+    public ${entity} view(String id) {
+        return this.getById(id);
+    }
+
+}
+#end

+ 24 - 0
coffee-codegen/src/main/resources/templates/sql.vm

@@ -0,0 +1,24 @@
+-- 菜单 SQL
+insert into sys_menu (id, menu_type, menu_name, parent_id, route_path, component, permission, icon, keepalive, link_external, visible, frame, link_url, sort, remarks, create_by, create_time, update_by, update_time)
+values('${cfg.get("${table.name}menuId")}', 'menu', '${table.comment}管理', '${cfg.get("${table.name}parentMenuId")}', '${table.entityPath}', '/modules/${package.ModuleName}/${table.entityPath}/index', null, '${cfg.get("${table.name}icon")}', '0', '1', '0', null, null, 1, null, '${cfg.get("${table.name}createBy")}', sysdate(), '${cfg.get("${table.name}updateBy")}', sysdate());
+
+-- 按钮 SQL
+insert into sys_menu (id, menu_type, menu_name, parent_id, route_path, component, permission, icon, keepalive, link_external, visible, frame, link_url, sort, remarks, create_by, create_time, update_by, update_time)
+values('${cfg.get("${table.name}pageButtonId")}', 'button', '${table.comment}查询', '${cfg.get("${table.name}menuId")}', null, null, '#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:page', null, null, null, null, null, null, 1, null, '${cfg.get("${table.name}createBy")}', sysdate(), '${cfg.get("${table.name}updateBy")}', sysdate());
+
+insert into sys_menu (id, menu_type, menu_name, parent_id, route_path, component, permission, icon, keepalive, link_external, visible, frame, link_url, sort, remarks, create_by, create_time, update_by, update_time)
+values('${cfg.get("${table.name}addButtonId")}', 'button', '${table.comment}新增', '${cfg.get("${table.name}menuId")}', null, null, '#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:add', null, null, null, null, null, null, 2, null, '${cfg.get("${table.name}createBy")}', sysdate(), '${cfg.get("${table.name}updateBy")}', sysdate());
+
+insert into sys_menu (id, menu_type, menu_name, parent_id, route_path, component, permission, icon, keepalive, link_external, visible, frame, link_url, sort, remarks, create_by, create_time, update_by, update_time)
+values('${cfg.get("${table.name}editButtonId")}', 'button', '${table.comment}修改', '${cfg.get("${table.name}menuId")}', null, null, '#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:edit', null, null, null, null, null, null, 3, null, '${cfg.get("${table.name}createBy")}', sysdate(), '${cfg.get("${table.name}updateBy")}', sysdate());
+
+insert into sys_menu (id, menu_type, menu_name, parent_id, route_path, component, permission, icon, keepalive, link_external, visible, frame, link_url, sort, remarks, create_by, create_time, update_by, update_time)
+values('${cfg.get("${table.name}removeButtonId")}', 'button', '${table.comment}删除', '${cfg.get("${table.name}menuId")}', null, null, '#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:remove', null, null, null, null, null, null, 4, null, '${cfg.get("${table.name}createBy")}', sysdate(), '${cfg.get("${table.name}updateBy")}', sysdate());
+
+insert into sys_menu (id, menu_type, menu_name, parent_id, route_path, component, permission, icon, keepalive, link_external, visible, frame, link_url, sort, remarks, create_by, create_time, update_by, update_time)
+values('${cfg.get("${table.name}viewButtonId")}', 'button', '${table.comment}查看', '${cfg.get("${table.name}menuId")}', null, null, '#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:view', null, null, null, null, null, null, 5, null, '${cfg.get("${table.name}createBy")}', sysdate(), '${cfg.get("${table.name}updateBy")}', sysdate());
+
+#if(${cfg.get("${table.name}exportExcel")})
+insert into sys_menu (id, menu_type, menu_name, parent_id, route_path, component, permission, icon, keepalive, link_external, visible, frame, link_url, sort, remarks, create_by, create_time, update_by, update_time)
+values('${cfg.get("${table.name}exportButtonId")}', 'button', '${table.comment}导出', '${cfg.get("${table.name}menuId")}', null, null, '#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:export', null, null, null, null, null, null, 6, null, '${cfg.get("${table.name}createBy")}', sysdate(), '${cfg.get("${table.name}updateBy")}', sysdate());
+#end

+ 65 - 0
coffee-codegen/src/main/resources/templates/vue/FormModal.vue.vm

@@ -0,0 +1,65 @@
+<template>
+  <BasicModal
+    v-bind="$attrs"
+    destroyOnClose
+    @register="registerModal"
+    :title="getTitle"
+    @ok="handleSubmit"
+  >
+    <BasicForm @register="registerForm" />
+  </BasicModal>
+</template>
+<script lang="ts" setup>
+  import { ref, computed, unref } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { BasicForm, useForm } from '/@/components/Form';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { dataFormSchema } from './data';
+
+  import { addObj, editObj, viewObj } from '/@/api/modules#if(${package.ModuleName})/${package.ModuleName}#end/${table.entityPath}Api';
+
+  const emit = defineEmits(['success', 'register']);
+
+  const getTitle = computed(() => (!unref(isUpdate) ? '新增#if("$!table.comment" != "")${table.comment}#end' : '编辑#if("$!table.comment" != "")${table.comment}#end'));
+  const isUpdate = ref(false);
+  const rowId = ref();
+
+  const { createMessage } = useMessage();
+  const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
+    labelWidth: 100,
+    schemas: dataFormSchema,
+    showActionButtonGroup: false,
+    actionColOptions: {
+      span: 23,
+    },
+  });
+  const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
+    await resetFields();
+    setModalProps({ confirmLoading: false });
+    isUpdate.value = !!data?.isUpdate;
+
+    if (unref(isUpdate)) {
+      rowId.value = data.record.id;
+      const resData = await viewObj({ id: data.record.id });
+      await setFieldsValue({
+        ...resData,
+      });
+    }
+  });
+
+  // 提交按钮事件
+  async function handleSubmit() {
+    try {
+      const values = await validate();
+      setModalProps({ confirmLoading: true });
+      !unref(isUpdate)
+        ? await addObj({ ...values })
+        : await editObj({ ...values, id: rowId.value });
+      !unref(isUpdate) ? createMessage.success('新增成功!') : createMessage.success('编辑成功!');
+      closeModal();
+      emit('success', { isUpdate: unref(isUpdate), values: { ...values, id: rowId.value } });
+    } finally {
+      setModalProps({ confirmLoading: false });
+    }
+  }
+</script>

+ 37 - 0
coffee-codegen/src/main/resources/templates/vue/api.ts.vm

@@ -0,0 +1,37 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+    fetchList = '#if(${package.ModuleName})/${package.ModuleName}#end/${table.entityPath}/page',
+    addObj = '#if(${package.ModuleName})/${package.ModuleName}#end/${table.entityPath}/add',
+    editObj = '#if(${package.ModuleName})/${package.ModuleName}#end/${table.entityPath}/edit',
+    removeObj = '#if(${package.ModuleName})/${package.ModuleName}#end/${table.entityPath}/remove',
+    viewObj = '#if(${package.ModuleName})/${package.ModuleName}#end/${table.entityPath}/view',
+#if(${cfg.get("${table.name}exportExcel")})
+    exportList = '#if(${package.ModuleName})/${package.ModuleName}#end/${table.entityPath}/export',
+#end
+}
+
+export function fetchList(params?: object) {
+    return defHttp.get({ url: Api.fetchList, params: params });
+}
+
+export function addObj(params?: object) {
+    return defHttp.post({ url: Api.addObj, params: params });
+}
+
+export function editObj(params?: object) {
+    return defHttp.post({ url: Api.editObj, params: params });
+}
+
+export function removeObj(params?: object) {
+    return defHttp.post({ url: Api.removeObj, params: params }, { joinParamsToUrl: true });
+}
+
+export function viewObj(params?: object) {
+    return defHttp.get({ url: Api.viewObj, params: params });
+}
+#if(${cfg.get("${table.name}exportExcel")})
+export function exportList(params?: object) {
+    return defHttp.get({ url: Api.exportList, params: params });
+}
+#end

+ 65 - 0
coffee-codegen/src/main/resources/templates/vue/data.ts.vm

@@ -0,0 +1,65 @@
+import { BasicColumn, FormSchema } from '/@/components/Table';
+
+export const columns: BasicColumn[] = [
+## ----------BEGIN 字段循环遍历----------
+#foreach($field in ${table.fields})
+#if(${field.keyFlag})
+## 主键
+#if(${field.keyIdentityFlag})
+#end
+## 普通字段
+#else
+    {
+        title: '#if("$!field.comment" != "")${field.comment}#else${field.propertyName}#end',
+        dataIndex: '${field.propertyName}',
+        width: 120,
+    },
+#end
+#end
+## ----------END 字段循环遍历----------
+];
+
+export const searchFormSchema: FormSchema[] = [
+## ----------BEGIN 字段循环遍历----------
+#foreach($field in ${table.fields})
+#if(${field.keyFlag})
+## 主键
+#if(${field.keyIdentityFlag})
+#end
+## 普通字段
+#else
+    {
+        field: '${field.propertyName}',
+        label: '#if("$!field.comment" != "")${field.comment}#else${field.propertyName}#end',
+        component: 'Input',
+        componentProps: {
+            placeholder: '请输入#if("$!field.comment" != "")${field.comment}#else${field.propertyName}#end',
+        },
+    },
+#end
+#end
+## ----------END 字段循环遍历----------
+];
+
+export const dataFormSchema: FormSchema[] = [
+## ----------BEGIN 字段循环遍历----------
+#foreach($field in ${table.fields})
+#if(${field.keyFlag})
+## 主键
+#if(${field.keyIdentityFlag})
+#end
+## 普通字段
+#else
+    {
+        field: '${field.propertyName}',
+        label: '#if("$!field.comment" != "")${field.comment}#else${field.propertyName}#end',
+        component: 'Input',
+        required: true,
+        componentProps: {
+            placeholder: '请输入#if("$!field.comment" != "")${field.comment}#else${field.propertyName}#end',
+        },
+    },
+#end
+#end
+## ----------END 字段循环遍历----------
+];

+ 139 - 0
coffee-codegen/src/main/resources/templates/vue/index.vue.vm

@@ -0,0 +1,139 @@
+<template>
+  <div>
+    <BasicTable @register="registerTable">
+      <template #toolbar>
+        <a-button
+          v-auth="['#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:add']"
+          type="primary"
+          @click="handleCreate"
+          preIcon="ant-design:plus-outlined"
+          >新增</a-button
+        >
+#if(${cfg.get("${table.name}exportExcel")})
+        <a-button
+          v-auth="['#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:export']"
+          color="warning"
+          @click="handleExport"
+          preIcon="ant-design:download-outlined"
+          >导出</a-button
+        >
+#end
+      </template>
+      <template #action="{ record }">
+        <TableAction
+          :actions="[
+            {
+              auth: '#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:edit',
+              icon: 'clarity:note-edit-line',
+              tooltip: '编辑',
+              onClick: handleEdit.bind(null, record),
+            },
+            {
+              auth: ['#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:remove'],
+              icon: 'ant-design:delete-outlined',
+              tooltip: '删除',
+              color: 'error',
+              popConfirm: {
+                title: '是否确认删除',
+                confirm: handleDelete.bind(null, record),
+              },
+            },
+          ]"
+        />
+      </template>
+    </BasicTable>
+    <FormModal @register="registerModal" @success="handleSuccess" />
+  </div>
+</template>
+<script lang="ts" setup>
+  import { ref } from 'vue';
+  import { Tag } from 'ant-design-vue';
+  import { BasicTable, useTable, TableAction } from '/@/components/Table';
+  import { useModal } from '/@/components/Modal';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import FormModal from './FormModal.vue';
+  import { columns, searchFormSchema } from './data';
+
+  import { fetchList, removeObj#if(${cfg.get("${table.name}exportExcel")}), exportList#end } from '/@/api/modules#if(${package.ModuleName})/${package.ModuleName}#end/${table.entityPath}Api';
+#if(${cfg.get("${table.name}exportExcel")})
+  import { downloadFile } from '/@/api/common';
+#end
+
+  const { createMessage, createConfirm } = useMessage();
+  const [registerModal, { openModal }] = useModal();
+  const [registerTable, { reload, getForm }] = useTable({
+    title: '#if("$!table.comment" != "")${table.comment}列表#end',
+    api: fetchList,
+    rowKey: 'id',
+    columns,
+    formConfig: {
+      labelWidth: 120,
+      schemas: searchFormSchema,
+      autoSubmitOnEnter: true,
+      resetButtonOptions: {
+        preIcon: 'ant-design:delete-outlined',
+      },
+      submitButtonOptions: {
+        preIcon: 'ant-design:search-outlined',
+      },
+    },
+    showIndexColumn: false,
+    useSearchForm: true,
+    showTableSetting: true,
+    bordered: true,
+    actionColumn: {
+      auth: ['#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:edit', '#if(${package.ModuleName})${package.ModuleName}:#end${table.entityPath}:remove'],
+      width: 80,
+      title: '操作',
+      dataIndex: 'action',
+      slots: { customRender: 'action' },
+    },
+  });
+
+  // 新增按钮事件
+  function handleCreate() {
+    openModal(true, {
+      isUpdate: false,
+    });
+  }
+
+  // 编辑按钮事件
+  function handleEdit(record: Recordable) {
+    console.log(record);
+    openModal(true, {
+      record,
+      isUpdate: true,
+    });
+  }
+
+  // 删除按钮事件
+  async function handleDelete(record: Recordable) {
+    console.log(record);
+    await removeObj({ ids: record.id });
+    createMessage.success('删除成功!');
+    await reload();
+  }
+#if(${cfg.get("${table.name}exportExcel")})
+
+  // 导出按钮事件
+  async function handleExport() {
+    createConfirm({
+      iconType: 'warning',
+      title: '提示',
+      content: '确认导出?',
+      onOk: async () => {
+        const params = getForm().getFieldsValue();
+        const filepath = await exportList(params);
+        downloadFile(filepath);
+      },
+    });
+  }
+#end
+
+  // 弹窗回调事件
+  async function handleSuccess({ isUpdate, values }) {
+    console.log(isUpdate);
+    console.log(values);
+    await reload();
+  }
+</script>

+ 96 - 0
coffee-common/pom.xml

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>coffee-boot</artifactId>
+        <groupId>com.coffee</groupId>
+        <version>1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>coffee-common</artifactId>
+
+    <dependencies>
+        <!-- web 模块 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>spring-boot-starter-tomcat</artifactId>
+                    <groupId>org.springframework.boot</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- sa-token -->
+        <dependency>
+            <groupId>cn.dev33</groupId>
+            <artifactId>sa-token-spring-boot-starter</artifactId>
+        </dependency>
+        <!-- sa-token redis -->
+        <dependency>
+            <groupId>cn.dev33</groupId>
+            <artifactId>sa-token-dao-redis-jackson</artifactId>
+        </dependency>
+        <!--redis依赖 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-boot-starter</artifactId>
+        </dependency>
+        <!-- pool 对象池 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+        <!-- mybatis plus -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+        <!-- hutool -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+        <!-- guava -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+        <!-- fastjson -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+        </dependency>
+        <!-- servlet -->
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+        </dependency>
+        <!-- hibernate validator -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <!--  自动生成YML配置关联JSON文件  -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+        </dependency>
+        <!-- easyexcel -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+        </dependency>
+        <!-- minio -->
+        <dependency>
+            <groupId>io.minio</groupId>
+            <artifactId>minio</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 46 - 0
coffee-common/src/main/java/com/coffee/common/Constants.java

@@ -0,0 +1,46 @@
+package com.coffee.common;
+
+import com.coffee.common.config.AppConfig;
+
+/**
+ * 常量类
+ *
+ * @author Kevin
+ */
+public class Constants {
+
+    /**
+     * header token
+     */
+    public static final String HEADER_TOKEN = "Authorization";
+
+    /**
+     * sa token, token session, user key
+     */
+    public static final String LOGIN_USER_KEY = "user";
+
+    /**
+     * redis缓存,项目统一前缀
+     */
+    public static final String REDIS_PREFIX = AppConfig.getCachePrefix() + ":";
+    /**
+     * redis缓存,系统字典缓存模板
+     */
+    public static final String SYS_DICT_TPL = REDIS_PREFIX + "sys_dict:%s";
+    /**
+     * redis缓存,系统参数缓存模板
+     */
+    public static final String SYS_CONFIG_TPL = REDIS_PREFIX + "sys_config:%s";
+
+    /**
+     * 所有权限常量
+     */
+    public static final String ALL_PERMISSION = "*:*:*";
+
+    /**
+     * 默认短信验证码
+     */
+    public static final String DEFAULT_SMS_CODE = "000000";
+
+
+}

+ 16 - 0
coffee-common/src/main/java/com/coffee/common/MenuConstants.java

@@ -0,0 +1,16 @@
+package com.coffee.common;
+
+/**
+ * 菜单常量类
+ *
+ * @author Kevin
+ */
+public class MenuConstants {
+
+    public static final String LAYOUT = "LAYOUT";
+
+    public static final String IFRAME = "IFrame";
+
+    public static final String GET_PARENT_LAYOUT = "getParentLayout('%s')";
+
+}

+ 37 - 0
coffee-common/src/main/java/com/coffee/common/annotation/DataScope.java

@@ -0,0 +1,37 @@
+package com.coffee.common.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据权限注解
+ *
+ * @author Kevin
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface DataScope {
+
+    /**
+     * 表别名
+     *
+     * @return
+     */
+    String tableAlias() default "t";
+
+    /**
+     * 部门字段名
+     *
+     * @return
+     */
+    String deptColumnName() default "dept_id";
+
+    /**
+     * 用户字段名
+     *
+     * @return
+     */
+    String userColumnName() default "create_by";
+
+}

+ 22 - 0
coffee-common/src/main/java/com/coffee/common/annotation/DataSource.java

@@ -0,0 +1,22 @@
+package com.coffee.common.annotation;
+
+import com.coffee.common.enums.DataSourceTypeEnum;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义多数据源切换注解
+ * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
+ *
+ * @author Kevin
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface DataSource {
+    /**
+     * 切换数据源名称
+     */
+    DataSourceTypeEnum value() default DataSourceTypeEnum.MASTER;
+}

+ 22 - 0
coffee-common/src/main/java/com/coffee/common/annotation/ExcelDict.java

@@ -0,0 +1,22 @@
+package com.coffee.common.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据字典注解
+ *
+ * @author Kevin
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelDict {
+
+    /**
+     * 字典编码
+     *
+     * @return
+     */
+    String value() default "";
+
+}

+ 18 - 0
coffee-common/src/main/java/com/coffee/common/annotation/IgnoreToken.java

@@ -0,0 +1,18 @@
+package com.coffee.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * 忽略token的注解
+ *
+ * @author Kevin
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RUNTIME)
+public @interface IgnoreToken {
+
+}

+ 23 - 0
coffee-common/src/main/java/com/coffee/common/annotation/Log.java

@@ -0,0 +1,23 @@
+package com.coffee.common.annotation;
+
+import com.coffee.common.enums.UserPlatformEnum;
+
+import java.lang.annotation.*;
+
+/**
+ * 日志注解
+ *
+ * @author Kevin
+ */
+@Target({ElementType.PARAMETER, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Log {
+    String title() default "";
+
+    /**
+     * 用户平台
+     */
+    UserPlatformEnum userPlatform() default UserPlatformEnum.WEB;
+
+}

+ 15 - 0
coffee-common/src/main/java/com/coffee/common/bo/DictModel.java

@@ -0,0 +1,15 @@
+package com.coffee.common.bo;
+
+import lombok.Data;
+
+/**
+ * 字典模型
+ *
+ * @author Kevin
+ */
+@Data
+public class DictModel {
+
+    String label;
+    String value;
+}

+ 77 - 0
coffee-common/src/main/java/com/coffee/common/bo/LoginUser.java

@@ -0,0 +1,77 @@
+package com.coffee.common.bo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Set;
+
+/**
+ * 登录用户
+ *
+ * @author Kevin
+ */
+@Data
+public class LoginUser implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * token
+     */
+    private String token;
+
+    /**
+     * 用户平台
+     */
+    private String userPlatform;
+
+    /**
+     * 授权类型
+     */
+    private String grantType;
+
+    /**
+     * 系统用户
+     */
+    private SysUserBO sysUser;
+
+    /**
+     * 用户名
+     */
+    private String username;
+
+    /**
+     * 登录时间
+     */
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date loginTime;
+
+    /**
+     * 登录IP地址
+     */
+    private String ipAddress;
+
+    /**
+     * 登录地点
+     */
+    private String loginLocation;
+
+    /**
+     * 浏览器类型
+     */
+    private String browser;
+
+    /**
+     * 操作系统
+     */
+    private String os;
+
+    /**
+     * 权限列表
+     */
+    private Set<String> permissions;
+
+}

+ 40 - 0
coffee-common/src/main/java/com/coffee/common/bo/SysRoleBO.java

@@ -0,0 +1,40 @@
+package com.coffee.common.bo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 角色
+ *
+ * @author Kevin
+ */
+@Data
+public class SysRoleBO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /**
+     * 角色编码
+     */
+    private String roleCode;
+
+    /**
+     * 角色名称
+     */
+    private String roleName;
+
+    /**
+     * 数据范围 1全部数据权限;2自定数据权限;3本部门数据权限;4本部门及以下数据权限
+     */
+    private String dataScope;
+
+}

+ 106 - 0
coffee-common/src/main/java/com/coffee/common/bo/SysUserBO.java

@@ -0,0 +1,106 @@
+package com.coffee.common.bo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 用户
+ *
+ * @author Kevin
+ */
+@Data
+public class SysUserBO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /**
+     * 账号
+     */
+    private String account;
+
+    /**
+     * 昵称
+     */
+    private String nickname;
+
+    /**
+     * 姓名
+     */
+    private String realname;
+
+    /**
+     * 英文名
+     */
+    private String englishName;
+
+    /**
+     * 头像
+     */
+    private String avatar;
+
+    /**
+     * 邮箱
+     */
+    private String email;
+
+    /**
+     * 手机号
+     */
+    private String phone;
+
+    /**
+     * 工号
+     */
+    private String staffNumber;
+
+    /**
+     * 生日
+     */
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
+    private Date birthday;
+
+    /**
+     * 性别 1男;2女;3未知
+     */
+    private String sex;
+
+    /**
+     * 部门ID
+     */
+    private String deptId;
+
+    /**
+     * 锁定标记 0正常;1锁定
+     */
+    private String lockFlag;
+
+    /**
+     * 排序
+     */
+    private Integer sort;
+
+    /**
+     * 备注
+     */
+    private String remarks;
+
+    private String status;
+
+    /**
+     * 角色列表
+     */
+    private List<SysRoleBO> roles;
+
+}

+ 77 - 0
coffee-common/src/main/java/com/coffee/common/config/AppConfig.java

@@ -0,0 +1,77 @@
+package com.coffee.common.config;
+
+import lombok.Getter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 读取项目相关配置
+ *
+ * @author Kevin
+ */
+@Component
+@ConfigurationProperties(prefix = "app")
+@Getter
+public class AppConfig {
+
+    /**
+     * 项目名称
+     */
+    @Getter
+    private static String name;
+
+    /**
+     * 实例演示开关
+     */
+    @Getter
+    private static boolean demoEnabled = false;
+
+    /**
+     * 获取地址开关
+     */
+    @Getter
+    private static boolean addressEnabled = false;
+
+    /**
+     * 上传目录
+     */
+    @Getter
+    private static String uploadType;
+
+    /**
+     * 上传目录
+     */
+    @Getter
+    private static String uploadDir;
+
+    /**
+     * 缓存前缀
+     */
+    @Getter
+    private static String cachePrefix;
+
+    public void setName(String name) {
+        AppConfig.name = name;
+    }
+
+    public void setDemoEnabled(boolean demoEnabled) {
+        AppConfig.demoEnabled = demoEnabled;
+    }
+
+    public void setAddressEnabled(boolean addressEnabled) {
+        AppConfig.addressEnabled = addressEnabled;
+    }
+
+    public void setUploadType(String uploadType) {
+        AppConfig.uploadType = uploadType;
+    }
+
+    public void setUploadDir(String uploadDir) {
+        AppConfig.uploadDir = uploadDir;
+    }
+
+    public void setCachePrefix(String cachePrefix) {
+        AppConfig.cachePrefix = cachePrefix;
+    }
+
+}

+ 25 - 0
coffee-common/src/main/java/com/coffee/common/config/MinioConfig.java

@@ -0,0 +1,25 @@
+package com.coffee.common.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 读取Minio相关配置
+ *
+ * @author Kevin
+ */
+@Component
+@ConfigurationProperties(prefix = "minio")
+@Data
+public class MinioConfig {
+
+    private String endpoint;
+
+    private String accessKey;
+
+    private String secretKey;
+
+    private String bucketName;
+
+}

+ 49 - 0
coffee-common/src/main/java/com/coffee/common/convert/ExcelDictConverter.java

@@ -0,0 +1,49 @@
+package com.coffee.common.convert;
+
+import cn.hutool.core.annotation.AnnotationUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import com.coffee.common.annotation.ExcelDict;
+import com.coffee.common.util.DictUtil;
+
+import java.lang.reflect.Field;
+
+/**
+ * Excel数据字典转换器
+ *
+ * @author Kevin
+ */
+public class ExcelDictConverter implements Converter<String> {
+
+    @Override
+    public Class supportJavaTypeKey() {
+        return String.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+
+    @Override
+    public String convertToJavaData(ReadCellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
+        Field field = excelContentProperty.getField();
+        String dictCode = AnnotationUtil.getAnnotationValue(field, ExcelDict.class);
+        String dictValue = DictUtil.getDictValue(dictCode, cellData.getStringValue());
+        return StrUtil.isBlank(dictValue) ? cellData.getStringValue() : dictValue;
+    }
+
+    @Override
+    public WriteCellData convertToExcelData(String s, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
+        Field field = excelContentProperty.getField();
+        String dictCode = AnnotationUtil.getAnnotationValue(field, ExcelDict.class);
+        String dictLabel = DictUtil.getDictLabel(dictCode, s);
+        return new WriteCellData(StrUtil.isBlank(dictLabel) ? s : dictLabel);
+    }
+
+}

+ 214 - 0
coffee-common/src/main/java/com/coffee/common/crud/BaseService.java

@@ -0,0 +1,214 @@
+package com.coffee.common.crud;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.enums.SqlMethod;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.TableInfo;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.baomidou.mybatisplus.core.toolkit.Assert;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
+import com.coffee.common.entity.QueryParamEntity;
+import com.coffee.common.entity.param.Term;
+import org.apache.ibatis.binding.MapperMethod;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * @Author lifang
+ * @Date 10:02 2022/3/14
+ * @Description  PK为主键类型
+ * @Param
+ * @return
+ **/
+
+public abstract class BaseService<M extends BaseMapper<E>, E,PK extends Serializable>  extends ServiceImpl<M, E> {
+    /**
+     *  当使用缓存时使用该属性表示缓存名称
+     **/
+    private final String cacheBucket;
+
+
+    public String getCacheBucketName(){
+        return cacheBucket;
+    }
+
+    public BaseService() {
+        this.cacheBucket = this.getClass().getSimpleName();
+    }
+
+    public BaseService(String cacheBucket) {
+        this.cacheBucket = cacheBucket;
+    }
+
+    @Override
+    public boolean saveBatch(Collection<E> entityList, int batchSize) {
+        entityList.forEach(this::validateBeforeSave);
+        return super.saveBatch(entityList, batchSize);
+    }
+
+    /**
+     * @Author lifang
+     * @Date 10:05 2022/3/14
+     * @Description 在使用时请尽量将update操作和save操作分开
+     * @Param [entity]
+     * @return boolean
+     **/
+
+    @Override
+    @Deprecated
+    public boolean saveOrUpdate(E entity) {
+        if (null != entity) {
+            TableInfo tableInfo = TableInfoHelper.getTableInfo(this.entityClass);
+            Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!");
+            String keyProperty = tableInfo.getKeyProperty();
+            Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!");
+            Object idVal = tableInfo.getPropertyValue(entity, tableInfo.getKeyProperty());
+            return StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal)) ? save(entity) : updateById(entity);
+        }
+        return false;
+    }
+
+    /**
+     * @Author lifang
+     * @Date 10:05 2022/3/14
+     * @Description 在使用时请尽量将update操作和save操作分开
+     * @Param [entity]
+     * @return boolean
+     **/
+    @Override
+    @Deprecated
+    public boolean saveOrUpdateBatch(Collection<E> entityList, int batchSize) {
+        TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
+        Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!");
+        String keyProperty = tableInfo.getKeyProperty();
+        Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!");
+        return SqlHelper.saveOrUpdateBatch(this.entityClass, this.mapperClass, this.log, entityList, batchSize, (sqlSession, entity) -> {
+            Object idVal = tableInfo.getPropertyValue(entity, keyProperty);
+            validateBeforeSave(entity);
+            return StringUtils.checkValNull(idVal)
+                    || CollectionUtils.isEmpty(sqlSession.selectList(getSqlStatement(SqlMethod.SELECT_BY_ID), entity));
+        }, (sqlSession, entity) -> {
+            MapperMethod.ParamMap<E> param = new MapperMethod.ParamMap<>();
+            validateBeforeUpdate(entity);
+            param.put(Constants.ENTITY, entity);
+            sqlSession.update(getSqlStatement(SqlMethod.UPDATE_BY_ID), param);
+        });
+    }
+
+    @Override
+    public boolean removeById(Serializable id) {
+        validateBeforeDelete((PK) id);
+        return super.removeById(id);
+    }
+
+    @Override
+    public boolean removeByIds(Collection<?> list) {
+        list.stream().map(id->(PK)id).forEach(this::validateBeforeDelete);
+        return super.removeByIds(list);
+    }
+
+
+    @Override
+    public boolean removeById(Serializable id, boolean useFill) {
+        this.validateBeforeDelete((PK) id);
+        return super.removeById(id, useFill);
+    }
+
+    @Override
+    public boolean removeBatchByIds(Collection<?> list, int batchSize) {
+        list.stream().map(id->(PK)id).forEach(this::validateBeforeDelete);
+        return super.removeBatchByIds(list, batchSize);
+    }
+
+    @Override
+    public boolean removeBatchByIds(Collection<?> list, int batchSize, boolean useFill) {
+        list.stream().map(id->(PK)id).forEach(this::validateBeforeDelete);
+        return super.removeBatchByIds(list, batchSize, useFill);
+    }
+
+    @Override
+    public boolean updateById(E entity) {
+        validateBeforeUpdate(entity);
+        return super.updateById(entity);
+    }
+
+    @Override
+    public boolean updateBatchById(Collection<E> entityList) {
+        entityList.forEach(this::validateBeforeUpdate);
+        return false;
+    }
+
+    public Page<E> list(QueryParamEntity<E> param) {
+        //是否为分页查询
+        if(ObjectUtil.isNotNull(param.getPage())){
+            return this.page(param.getPage(),build(param));
+        }else {
+            List<E> list = list(build(param));
+            Page<E> page = Page.of(0,1,CollectionUtil.isEmpty(list)?0:list.size());
+            page.setRecords(list);
+            return page;
+        }
+    }
+
+
+    public long count(QueryParamEntity<E> param) {
+        return this.count(build(param));
+    }
+
+
+    public QueryWrapper<E> build(QueryParamEntity<?> param){
+        QueryWrapper<E> queryWrapper = new QueryWrapper<>();
+        Set<String> includes = param.getIncludes();
+        Set<Term> wheres = param.getWheres();
+        if(CollectionUtil.isNotEmpty(includes)){
+            queryWrapper.select(includes.toArray(new String[includes.size()+1]));
+        }
+        if (CollectionUtil.isNotEmpty(wheres)) {
+            wheres.forEach(term -> term.getType().build(queryWrapper,term));
+        }
+        return queryWrapper;
+    }
+
+
+    /**
+     * @Author lifang
+     * @Date 10:00 2022/3/14
+     * @Description 保存数据前进行验证
+     * @Param [entity]
+     * @return void
+     **/
+    public abstract void validateBeforeSave(E entity) ;
+
+    /**
+     * @Author lifang
+     * @Date 10:00 2022/3/14
+     * @Description 更新数据前进行验证
+     * @Param [entity]
+     * @return void
+     **/
+   public abstract void validateBeforeUpdate(E entity) ;
+
+
+    /**
+     * @Author lifang
+     * @Date 10:02 2022/3/14
+     * @Description 删除数据前进行验证
+     * @Param [id]
+     * @return void
+     **/
+
+    public  abstract void validateBeforeDelete(PK id) ;
+
+
+}

+ 38 - 0
coffee-common/src/main/java/com/coffee/common/crud/controller/BaseCrudController.java

@@ -0,0 +1,38 @@
+package com.coffee.common.crud.controller;
+
+import cn.dev33.satoken.SaManager;
+import cn.dev33.satoken.stp.StpLogic;
+import java.io.Serializable;
+
+/**
+ * @Author lifang
+ * @Date 14:34 2022/3/14
+ * @Description
+ * @See com.coffee.framework.test.controller.TestController
+ * @Param
+ * @return
+ **/
+
+public abstract class BaseCrudController<E, K extends Serializable> implements
+        BaseSaveController<E, K>,
+        BaseQueryController<E, K>,
+        BaseDeleteController<E, K>{
+
+
+    /**
+     * @Author lifang
+     * @Date 14:36 2022/3/14
+     * @Description 基本增删改查接口权限,权限默认为getPermissionPrefix()+"add/edit/save/query" ,为空时不检查权限
+     * @Param []
+     * @return java.lang.String
+     **/
+    @Override
+    public String getPermissionPrefix() {
+        return null;
+    }
+
+    @Override
+    public StpLogic getStpLogin() {
+        return SaManager.getStpLogic("");
+    }
+}

+ 57 - 0
coffee-common/src/main/java/com/coffee/common/crud/controller/BaseCurdAuthController.java

@@ -0,0 +1,57 @@
+package com.coffee.common.crud.controller;
+
+import cn.dev33.satoken.stp.StpLogic;
+import cn.hutool.core.util.StrUtil;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName BaseAuthController.java
+ * @Description 基本的权限识别接口
+ * @createTime 2022年03月14日 14:17:00
+ */
+public interface BaseCurdAuthController {
+    /**
+     * @Author lifang
+     * @Date 14:17 2022/3/14
+     * @Description 权限识别前缀
+     * @Param []
+     * @return java.lang.String
+     **/
+    String getPermissionPrefix();
+
+    StpLogic getStpLogin();
+
+    default void saveAuth(){
+        if(isAuth()){
+            List<String> savePermission = Arrays.asList(getPermissionPrefix() + ":save", getPermissionPrefix() + ":all");
+            getStpLogin().checkPermissionOr(savePermission.toArray(new String[2]));
+        }
+    }
+
+    default void deleteAuth(){
+        if(isAuth()){
+            List<String> savePermission = Arrays.asList(getPermissionPrefix() + ":delete", getPermissionPrefix() + ":all");
+            getStpLogin().checkPermissionOr(savePermission.toArray(new String[2]));
+        }
+    }
+
+    default void editAuth(){
+        if(isAuth()){
+            List<String> savePermission = Arrays.asList(getPermissionPrefix() + ":edit", getPermissionPrefix() + ":all");
+            getStpLogin().checkPermissionOr(savePermission.toArray(new String[2]));
+        }
+    }
+
+    default void queryAuth(){
+        if(isAuth()){
+            List<String> savePermission = Arrays.asList(getPermissionPrefix() + ":query", getPermissionPrefix() + ":all");
+            getStpLogin().checkPermissionOr(savePermission.toArray(new String[2]));
+        }
+    }
+    default boolean isAuth(){
+        return StrUtil.isEmpty(getPermissionPrefix());
+    }
+}

+ 59 - 0
coffee-common/src/main/java/com/coffee/common/crud/controller/BaseDeleteController.java

@@ -0,0 +1,59 @@
+package com.coffee.common.crud.controller;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.mapper.Mapper;
+import com.coffee.common.crud.BaseService;
+import com.coffee.common.result.R;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.io.Serializable;
+import java.util.*;
+/**
+ * @Author lifang
+ * @Date 14:29 2022/3/12
+ * @Description 统一删除接口
+ **/
+
+public interface BaseDeleteController<E, K extends Serializable> extends BaseCurdAuthController{
+    /**
+     * 获取服务类
+     */
+    BaseService<? extends Mapper<E>,E,K> getService();
+
+    @DeleteMapping("/{id:.+}")
+    @Operation(summary = "根据ID删除")
+    default R delete(@PathVariable K id) {
+        deleteAuth();
+        if(StrUtil.isNullOrUndefined(String.valueOf(id))){
+            return R.success();
+        }
+        String key=String.valueOf(id);
+        return getService()
+                .removeById(key)? R.success():R.fail("删除失败");
+    }
+
+    /**
+     * @Author lifang
+     * @Date 14:31 2022/3/12
+     * @Description 此处Delete接收消息体数组略显麻烦,直接使用Post请求进行接收
+     * @Param [ids]
+     * @return com.coffee.common.result.R
+     **/
+
+    @PostMapping("/delete/_batch")
+    @Operation(summary = "根据ID删除")
+    default R delete(@RequestBody List<String> ids) {
+        deleteAuth();
+        if(CollectionUtil.isEmpty(ids)){
+            return R.success();
+        }
+        return getService()
+                .removeBatchByIds(ids)? R.success():R.fail("删除失败");
+    }
+
+}

+ 60 - 0
coffee-common/src/main/java/com/coffee/common/crud/controller/BaseQueryController.java

@@ -0,0 +1,60 @@
+package com.coffee.common.crud.controller;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.Mapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.coffee.common.crud.BaseService;
+import com.coffee.common.entity.QueryParamEntity;
+import com.coffee.common.result.R;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.io.Serializable;
+
+public interface BaseQueryController<E, K extends Serializable> extends
+        BaseCurdAuthController{
+
+
+    BaseService<? extends Mapper<E>,E,K> getService();
+    /**
+     * POST方式分页查询
+     *
+     *
+     * @param query 查询条件
+     * @return 分页查询结果
+     * @see
+     */
+    @PostMapping("/_query")
+    @Operation(summary = "使用POST方式分页动态查询")
+    default R<IPage<E>> queryPager(@Parameter(hidden = true)@RequestBody QueryParamEntity<E> query) {
+        queryAuth();
+        return R.success(this.getService().list(query));
+
+    }
+
+    @PostMapping("/_count")
+    @Operation(summary = "使用POST方式查询总数")
+    default R<Long> count(@Parameter(hidden = true) @RequestBody QueryParamEntity<E> query) {
+        queryAuth();
+        return R.success(this.getService().count(query));
+    }
+
+
+    @GetMapping("/{id:.+}")
+    @Operation(summary = "根据ID查询")
+    default R<E> getById(@PathVariable K id) {
+        queryAuth();
+        String key=String.valueOf(id);
+        if(StrUtil.isNullOrUndefined(key)){
+            return R.fail("查询结果不存在");
+        }
+        return R.success(getService().getById(key));
+    }
+
+}

+ 90 - 0
coffee-common/src/main/java/com/coffee/common/crud/controller/BaseSaveController.java

@@ -0,0 +1,90 @@
+package com.coffee.common.crud.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.mybatisplus.core.mapper.Mapper;
+import com.coffee.common.crud.BaseService;
+import com.coffee.common.entity.RecordCreationEntity;
+import com.coffee.common.entity.RecordModifierEntity;
+import com.coffee.common.result.R;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.Serializable;
+import java.util.*;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName ServiceSaveController.java
+ * @Description TODO
+ * @createTime 2022年03月12日 11:25:00
+ */
+public interface BaseSaveController<E,K extends Serializable> extends
+        BaseCurdAuthController{
+    /**
+     * 获取服务类
+     */
+    BaseService<? extends Mapper<E>,E,K> getService();
+
+
+
+    default E applyCreationEntity(E entity) {
+        RecordCreationEntity creationEntity = ((RecordCreationEntity) entity);
+        creationEntity.setCreateTimeNow();
+        try {
+            creationEntity.setCreateBy(StpUtil.getLoginIdAsString());
+        }catch (Exception e){
+            creationEntity.setCreateBy("-1");
+        }
+        return entity;
+    }
+
+    default E applyModifierEntity( E entity) {
+        RecordModifierEntity modifierEntity = ((RecordModifierEntity) entity);
+        modifierEntity.setModifyTimeNow();
+        try {
+            modifierEntity.setUpdateBy(StpUtil.getLoginIdAsString());
+        }catch (Exception e){
+            modifierEntity.setUpdateBy("-1");
+        }
+
+        return entity;
+    }
+
+    default E applyAuthentication(E entity) {
+        if (entity instanceof RecordCreationEntity) {
+            entity = applyCreationEntity(entity);
+        }
+        if (entity instanceof RecordModifierEntity) {
+            entity = applyModifierEntity(entity);
+        }
+        return entity;
+    }
+
+    @PostMapping("/_batch")
+    @Operation(summary = "批量新增数据")
+    default R add(@RequestBody List<E> payload) {
+        saveAuth();
+        payload.forEach(s->{
+            applyCreationEntity(s);
+        });
+        return getService().saveBatch(payload)?R.success():R.fail("数据新增失败");
+    }
+
+    @PostMapping
+    @Operation(summary = "新增单个数据,并返回新增后的数据.")
+    default R add(@RequestBody E payload) {
+        saveAuth();
+        applyCreationEntity(payload);
+        return getService().save(payload)?R.success(payload):R.fail("数据新增失败");
+    }
+
+
+    @PutMapping
+    @Operation(summary = "根据ID修改数据")
+    default R update(@RequestBody E payload) {
+        editAuth();
+        applyModifierEntity(payload);
+        return getService().updateById(payload)?R.success():R.fail("更新失败");
+    }
+}

+ 24 - 0
coffee-common/src/main/java/com/coffee/common/dto/LoginDTO.java

@@ -0,0 +1,24 @@
+package com.coffee.common.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 登录实体类
+ *
+ * @author Kevin
+ */
+@Data
+public class LoginDTO {
+
+    @NotBlank(message = "授权类型不能为空")
+    String grantType;
+
+    String username;
+    String password;
+
+    String mobile;
+    String code;
+
+}

+ 30 - 0
coffee-common/src/main/java/com/coffee/common/entity/Entity.java

@@ -0,0 +1,30 @@
+package com.coffee.common.entity;
+
+import cn.hutool.core.bean.BeanUtil;
+
+import java.io.Serializable;
+import java.util.function.Predicate;
+
+
+public interface Entity extends Serializable {
+    long serialVersionUID = 1L;
+
+    default boolean tryValidate(Predicate<? super Entity> predicate) {
+        return predicate.test(this);
+    }
+
+    default <T> T copyTo(Class<T> target, String... ignoreProperties) {
+        return BeanUtil.copyProperties(this, target, ignoreProperties);
+    }
+
+    default <T> T copyTo(T target, String... ignoreProperties) {
+        BeanUtil.copyProperties(this, target, ignoreProperties);
+        return target;
+    }
+
+
+    default Entity copyFrom(Object target, String... ignoreProperties) {
+        BeanUtil.copyProperties(target, this, ignoreProperties);
+        return this;
+    }
+}

+ 13 - 0
coffee-common/src/main/java/com/coffee/common/entity/GenericEntity.java

@@ -0,0 +1,13 @@
+package com.coffee.common.entity;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+
+public abstract class  GenericEntity<PK> implements Entity {
+    @TableId
+    @Schema(description = "id")
+    @Getter
+    private PK id;
+}

+ 64 - 0
coffee-common/src/main/java/com/coffee/common/entity/QueryParamEntity.java

@@ -0,0 +1,64 @@
+package com.coffee.common.entity;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.coffee.common.entity.param.Term;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import java.util.Set;
+
+/**
+ * @Author lifang
+ * @Date 14:50 2022/3/12
+ * @Description 仅用来进行简单的查询,若需要构建复杂查询语句,请直接使用mp函数方法进行构建
+ **/
+@Data
+public class QueryParamEntity<T> {
+
+    private static final long serialVersionUID = 8097500947924037523L;
+
+    @Schema(description = "指定要查询的列")
+    private Set<String> includes;
+
+//    @Schema(description = "指定不查询的列")
+//    private Set<String> excludes;
+
+    @Schema(description = "where条件表达式,与terms参数不能共存.语法: name = 张三 and age > 16")
+    private Set<Term> wheres;
+
+    @Schema(description = "分页查询")
+    private Page<T> page;
+
+    /**
+     * @Author lifang
+     * @Date 14:59 2022/3/12
+     * @Description 默认分页
+     * @Param []
+     * @return com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<T>
+     **/
+    public QueryWrapper<T> build(){
+        return build(ObjectUtil.isNotNull(page));
+    }
+
+    public QueryWrapper<T> build(boolean page){
+        QueryWrapper<T> queryWrapper = new QueryWrapper<>();
+        buildIncludes(queryWrapper);
+        buildWhere(queryWrapper);
+        return queryWrapper;
+    }
+
+    private void buildWhere(QueryWrapper<T> queryWrapper){
+        if (CollectionUtil.isNotEmpty(wheres)) {
+            wheres.forEach(term -> term.getType().build(queryWrapper,term));
+        }
+    }
+
+    private void buildIncludes(QueryWrapper<T> queryWrapper){
+        if(CollectionUtil.isNotEmpty(includes)){
+            queryWrapper.select(includes.toArray(new String[includes.size()+1]));
+        }
+    }
+
+}

+ 29 - 0
coffee-common/src/main/java/com/coffee/common/entity/RecordCreationEntity.java

@@ -0,0 +1,29 @@
+package com.coffee.common.entity;
+
+import cn.hutool.core.date.DateUtil;
+import com.alibaba.fastjson.annotation.JSONField;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import java.util.Date;
+
+/**
+ * @Author lifang
+ * @Date 13:54 2022/3/12
+ * @Description 创建字段
+ **/
+
+public interface RecordCreationEntity extends Entity {
+
+    String getCreateBy();
+
+    void setCreateBy(String createBy);
+
+    Date getCreateTime();
+
+    void setCreateTime(Date createTime);
+
+
+    default void setCreateTimeNow() {
+        setCreateTime(new Date());
+    }
+}

+ 32 - 0
coffee-common/src/main/java/com/coffee/common/entity/RecordModifierEntity.java

@@ -0,0 +1,32 @@
+package com.coffee.common.entity;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import java.util.Date;
+
+
+/**
+ * @Author lifang
+ * @Date 13:54 2022/3/12
+ * @Description 修改字段
+ **/
+
+public interface RecordModifierEntity extends Entity {
+
+    String getUpdateBy();
+
+    void setUpdateBy(String updateBy);
+
+    Date getUpdateTime();
+
+    void setUpdateTime(Date updateTime);
+
+
+
+
+    default void setModifyTimeNow() {
+        setUpdateTime(new Date());
+    }
+
+}

+ 70 - 0
coffee-common/src/main/java/com/coffee/common/entity/param/Term.java

@@ -0,0 +1,70 @@
+package com.coffee.common.entity.param;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.SneakyThrows;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 执行条件
+ */
+@Getter
+@Setter
+public class Term implements Cloneable, Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * 字段
+     */
+    @Schema(description = "字段名")
+    private String column;
+
+    /**
+     * 单值
+     */
+    @Schema(description = "单条件")
+    private Object value;
+
+    /**
+     * 多值 即用于in ,between
+     */
+    @Schema(description = "多条件")
+    private List<Object> values;
+    /**
+     * 条件类型
+     */
+    @Schema(description = "动态条件类型",defaultValue = "eq")
+    private TermType termType = TermType.eq;
+
+    private Type type =Type.and;
+
+
+    public enum Type {
+        or{
+            @Override
+            public void build(QueryWrapper<?> queryWrapper, Term term) {
+                term.getTermType().build(queryWrapper.or(),term.getColumn(),term.getValue());
+            }
+        }
+        ,
+        and{
+            @Override
+            public void build(QueryWrapper<?> queryWrapper,Term term){
+                term.getTermType().build(queryWrapper,term.getColumn(),term.getValue());
+            };
+        };
+
+
+        public void build(QueryWrapper<?> queryWrapper,Term term) {
+        }
+    }
+
+}

+ 129 - 0
coffee-common/src/main/java/com/coffee/common/entity/param/TermType.java

@@ -0,0 +1,129 @@
+package com.coffee.common.entity.param;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import java.util.*;
+/**
+ * @Author lifang
+ * @Date 15:10 2022/3/12
+ * @Description 提供默认支持的查询条件类型,用于动态指定查询条件
+ **/
+
+public enum  TermType {
+    eq
+            {
+                @Override
+                public void build(QueryWrapper<?> queryWrapper,String column, Object value) {
+                    queryWrapper.eq(column,value);
+                }
+            }
+    ,
+    neq
+            {
+                @Override
+                public void build(QueryWrapper<?> queryWrapper,String column, Object value) {
+                    queryWrapper.ne(column,value);
+                }
+            },
+    like
+            {
+                @Override
+                public void build(QueryWrapper<?> queryWrapper,String column, Object value) {
+                    value="%"+value+"%";
+                    queryWrapper.like(column,value);
+                }
+            },
+    llike
+            {
+                @Override
+                public void build(QueryWrapper<?> queryWrapper,String column, Object value) {
+                    value="%"+value;
+                    queryWrapper.eq(column,value);
+                }
+            },
+    rlike
+            {
+                @Override
+                public void build(QueryWrapper<?> queryWrapper,String column, Object value) {
+                    value=value+"%";
+                    queryWrapper.eq(column,value);
+                }
+            },
+    gt
+            {
+                @Override
+                public void build(QueryWrapper<?> queryWrapper,String column, Object value) {
+                    queryWrapper.gt(column,value);
+                }
+            },
+    lt
+            {
+                @Override
+                public void build(QueryWrapper<?> queryWrapper,String column, Object value) {
+                    queryWrapper.lt(column,value);
+                }
+            },
+    gte
+            {
+                @Override
+                public void build(QueryWrapper<?> queryWrapper,String column, Object value) {
+                    queryWrapper.ge(column,value);
+                }
+            },
+    lte
+            {
+                @Override
+                public void build(QueryWrapper<?> queryWrapper,String column, Object value) {
+                    queryWrapper.le(column,value);
+                }
+            },
+    in
+            {
+                @Override
+                public void buildCollection(QueryWrapper<?> queryWrapper,String column, List<Object> value) {
+                    queryWrapper.in(column,value);
+                }
+            },
+    nin
+            {
+                @Override
+                public void buildCollection(QueryWrapper<?> queryWrapper,String column, List<Object> value) {
+                    queryWrapper.notIn(column,value);
+                }
+            },
+    isnull
+            {
+                @Override
+                public void build(QueryWrapper<?> queryWrapper,String column, Object value) {
+                    queryWrapper.isNull(column);
+                }
+            },
+    notnull
+            {
+                @Override
+                public void build(QueryWrapper<?> queryWrapper,String column, Object value) {
+                    queryWrapper.isNotNull(column);
+                }
+            },
+    btw
+            {
+                @Override
+                public void buildCollection(QueryWrapper<?> queryWrapper,String column, List<Object> value) {
+                    queryWrapper.between(column,value.get(0),value.get(1));
+                }
+            },
+    nbtw
+            {
+                @Override
+                public void buildCollection(QueryWrapper<?> queryWrapper,String column, List<Object> value) {
+                    queryWrapper.notBetween(column,value.get(0),value.get(1));
+                }
+            };
+
+    public void build(QueryWrapper<?> queryWrapper,String column,Object value){
+
+    };
+
+    public void buildCollection(QueryWrapper<?> queryWrapper,String column,List<Object> value){
+
+    };
+}

+ 31 - 0
coffee-common/src/main/java/com/coffee/common/enums/BizEnum.java

@@ -0,0 +1,31 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 业务类型
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum BizEnum {
+    /**
+     * 临时文件
+     */
+    TEMP("temp", "临时文件"),
+    /**
+     * 图片
+     */
+    IMAGE("image", "图片"),
+    /**
+     * Excel
+     */
+    EXCEL("excel", "Excel");
+
+    private String code;
+    private String desc;
+
+}

+ 38 - 0
coffee-common/src/main/java/com/coffee/common/enums/DataScopeEnum.java

@@ -0,0 +1,38 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 数据范围 1全部数据权限;2自定义数据权限;3本部门数据权限;4本部门及以下数据权限;5仅本人数据权限
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum DataScopeEnum {
+
+    /**
+     * 全部数据权限
+     */
+    ALL("1", "全部数据权限"),
+    /**
+     * 自定义数据权限
+     */
+    CUSTOM("2", "自定义数据权限"),
+    /**
+     * 本部门数据权限
+     */
+    DEPT("3", "本部门数据权限"),
+    /**
+     * 不内嵌
+     */
+    DEPT_AND_CHILD("4", "本部门及以下数据权限"),
+    /**
+     * 不内嵌
+     */
+    SELF("5", "仅本人数据权限");
+
+    private String code;
+    private String desc;
+}

+ 19 - 0
coffee-common/src/main/java/com/coffee/common/enums/DataSourceTypeEnum.java

@@ -0,0 +1,19 @@
+package com.coffee.common.enums;
+
+/**
+ * 数据源
+ *
+ * @author Kevin
+ */
+public enum DataSourceTypeEnum {
+
+    /**
+     * 主库
+     */
+    MASTER,
+
+    /**
+     * 从库
+     */
+    SLAVE
+}

+ 28 - 0
coffee-common/src/main/java/com/coffee/common/enums/DelFlagEnum.java

@@ -0,0 +1,28 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 删除标记
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum DelFlagEnum {
+
+    /**
+     * 存在
+     */
+    NO("0", "存在"),
+    /**
+     * 删除
+     */
+    YES("1", "删除");
+
+    private String code;
+    private String desc;
+
+}

+ 20 - 0
coffee-common/src/main/java/com/coffee/common/enums/FileStorageStrategyEnum.java

@@ -0,0 +1,20 @@
+package com.coffee.common.enums;
+
+/**
+ * 文件存储策略
+ *
+ * @author Kevin
+ */
+public enum FileStorageStrategyEnum {
+
+    /**
+     * MinIO文件存储
+     */
+    MINIO,
+
+    /**
+     * Aliyun文件存储
+     */
+    ALIYUN;
+
+}

+ 27 - 0
coffee-common/src/main/java/com/coffee/common/enums/FrameEnum.java

@@ -0,0 +1,27 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 是否内嵌
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum FrameEnum {
+
+    /**
+     * 内嵌
+     */
+    YES("0", "内嵌"),
+    /**
+     * 不内嵌
+     */
+    NO("1", "不内嵌");
+
+    private String code;
+    private String desc;
+
+}

+ 34 - 0
coffee-common/src/main/java/com/coffee/common/enums/GrantTypeEnum.java

@@ -0,0 +1,34 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 授权类型
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum GrantTypeEnum {
+
+    /**
+     * 用户名密码模式
+     */
+    USERNAME_PASSWORD("1", "用户名密码模式"),
+    /**
+     * 手机号短信模式
+     */
+    MOBILE_CODE("2", "手机号短信模式");
+
+    private String code;
+    private String desc;
+
+
+    public static Boolean contains(String code) {
+        return Arrays.stream(GrantTypeEnum.values()).anyMatch(temp -> temp.getCode().equals(code));
+    }
+
+}

+ 27 - 0
coffee-common/src/main/java/com/coffee/common/enums/KeepaliveEnum.java

@@ -0,0 +1,27 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 是否缓存
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum KeepaliveEnum {
+
+    /**
+     * 缓存
+     */
+    NO("0", "缓存"),
+    /**
+     * 不缓存
+     */
+    YES("1", "不缓存");
+
+    private String code;
+    private String desc;
+
+}

+ 28 - 0
coffee-common/src/main/java/com/coffee/common/enums/LinkExternalEnum.java

@@ -0,0 +1,28 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 是否外链
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum LinkExternalEnum {
+
+    /**
+     * 是
+     */
+    YES("0", "是"),
+    /**
+     * 否
+     */
+    NO("1", "否");
+
+    private String code;
+    private String desc;
+
+}

+ 28 - 0
coffee-common/src/main/java/com/coffee/common/enums/LockFlagEnum.java

@@ -0,0 +1,28 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 锁定标记
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum LockFlagEnum {
+
+    /**
+     * 正常
+     */
+    NORMAL("0", "正常"),
+    /**
+     * 锁定
+     */
+    LOCKED("1", "锁定");
+
+    private String code;
+    private String desc;
+
+}

+ 28 - 0
coffee-common/src/main/java/com/coffee/common/enums/LogStatusEnum.java

@@ -0,0 +1,28 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 日志状态
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum LogStatusEnum {
+
+    /**
+     * 成功
+     */
+    SUCCESS("0", "成功"),
+    /**
+     * 删除
+     */
+    FAILURE("1", "失败");
+
+    private String code;
+    private String desc;
+
+}

+ 31 - 0
coffee-common/src/main/java/com/coffee/common/enums/MenuTypeEnum.java

@@ -0,0 +1,31 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 菜单类型
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum MenuTypeEnum {
+
+    /**
+     * 目录
+     */
+    DIR("dir", "目录"),
+    /**
+     * 菜单
+     */
+    MENU("menu", "菜单"),
+    /**
+     * 按钮
+     */
+    BUTTON("button", "按钮");
+
+    private String code;
+    private String desc;
+
+}

+ 27 - 0
coffee-common/src/main/java/com/coffee/common/enums/PeriodEnum.java

@@ -0,0 +1,27 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 有效期
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum PeriodEnum {
+    /**
+     * 长期
+     */
+    LONG_PERIOD("0", "长期"),
+    /**
+     * 短期
+     */
+    SHORT_PERIOD("1", "短期");
+
+    private String code;
+    private String desc;
+
+}

+ 28 - 0
coffee-common/src/main/java/com/coffee/common/enums/PswModifiedEnum.java

@@ -0,0 +1,28 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 修改标记
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum PswModifiedEnum {
+
+    /**
+     * 未修改
+     */
+    NO("0", "未修改"),
+    /**
+     * 已修改
+     */
+    YES("1", "已修改");
+
+    private String code;
+    private String desc;
+
+}

+ 32 - 0
coffee-common/src/main/java/com/coffee/common/enums/SexEnum.java

@@ -0,0 +1,32 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 性别
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum SexEnum {
+
+    /**
+     * 男
+     */
+    MAN("1", "男"),
+    /**
+     * 女
+     */
+    WOMAN("2", "女"),
+    /**
+     * 未知
+     */
+    UNKNOW("3", "未知");
+
+    private String code;
+    private String desc;
+
+}

+ 26 - 0
coffee-common/src/main/java/com/coffee/common/enums/StatusEnum.java

@@ -0,0 +1,26 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 状态
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum StatusEnum {
+    /**
+     * 正常
+     */
+    YES("0", "正常"),
+    /**
+     * 停用
+     */
+    NO("1", "停用");
+
+    private String code;
+    private String desc;
+
+}

+ 26 - 0
coffee-common/src/main/java/com/coffee/common/enums/UniqueEnum.java

@@ -0,0 +1,26 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 是否唯一
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum UniqueEnum {
+    /**
+     * 否
+     */
+    NO("0", "否"),
+    /**
+     * 是
+     */
+    YES("1", "是");
+
+    private String code;
+    private String desc;
+
+}

+ 28 - 0
coffee-common/src/main/java/com/coffee/common/enums/UserPlatformEnum.java

@@ -0,0 +1,28 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 用户平台
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum UserPlatformEnum {
+
+    /**
+     * web管理后台
+     */
+    WEB("WEB", "web管理后台"),
+    /**
+     * 前端用户平台
+     */
+    APP("APP", "前端用户平台");
+
+    private String code;
+    private String desc;
+
+}

+ 27 - 0
coffee-common/src/main/java/com/coffee/common/enums/VisibleEnum.java

@@ -0,0 +1,27 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 是否显示
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum VisibleEnum {
+
+    /**
+     * 显示
+     */
+    SHOW("0", "显示"),
+    /**
+     * 隐藏
+     */
+    HIDE("1", "隐藏");
+
+    private String code;
+    private String desc;
+
+}

+ 26 - 0
coffee-common/src/main/java/com/coffee/common/enums/YesNoEnum.java

@@ -0,0 +1,26 @@
+package com.coffee.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 是否
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum YesNoEnum {
+    /**
+     * 否
+     */
+    NO("0", "否"),
+    /**
+     * 是
+     */
+    YES("1", "是");
+
+    private String code;
+    private String desc;
+
+}

+ 20 - 0
coffee-common/src/main/java/com/coffee/common/exception/CustomException.java

@@ -0,0 +1,20 @@
+package com.coffee.common.exception;
+
+import lombok.Data;
+
+/**
+ * 自定义异常
+ *
+ * @author Kevin
+ */
+@Data
+public class CustomException extends RuntimeException {
+
+    private Integer code;
+
+    private String message;
+
+    public CustomException(String message) {
+        this.message = message;
+    }
+}

+ 10 - 0
coffee-common/src/main/java/com/coffee/common/exception/DemoModeException.java

@@ -0,0 +1,10 @@
+package com.coffee.common.exception;
+
+/**
+ * 演示模式异常
+ *
+ * @author Kevin
+ */
+public class DemoModeException extends RuntimeException {
+
+}

+ 54 - 0
coffee-common/src/main/java/com/coffee/common/redis/RedisConfig.java

@@ -0,0 +1,54 @@
+package com.coffee.common.redis;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * redis基础配置
+ *
+ * @author Kevin
+ */
+@Configuration
+@EnableCaching
+@Slf4j
+public class RedisConfig extends CachingConfigurerSupport {
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+
+        // 使用Jackson2JsonRedisSerialize 替换默认序列化
+        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
+
+        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
+
+        // 设置value的序列化规则和 key的序列化规则
+        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+
+        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
+        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
+
+        redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);
+        redisTemplate.setEnableDefaultSerializer(true);
+
+        redisTemplate.setConnectionFactory(connectionFactory);
+        return redisTemplate;
+    }
+}

+ 582 - 0
coffee-common/src/main/java/com/coffee/common/redis/RedisUtils.java

@@ -0,0 +1,582 @@
+package com.coffee.common.redis;
+
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * 基于spring和redis的redisTemplate工具类
+ * 针对所有的hash  都是以h开头的方法
+ * 针对所有的Set  都是以s开头的方法
+ * 针对所有的List  都是以l开头的方法
+ *
+ * @author Kevin
+ */
+@Component
+public class RedisUtils {
+
+    @Resource
+    private RedisTemplate<String, Object> redisTemplate;
+
+    /**
+     * 指定缓存失效时间
+     *
+     * @param key  键
+     * @param time 时间(秒)
+     * @return
+     */
+    public boolean expire(String key, long time) {
+        try {
+            if (time > 0) {
+                redisTemplate.expire(key, time, TimeUnit.SECONDS);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 根据key  获取过期时间
+     *
+     * @param key 键  不能为null
+     * @return 时间(秒)  返回0代表为永久有效
+     */
+    public long getExpire(String key) {
+        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 判断key是否存在
+     *
+     * @param key 键
+     * @return true  存在  false不存在
+     */
+    public boolean hasKey(String key) {
+        try {
+            return redisTemplate.hasKey(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 删除缓存
+     *
+     * @param key 可以传一个值  或多个
+     */
+    @SuppressWarnings("unchecked")
+    public void del(String... key) {
+        if (key != null && key.length > 0) {
+            if (key.length == 1) {
+                redisTemplate.delete(key[0]);
+            } else {
+                redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
+            }
+        }
+    }
+
+    //============================String=============================
+
+    /**
+     * 普通缓存获取
+     *
+     * @param key 键
+     * @return 值
+     */
+    public Object get(String key) {
+        return key == null ? null : redisTemplate.opsForValue().get(key);
+    }
+
+    /**
+     * 普通缓存放入
+     *
+     * @param key   键
+     * @param value 值
+     * @return true成功  false失败
+     */
+    public boolean set(String key, Object value) {
+        try {
+            redisTemplate.opsForValue().set(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+
+    }
+
+    /**
+     * 普通缓存放入并设置时间
+     *
+     * @param key   键
+     * @param value 值
+     * @param time  时间(秒)  time要大于0  如果time小于等于0  将设置无限期
+     * @return true成功  false  失败
+     */
+    public boolean set(String key, Object value, long time) {
+        try {
+            if (time > 0) {
+                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
+            } else {
+                set(key, value);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 递增
+     *
+     * @param key   键
+     * @param delta 要增加几(大于0)
+     * @return
+     */
+    public long incr(String key, long delta) {
+        if (delta < 0) {
+            throw new RuntimeException("递增因子必须大于0");
+        }
+        return redisTemplate.opsForValue().increment(key, delta);
+    }
+
+    /**
+     * 递减
+     *
+     * @param key   键
+     * @param delta 要减少几(小于0)
+     * @return
+     */
+    public long decr(String key, long delta) {
+        if (delta < 0) {
+            throw new RuntimeException("递减因子必须大于0");
+        }
+        return redisTemplate.opsForValue().increment(key, -delta);
+    }
+
+    //================================Map=================================
+
+    /**
+     * HashGet
+     *
+     * @param key  键  不能为null
+     * @param item 项  不能为null
+     * @return 值
+     */
+    public Object hget(String key, String item) {
+        return redisTemplate.opsForHash().get(key, item);
+    }
+
+    /**
+     * 获取hashKey对应的所有键值
+     *
+     * @param key 键
+     * @return 对应的多个键值
+     */
+    public Map<Object, Object> hmget(String key) {
+        return redisTemplate.opsForHash().entries(key);
+    }
+
+    /**
+     * HashSet
+     *
+     * @param key 键
+     * @param map 对应多个键值
+     * @return true  成功  false  失败
+     */
+    public boolean hmset(String key, Map<String, Object> map) {
+        try {
+            redisTemplate.opsForHash().putAll(key, map);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * HashSet  并设置时间
+     *
+     * @param key  键
+     * @param map  对应多个键值
+     * @param time 时间(秒)
+     * @return true成功  false失败
+     */
+    public boolean hmset(String key, Map<String, Object> map, long time) {
+        try {
+            redisTemplate.opsForHash().putAll(key, map);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 向一张hash表中放入数据,如果不存在将创建
+     *
+     * @param key   键
+     * @param item  项
+     * @param value 值
+     * @return true  成功  false失败
+     */
+    public boolean hset(String key, String item, Object value) {
+        try {
+            redisTemplate.opsForHash().put(key, item, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 向一张hash表中放入数据,如果不存在将创建
+     *
+     * @param key   键
+     * @param item  项
+     * @param value 值
+     * @param time  时间(秒)注意:如果已存在的hash表有时间,这里将会替换原有的时间
+     * @return true  成功  false失败
+     */
+    public boolean hset(String key, String item, Object value, long time) {
+        try {
+            redisTemplate.opsForHash().put(key, item, value);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 删除hash表中的值
+     *
+     * @param key  键  不能为null
+     * @param item 项  可以使多个  不能为null
+     */
+    public void hdel(String key, Object... item) {
+        redisTemplate.opsForHash().delete(key, item);
+    }
+
+    /**
+     * 判断hash表中是否有该项的值
+     *
+     * @param key  键  不能为null
+     * @param item 项  不能为null
+     * @return true  存在  false不存在
+     */
+    public boolean hHasKey(String key, String item) {
+        return redisTemplate.opsForHash().hasKey(key, item);
+    }
+
+    /**
+     * hash递增  如果不存在,就会创建一个  并把新增后的值返回
+     *
+     * @param key  键
+     * @param item 项
+     * @param by   要增加几(大于0)
+     * @return
+     */
+    public double hincr(String key, String item, double by) {
+        return redisTemplate.opsForHash().increment(key, item, by);
+    }
+
+    /**
+     * hash递减
+     *
+     * @param key  键
+     * @param item 项
+     * @param by   要减少记(小于0)
+     * @return
+     */
+    public double hdecr(String key, String item, double by) {
+        return redisTemplate.opsForHash().increment(key, item, -by);
+    }
+
+    //============================set=============================
+
+    /**
+     * 根据key获取Set中的所有值
+     *
+     * @param key 键
+     * @return
+     */
+    public Set<Object> sGet(String key) {
+        try {
+            return redisTemplate.opsForSet().members(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 根据value从一个set中查询,是否存在
+     *
+     * @param key   键
+     * @param value 值
+     * @return true  存在  false不存在
+     */
+    public boolean sHasKey(String key, Object value) {
+        try {
+            return redisTemplate.opsForSet().isMember(key, value);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将数据放入set缓存
+     *
+     * @param key    键
+     * @param values 值  可以是多个
+     * @return 成功个数
+     */
+    public long sSet(String key, Object... values) {
+        try {
+            return redisTemplate.opsForSet().add(key, values);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+
+    /**
+     * 将set数据放入缓存
+     *
+     * @param key    键
+     * @param time   时间(秒)
+     * @param values 值  可以是多个
+     * @return 成功个数
+     */
+    public long sSetAndTime(String key, long time, Object... values) {
+        try {
+            Long count = redisTemplate.opsForSet().add(key, values);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return count;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+
+    /**
+     * 获取set缓存的长度
+     *
+     * @param key 键
+     * @return
+     */
+    public long sGetSetSize(String key) {
+        try {
+            return redisTemplate.opsForSet().size(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+
+    /**
+     * 移除值为value的
+     *
+     * @param key    键
+     * @param values 值  可以是多个
+     * @return 移除的个数
+     */
+    public long setRemove(String key, Object... values) {
+        try {
+            Long count = redisTemplate.opsForSet().remove(key, values);
+            return count;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+    //===============================list=================================
+
+    /**
+     * 获取list缓存的内容
+     *
+     * @param key   键
+     * @param start 开始
+     * @param end   结束0  到  -1代表所有值
+     * @return
+     */
+    public List<Object> lGet(String key, long start, long end) {
+        try {
+            return redisTemplate.opsForList().range(key, start, end);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 获取list缓存的长度
+     *
+     * @param key 键
+     * @return
+     */
+    public long lGetListSize(String key) {
+        try {
+            return redisTemplate.opsForList().size(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+
+    /**
+     * 通过索引  获取list中的值
+     *
+     * @param key   键
+     * @param index 索引index>=0时,  0  表头,1  第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
+     * @return
+     */
+    public Object lGetIndex(String key, long index) {
+        try {
+            return redisTemplate.opsForList().index(key, index);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @return
+     */
+    public boolean lSet(String key, Object value) {
+        try {
+            redisTemplate.opsForList().rightPush(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @param time  时间(秒)
+     * @return
+     */
+    public boolean lSet(String key, Object value, long time) {
+        try {
+            redisTemplate.opsForList().rightPush(key, value);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @return
+     */
+    public boolean lSet(String key, List<Object> value) {
+        try {
+            redisTemplate.opsForList().rightPushAll(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @param time  时间(秒)
+     * @return
+     */
+    public boolean lSet(String key, List<Object> value, long time) {
+        try {
+            redisTemplate.opsForList().rightPushAll(key, value);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 根据索引修改list中的某条数据
+     *
+     * @param key   键
+     * @param index 索引
+     * @param value 值
+     * @return
+     */
+    public boolean lUpdateIndex(String key, long index, Object value) {
+        try {
+            redisTemplate.opsForList().set(key, index, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 移除N个值为value
+     *
+     * @param key   键
+     * @param count 移除多少个
+     * @param value 值
+     * @return 移除的个数
+     */
+    public long lRemove(String key, long count, Object value) {
+        try {
+            Long remove = redisTemplate.opsForList().remove(key, count, value);
+            return remove;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0;
+        }
+    }
+
+    public Collection<String> keys(final String pattern) {
+        return redisTemplate.keys(pattern);
+    }
+
+}

+ 26 - 0
coffee-common/src/main/java/com/coffee/common/result/IResultCode.java

@@ -0,0 +1,26 @@
+package com.coffee.common.result;
+
+import java.io.Serializable;
+
+/**
+ * 返回码
+ *
+ * @author Kevin
+ */
+public interface IResultCode extends Serializable {
+
+    /**
+     * 消息
+     *
+     * @return String
+     */
+    String getMessage();
+
+    /**
+     * 状态码
+     *
+     * @return int
+     */
+    int getCode();
+
+}

+ 95 - 0
coffee-common/src/main/java/com/coffee/common/result/R.java

@@ -0,0 +1,95 @@
+package com.coffee.common.result;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serializable;
+
+/**
+ * 统一API响应结果封装
+ *
+ * @author Kevin
+ */
+@SuppressWarnings("ALL")
+@Getter
+@Setter
+@ToString
+@NoArgsConstructor
+public class R<T> implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 状态码
+     */
+    private int code;
+    /**
+     * 承载数据
+     */
+    private T data;
+    /**
+     * 返回消息
+     */
+    private String msg;
+
+    private R(int code, T data, String msg) {
+        this.code = code;
+        this.data = data;
+        this.msg = msg;
+    }
+
+    private R(IResultCode resultCode) {
+        this(resultCode, null, resultCode.getMessage());
+    }
+
+    private R(IResultCode resultCode, T data) {
+        this(resultCode, data, resultCode.getMessage());
+    }
+
+    private R(IResultCode resultCode, String msg) {
+        this(resultCode, null, msg);
+    }
+
+    private R(IResultCode resultCode, T data, String msg) {
+        this(resultCode.getCode(), data, msg);
+    }
+
+    public static <T> R<T> success() {
+        return new R<>(ResultCode.SUCCESS);
+    }
+
+    public static <T> R<T> success(T data) {
+        return new R<>(ResultCode.SUCCESS, data);
+    }
+
+    public static <T> R<T> success(T data, String msg) {
+        return new R<>(ResultCode.SUCCESS, data, msg);
+    }
+
+    public static <T> R<T> fail(String msg) {
+        return new R<>(ResultCode.FAILURE, msg);
+    }
+
+    public static <T> R<T> result(IResultCode resultCode) {
+        return new R<>(resultCode);
+    }
+
+    public static <T> R<T> result(IResultCode resultCode, T data) {
+        return new R<>(resultCode, data);
+    }
+
+    public static <T> R<T> result(IResultCode resultCode, T data, String msg) {
+        return new R<>(resultCode, data, msg);
+    }
+
+    public static <T> R<T> result(int code, String msg) {
+        return new R<>(code, null, msg);
+    }
+
+    public static <T> R<T> result(int code, T data, String msg) {
+        return new R<>(code, data, msg);
+    }
+
+}

+ 98 - 0
coffee-common/src/main/java/com/coffee/common/result/ResultCode.java

@@ -0,0 +1,98 @@
+package com.coffee.common.result;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 返回码
+ *
+ * @author Kevin
+ */
+@Getter
+@AllArgsConstructor
+public enum ResultCode implements IResultCode {
+
+    /**
+     * 操作成功
+     */
+    SUCCESS(HttpServletResponse.SC_OK, "操作成功"),
+
+    /**
+     * 业务异常
+     */
+    FAILURE(HttpServletResponse.SC_BAD_REQUEST, "业务异常"),
+
+    /**
+     * 请求未授权
+     */
+    UN_AUTHORIZED(HttpServletResponse.SC_UNAUTHORIZED, "请求未授权,请联系管理员"),
+
+    /**
+     * 404 没找到请求
+     */
+    NOT_FOUND(HttpServletResponse.SC_NOT_FOUND, "404 没找到请求"),
+
+    /**
+     * 消息不能读取
+     */
+    MSG_NOT_READABLE(HttpServletResponse.SC_BAD_REQUEST, "消息不能读取"),
+
+    /**
+     * 不支持当前请求方法
+     */
+    METHOD_NOT_SUPPORTED(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "不支持当前请求方法"),
+
+    /**
+     * 不支持当前媒体类型
+     */
+    MEDIA_TYPE_NOT_SUPPORTED(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "不支持当前媒体类型"),
+
+    /**
+     * 请求被拒绝
+     */
+    REQ_REJECT(HttpServletResponse.SC_FORBIDDEN, "请求被拒绝"),
+
+    /**
+     * 服务器异常
+     */
+    INTERNAL_SERVER_ERROR(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "服务器异常,请联系管理员"),
+
+    /**
+     * 缺少必要的请求参数
+     */
+    PARAM_MISS(HttpServletResponse.SC_BAD_REQUEST, "缺少必要的请求参数"),
+
+    /**
+     * 请求参数类型错误
+     */
+    PARAM_TYPE_ERROR(HttpServletResponse.SC_BAD_REQUEST, "请求参数类型错误"),
+
+    /**
+     * 请求参数绑定错误
+     */
+    PARAM_BIND_ERROR(HttpServletResponse.SC_BAD_REQUEST, "请求参数绑定错误"),
+
+    /**
+     * 参数校验失败
+     */
+    PARAM_VALID_ERROR(HttpServletResponse.SC_BAD_REQUEST, "参数校验失败"),
+
+    /**
+     * 登录超时,请重新登录
+     */
+    TOKEN_ERROR(HttpServletResponse.SC_REQUEST_TIMEOUT, "登录超时,请重新登录"),
+    ;
+
+    /**
+     * code编码
+     */
+    final int code;
+
+    /**
+     * 中文信息描述
+     */
+    final String message;
+
+}

+ 51 - 0
coffee-common/src/main/java/com/coffee/common/util/AddressUtil.java

@@ -0,0 +1,51 @@
+package com.coffee.common.util;
+
+import cn.hutool.core.net.NetUtil;
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.coffee.common.config.AppConfig;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 获取地址类
+ *
+ * @author Kevin
+ */
+@Slf4j
+public class AddressUtil {
+    /**
+     * IP地址查询
+     */
+    public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp";
+
+    /**
+     * 未知地址
+     */
+    public static final String UNKNOWN = "XX XX";
+
+    public static String getRealAddressByIp(String ip) {
+        String address = UNKNOWN;
+        // 内网不查询
+        if (NetUtil.isInnerIP(ip)) {
+            return "内网IP";
+        }
+        if (AppConfig.isAddressEnabled()) {
+            try {
+                String rspStr = HttpUtil.get(IP_URL + "?ip=" + ip + "&json=true", CharsetUtil.CHARSET_GBK);
+                if (StrUtil.isEmpty(rspStr)) {
+                    log.error("获取地理位置异常 {}", ip);
+                    return UNKNOWN;
+                }
+                JSONObject obj = JSONObject.parseObject(rspStr);
+                String region = obj.getString("pro");
+                String city = obj.getString("city");
+                return String.format("%s %s", region, city);
+            } catch (Exception e) {
+                log.error("获取地理位置异常 {}", ip);
+            }
+        }
+        return address;
+    }
+}

+ 42 - 0
coffee-common/src/main/java/com/coffee/common/util/ConfigUtil.java

@@ -0,0 +1,42 @@
+package com.coffee.common.util;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import com.coffee.common.Constants;
+import com.coffee.common.redis.RedisUtils;
+
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * 系统参数缓存工具类
+ *
+ * @author Kevin
+ */
+public class ConfigUtil {
+
+    public static String getCacheKey(String key) {
+        return String.format(Constants.SYS_CONFIG_TPL, key);
+    }
+
+    public static String getConfigValue(String key) {
+        if (StrUtil.isBlank(key)) {
+            return "";
+        }
+        Object obj = SpringUtil.getBean(RedisUtils.class).get(getCacheKey(key));
+        return Objects.nonNull(obj) ? obj.toString() : "";
+    }
+
+    public static void setConfigCache(String key, String value) {
+        if (StrUtil.isBlank(key) || StrUtil.isBlank(value)) {
+            return;
+        }
+        SpringUtil.getBean(RedisUtils.class).set(getCacheKey(key), value);
+    }
+
+    public static void clearConfigCache() {
+        Collection<String> keys = SpringUtil.getBean(RedisUtils.class).keys(getCacheKey("*"));
+        SpringUtil.getBean(RedisUtils.class).del(keys.toArray(new String[0]));
+    }
+
+}

+ 60 - 0
coffee-common/src/main/java/com/coffee/common/util/DictUtil.java

@@ -0,0 +1,60 @@
+package com.coffee.common.util;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import com.coffee.common.Constants;
+import com.coffee.common.bo.DictModel;
+import com.coffee.common.redis.RedisUtils;
+import com.google.common.collect.Lists;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 字典缓存工具类
+ *
+ * @author Kevin
+ */
+public class DictUtil {
+
+    public static String getCacheKey(String key) {
+        return String.format(Constants.SYS_DICT_TPL, key);
+    }
+
+    public static List<DictModel> getDictList(String dictCode) {
+        Object obj = SpringUtil.getBean(RedisUtils.class).get(getCacheKey(dictCode));
+        return Objects.nonNull(obj) ? (List<DictModel>) obj : Lists.newArrayList();
+    }
+
+    public static String getDictLabel(String dictCode, String dictValue) {
+        if (StrUtil.isBlank(dictCode) || StrUtil.isBlank(dictValue)) {
+            return "";
+        }
+        List<DictModel> dictModelList = getDictList(dictCode);
+        DictModel dictModel = dictModelList.stream().filter(x -> x.getValue().equals(dictValue)).findFirst().orElse(null);
+        return Objects.nonNull(dictModel) ? dictModel.getLabel() : "";
+    }
+
+    public static String getDictValue(String dictCode, String dictLabel) {
+        if (StrUtil.isBlank(dictCode) || StrUtil.isBlank(dictLabel)) {
+            return "";
+        }
+        List<DictModel> dictModelList = getDictList(dictCode);
+        DictModel dictModel = dictModelList.stream().filter(x -> x.getLabel().equals(dictLabel)).findFirst().orElse(null);
+        return Objects.nonNull(dictModel) ? dictModel.getValue() : "";
+    }
+
+    public static void setDictCache(String dictCode, List<DictModel> dictModelList) {
+        if (StrUtil.isBlank(dictCode) || Objects.isNull(dictModelList) || dictModelList.isEmpty()) {
+            return;
+        }
+        SpringUtil.getBean(RedisUtils.class).set(getCacheKey(dictCode), dictModelList);
+    }
+
+    public static void clearDictCache() {
+        Collection<String> keys = SpringUtil.getBean(RedisUtils.class).keys(getCacheKey("*"));
+        SpringUtil.getBean(RedisUtils.class).del(keys.toArray(new String[0]));
+    }
+
+}

+ 35 - 0
coffee-common/src/main/java/com/coffee/common/util/ExcelUtil.java

@@ -0,0 +1,35 @@
+package com.coffee.common.util;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.IdUtil;
+import com.alibaba.excel.EasyExcel;
+import com.coffee.common.config.AppConfig;
+import com.coffee.common.enums.BizEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+/**
+ * Excel工具类
+ *
+ * @author Kevin
+ */
+@Slf4j
+public class ExcelUtil {
+
+    public static String export(String filename, Class clazz, List list) {
+        return export(filename, clazz, list, filename);
+    }
+
+    public static String export(String filename, Class clazz, List list, String sheetName) {
+        filename = filename + "_" + IdUtil.fastSimpleUUID() + ".xlsx";
+        String filepath = "/" + BizEnum.TEMP.getCode() + "/" + DateUtil.today() + "/" + filename;
+        String absoluteFilepath = AppConfig.getUploadDir() + filepath;
+        log.info("导出文件路径,{}", absoluteFilepath);
+        FileUtil.touch(absoluteFilepath);
+        EasyExcel.write(absoluteFilepath, clazz).head(clazz).sheet(sheetName).doWrite(list);
+        return filepath;
+    }
+
+}

+ 21 - 0
coffee-common/src/main/java/com/coffee/common/util/IdUtil.java

@@ -0,0 +1,21 @@
+package com.coffee.common.util;
+
+import cn.hutool.core.lang.Singleton;
+import cn.hutool.core.lang.Snowflake;
+
+/**
+ * ID生成器工具类
+ *
+ * @author Kevin
+ */
+public class IdUtil {
+    /**
+     * 雪花算法,采用系统时钟
+     * https://blog.csdn.net/weixin_42444592/article/details/109643200
+     *
+     * @return 随机UUID
+     */
+    public static Long getSnowflakeId() {
+        return Singleton.get(Snowflake.class, 0L, 0L, true).nextId();
+    }
+}

+ 26 - 0
coffee-common/src/main/java/com/coffee/common/util/IpUtil.java

@@ -0,0 +1,26 @@
+package com.coffee.common.util;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.servlet.ServletUtil;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * IP获取工具
+ *
+ * @author Kevin
+ */
+public class IpUtil {
+
+    public static String getClientIp(HttpServletRequest request) {
+        // natapp穿透工具搭建的环境,通过header,X-Natapp-Ip,获取
+        String ipAddress = ServletUtil.getClientIPByHeader(request, "X-Natapp-Ip");
+        if (StrUtil.isBlank(ipAddress)) {
+            ipAddress = ServletUtil.getClientIP(request);
+        }
+        // 本地开发,客户端和服务器在同一台机器,获取到是0:0:0:0:0:0:0:1,ip6地址,需要进行转换
+        ipAddress = "0:0:0:0:0:0:0:1".equals(ipAddress) ? "127.0.0.1" : ipAddress;
+        return ipAddress;
+    }
+
+}

+ 60 - 0
coffee-common/src/main/java/com/coffee/common/util/MinioUtil.java

@@ -0,0 +1,60 @@
+package com.coffee.common.util;
+
+import com.coffee.common.config.MinioConfig;
+import com.coffee.common.exception.CustomException;
+import io.minio.BucketExistsArgs;
+import io.minio.MakeBucketArgs;
+import io.minio.MinioClient;
+import io.minio.PutObjectArgs;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.io.InputStream;
+
+/**
+ * Minio工具类
+ *
+ * @author Kevin
+ */
+@Component
+@Slf4j
+public class MinioUtil {
+
+    @Resource
+    MinioConfig minioConfig;
+
+    @Resource
+    MinioClient minioClient;
+
+    @Bean
+    private MinioClient minioClient() {
+        MinioClient minioClient = MinioClient.builder()
+                .endpoint(minioConfig.getEndpoint())
+                .credentials(minioConfig.getAccessKey(), minioConfig.getSecretKey())
+                .build();
+        return minioClient;
+    }
+
+    public String uploadObject(InputStream stream, String filepath) {
+        try {
+            boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(minioConfig.getBucketName()).build());
+            if (!exists) {
+                minioClient.makeBucket(MakeBucketArgs.builder().bucket(minioConfig.getBucketName()).build());
+            }
+            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
+                    .bucket(minioConfig.getBucketName())
+                    .object(filepath.substring(1))
+                    .contentType(MediaType.APPLICATION_OCTET_STREAM_VALUE)
+                    .stream(stream, stream.available(), -1).build();
+            minioClient.putObject(putObjectArgs);
+        } catch (Exception e) {
+            log.error("上传文件失败,{}", e.getMessage());
+            throw new CustomException("上传文件失败," + e.getMessage());
+        }
+        return minioConfig.getEndpoint() + "/" + minioConfig.getBucketName() + filepath;
+    }
+
+}

+ 77 - 0
coffee-common/src/main/java/com/coffee/common/util/SecurityUtil.java

@@ -0,0 +1,77 @@
+package com.coffee.common.util;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.crypto.digest.BCrypt;
+import com.coffee.common.Constants;
+import com.coffee.common.bo.LoginUser;
+import com.coffee.common.bo.SysUserBO;
+
+import java.util.Objects;
+
+/**
+ * 安全服务工具类
+ *
+ * @author Kevin
+ */
+public class SecurityUtil {
+    /**
+     * 获取用户账户
+     **/
+    public static String getUsername() {
+        return getLoginUser().getUsername();
+    }
+
+    /**
+     * 获取系统用户
+     **/
+    public static SysUserBO getSysUser() {
+        return getLoginUser().getSysUser();
+    }
+
+    /**
+     * 获取用户
+     **/
+    public static LoginUser getLoginUser() {
+        try {
+            return (LoginUser) StpUtil.getTokenSession().get(Constants.LOGIN_USER_KEY);
+        } catch (Exception ex) {
+            return null;
+        }
+    }
+
+    /**
+     * 是否是超级管理员
+     **/
+    public static Boolean isSuperAdmin() {
+        return isSuperAdmin(getSysUser().getId());
+    }
+
+    /**
+     * 是否是超级管理员
+     **/
+    public static Boolean isSuperAdmin(Long userId) {
+        return Objects.nonNull(userId) && userId == 1L;
+    }
+
+    /**
+     * 生成BCryptPasswordEncoder密码
+     *
+     * @param password 密码
+     * @return 加密字符串
+     */
+    public static String encryptPassword(String password) {
+        return BCrypt.hashpw(password);
+    }
+
+    /**
+     * 判断密码是否相同
+     *
+     * @param rawPassword     真实密码
+     * @param encodedPassword 加密后字符
+     * @return 结果
+     */
+    public static boolean matchesPassword(String rawPassword, String encodedPassword) {
+        return BCrypt.checkpw(rawPassword, encodedPassword);
+    }
+
+}

+ 54 - 0
coffee-framework/pom.xml

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>coffee-boot</artifactId>
+        <groupId>com.coffee</groupId>
+        <version>1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>coffee-framework</artifactId>
+
+    <dependencies>
+        <!-- web 容器使用 undertow 性能更强 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-undertow</artifactId>
+        </dependency>
+        <!-- SpringBoot拦截器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+        <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+        <!--swagger模块 -->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-boot-starter</artifactId>
+        </dependency>
+        <!-- 阿里数据库连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+        <!-- 系统模块 -->
+        <dependency>
+            <groupId>com.coffee</groupId>
+            <artifactId>coffee-system</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 59 - 0
coffee-framework/src/main/java/com/coffee/framework/aspect/DataSourceAspect.java

@@ -0,0 +1,59 @@
+package com.coffee.framework.aspect;
+
+import com.coffee.common.annotation.DataSource;
+import com.coffee.framework.datasource.DynamicDataSourceContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+
+/**
+ * 多数据源处理
+ *
+ * @author Kevin
+ */
+@Aspect
+@Order(1)
+@Component
+@Slf4j
+public class DataSourceAspect {
+
+    @Pointcut("@annotation(com.coffee.common.annotation.DataSource)"
+            + "|| @within(com.coffee.common.annotation.DataSource)")
+    public void dsPointCut() {
+
+    }
+
+    @Around("dsPointCut()")
+    public Object around(ProceedingJoinPoint point) throws Throwable {
+        DataSource dataSource = getDataSource(point);
+        if (Objects.nonNull(dataSource)) {
+            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
+        }
+        try {
+            return point.proceed();
+        } finally {
+            // 销毁数据源 在执行方法之后
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+
+    /**
+     * 获取需要切换的数据源
+     */
+    public DataSource getDataSource(ProceedingJoinPoint point) {
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
+        if (Objects.nonNull(dataSource)) {
+            return dataSource;
+        }
+        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
+    }
+}

+ 39 - 0
coffee-framework/src/main/java/com/coffee/framework/aspect/DemoModelAspect.java

@@ -0,0 +1,39 @@
+package com.coffee.framework.aspect;
+
+import com.coffee.common.config.AppConfig;
+import com.coffee.common.exception.DemoModeException;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 演示环境AOP
+ *
+ * @author Kevin
+ */
+@Aspect
+@Component
+@Slf4j
+public class DemoModelAspect {
+
+    @Pointcut("execution(public * com.coffee..*.controller..*(..))")
+    public void pointcut() {
+    }
+
+    @Before("pointcut()")
+    public void before() {
+        HttpServletRequest requset = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
+        if (AppConfig.isDemoEnabled()
+                && "POST".equals(requset.getMethod())
+                && !requset.getRequestURI().contains("/login")) {
+            throw new DemoModeException();
+        }
+    }
+
+}

+ 146 - 0
coffee-framework/src/main/java/com/coffee/framework/aspect/LogAspect.java

@@ -0,0 +1,146 @@
+package com.coffee.framework.aspect;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.thread.ThreadUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.hutool.http.useragent.UserAgent;
+import cn.hutool.http.useragent.UserAgentUtil;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.coffee.common.annotation.Log;
+import com.coffee.common.enums.LogStatusEnum;
+import com.coffee.common.util.AddressUtil;
+import com.coffee.common.util.IpUtil;
+import com.coffee.common.util.SecurityUtil;
+import com.coffee.system.entity.SysLog;
+import com.coffee.system.service.ISysLogService;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.BindingResult;
+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.*;
+import java.util.stream.Collectors;
+
+/**
+ * 日志注解
+ *
+ * @author Kevin
+ */
+@Aspect
+@Component
+@Slf4j
+public class LogAspect {
+
+    @Pointcut("@annotation(com.coffee.common.annotation.Log)")
+    public void logPointCut() {
+    }
+
+    @Around("logPointCut()")
+    @SneakyThrows
+    public Object around(ProceedingJoinPoint point) {
+        Long startTime = System.currentTimeMillis();
+        Object obj = null;
+        Exception exception = null;
+        try {
+            obj = point.proceed();
+        } catch (Exception e) {
+            exception = e;
+            throw e;
+        } finally {
+            Long endTime = System.currentTimeMillis();
+            handleLog(point, exception, obj, endTime - startTime);
+        }
+        return obj;
+    }
+
+    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object retValue, long time) {
+        try {
+            // 获得注解
+            Signature signature = joinPoint.getSignature();
+            MethodSignature methodSignature = (MethodSignature) signature;
+            Log controllerLog = AnnotationUtils.findAnnotation(methodSignature.getMethod(), Log.class);
+            if (Objects.isNull(controllerLog)) {
+                return;
+            }
+            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
+            SysLog sysLog = new SysLog();
+            sysLog.setTitle(controllerLog.title());
+            sysLog.setLogStatus(LogStatusEnum.SUCCESS.getCode());
+            sysLog.setUserPlatform(controllerLog.userPlatform().getCode());
+            sysLog.setRequsetUri(request.getRequestURI());
+            sysLog.setRequsetType(request.getMethod());
+            // 设置方法名称
+            String className = joinPoint.getTarget().getClass().getName();
+            String methodName = joinPoint.getSignature().getName();
+            sysLog.setRequsetMethod(className + "." + methodName + "()");
+
+            List<Object> objectList = Arrays.stream(joinPoint.getArgs()).filter(item -> Objects.nonNull(item) && !isFilterObject(item)).collect(Collectors.toList());
+            sysLog.setRequsetParams(JSONUtil.toJsonStr(objectList));
+            sysLog.setResponseResult(JSONUtil.toJsonStr(retValue));
+            sysLog.setRequsetTime(String.valueOf(time));
+
+            String ipAddress = IpUtil.getClientIp(request);
+            sysLog.setIpAddress(ipAddress);
+            sysLog.setOperLocation(AddressUtil.getRealAddressByIp(ipAddress));
+            UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
+            sysLog.setBrowser(userAgent.getBrowser().getName());
+            sysLog.setOs(userAgent.getOs().getName());
+
+            if (StpUtil.isLogin()) {
+                sysLog.setOperName(SecurityUtil.getUsername());
+            }
+            if (e != null) {
+                sysLog.setLogStatus(LogStatusEnum.FAILURE.getCode());
+                sysLog.setException(e.getMessage());
+            }
+            ThreadUtil.execAsync(() -> {
+                ISysLogService sysLogService = SpringUtil.getBean(ISysLogService.class);
+                sysLogService.save(sysLog);
+            });
+            log.debug("接口:{},URI:{},执行耗时:{}", sysLog.getTitle(), sysLog.getRequsetUri(), time >= 1000 ? time / 1000 + "s" : time + "ms");
+        } catch (Exception exception) {
+            log.error("异常信息:{}", exception.getMessage());
+            exception.printStackTrace();
+        }
+    }
+
+    /**
+     * 判断是否需要过滤的对象。
+     *
+     * @param o 对象信息。
+     * @return 如果是需要过滤的对象,则返回true;否则返回false。
+     */
+    public boolean isFilterObject(final Object o) {
+        Class<?> clazz = o.getClass();
+        if (clazz.isArray()) {
+            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+        } else if (Collection.class.isAssignableFrom(clazz)) {
+            Collection collection = (Collection) o;
+            for (Object value : collection) {
+                return value instanceof MultipartFile;
+            }
+        } else if (Map.class.isAssignableFrom(clazz)) {
+            Map map = (Map) o;
+            for (Object value : map.entrySet()) {
+                Map.Entry entry = (Map.Entry) value;
+                return entry.getValue() instanceof MultipartFile;
+            }
+        }
+        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+                || o instanceof BindingResult || o instanceof Page;
+    }
+}

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov