主分支由改为main

This commit is contained in:
15858193327
2020-10-07 17:08:13 +08:00
commit ddf1f68615
186 changed files with 7877 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
### 该问题是怎么引起的?
### 重现步骤
### 报错信息
### 尝试过的解决手段

View File

@@ -0,0 +1,15 @@
### 相关的Issue
### 原因(目的、解决的问题等)
### 描述(做了什么,变更了什么)
### 测试用例(新增、改动、可能影响的功能)

29
.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# IDEA
.idea
*.iml
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
target
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

201
LICENSE Normal file
View File

@@ -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.

195
README.md Normal file
View File

@@ -0,0 +1,195 @@
# shoulder-platform
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/ChinaLym/Shoulder-Platform)
[![](https://img.shields.io/badge/Author-lym-blue.svg)](https://github.com/ChinaLym)
[![](https://img.shields.io/badge/version-1.0-brightgreen.svg)](https://github.com/ChinaLym/Shoulder-Platform)
[![GitHub stars](https://img.shields.io/github/stars/ChinaLym/Shoulder-Framework.svg?style=social&label=Stars)](https://github.com/ChinaLym/Shoulder-Platform/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/ChinaLym/Shoulder-Framework.svg?style=social&label=Fork)](https://github.com/ChinaLym/Shoulder-Framework/network/members)
## 简介:
`shoulder-platform` 是一个 `SaaS` 平台(仅实现基础能力,不包含具体业务),代码简洁,架构清晰,非常适合学习使用。
## 架构图
![架构图.png](img/architecture.png)
在线工具:[https://app.diagrams.net/](https://app.diagrams.net/) [https://processon.com/](https://processon.com/)
## 在线预览
- [开发规范地址](http://spec.itlym.cn)
- [Grafana + Prometheus 监控系统](http://grafana.itlym.cn)(访客账号密码:`shoulder` / `shoulder123`,仅包含仪表盘查看权限)
- [EFK 日志系统](http://kibana.itlym.cn)(访客账号密码:`shoulder` / `shoulder123`,仅包含日志检索查看权限)
- 平台地址( 开发中 )
- ~~[zipkin 链路追踪系统](http://zipkin.itlym.cn)(暂时下线)~~
# 能力介绍
- 能力概览
- 单点登录
- 用户管理
- 资源权限管理
- 通知推送中心(短信、邮件)
- 错误码中心(查询错误码,大概产生原因,解决措施)
- 知识库(记录常见问题排查方式等)
- 在线 api 文档中心
- 核心框架
- `SpringBoot`
- `SpringCloud`
- `Shoulder Framework`
- 服务认证: Spring SecurityOauth、JWT
- 微服务治理方案选型
- 服务注册、服务发现: nacos
- 服务调用: feign + 负载均衡: Ribbon / Dubbo
- 限流 & 断路器: Sentinel
- 配置中心:nacos
- 消息通知
- rabbitMQ、KafKa
- 文件存储
- ceph、OSS..
- 分布式任务调度
- Power Job
- 分布式事务
- Seata
- 数据同步
- canal
- 监控
- 集群监控spring-boot-actuator + spring-boot-admin
- 服务监控sentinel
- 链路追踪zipkin/Skywalking根据部署机器性能选择
- 指标监控metrics + exporter + prometheus + grafana
- 主机监控、容器监控cAdvisor
- 告警alertManager
- 日志监控 EFKElastic Search + Fluentd + Kibana
- 持续集成、持续部署(不限制)
- 版本控制Git
- 接口文档openApi3
- 代码审查Sonar
- 自动测试AutoTest
- 持续集成Maven、Jenkins、Drone
- 部署Docker、K8s
- 发布方式金丝雀发布、蓝绿发布、灰度发布Ribbon
- 数据智能
- ETL
- 数据处理Flink、Google Data Flow、Beam
- 认证中心
- 单点登录
- 会话管理
- 授权管理
- 用户中心
- 用户、组织、人事管理
- 租户管理
- 权限中心
- 菜单权限
- 角色权限
- 岗位管理
- 资源管理
- 应用管理
- 消息推送
- 短信
- 邮件
- 钉钉
- 企业微信
- App第三方
- 存储中心
- 本地文件
- 结合数据库、本地文件路径
- 自建存储系统
- **minio**、FastDFS、Hadoop、GDFS等
- 第三方OOS存储
- **七牛云、阿里云、亚马逊云**、腾讯云、华为云
- 平台
- 用户平台
- 监控门户
- 运维平台
- 运营平台(后台管理)
- 网关
- Web 浏览器
- H5 小程序
- App
- OpenApi
- 静态资源
## 启动与使用
- IDEA
- jar
- docker
## 如果觉得对您有帮助,请点右上角 "Star" 支持一下吧,谢谢!
## 文档
## 展示
#### 监控
[监控系统预览地址](http://grafana.itlym.cn)(访客账号密码:`shoulder` / `shoulder123`,演示账号仅包含仪表盘查看权限,不能编辑)
![主机监控](img/host.png)
![prometheus + grafana 监控 docker](img/docker.png)
![监控redis](img/redis.png)
![nacos1](img/nacos1.png)
![nacos2](img/nacos2.png)
![mysql1](img/mysql1.png)
![mysql2](img/mysql2.png)
![mysql3](img/mysql3.png)
#### 日志收集
ELK展示nginx日志演示
查看所有访问 grafana.itlym.cn 的访问日志
![ELK展示nginx日志演示过滤访问 grafana.itlym.cn 的记录](img/elk-nginx.png)
查看所有请求时间大于 200ms 的访问日志
![ELK展示nginx日志演示过滤访问 grafana.itlym.cn 的记录](img/elk-nginx.png)
## 项目代码地址
| 项目 | 开源地址 | 说明 |
|---|---|---|
| Shoulder Framework | [github](https://github.com/ChinaLym/Shoulder-Framework)、[gitee](https://gitee.com/ChinaLym/shoulder-framework) | 开发框架,在 Spring Boot 基础之上,结合[软件优雅设计与开发最佳实践](http://spec.itlym.cn),增加常用的功能,任何基于`Spring Boot`/`Spring Cloud`的项目都可以使用。 |
| shoulder-framework-demo | [github](https://github.com/ChinaLym/shoulder-framework-demo)、[gitee](https://gitee.com/ChinaLym/shoulder-framework) | 以简单的例子介绍 `Shoulder Framework` 的使用 |
| shoulder-plugins | [github](https://github.com/ChinaLym/shoulder-plugins)、[gitee](https://gitee.com/ChinaLym/shoulder-plugins) | shoulder 提供的的减少开发工作量的`maven`插件(非必须,如遵循[软件优雅设计与开发最佳实践-国际化开发](http://doc.itlym.cn/specs/base/i18n.html)时推荐希望使用自动生成多语言翻译资源文件的插件减少开发工作量) |
| shoulder-lombok | [github](https://github.com/ChinaLym/shoulder-lombok)、[gitee](https://gitee.com/ChinaLym/shoulder-lombok) | 在`lombok`之上,增加 `@SLog` 注解,用于简化[软件优雅设计与开发最佳实践-错误码与日志](http://spec.itlym.cn/specs/base/errorCode.html) -shoulder 实现的日志框架的使用(非必须) |
| shoulder-lombok-idea-plugin | [github](https://github.com/ChinaLym/lombok-intellij-plugin)、[gitee](https://gitee.com/ChinaLym/lombok-intellij-plugin) | 在 `lombok-idea-plugin`之上,在 IDEA 中增加`@SLog`的编码提示,以更好的使用 `shoulder-lombok`(非必须,使用 shoulder-lombok 时推荐) |
| **Shoulder Platform** | [github](https://github.com/ChinaLym/Shoulder-Platform)、[gitee](https://gitee.com/ChinaLym/shoulder-Platform) | SaaS 开发平台,提供了基础通用能力,与具体业务无关 |
| Shoulder iPaaS | [github](https://github.com/ChinaLym/shoulder-iPaaS)、[gitee](https://gitee.com/ChinaLym/shoulder-iPaaS) | iPaaS 平台,介绍了常见中间件、监控系统、私有基础平台如何部署 |
## 层次设计
| 层次 | 定位 | 方案 | Shoulder 支持 |
|---|---|---|---|
| 业务应用服务 `SaaS` | 面向用户设计,更应该考虑如何方便用户 | 使用者根据实际业务把握 | `shoulder-framework` 提供了一些常用的能力,以及规约的对接;`shoulder-platform-common` 提供了快速开发一个与 `shoulder-platform` 设计、技术、风格统一的应用服务 |
| 平台对接开发包 `SDK` | 降低使用者调用 `shoulder` 的开发成本和难度 | 以 Spring Boot 自动装配形式提供包含使用文档和Demo | 提供对接 shoulder-platform的默认实现使用者也可根据平台api接口文档自行实现 |
| 共性业务层 `aPaaS` | 通用基础功能如认证、注册、授权、通知推送、知识库、错误码查询等 | api网关、web管理平台、用户中心、通知中心 | |
| 开发脚手架 `工具` | 统一维护共性代码,提供常用能力如异常拦截、错误码、安全加密等,统一管理技术和依赖版本 | `spring boot``spring cloud``shoulder-framework``shoulder-platform-common` 等 | 提供一些常用的功能封装,**可直接用于任何项目** |
| 软件开发设计理论指导 `理论` | 软件开发设计理论指导,主要为了系统的易维护、易扩展、易观测、安全性 | 总结业界开发设计实践经验如 `阿里巴巴Java开发规范` 结合而成,详见[优雅软件设计规范](http://spec.itlym.cn) | shoulder给予了一定的理论指导但这是**可选的**,不强制使用者必须遵循 |
| 软件平台基础层 `iPaaS` | 无业务含义的基础中间件,数据库、消息队列、监控中间件、告警中间件等 | MySql、RabbitMQ、Nacos、Zipkin、ElasticSearch、Docker、K8s 等,以 `Docker` 镜像方式提供 | 提供大部分场景的最佳技术方案选型,安装、部署、参数调优方案,**可直接用于任何项目** |
| 硬件基础层 `IaaS` | 硬件支撑如CPU、内存、网络、存储等 | 依赖云主机厂商,如阿里云、腾讯云、亚马逊云等 | 无shoulder不干涉该层 |

109
banner-font.txt Normal file
View File

@@ -0,0 +1,109 @@
- http://patorjk.com/software/taag/#p=display&f=Ivrit&t=shoulder
- http://www.network-science.de/ascii/
---
Ivrit
____ _ _ _ ____ _
/ ___|| |__ ___ _ _| | __| | ___ _ __ / ___| __ _| |_ _____ ____ _ _ _
\___ \| '_ \ / _ \| | | | |/ _` |/ _ \ '__| | | _ / _` | __/ _ \ \ /\ / / _` | | | |
___) | | | | (_) | |_| | | (_| | __/ | | |_| | (_| | || __/\ V V / (_| | |_| |
|____/|_| |_|\___/ \__,_|_|\__,_|\___|_| \____|\__,_|\__\___| \_/\_/ \__,_|\__, |
|___/
Rounded
______ _ _ _ ______
/ _____) | | | | | / ______) _
( (____ | |__ ___ _ _| | __| |_____ ____ | | ___ _____ _| |_ _____ _ _ _ _____ _ _
\____ \| _ \ / _ \| | | | |/ _ | ___ |/ ___) | | (_ (____ (_ _) ___ | | | (____ | | | |
_____) ) | | | |_| | |_| | ( (_| | ____| | | |___) / ___ | | |_| ____| | | / ___ | |_| |
(______/|_| |_|\___/|____/ \_)____|_____)_| \_____/\_____| \__)_____)\___/\_____|\__ |
(____/
|___/
Slant
_____ __ __ __ ______ __
/ ___// /_ ____ __ __/ /___/ /__ _____ / ____/___ _/ /____ _ ______ ___ __
\__ \/ __ \/ __ \/ / / / / __ / _ \/ ___/ / / __/ __ `/ __/ _ \ | /| / / __ `/ / / /
___/ / / / / /_/ / /_/ / / /_/ / __/ / / /_/ / /_/ / /_/ __/ |/ |/ / /_/ / /_/ /
/____/_/ /_/\____/\__,_/_/\__,_/\___/_/ \____/\__,_/\__/\___/|__/|__/\__,_/\__, /
/____/
Doom
_____ _ _ _ _____ _
/ ___| | | | | | | __ \ | |
\ `--.| |__ ___ _ _| | __| | ___ _ __ | | \/ __ _| |_ _____ ____ _ _ _
`--. \ '_ \ / _ \| | | | |/ _` |/ _ \ '__| | | __ / _` | __/ _ \ \ /\ / / _` | | | |
/\__/ / | | | (_) | |_| | | (_| | __/ | | |_\ \ (_| | || __/\ V V / (_| | |_| |
\____/|_| |_|\___/ \__,_|_|\__,_|\___|_| \____/\__,_|\__\___| \_/\_/ \__,_|\__, |
__/ |
Big Money-ne
/$$$$$$ /$$ /$$ /$$ /$$$$$$ /$$
/$$__ $$| $$ | $$ | $$ /$$__ $$ | $$
| $$ \__/| $$$$$$$ /$$$$$$ /$$ /$$| $$ /$$$$$$$ /$$$$$$ /$$$$$$ | $$ \__/ /$$$$$$ /$$$$$$ /$$$$$$ /$$ /$$ /$$ /$$$$$$ /$$ /$$
| $$$$$$ | $$__ $$ /$$__ $$| $$ | $$| $$ /$$__ $$ /$$__ $$ /$$__ $$ | $$ /$$$$ |____ $$|_ $$_/ /$$__ $$| $$ | $$ | $$ |____ $$| $$ | $$
\____ $$| $$ \ $$| $$ \ $$| $$ | $$| $$| $$ | $$| $$$$$$$$| $$ \__/ | $$|_ $$ /$$$$$$$ | $$ | $$$$$$$$| $$ | $$ | $$ /$$$$$$$| $$ | $$
/$$ \ $$| $$ | $$| $$ | $$| $$ | $$| $$| $$ | $$| $$_____/| $$ | $$ \ $$ /$$__ $$ | $$ /$$| $$_____/| $$ | $$ | $$ /$$__ $$| $$ | $$
| $$$$$$/| $$ | $$| $$$$$$/| $$$$$$/| $$| $$$$$$$| $$$$$$$| $$ | $$$$$$/| $$$$$$$ | $$$$/| $$$$$$$| $$$$$/$$$$/| $$$$$$$| $$$$$$$
\______/ |__/ |__/ \______/ \______/ |__/ \_______/ \_______/|__/ \______/ \_______/ \___/ \_______/ \_____/\___/ \_______/ \____ $$
/$$ | $$
| $$$$$$/
\______/
Sub-Zero
______ __ __ ______ __ __ __ _____ ______ ______ ______ ______ ______ ______ __ __ ______ __ __
/\ ___\ /\ \_\ \ /\ __ \ /\ \/\ \ /\ \ /\ __-. /\ ___\ /\ == \ /\ ___\ /\ __ \ /\__ _\ /\ ___\ /\ \ _ \ \ /\ __ \ /\ \_\ \
\ \___ \ \ \ __ \ \ \ \/\ \ \ \ \_\ \ \ \ \____ \ \ \/\ \ \ \ __\ \ \ __< \ \ \__ \ \ \ __ \ \/_/\ \/ \ \ __\ \ \ \/ ".\ \ \ \ __ \ \ \____ \
\/\_____\ \ \_\ \_\ \ \_____\ \ \_____\ \ \_____\ \ \____- \ \_____\ \ \_\ \_\ \ \_____\ \ \_\ \_\ \ \_\ \ \_____\ \ \__/".~\_\ \ \_\ \_\ \/\_____\
\/_____/ \/_/\/_/ \/_____/ \/_____/ \/_____/ \/____/ \/_____/ \/_/ /_/ \/_____/ \/_/\/_/ \/_/ \/_____/ \/_/ \/_/ \/_/\/_/ \/_____/
3D-ASCII
________ ___ ___ ________ ___ ___ ___ ________ _______ ________ ________ ________ _________ _______ ___ __ ________ ___ ___
|\ ____\|\ \|\ \|\ __ \|\ \|\ \|\ \ |\ ___ \|\ ___ \ |\ __ \ |\ ____\|\ __ \|\___ ___\\ ___ \ |\ \ |\ \|\ __ \ |\ \ / /|
\ \ \___|\ \ \\\ \ \ \|\ \ \ \\\ \ \ \ \ \ \_|\ \ \ __/|\ \ \|\ \ \ \ \___|\ \ \|\ \|___ \ \_\ \ __/|\ \ \ \ \ \ \ \|\ \ \ \ \/ / /
\ \_____ \ \ __ \ \ \\\ \ \ \\\ \ \ \ \ \ \ \\ \ \ \_|/_\ \ _ _\ \ \ \ __\ \ __ \ \ \ \ \ \ \_|/_\ \ \ __\ \ \ \ __ \ \ \ / /
\|____|\ \ \ \ \ \ \ \\\ \ \ \\\ \ \ \____\ \ \_\\ \ \ \_|\ \ \ \\ \| \ \ \|\ \ \ \ \ \ \ \ \ \ \ \_|\ \ \ \|\__\_\ \ \ \ \ \ \/ / /
____\_\ \ \__\ \__\ \_______\ \_______\ \_______\ \_______\ \_______\ \__\\ _\ \ \_______\ \__\ \__\ \ \__\ \ \_______\ \____________\ \__\ \__\__/ / /
|\_________\|__|\|__|\|_______|\|_______|\|_______|\|_______|\|_______|\|__|\|__| \|_______|\|__|\|__| \|__| \|_______|\|____________|\|__|\|__|\___/ /
\|_________| \|___|/
Larry 3D
____ __ ___ __ ____ __
/\ _`\ /\ \ /\_ \ /\ \ /\ _`\ /\ \__
\ \,\L\_\ \ \___ ___ __ __\//\ \ \_\ \ __ _ __ \ \ \L\_\ __ \ \ ,_\ __ __ __ __ __ __ __
\/_\__ \\ \ _ `\ / __`\/\ \/\ \ \ \ \ /'_` \ /'__`\/\`'__\ \ \ \L_L /'__`\ \ \ \/ /'__`\/\ \/\ \/\ \ /'__`\ /\ \/\ \
/\ \L\ \ \ \ \ \/\ \L\ \ \ \_\ \ \_\ \_/\ \L\ \/\ __/\ \ \/ \ \ \/, \/\ \L\.\_\ \ \_/\ __/\ \ \_/ \_/ \/\ \L\.\_\ \ \_\ \
\ `\____\ \_\ \_\ \____/\ \____/ /\____\ \___,_\ \____\\ \_\ \ \____/\ \__/.\_\\ \__\ \____\\ \___x___/'\ \__/.\_\\/`____ \
\/_____/\/_/\/_/\/___/ \/___/ \/____/\/__,_ /\/____/ \/_/ \/___/ \/__/\/_/ \/__/\/____/ \/__//__/ \/__/\/_/ `/___/> \
/\___/
Alpha
_____ _____ _______ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____
/\ \ /\ \ /::\ \ /\ \ /\ \ /\ \ /\ \ /\ \ /\ \ /\ \ /\ \ /\ \ /\ \ /\ \ |\ \
/::\ \ /::\____\ /::::\ \ /::\____\ /::\____\ /::\ \ /::\ \ /::\ \ /::\ \ /::\ \ /::\ \ /::\ \ /::\____\ /::\ \ |:\____\
/::::\ \ /:::/ / /::::::\ \ /:::/ / /:::/ / /::::\ \ /::::\ \ /::::\ \ /::::\ \ /::::\ \ \:::\ \ /::::\ \ /:::/ / /::::\ \ |::| |
/::::::\ \ /:::/ / /::::::::\ \ /:::/ / /:::/ / /::::::\ \ /::::::\ \ /::::::\ \ /::::::\ \ /::::::\ \ \:::\ \ /::::::\ \ /:::/ _/___ /::::::\ \ |::| |
/:::/\:::\ \ /:::/ / /:::/~~\:::\ \ /:::/ / /:::/ / /:::/\:::\ \ /:::/\:::\ \ /:::/\:::\ \ /:::/\:::\ \ /:::/\:::\ \ \:::\ \ /:::/\:::\ \ /:::/ /\ \ /:::/\:::\ \ |::| |
/:::/__\:::\ \ /:::/____/ /:::/ \:::\ \ /:::/ / /:::/ / /:::/ \:::\ \ /:::/__\:::\ \ /:::/__\:::\ \ /:::/ \:::\ \ /:::/__\:::\ \ \:::\ \ /:::/__\:::\ \ /:::/ /::\____\ /:::/__\:::\ \ |::| |
\:::\ \:::\ \ /::::\ \ /:::/ / \:::\ \ /:::/ / /:::/ / /:::/ \:::\ \ /::::\ \:::\ \ /::::\ \:::\ \ /:::/ \:::\ \ /::::\ \:::\ \ /::::\ \ /::::\ \:::\ \ /:::/ /:::/ / /::::\ \:::\ \ |::| |
___\:::\ \:::\ \ /::::::\ \ _____ /:::/____/ \:::\____\ /:::/ / _____ /:::/ / /:::/ / \:::\ \ /::::::\ \:::\ \ /::::::\ \:::\ \ /:::/ / \:::\ \ /::::::\ \:::\ \ /::::::\ \ /::::::\ \:::\ \ /:::/ /:::/ _/___ /::::::\ \:::\ \ |::|___|______
/\ \:::\ \:::\ \ /:::/\:::\ \ /\ \ |:::| | |:::| | /:::/____/ /\ \ /:::/ / /:::/ / \:::\ ___\ /:::/\:::\ \:::\ \ /:::/\:::\ \:::\____\ /:::/ / \:::\ ___\ /:::/\:::\ \:::\ \ /:::/\:::\ \ /:::/\:::\ \:::\ \ /:::/___/:::/ /\ \ /:::/\:::\ \:::\ \ /::::::::\ \
/::\ \:::\ \:::\____\/:::/ \:::\ /::\____\|:::|____| |:::| ||:::| / /::\____\/:::/____/ /:::/____/ \:::| |/:::/__\:::\ \:::\____\/:::/ \:::\ \:::| | /:::/____/ ___\:::| |/:::/ \:::\ \:::\____\ /:::/ \:::\____\/:::/__\:::\ \:::\____\|:::| /:::/ /::\____\/:::/ \:::\ \:::\____\ /::::::::::\____\
\:::\ \:::\ \::/ /\::/ \:::\ /:::/ / \:::\ \ /:::/ / |:::|____\ /:::/ /\:::\ \ \:::\ \ /:::|____|\:::\ \:::\ \::/ /\::/ |::::\ /:::|____| \:::\ \ /\ /:::|____|\::/ \:::\ /:::/ / /:::/ \::/ /\:::\ \:::\ \::/ /|:::|__/:::/ /:::/ /\::/ \:::\ /:::/ / /:::/~~~~/~~
\:::\ \:::\ \/____/ \/____/ \:::\/:::/ / \:::\ \ /:::/ / \:::\ \ /:::/ / \:::\ \ \:::\ \ /:::/ / \:::\ \:::\ \/____/ \/____|:::::\/:::/ / \:::\ /::\ \::/ / \/____/ \:::\/:::/ / /:::/ / \/____/ \:::\ \:::\ \/____/ \:::\/:::/ /:::/ / \/____/ \:::\/:::/ / /:::/ /
\:::\ \:::\ \ \::::::/ / \:::\ /:::/ / \:::\ \ /:::/ / \:::\ \ \:::\ \ /:::/ / \:::\ \:::\ \ |:::::::::/ / \:::\ \:::\ \/____/ \::::::/ / /:::/ / \:::\ \:::\ \ \::::::/ /:::/ / \::::::/ / /:::/ /
\:::\ \:::\____\ \::::/ / \:::\__/:::/ / \:::\ /:::/ / \:::\ \ \:::\ /:::/ / \:::\ \:::\____\ |::|\::::/ / \:::\ \:::\____\ \::::/ / /:::/ / \:::\ \:::\____\ \::::/___/:::/ / \::::/ / /:::/ /
\:::\ /:::/ / /:::/ / \::::::::/ / \:::\__/:::/ / \:::\ \ \:::\ /:::/ / \:::\ \::/ / |::| \::/____/ \:::\ /:::/ / /:::/ / \::/ / \:::\ \::/ / \:::\__/:::/ / /:::/ / \::/ /
\:::\/:::/ / /:::/ / \::::::/ / \::::::::/ / \:::\ \ \:::\/:::/ / \:::\ \/____/ |::| ~| \:::\/:::/ / /:::/ / \/____/ \:::\ \/____/ \::::::::/ / /:::/ / \/____/
\::::::/ / /:::/ / \::::/ / \::::::/ / \:::\ \ \::::::/ / \:::\ \ |::| | \::::::/ / /:::/ / \:::\ \ \::::::/ / /:::/ /
\::::/ / /:::/ / \::/____/ \::::/ / \:::\____\ \::::/ / \:::\____\ \::| | \::::/ / /:::/ / \:::\____\ \::::/ / /:::/ /
\::/ / \::/ / ~~ \::/____/ \::/ / \::/____/ \::/ / \:| | \::/____/ \::/ / \::/ / \::/____/ \::/ /
\/____/ \/____/ ~~ \/____/ ~~ \/____/ \|___| \/____/ \/____/ ~~ \/____/

6
doc/README.MD Normal file
View File

@@ -0,0 +1,6 @@
需要放到 NACOS 里的应用配置信息
appConfig 中的配置文件信息是在启动时读取的信息,
实际中这些信息应放到 nacos 中维护并在 mysql 持久化备份,
这里为了将其一并开源,特地放在这里
注意配置文件中ip、端口号、账号密码需要修改为自己的

View File

@@ -0,0 +1,9 @@
boot:
admin:
client:
url: localhost:12365
#username:
#password:
instance:
prefer-ip: true
service-url: localhost:8080

100
doc/appConfig/common.yml Normal file
View File

@@ -0,0 +1,100 @@
# 所有服务和环境下都不变的配置
# 个性化配置:复制本配置到 {服务}-${profiles.active}.yml 文件中进行修改
shoulder:
application:
id: ${spring.application.name}
# errorCodePrefix: # 每个应用唯一
# version: # 从 pom.xml 获取
# dateFormat: "yyyy-MM-dd'T'HH:mm:ss.SSS Z" # 默认 yyyy-MM-dd'T'HH:mm:ss.SSS Z
defaultLocale: zh_CN
charset: UTF-8
cluster: false
# 服务器配置
server:
undertow:
io-threads: 8 # 线程数, 主要执行非阻塞的任务。推荐与 CPU 核数相同
worker-threads: 120 # 阻塞任务线程池, 执行类似servlet请求阻塞操作。推荐为 CPU 核数 * 8
buffer-size: 1024 # 用于服务器连接的IO操作,类似netty的池化内存管理。推荐略大于绝大多数请求的大小根据自己的实际场景决定
direct-buffers: true # 是否分配的直接内存(堆外内存,避免 GC、复制。推荐开启
spring:
# servlet 配置
servlet:
multipart:
max-file-size: 128MB # 上传文件最大大小默认1M
max-request-size: 128MB # 请求最大大小默认10M
# http 配置
http:
encoding:
charset: ${shoulder.application.charset} # 使用统一编码
force: true
enabled: true
zipkin:
sender:
type: RABBIT
enabled: ${shoulder.zipkin.enabled}
discoveryClientEnabled: true
baseUrl: http://localhost:9411/ #http://shoulder-zipkin:8772/
compression: # 压缩
enabled: true
locator: # 通过 nacos 动态获取地址
discovery:
enabled: true
rabbitmq: # 使用指定的队列
queue: shoulder_zipkin
# 采集率,默认 0.1 (记录 10% 的请求,过高会影响性能)
sleuth:
enabled: ${shoulder.zipkin.enabled}
sampler:
probability: 1.0
# 健康检查
management:
endpoints:
web:
base-path: /actuator
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS
enabled: true
# Feign 配置
feign:
httpclient:
enabled: false
okhttp:
enabled: true
hystrix:
enabled: true # 开启熔断机制
compression: # 压缩请求
request:
enabled: true
mime-types: text/xml,application/xml,application/json
min-request-size: 2048
response: # 响应压缩
enabled: true
# ribbon 配置
ribbon:
httpclient:
enabled: false
okhttp:
enabled: true
ReadTimeout: 30000 # 响应流读取超时时间,
ConnectTimeout: 30000 # 注意:要小于熔断超时时间,否则将被熔断
MaxAutoRetries: 0 # 最大自动重试次数(不切换服务地址)
MaxAutoRetriesNextServer: 2 # 最大自动服务地址切换重试次数
OkToRetryOnAllOperations: false #无论是请求超时或者socket read timeout都进行重试
# 统一日志记录位置
logging:
file:
path: /logs
name: ${logging.file.path}/${spring.application.name}/${spring.application.name}.log

103
doc/appConfig/db-mysql.yml Normal file
View File

@@ -0,0 +1,103 @@
# 数据库 配置模板
# 优先从环境变量里取值
shoulder:
database: # 数据库配置请看DatabaseProperties类上的注释
driverClassName: com.mysql.cj.jdbc.Driver
conn-schema: jdbc:mysql
ip: ${MYSQL_IP:127.0.0.1}
port: ${MYSQL_PORT:3306}
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PWD:root}
addr: ${MYSQL_ADDR:'${shoulder.database.ip}:${shoulder.database.port}'}
database: shoulder_database
# utf8字符集、+8 时区、使用 unicode、关闭 ssl、自动重连、忽略错误的时间使用null代替、单次发送多条语句分号分隔
conn-param: characterEncoding=utf8&serverTimezone=CTT&useUnicode=true&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
url: ${shoulder.database.conn-schema}://${shoulder.database.ip}:${shoulder.database.port}/${shoulder.database.database}?${shoulder.database.conn-param}
bizDatabase: shoulder_base
multiTenantType: SCHEMA
isNotWrite: false
isBlockAttack: false # 是否启用 攻击 SQL 阻断解析器
worker-id: 0
data-center-id: 0
spring:
jpa:
database: MYSQL
hibernate:
#ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
show-sql: true
# ============================ 数据库无关的配置 ============================
datasource:
# 多数据源配置
#nameList: beecp
# beecp
type: cn.beecp.BeeDataSource
driverClassName: cn.beecp.BeeDataSource
url: ${shoulder.database.url}
username: ${shoulder.database.username}
password: ${shoulder.database.password}
druid:
username: ${shoulder.database.username}
password: ${shoulder.database.password}
driver-class-name: ${shoulder.database.driverClassName}
url: ${shoulder.database.url}
db-type: mysql
initialSize: 10
minIdle: 10
maxActive: 500
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
validation-query: SELECT '1'
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
min-evictable-idle-time-millis: 300000 #配置一个连接在池中最小生存的时间,单位是毫秒
filters: stat,wall
filter:
wall:
enabled: true
config:
commentAllow: true
multiStatementAllow: true
noneBaseStatementAllow: true
web-stat-filter: # WebStatFilter配置说明请参考Druid Wiki配置_配置WebStatFilter
enabled: true
url-pattern: /*
exclusions: "*.js , *.gif ,*.jpg ,*.png ,*.css ,*.ico , /druid/*"
session-stat-max-count: 1000
profile-enable: true
session-stat-enable: false
stat-view-servlet: #展示Druid的统计信息,StatViewServlet的用途包括1.提供监控信息展示的html页面2.提供监控信息的JSON API
enabled: true
url-pattern: /druid/* #根据配置中的url-pattern来访问内置监控页面如果是上面的配置内置监控页面的首页是/druid/index.html例如http://127.0.0.1:9000/druid/index.html
reset-enable: true #允许清空统计数据
login-username: shoulder
login-password: shoulder
mybatis-plus:
mapper-locations:
- classpath*:mapper_**/**/*Mapper.xml
#实体扫描多个package用逗号或者分号分隔 todo 修改这里
typeAliasesPackage: com.github.shoulder.*.entity;com.github.shoulder.database.mybatis.typehandler
typeEnumsPackage: com.github.shoulder.*.enumeration
global-config:
db-config:
id-type: INPUT
insert-strategy: NOT_NULL
update-strategy: NOT_NULL
select-strategy: NOT_EMPTY
configuration:
#配置返回数据库(column下划线命名&&返回java实体是驼峰命名)自动匹配无需as没开启这个SQL需要写as select user_id as userId
map-underscore-to-camel-case: true
cache-enabled: false
#配置JdbcTypeForNull, oracle数据库必须配置
jdbc-type-for-null: 'null'

89
doc/appConfig/db.yml Normal file
View File

@@ -0,0 +1,89 @@
# mysql 配置模板
# 优先从环境变量里取值
shoulder:
mysql:
ip: 127.0.0.1
port: 3306
driverClassName: com.mysql.cj.jdbc.Driver
database: db_shoulder_platform
username: root
password: root
database: # 数据库配置请看 DatabaseProperties 类上的注释
bizDatabase: shoulder_base
multiTenantType: SCHEMA
isNotWrite: false
isBlockAttack: false # 是否启用 攻击 SQL 阻断解析器
worker-id: 0
data-center-id: 0
# mysql 通用配置
spring:
datasource:
druid:
username: ${shoulder.mysql.username}
password: ${shoulder.mysql.password}
driver-class-name: ${shoulder.mysql.driverClassName}
url: jdbc:mysql://${shoulder.mysql.ip}:${shoulder.mysql.port}/${shoulder.mysql.database}?serverTimezone=CTT&characterEncoding=utf8&useUnicode=true&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
db-type: mysql
initialSize: 10
minIdle: 10
maxActive: 500
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
validation-query: SELECT 'x'
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
min-evictable-idle-time-millis: 300000 #配置一个连接在池中最小生存的时间,单位是毫秒
filters: stat,wall
filter:
wall:
enabled: true
config:
commentAllow: true
multiStatementAllow: true
noneBaseStatementAllow: true
web-stat-filter: # WebStatFilter配置说明请参考Druid Wiki配置_配置WebStatFilter
enabled: true
url-pattern: /*
exclusions: "*.js , *.gif ,*.jpg ,*.png ,*.css ,*.ico , /druid/*"
session-stat-max-count: 1000
profile-enable: true
session-stat-enable: false
stat-view-servlet: #展示Druid的统计信息,StatViewServlet的用途包括1.提供监控信息展示的html页面2.提供监控信息的JSON API
enabled: true
url-pattern: /druid/* #根据配置中的url-pattern来访问内置监控页面如果是上面的配置内置监控页面的首页是/druid/index.html例如http://127.0.0.1:9000/druid/index.html
reset-enable: true #允许清空统计数据
login-username: shoulder
login-password: shoulder
jpa:
database: MYSQL
hibernate:
#ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
show-sql: true
mybatis-plus:
mapper-locations:
- classpath*:mapper_**/**/*Mapper.xml
#实体扫描多个package用逗号或者分号分隔 todo 修改这里
#typeAliasesPackage:
#typeEnumsPackage:
global-config:
db-config:
id-type: INPUT
insert-strategy: NOT_NULL
update-strategy: NOT_NULL
select-strategy: NOT_EMPTY
configuration:
# 下划线列名自动转驼峰java对象属性名
map-underscore-to-camel-case: true
cache-enabled: false
#配置JdbcTypeForNull, oracle数据库必须配置
jdbc-type-for-null: 'null'

View File

@@ -0,0 +1,13 @@
spring:
freemarker:
allow-request-override: false
cache: false
charset: UTF-8
check-template-location: true
content-type: text/html
enabled: true
expose-request-attributes: false
expose-session-attributes: false
expose-spring-macro-helpers: false
suffix: .flt
template-loader-path: classpath:/static/template/

View File

@@ -0,0 +1,19 @@
# rabbitmq 配置模板
# 优先从环境变量里取值
shoulder:
rabbitmq:
ip: ${RABBITMQ_IP:127.0.0.1}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:shoulder}
password: ${RABBITMQ_PASSWORD:shoulder}
spring:
rabbitmq:
enable: true
host: ${shoulder.rabbitmq.ip}
port: ${shoulder.rabbitmq.port}
username: ${shoulder.rabbitmq.username}
password: ${shoulder.rabbitmq.password}
listener:
type: direct # simple direct

54
doc/appConfig/redis.yml Normal file
View File

@@ -0,0 +1,54 @@
# redis 配置模板
# 优先从环境变量里取值
shoulder:
redis:
ip: ${REDIS_IP:127.0.0.1}
port: ${REDIS_PORT:5672}
username: ${REDIS_USERNAME:shoulder} # redis6 支持用户名
password: ${REDIS_PASSWORD:shoulder}
database: 0
spring:
cache:
type: GENERIC
redis:
host: ${shoulder.redis.ip}
password: ${shoulder.redis.password}
port: ${shoulder.redis.port}
database: ${shoulder.redis.database}
j2cache:
# config-location: /j2cache.properties
open-spring-cache: true
cache-clean-mode: passive
allow-null-values: true
redis-client: lettuce
l2-cache-open: true
# l2-cache-open: false # 关闭二级缓存
broadcast: net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
# broadcast: jgroups # 关闭二级缓存
L1:
provider_class: caffeine
L2:
provider_class: net.oschina.j2cache.cache.support.redis.SpringRedisProvider
config_section: lettuce
sync_ttl_to_redis: true
default_cache_null_object: false
serialization: fst
caffeine:
properties: /j2cache/caffeine.properties # 这个配置文件需要放在项目中
lettuce:
mode: single
namespace: ''
storage: generic
channel: j2cache
scheme: redis
hosts: ${shoulder.redis.ip}:${shoulder.redis.port}
password: ${shoulder.redis.password}
database: ${shoulder.redis.database}
sentinelMasterId: ''
maxTotal: 100
maxIdle: 10
minIdle: 10
timeout: 10000

View File

@@ -0,0 +1,88 @@
shoulder:
log:
enabled: false
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #表明gateway开启服务注册和发现的功能并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router这个router将以服务名开头的请求路径转发到对应的服务。
lowerCaseServiceId: true #是将请求路径上的服务名配置为小写(因为服务注册的时候,向注册中心注册时将服务名转成大写的了),比如以/service-hi/*的请求路径被路由转发到服务名为service-hi的服务上。
filters:
- StripPrefix=1 # 去掉前缀
x-forwarded:
prefixEnabled: false
routes:
- id: storage
uri: lb://shoulder-storage-center
predicates:
- Path=/file/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: default
fallbackUri: 'forward:/fallback'
- id: authority
uri: lb://shoulder-authority-server
predicates:
- Path=/authority/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: default
fallbackUri: 'forward:/fallback'
- id: msgs
uri: lb://shoulder-msgs-server
predicates:
- Path=/msgs/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: default
fallbackUri: 'forward:/fallback'
- id: demo
uri: lb://shoulder-demo-server
predicates:
- Path=/demo/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: default
fallbackUri: 'forward:/fallback'
- id: order
uri: lb://shoulder-order-server
predicates:
- Path=/order/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: default
fallbackUri: 'forward:/fallback'
filters:
- name: Hystrix
args:
name: default
fallbackUri: 'forward:/fallback'
server:
port: 8760
servlet:
context-path: /api # = server.servlet.context-path
authentication:
user:
header-name: token
pub-key: client/pub.key # 解密

View File

@@ -0,0 +1,9 @@
server:
port: 8768
test:
sms:
phoneNumber: 15858193327
template-code: SMS_184220232
email:
receiver-address: 1730398492@qq.com

View File

@@ -0,0 +1,48 @@
shoulder:
nginx:
ip: ${spring.cloud.client.ip-address} # 正式环境需要将该ip设置成nginx对应的 公网ip
port: 10000 # 正式环境需要将该ip设置成nginx对应的 公网端口
swagger:
enabled: true
docket:
file:
title: 存储服务
base-package: cn.itlym.shoulder.platform.storage.controller
general:
title: 通用模块
base-package: cn.itlym.shoulder.common.controller
file:
type: LOCAL # FAST_DFS LOCAL
storage-path: /data/projects/uploadfile/file/ # 文件存储路径 某些版本的 window 需要改成 D:\\data\\projects\\uploadfile\\file\\
uriPrefix: http://${shoulder.nginx.ip}:${shoulder.nginx.port}/file/ # 文件访问 需要通过这个uri前缀进行访问
inner-uri-prefix: null # 内网的url前缀
down-by-id: http://${shoulder.nginx.ip}:${shoulder.nginx.port}/api/file/attachment/download?ids[]=%s
down-by-biz-id: http://${shoulder.nginx.ip}:${shoulder.nginx.port}/api/file/attachment/download/biz?bizIds[]=%s
down-by-url: http://${shoulder.nginx.ip}:${shoulder.nginx.port}/api/file/attachment/download/url?url=%s&filename=%s
ali:
# 请填写自己的阿里云存储配置
uriPrefix: http://test.oss-cn-shenzhen.aliyuncs.com/
bucket-name: test
endpoint: http://oss-cn-shenzhen.aliyuncs.com
access-key-id: test
access-key-secret: test
#FAST_DFS配置
fdfs:
soTimeout: 1500
connectTimeout: 600
thumb-image:
width: 150
height: 150
tracker-list:
- 127.0.0.1:12345
pool:
#从池中借出的对象的最大数目
max-total: 128
max-wait-millis: 100
jmx-name-base: 1
jmx-name-prefix: 1
server:
port: 10000

View File

@@ -0,0 +1,21 @@
# 第三方开发者账号信息,如对接阿里云、腾讯云、微信登陆等信息
ali-cloud:
access-key-id: changeme
access-secret: changeme
spring:
mail:
username: changeme
host: smtp.qq.com
password: changeme
properties:
mail:
smtp:
auth: true
connectiontimeout: 5000
timeout: 3000
writetimeout: 5000
starttls:
enable: true
required: true

11
doc/nacos.md Normal file
View File

@@ -0,0 +1,11 @@
# NACOS 使用说明
### NACOS namespace 和 groupId 使用
`namespace` 默认值为 `Public``groupId` 默认值为 `DEFAULT_GROUP`
环境隔离方案:
- [NACOS 官方给的方案](https://nacos.io/zh-cn/blog/namespace-endpoint-best-practices.html)
- [其他方案](https://www.cnblogs.com/larscheng/p/11411423.html)

7
doc/sentinel.yml Normal file
View File

@@ -0,0 +1,7 @@
spring:
cloud:
sentinel:
filter:
enabled: false
transport:
dashboard: localhost:8080

1
dynamicConfig/README.MD Normal file
View File

@@ -0,0 +1 @@
maven 多环境打包 配置信息自动切换

View File

@@ -0,0 +1,7 @@
# ע<>⣺value<75>Ľ<EFBFBD>β<EFBFBD><CEB2><EFBFBD>ֲ<EFBFBD>Ҫ<EFBFBD>пո<D0BF><D5B8><EFBFBD>maven-resources-plugin <20><><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD>ȥ<EFBFBD><C8A5>
nacos.ip=nacos.itlym.cn
nacos.port=8848
# <20><><EFBFBD><EFBFBD><E2BBA7>ʶ<EFBFBD><CAB6>Ĭ<EFBFBD>Ͼ<EFBFBD><CFBE><EFBFBD> public<69><63><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD>ڸ<EFBFBD><DAB8>벻ͬ<EBB2BB><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> dev<65><76>test<73><74>ע<EFBFBD>⣺nacos1.4ǰ<EFBFBD><EFBFBD>Ҫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>κ<EFBFBD>ֵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD> public<69><63><EFBFBD><EFBFBD><EFBFBD><EFBFBD> github 3460
nacos.namespace=
# nacos.group Ĭ<><C4AC>Ϊ DEFAULT_GROUP<55><50><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD>ڸ<EFBFBD><DAB8>벻ͬ<EBB2BB><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> pay<61><79>notPay<61><79>NACOS Ŀǰ<C4BF><C7B0><EFBFBD><EFBFBD><EFBFBD>˸<EFBFBD><CBB8><EFBFBD>չ<EFBFBD>㣬ʵ<E3A3AC>ʻ<EFBFBD><CABB><EFBFBD>֧<EFBFBD><D6A7>
seata.namespace=

View File

@@ -0,0 +1,4 @@
nacos.ip=127.0.0.1
nacos.port=8848
nacos.namespace=
seata.namespace=

BIN
img/architecture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

BIN
img/docker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

BIN
img/elk-nginx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

BIN
img/elk-nginx2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

BIN
img/host.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

BIN
img/mysql1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
img/mysql2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

BIN
img/mysql3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
img/nacos1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
img/nacos2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
img/redis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

25
pom.xml Normal file
View File

@@ -0,0 +1,25 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version><!-- shoulder-platform-version -->
<modules>
<module>shoulder-platform-common</module>
<module>shoulder-gateway</module>
<module>shoulder-auth-center</module>
<module>shoulder-notify-center</module>
<module>shoulder-pay-center</module>
<module>shoulder-storage-center</module>
<module>shoulder-system-center</module>
<module>shoulder-backstage</module>
<module>shoulder-generator</module>
</modules>
</project>

View File

@@ -0,0 +1,30 @@
# shoulder-platform
USER-CENTER
用户中心,提供 注册、登录、注销、RBAC 权限管理、认证、授权
支持单点登录Oauth2 给第三方授权,通过第三方 OIDC认证通过第三方 Oauth2 登录
提供管理租户、appKey 等 API
分为以下模块
- Account
- 提供账户能力与管理,包含 注册、登录、注销、RBAC 权限管理、认证、授权
- Authentication
- 认证
- Authority
- 授权
- Audit
- 日志审计
权限相关:
- [RBAC](https://zhuanlan.zhihu.com/p/98559681)
- [ACL, DAC, MAC, RBAC, ABAC模型的不同应用场景](https://zhuanlan.zhihu.com/p/70548562)
- [服务认证与鉴权](https://zhuanlan.zhihu.com/p/101595143)适合权限数量较少如OpenAPI
- 服务认证accessToken + refreshToken。前端每10分钟检查一下 refreshToken 过期时间,如果发现即将过期,提前申请 refreshToken
[gitee:六个高Star开源项目让你更懂OAuth和单点登录](https://zhuanlan.zhihu.com/p/187131269)

View File

@@ -0,0 +1,24 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform-parent</artifactId>
<version>1.0-SNAPSHOT</version><!-- shoulder-platform-version -->
<relativePath>../shoulder-platform-common/shoulder-platform-parent/pom.xml</relativePath>
</parent>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-auth-center</artifactId>
<packaging>pom</packaging>
<modules>
<module>shoulder-auth-api</module>
</modules>
</project>

View File

@@ -0,0 +1,16 @@
<?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>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform-parent</artifactId>
<version>1.0-SNAPSHOT</version><!-- shoulder-platform-version -->
<relativePath>../../shoulder-platform-common/shoulder-platform-parent/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shoulder-auth-api</artifactId>
</project>

View File

@@ -0,0 +1,72 @@
# shoulder-backstage
后台
- 用户管理
- 后台用户管理
- 用户组
- 锁定、冻结、解锁
- 登录、退出
- ~~组织管理~~
- 部门
- 地区
- 权限管理
- 权限(接口、界面、按钮、菜单、字段)
- 角色
- 用户-角色
- 菜单管理
- 跳到哪个url
- 提升管理系统的扩展性:跳转到应用个性后台
- 消息管理
- 消息推送记录
-
- 内容管理
- 留言管理
- 友情链接
- 调查问卷管理
- 运维体验提升
- 错误码统一管理(错误码、含义、建议措施、关联 FAQ 等)
- FAQ 文档管理(文档、标签、评论)
- 应用数据统计
- DNU 日新增用户
- DAU(Daily Active User)日活跃用户数量
- WAU 周活跃用户数量
- MAU(monthly active users)月活跃用户人数(重点)
- 用户留存
- 终端信息统计:手机型号、分辨率
- 用户群体与画像:年龄、位置、喜好等
- 租户管理
- 租户
- 租户权限
- 租户调用记录
- 运营管理
- 虚拟用户/内容系统(避免初期因数据少,留不住用户)
- 批量操作系统(一键批量随机点赞、
- ~~日志管理~~ 【放置于 ELK】
- 操作日志,记录对管理系统的操作
- 系统日志linux命令执行记录
- 运行日志(应用运行时产生的日志)
- 登录日志。记录管理系统的登录记录
- 开放能力管理
- 第三方管理:接入、登记
- 第三方使用记录
- 下载中心(可分类增加各种文件)
- 对接文档等
- 广告管理
- 广告商
- 展示位置、大小、优先级、目标用户
- 定期展示
- 系统设置
- 后台系统参数设置
- 应用系统参数设置:图标,节日图标
- 企业信息
- 新闻
- 语言
- 应用系统状态监控
- 各个服务状态
- 业务指标监控
- 告警、推送
- 安全项
- 数据备份
- 拦截设置
- 反爬强度

View File

@@ -0,0 +1,15 @@
<?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>shoulder-platform</artifactId>
<groupId>cn.itlym.platform</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shoulder-backstage</artifactId>
</project>

View File

@@ -0,0 +1,29 @@
# shoulder-platform-gateway
- 对外网关
- api-gateway 开放 api 网关
- web-gateway web网关
- app-gateway app网关
- 注:这些网关用于隔离内部细节,虽然是系统对外服务的门面,但一般也不会直接暴露于外部网络,而是位于 nginx 等代理服务器之后,系统服务之前。
- 对内网关
- biz-gateway 业务网关
- storage-gateway 对象存储,统一存储
- third-api-gateway 调用第三方接口过该网关,按照业务重要程度选择性单独部署
i.
.` i.
.` `i
,.·` i
.··` i
.··`` Spring 1
·` ./ i
: .// i
( ,.// i
: .##` ,:
`· .##i` .:
`:.###:, ,·`
.#. :###::` `--....-`
`·` ```

31
shoulder-gateway/pom.xml Normal file
View File

@@ -0,0 +1,31 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<parent>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform-parent</artifactId>
<version>1.0-SNAPSHOT</version><!-- shoulder-platform-version -->
<relativePath>../shoulder-platform-common/shoulder-platform-parent/pom.xml</relativePath>
</parent>
<artifactId>shoulder-gateway</artifactId>
<version>1.0-SNAPSHOT</version><!-- shoulder-platform-version -->
<modules>
<module>shoulder-api-gateway</module>
<module>shoulder-storage-gateway</module>
</modules>
<properties>
<shoulder.version>0.4-SNAPSHOT</shoulder.version><!-- shoulder-version -->
<spring-boot.version>2.2.8.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
</properties>
</project>

View File

@@ -0,0 +1,10 @@
# shoulder-api-gateway
接入层网关:安全、限流、日志、监控、缓存(业务无关)
API 服务网关超时、缓存、熔断、重试、查询聚合、数据校验时间、方法、版本、AppKey、签名
【贴近业务】
实际应用中,也可以根据自己的设计,将代理网关的能力赋予 API gateway 中。

View File

@@ -0,0 +1,76 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform-parent</artifactId>
<version>1.0-SNAPSHOT</version><!-- shoulder-platform-version -->
<relativePath>../../shoulder-platform-common/shoulder-platform-parent/pom.xml</relativePath>
</parent>
<artifactId>shoulder-api-gateway</artifactId>
<description>Shoulder 网关,支持跨域认证,追踪,限流,接口发布与撤销</description>
<name>${project.artifactId}</name>
<properties>
<shoulder.version>0.4-SNAPSHOT</shoulder.version><!-- shoulder-version -->
<spring-boot.version>2.2.8.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.itlym</groupId>
<artifactId>shoulder-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 服务注册与发现 -->
<dependency>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform-starter-discovery-client</artifactId>
<version>${shoulder-platform.version}</version>
</dependency>
<!-- 配置动态刷新 -->
<dependency>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform-starter-config-client</artifactId>
<version>${shoulder-platform.version}</version>
</dependency>
</dependencies>
<build>
<!-- maven 默认使用 artifactId+version拼接 -->
<finalName>${project.artifactId}</finalName>
<!-- 动态打包环境配置源文件 -->
<filters>
<filter>../../dynamicConfig/config-${profile.active}.properties</filter>
</filters>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>

View File

@@ -0,0 +1,27 @@
package cn.itlym.shoulder.platform.gateway;
import org.shoulder.core.dto.response.RestResult;
import org.shoulder.core.exception.CommonErrorCodeEnum;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import reactor.core.publisher.Mono;
/**
* 网关启动类
*
* @author lym
*/
@SpringBootApplication
@EnableDiscoveryClient
public class ShoulderApiGateway {
public static void main(String[] args) {
SpringApplication.run(ShoulderApiGateway.class, args);
}
@RequestMapping("/fallback")
public Mono<RestResult> fallback() {
return Mono.just(RestResult.error(CommonErrorCodeEnum.REQUEST_TIMEOUT));
}
}

View File

@@ -0,0 +1,30 @@
package cn.itlym.shoulder.platform.gateway.client;
import reactor.core.publisher.Mono;
/**
* st 相关
*
* @author lym
*/
public interface ServiceTokenClient {
/**
* 通过 accessToken 获取内部 st
*
* @param accessToken Oauth2 accessToken
* @param appId 接入方应用标识
* @return serviceToken
*/
Mono<String> getServiceToken(String accessToken, String appId);
/**
* 删除 serviceToken
*
* @param serviceToken st
* @return void
*/
Mono<Void> deleteServiceToken(String serviceToken);
}

View File

@@ -0,0 +1,39 @@
package cn.itlym.shoulder.platform.gateway.client.dto.param;
import javax.validation.constraints.NotEmpty;
/**
* @author lym
*/
public class AccessToken2ServiceTokenParam {
@NotEmpty
private String accessToken;
@NotEmpty
private String appId;
public AccessToken2ServiceTokenParam() {
}
public AccessToken2ServiceTokenParam(String accessToken, String appId) {
this.accessToken = accessToken;
this.appId = appId;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
}

View File

@@ -0,0 +1,27 @@
package cn.itlym.shoulder.platform.gateway.client.dto.param;
import javax.validation.constraints.NotEmpty;
/**
* @author lym
*/
public class DeleteServiceTokenParam {
@NotEmpty
private String st;
public DeleteServiceTokenParam() {
}
public DeleteServiceTokenParam(String st) {
this.st = st;
}
public String getSt() {
return st;
}
public void setSt(String st) {
this.st = st;
}
}

View File

@@ -0,0 +1,83 @@
package cn.itlym.shoulder.platform.gateway.client.impl;
import cn.itlym.shoulder.platform.gateway.client.ServiceTokenClient;
import cn.itlym.shoulder.platform.gateway.client.dto.param.DeleteServiceTokenParam;
import lombok.extern.slf4j.Slf4j;
import org.shoulder.core.dto.response.RestResult;
import org.shoulder.core.exception.BaseRuntimeException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.Map;
import static org.springframework.http.MediaType.APPLICATION_JSON;
/**
* 调用访问管理服务 ST 接口实现
*
* @author lym
*/
@Slf4j
@Service
public class ServiceTokenClientImpl implements ServiceTokenClient {
/**
* 使用 accessToken 换取 serviceToken 接口路径
*/
private static final String GET_ST_BY_ACCESS_TOKEN_URI = "/api/authentication/st/get?accessToken=%s&appId=%s";
/**
* 删除 ST 接口路径
*/
private static final String DELETE_ST_URI = "/api/authentication/st/delete";
@Autowired
private RouteDefinitionLocator locator;
private WebClient webClient;
/**
* todo 注入地址
*
* @param accessManagerServiceUrl
*/
public ServiceTokenClientImpl(@Value("xxx") String accessManagerServiceUrl) {
webClient = WebClient.create(accessManagerServiceUrl);
}
@Override
public Mono<String> getServiceToken(String accessToken, String appId) {
return webClient
.post().uri(String.format(GET_ST_BY_ACCESS_TOKEN_URI, accessToken, appId))
.accept(APPLICATION_JSON)
.retrieve()
.bodyToMono(RestResult.class)
.map(RestResult::getData)
.cast(Map.class)
.map(map -> map.get("st"))
.cast(String.class)
.onErrorResume(e -> {
log.warn("get ST fail by [token=" + accessToken + ",appId=" + appId + "]");
throw new BaseRuntimeException("get ST fail", e);
});
}
@Override
public Mono<Void> deleteServiceToken(String serviceToken) {
DeleteServiceTokenParam deleteServiceTokenParam = new DeleteServiceTokenParam();
deleteServiceTokenParam.setSt(serviceToken);
return webClient.post().uri(DELETE_ST_URI)
.bodyValue(deleteServiceTokenParam)
.retrieve()
.bodyToFlux(DataBuffer.class)
.map(DataBufferUtils::release)
.then();
}
}

View File

@@ -0,0 +1,45 @@
package cn.itlym.shoulder.platform.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* 处理跨域请求
* 简单跨域: GETHEAD、以及部分 POST请求请求头的"Content-Type"为 application/x-www-form-urlencoded、multipart/form-data、text/plain
* 其他跨域请求会在实际发送前进行一次 OPTIONS 探测请求
*
* @author lym
*/
@Configuration(
proxyBeanMethods = false
)
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
final String all = "*";
CorsConfiguration config = new CorsConfiguration();
// 是否允许请求带有验证信息 cookie跨域
config.setAllowCredentials(Boolean.TRUE);
// 允许访问的客户端域名
config.addAllowedOrigin(all);
// 允许服务端访问的客户端请求头
config.addAllowedHeader(all);
// 允许访问的方法名,GET POST等
config.addAllowedMethod(all);
// 允许前端js访问自定义响应头
//config.addExposedHeader("setToken");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}

View File

@@ -0,0 +1,147 @@
package cn.itlym.shoulder.platform.gateway.config;
import cn.itlym.shoulder.platform.gateway.ex.ShoulderGatewayException;
import lombok.extern.shoulder.SLog;
import org.shoulder.core.dto.response.RestResult;
import org.shoulder.core.exception.ErrorCode;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.lang.NonNull;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.*;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ResponseStatusException;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 异常 json 化处理,并返回 shoulder 定义的统一返回值类型
*
* @author lym
*/
@SLog
@Configuration(
proxyBeanMethods = false
)
@Order(Ordered.HIGHEST_PRECEDENCE)
public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
private static final String ATTRIBUTE_NAME_HTTP_STATUS = "httpStatus";
private static final String ATTRIBUTE_NAME_RESPONSE = "shoulderResponseBody";
/**
* 默认异常错误码500
*/
private static final int DEFAULT_SERVER_ERROR_HTTP_STATUS = HttpStatus.INTERNAL_SERVER_ERROR.value();
/**
* @see ErrorWebFluxAutoConfiguration#errorWebExceptionHandler
*/
public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
ServerProperties serverProperties, ApplicationContext applicationContext,
ObjectProvider<ViewResolver> viewResolvers,
ServerCodecConfigurer serverCodecConfigurer) {
super(errorAttributes, resourceProperties, serverProperties.getError(), applicationContext);
super.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()));
super.setMessageWriters(serverCodecConfigurer.getWriters());
super.setMessageReaders(serverCodecConfigurer.getReaders());
}
/**
* 构建返回的JSON数据格式
*
* @param httpStatus http 状态码
* @param response 如何响应
* @return {"httpStatus": 500, "shoulderResponseBody": {"code":"xxx", "msg":"xxx", "data":xxx}}
*/
public static Map<String, Object> buildErrorAttributes(int httpStatus, RestResult response) {
Map<String, Object> map = new HashMap<>(2);
map.put(ATTRIBUTE_NAME_HTTP_STATUS, httpStatus);
map.put(ATTRIBUTE_NAME_RESPONSE, response);
return map;
}
/**
* 获取异常属性
* 为了安全,不打印堆栈信息,即使使用者手动开启
*/
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
int httpStatus = DEFAULT_SERVER_ERROR_HTTP_STATUS;
Throwable error = super.getError(request);
if (error instanceof ResponseStatusException) {
// spring 定义的 http 异常
httpStatus = ((ResponseStatusException) error).getStatus().value();
} else if (error instanceof ErrorCode) {
// shoulder 定义的异常
ErrorCode errorCode = (ErrorCode) error;
httpStatus = errorCode.getHttpStatusCode().value();
}
return buildErrorAttributes(httpStatus, this.buildResponse(request, error));
}
/**
* 指定响应处理方法为JSON处理的方法
*
* @param errorAttributes 错误的属性
*/
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
/**
* Render the error information as a JSON payload.
*
* @param request the current request
* @return a {@code Publisher} of the HTTP response
* @see super#renderErrorResponse
*/
@NonNull
@Override
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL);
Map<String, Object> error = getErrorAttributes(request, includeStackTrace);
return ServerResponse.status(getHttpStatus(error)).contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(error));
}
/**
* 根据code获取对应的HttpStatus
*
* @param errorAttributes 错误的属性
*/
@Override
protected int getHttpStatus(Map<String, Object> errorAttributes) {
return (int) errorAttributes.get(ATTRIBUTE_NAME_HTTP_STATUS);
}
/**
* 构建异常信息
*
* @param request 请求
* @param ex 异常
* @return 异常信息
*/
protected RestResult buildResponse(ServerRequest request, Throwable ex) {
ShoulderGatewayException exception = new ShoulderGatewayException(request, ex);
log.error(exception);
return RestResult.error(exception);
}
}

View File

@@ -0,0 +1,31 @@
package cn.itlym.shoulder.platform.gateway.ex;
import org.shoulder.core.exception.BaseRuntimeException;
import org.shoulder.core.exception.CommonErrorCodeEnum;
import org.shoulder.core.exception.ErrorCode;
import org.springframework.web.reactive.function.server.ServerRequest;
/**
* 网关异常
*
* @author lym
*/
public class ShoulderGatewayException extends BaseRuntimeException {
public ShoulderGatewayException(ServerRequest request, Throwable ex) {
super(CommonErrorCodeEnum.UNKNOWN.getCode(), new StringBuilder("Failed to handle request [")
.append(request.methodName())
.append(" ")
.append(request.uri())
.append("] ")
.append(ex.getMessage())
.toString(), ex);
if (ex instanceof ErrorCode) {
ErrorCode errorCode = (ErrorCode) ex;
setHttpStatus(errorCode.getHttpStatusCode());
setCode(errorCode.getCode());
}
}
}

View File

@@ -0,0 +1,104 @@
package cn.itlym.shoulder.platform.gateway.filter;
import cn.itlym.shoulder.platform.gateway.client.ServiceTokenClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 认证过滤器
* 请求头是否有 accessToken、如果有则将他换成内部的 STJWT token请求结束后将 ST 过期
*
* @author lym
*/
@Slf4j
@Component
public class AuthenticationGlobalFilter implements GlobalFilter, Ordered {
/**
* ST
*/
public static final String SERVICE_TOKEN_ATTRIBUTE = qualify("serviceToken");
private static final String ACCESS_TOKEN_IN_HEADER = "Authorization";
private static final String APP_ID_IN_HEADER = "appId";
private static final String SERVICE_TOKEN_IN_HEADER = "ST";
private ServiceTokenClient stClient;
private List<String> ignorePath;
private AntPathMatcher pathMatcher = new AntPathMatcher();
public AuthenticationGlobalFilter(ServiceTokenClient stClient, List<String> ignorePath) {
this.stClient = stClient;
this.ignorePath = ignorePath;
}
/**
* 按照 spring 的方式来格式化上下文 key 格式
*/
private static String qualify(String attr) {
return AuthenticationGlobalFilter.class.getName() + "." + attr;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 开发阶段debug 模式优化
if ("true".equals(System.getProperty("permitAll"))) {
String check = exchange.getRequest().getHeaders().getFirst("check");
if (StringUtils.isEmpty(check)) {
return chain.filter(exchange);
}
}
String accessToken = exchange.getRequest().getHeaders().getFirst(ACCESS_TOKEN_IN_HEADER);
if (StringUtils.isEmpty(accessToken)) {
String aimPath = exchange.getRequest().getPath().value();
// ignorePath
if (ignorePath.contains(aimPath)) {
log.debug("allow: " + aimPath);
return chain.filter(exchange);
}
// access denied if missing accessToken
log.info("deny for accessToken is empty: " + aimPath);
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// preFilter: add header(ST)
String appId = exchange.getRequest().getHeaders().getFirst(APP_ID_IN_HEADER);
return stClient.getServiceToken(accessToken, appId)
// 请求前,换取 ST
.map(st -> {
exchange.getAttributes().put(SERVICE_TOKEN_ATTRIBUTE, st);
ServerHttpRequest request =
exchange.getRequest().mutate().header(SERVICE_TOKEN_IN_HEADER, st).build();
return exchange.mutate().request(request).build();
})
.flatMap(chain::filter)
// 响应后,删除 ST
.then(Mono.just(exchange)).map(e -> {
String st = (String) exchange.getAttributes().get(SERVICE_TOKEN_ATTRIBUTE);
return stClient.deleteServiceToken(st);
}).then();
}
@Override
public int getOrder() {
return 0;
}
}

View File

@@ -0,0 +1,35 @@
package cn.itlym.shoulder.platform.gateway.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import reactor.core.publisher.Flux;
import java.nio.charset.StandardCharsets;
/**
* react 工具类
*
* @author lym
*/
public class RequestUtil {
private static final Logger log = LoggerFactory.getLogger(RequestUtil.class);
private static String getBodyFromRequest(ServerHttpRequest serverHttpRequest) {
//获取请求体
Flux<DataBuffer> body = serverHttpRequest.getBody();
StringBuilder sb = new StringBuilder();
body.subscribe(buffer -> {
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
DataBufferUtils.release(buffer);
String bodyString = new String(bytes, StandardCharsets.UTF_8);
sb.append(bodyString);
});
log.info("请求体内容;{}", sb.toString());
return sb.toString();
}
}

View File

@@ -0,0 +1,11 @@
${AnsiColor.CYAN} ____ _ _ _ ${AnsiColor.BRIGHT_YELLOW} _ ____ ___ ____ _ ${AnsiColor.CYAN} __ __ __
${AnsiColor.CYAN}/ ___|| |__ ___ _ _| | __| | ___ _ __ ${AnsiColor.BRIGHT_YELLOW} / \ | _ \_ _| / ___| __ _| |_ _____ ____ _ _ _ ${AnsiColor.CYAN} \ \ \ \ \ \
${AnsiColor.CYAN}\___ \| '_ \ / _ \| | | | |/ _` |/ _ \ '__|${AnsiColor.BRIGHT_YELLOW} / _ \ | |_) | |_____ | | _ / _` | __/ _ \ \ /\ / / _` | | | |${AnsiColor.CYAN} \ \ \ \ \ \
${AnsiColor.CYAN} ___) | | | | (_) | |_| | | (_| | __/ | ${AnsiColor.BRIGHT_YELLOW} / ___ \| __/| |_____|| |_| | (_| | || __/\ V V / (_| | |_| |${AnsiColor.CYAN} / / / / / /
${AnsiColor.CYAN}|____/|_| |_|\___/ \__,_|_|\__,_|\___|_| ${AnsiColor.BRIGHT_YELLOW} /_/ \_\_| |___| \____|\__,_|\__\___| \_/\_/ \__,_|\__, |${AnsiColor.CYAN} / / / / / /
${AnsiColor.CYAN}=======================================================================================================${AnsiColor.BRIGHT_YELLOW}|___/${AnsiColor.CYAN}=/_/==/_/===/_/
${AnsiColor.BLUE} :: Spring Boot :: ${AnsiColor.CYAN}${spring-boot.formatted-version}
${AnsiColor.BLUE} :: Shoulder-Framework :: ${AnsiColor.CYAN}(v@shoulder.version@)
${AnsiColor.BRIGHT_GREEN} :: @project.artifactId@ :: ${AnsiColor.GREEN}(v@project.version@)${AnsiColor.CYAN} @project.description@
${AnsiColor.DEFAULT}

View File

@@ -0,0 +1,50 @@
# 先从环境变量里取,若不存在,则以 maven 打包时的配置为准
shoulder:
nacos:
ip: ${NACOS_IP:itlym.cn}
port: ${NACOS_PORT:8848}
# spring-boot-actuate 展示信息
info:
name: "@project.name@"
description: "@project.description@"
version: "@project.version@"
spring-boot-version: "@spring-boot.version@"
spring-cloud-version: "@spring-cloud.version@"
shoulder-version: "@shoulder.version@"
profile: "@profile.active@"
spring:
#main:
#allow-bean-definition-overriding: true
application:
name: ${info.name}
profiles:
active: ${info.profile}
cloud:
nacos:
config:
server-addr: ${shoulder.nacos.ip}:${shoulder.nacos.port}
file-extension: yml
namespace: ${shoulder.nacos.namespace}
shared-configs:
- dataId: common.yml
refresh: true
- dataId: redis.yml
refresh: false
- dataId: db.yml
refresh: true
- dataId: mq-rabbitmq.yml
refresh: false
enabled: true
discovery:
server-addr: ${shoulder.nacos.ip}:${shoulder.nacos.port}
namespace: ${shoulder.nacos.namespace}
metadata:
management.context-path: ${server.servlet.context-path:}${spring.mvc.servlet.path:}${management.endpoints.web.base-path:}
logging:
file:
path: /logs
name: ${logging.file.path}/${spring.application.name}/${spring.application.name}.log

View File

@@ -0,0 +1,5 @@
# shoulder-storage-gateway
该网关用于屏蔽不同存储方式的差异,为系统内部提供统一的存储能力
可以在内部支持 `Minio``Ceph``七牛对象存储``阿里云对象存储``腾讯对象存储`...

View File

@@ -0,0 +1,15 @@
<?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>shoulder-gateway</artifactId>
<groupId>cn.itlym.platform</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shoulder-storage-gateway</artifactId>
</project>

109
shoulder-generator/pom.xml Normal file
View File

@@ -0,0 +1,109 @@
<?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>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform-parent</artifactId>
<version>1.0-SNAPSHOT</version><!-- shoulder-platform-version -->
<relativePath>../../shoulder-platform-common/shoulder-platform-parent/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shoulder-generator</artifactId>
<name>代码生成器</name>
<description>代码生成器</description>
<dependencies>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform-starter-db</artifactId>
</dependency>
<dependency>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform-starter-config-client</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
</dependency>
<dependency>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform-starter-rpc-server</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<!-- 生成带第三方jar包的可执行jar包 -->
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<!-- 项目名 -->
<!-- ${docker.image.prefix} dockerHub 上注册的名字 -->
<imageName>${docker.image.prefix}/${project.artifactId}</imageName>
<dockerDirectory>src/main/docker</dockerDirectory>
<!-- docker远程服务器地址 -->
<dockerHost>${docker.host}</dockerHost>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
<finalName>${project.artifactId}</finalName>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
<include>**/*.yml</include>
</includes>
<!-- 是否替换资源中的属性-->
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</project>

View File

@@ -0,0 +1,18 @@
package cn.itlym.shoulder.generator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
/**
* 启动类
*
* @author lym
*/
@Configuration
@SpringBootApplication
public class GeneratorApp {
public static void main(String[] args) {
SpringApplication.run(GeneratorApp.class, args);
}
}

View File

@@ -0,0 +1,76 @@
package cn.itlym.shoulder.generator.controller;
import cn.itlym.shoulder.generator.service.SysGeneratorService;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.annotations.Api;
import org.apache.commons.io.IOUtils;
import org.shoulder.core.dto.response.ListResult;
import org.shoulder.core.dto.response.RestResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
/**
* @author lym
*/
@RestController
@Api(tags = "代码生成器")
@RequestMapping("/generator")
public class GeneratorController {
@Autowired
private SysGeneratorService sysGeneratorService;
private ObjectMapper objectMapper = new ObjectMapper();
/**
* 列表
*/
@ResponseBody
@RequestMapping("/list")
public RestResult<ListResult> list(@RequestParam Map<String, Object> params) {
return RestResult.success(sysGeneratorService.queryList(params));
}
/**
* 生成代码
* web 中不需要主动关闭流
* http://localhost:8080/generator/code?tables=tb_shop
*/
@RequestMapping("/code")
public void code(String tables, HttpServletResponse response) throws IOException {
if (StringUtils.isEmpty(tables)) {
throw new IllegalArgumentException("tableName can't be empty");
}
response.reset();
byte[] data = sysGeneratorService.generatorCode(tables.split(","), response.getOutputStream());
if (data != null && data.length > 0) {
/*
// file out put stream 必须及时关闭
OutputStream out = new FileOutputStream("F:/te.zip");
IOUtils.write(data, out);
IOUtils.closeQuietly(out);
*/
response.setHeader("Content-Disposition", "attachment; filename=\"generator.zip\"");
response.setContentType("application/octet-stream; charset=UTF-8");
response.addHeader("Content-Length", String.valueOf(data.length));
// response out put stream 会自动关闭
IOUtils.write(data, response.getOutputStream());
}
}
}

View File

@@ -0,0 +1,24 @@
package cn.itlym.shoulder.generator.dao;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
/**
* @author lym
*/
@Mapper
@Repository
public interface SysGeneratorDao {
List<Map<String, Object>> queryList(Map<String, Object> map);
int queryTotal(Map<String, Object> map);
Map<String, String> queryTable(String tableName);
List<Map<String, String>> queryColumns(String tableName);
}

View File

@@ -0,0 +1,31 @@
<?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="cn.itlym.shoulder.generator.dao.SysGeneratorDao">
<select id="queryList" resultType="map">
select table_name tableName, engine, table_comment tableComment, create_time createTime from
information_schema.tables
where table_schema = (select database())
<if test="tableName != null and tableName.trim() != ''">
and table_name like concat('%', #{tableName}, '%')
</if>
order by create_time desc
</select>
<select id="queryTotal" resultType="int">
select count(*) from information_schema.tables where table_schema = (select database())
<if test="tableName != null and tableName.trim() != ''">
and table_name like concat('%', #{tableName}, '%')
</if>
</select>
<select id="queryTable" resultType="map">
select table_name tableName, engine, table_comment tableComment, create_time createTime from information_schema.tables
where table_schema = (select database()) and table_name = #{tableName}
</select>
<select id="queryColumns" resultType="map">
select column_name columnName, data_type dataType, column_comment columnComment, column_key columnKey, extra from information_schema.columns
where table_name = #{tableName} and table_schema = (select database()) order by ordinal_position
</select>
</mapper>

View File

@@ -0,0 +1,34 @@
package cn.itlym.shoulder.generator.model;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author lym
*/
@NoArgsConstructor
@Data
public class ColumnEntity {
//列名
private String columnName;
//列名类型
private String dataType;
//列名备注
private String comments;
//属性名称(第一个字母大写)user_name => UserName
private String attrName;
//属性名称(第一个字母小写)user_name => userName
private String attributeName;
//属性类型
private String attrType;
//auto_increment
private String extra;
}

View File

@@ -0,0 +1,27 @@
package cn.itlym.shoulder.generator.model;
import lombok.Data;
import java.util.List;
/**
* @author lym
*/
@Data
public class TableEntity {
//表的名称
private String tableName;
//表的备注
private String comments;
//表的主键
private ColumnEntity pk;
//表的列名(不包含主键)
private List<ColumnEntity> columns;
//类名(第一个字母大写)sys_user => SysUser
private String className;
//类名(第一个字母小写)sys_user => sysUser
private String lowClassName;
}

View File

@@ -0,0 +1,26 @@
package cn.itlym.shoulder.generator.service;
import org.shoulder.core.dto.response.PageResult;
import org.springframework.stereotype.Service;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
/**
* @author lym
*/
@Service
public interface SysGeneratorService {
PageResult queryList(Map<String, Object> map);
int queryTotal(Map<String, Object> map);
Map<String, String> queryTable(String tableName);
List<Map<String, String>> queryColumns(String tableName);
byte[] generatorCode(String[] tableNames, OutputStream out);
}

View File

@@ -0,0 +1,80 @@
package cn.itlym.shoulder.generator.service.impl;
import cn.itlym.shoulder.generator.dao.SysGeneratorDao;
import cn.itlym.shoulder.generator.service.SysGeneratorService;
import cn.itlym.shoulder.generator.utils.GenUtils;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.extern.shoulder.SLog;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.IOUtils;
import org.shoulder.core.dto.response.PageResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipOutputStream;
/**
* @author lym
*/
@SLog
@Service
public class SysGeneratorServiceImpl implements SysGeneratorService {
@Autowired
private SysGeneratorDao sysGeneratorDao;
@Override
public PageResult queryList(Map<String, Object> map) {
//设置分页信息分别是当前页数和每页显示的总记录数【记住必须在mapper接口中的方法执行之前设置该分页信息】
PageHelper.startPage(MapUtils.getInteger(map, "page"), MapUtils.getInteger(map, "limit"), true);
List<Map<String, Object>> list = sysGeneratorDao.queryList(map);
PageInfo<Map<String, Object>> pageInfo = new PageInfo<>(list);
return PageResult.PageInfoConverter.toResult(pageInfo);
}
@Override
public int queryTotal(Map<String, Object> map) {
return 0;
}
@Override
public Map<String, String> queryTable(String tableName) {
return sysGeneratorDao.queryTable(tableName);
}
@Override
public List<Map<String, String>> queryColumns(String tableName) {
return sysGeneratorDao.queryColumns(tableName);
}
@Override
public byte[] generatorCode(String[] tableNames, OutputStream out) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ZipOutputStream zip = new ZipOutputStream(outputStream);
for (String tableName : tableNames) {
//查询表信息
Map<String, String> table = queryTable(tableName);
//查询列信息
List<Map<String, String>> columns = queryColumns(tableName);
if (MapUtils.isEmpty(table) || CollectionUtils.isEmpty(columns)) {
log.warn("table {} not exist or without any columns", table);
continue;
}
//生成代码
GenUtils.generatorCode(table, columns, zip);
IOUtils.closeQuietly(zip);
}
return outputStream.toByteArray();
}
}

View File

@@ -0,0 +1,35 @@
package cn.itlym.shoulder.generator.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 时间工具类
*
* @author lym
*/
public class DateUtils {
/**
* 时间格式(yyyy-MM-dd)
*/
public final static String DATE_PATTERN = "yyyy-MM-dd";
/**
* 时间格式(yyyy-MM-dd HH:mm:ss)
*/
public final static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
public static String format(Date date) {
return format(date, DATE_PATTERN);
}
public static String format(Date date, String pattern) {
if (date != null) {
SimpleDateFormat df = new SimpleDateFormat(pattern);
return df.format(date);
}
return null;
}
}

View File

@@ -0,0 +1,220 @@
package cn.itlym.shoulder.generator.utils;
import cn.itlym.shoulder.generator.model.ColumnEntity;
import cn.itlym.shoulder.generator.model.TableEntity;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 代码生成器 工具类
*
* @author lym
*/
public class GenUtils {
public static List<String> getTemplates() {
List<String> templates = new ArrayList<String>();
templates.add("template/Entity.java.vm");
templates.add("template/Dao.java.vm");
templates.add("template/Dao.xml.vm");
templates.add("template/Service.java.vm");
templates.add("template/ServiceImpl.java.vm");
templates.add("template/Controller.java.vm");
templates.add("template/index.html.vm");
return templates;
}
/**
* 生成代码
*/
public static void generatorCode(Map<String, String> table,
List<Map<String, String>> columns, ZipOutputStream zip) {
//配置信息
Configuration config = getConfig();
boolean hasBigDecimal = false;
//表信息
TableEntity tableEntity = new TableEntity();
tableEntity.setTableName(table.get("tableName"));
tableEntity.setComments(table.get("tableComment"));
//表名转换成Java类名
String className = tableToJava(tableEntity.getTableName(), config.getString("tablePrefix"));
tableEntity.setClassName(className);
tableEntity.setLowClassName(StringUtils.uncapitalize(className));
//列信息
hasBigDecimal = fillColumnsInfo(columns, config, tableEntity);
//没主键,则第一个字段为主键
if (tableEntity.getPk() == null) {
tableEntity.setPk(tableEntity.getColumns().get(0));
}
//设置velocity资源加载器
Properties prop = new Properties();
prop.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
Velocity.init(prop);
String mainPath = config.getString("mainPath");
mainPath = StringUtils.isBlank(mainPath) ? "io.renren" : mainPath;
//封装模板数据
Map<String, Object> map = new HashMap<>();
map.put("tableName", tableEntity.getTableName());
map.put("pkgName", className.toLowerCase());
map.put("comments", tableEntity.getComments());
map.put("pk", tableEntity.getPk());
map.put("className", tableEntity.getClassName());
map.put("classname", tableEntity.getLowClassName());
map.put("pathName", tableEntity.getLowClassName().toLowerCase());
map.put("columns", tableEntity.getColumns());
map.put("hasBigDecimal", hasBigDecimal);
map.put("mainPath", mainPath);
map.put("package", config.getString("package"));
map.put("moduleName", config.getString("moduleName"));
map.put("author", config.getString("author"));
map.put("email", config.getString("email"));
map.put("datetime", DateUtils.format(new Date(), DateUtils.DATE_TIME_PATTERN));
VelocityContext context = new VelocityContext(map);
//获取模板列表
List<String> templates = getTemplates();
for (String template : templates) {
//渲染模板
StringWriter sw = new StringWriter();
Template tpl = Velocity.getTemplate(template, "UTF-8");
tpl.merge(context, sw);
try {
//添加到zip
String fileName = getFileName(template, tableEntity.getClassName(), config.getString("package"), tableEntity.getTableName());
zip.putNextEntry(new ZipEntry(fileName));
IOUtils.write(sw.toString(), zip, "UTF-8");
IOUtils.closeQuietly(sw);
zip.closeEntry();
} catch (IOException e) {
throw new RuntimeException("渲染模板失败,表名:" + tableEntity.getTableName(), e);
}
}
}
private static boolean fillColumnsInfo(List<Map<String, String>> columns, Configuration config, TableEntity tableEntity) {
boolean hasBigDecimal = false;
List<ColumnEntity> columnsList = new ArrayList<>();
for (Map<String, String> column : columns) {
ColumnEntity columnEntity = new ColumnEntity();
columnEntity.setColumnName(column.get("columnName"));
columnEntity.setDataType(column.get("dataType"));
columnEntity.setComments(column.get("columnComment"));
columnEntity.setExtra(column.get("extra"));
//列名转换成Java属性名
String attrName = columnToJava(columnEntity.getColumnName());
columnEntity.setAttrName(attrName);
columnEntity.setAttributeName(StringUtils.uncapitalize(attrName));
//列的数据类型转换成Java类型
String attrType = config.getString(columnEntity.getDataType(), "unknowType");
columnEntity.setAttrType(attrType);
if (attrType.equals("BigDecimal")) {
hasBigDecimal = true;
}
//是否主键
if ("PRI".equalsIgnoreCase(column.get("columnKey")) && tableEntity.getPk() == null) {
tableEntity.setPk(columnEntity);
}
columnsList.add(columnEntity);
}
tableEntity.setColumns(columnsList);
return hasBigDecimal;
}
/**
* 列名转换成Java属性名
*/
public static String columnToJava(String columnName) {
return WordUtils.capitalizeFully(columnName, new char[]{'_'}).replace("_", "");
}
/**
* 表名转换成Java类名
*/
public static String tableToJava(String tableName, String tablePrefix) {
if (StringUtils.isNotBlank(tablePrefix)) {
tableName = tableName.replace(tablePrefix, "");
}
return columnToJava(tableName);
}
/**
* 获取配置信息
*/
public static Configuration getConfig() {
try {
return new PropertiesConfiguration("generator.properties");
} catch (ConfigurationException e) {
throw new RuntimeException("获取配置文件失败,", e);
}
}
/**
* 获取文件名
*/
public static String getFileName(String template, String className, String packageName, String tableName) {
String packagePath = "main" + File.separator + "java" + File.separator;
tableName = tableName.replace("-", "").replace("_", "").toLowerCase();
if (StringUtils.isNotBlank(packageName)) {
packagePath += packageName.replace(".", File.separator) + File.separator + tableName + File.separator;
}
if (template.contains("Entity.java.vm")) {
return packagePath + "entity" + File.separator + className + ".java";
}
if (template.contains("Dao.java.vm")) {
return packagePath + "dao" + File.separator + className + "Dao.java";
}
if (template.contains("Service.java.vm")) {
return packagePath + "service" + File.separator + className + "Service.java";
}
if (template.contains("ServiceImpl.java.vm")) {
return packagePath + "service" + File.separator + "impl" + File.separator + className + "ServiceImpl.java";
}
if (template.contains("Controller.java.vm")) {
return packagePath + "controller" + File.separator + className + "Controller.java";
}
if (template.contains("Dao.xml.vm")) {
return packagePath + "dao" + File.separator + className + "Dao.xml";
}
if (template.contains("menu.sql.vm")) {
return className.toLowerCase() + "_menu.sql";
}
if (template.contains("index.html.vm")) {
return "main" + File.separator + "view" + File.separator + "pages" +
File.separator + tableName + File.separator + tableName + ".html";
}
return null;
}
}

View File

@@ -0,0 +1,15 @@
spring:
#allow-bean-definition-overriding: true
application:
name: generator
cloud:
nacos:
config:
server-addr: itlym.cn:8848
file-extension: yml
shared-configs:
- dataId: common.yml
refresh: true
- dataId: db.yml
refresh: true
enabled: true

View File

@@ -0,0 +1,31 @@
#\u4EE3\u7801\u751F\u6210\u5668\uFF0C\u914D\u7F6E\u4FE1\u606F
mainPath=cn.itlym.shoulder.platform.demo
#\u5305\u540D
package=cn.itlym.shoulder.platform.demo
moduleName=generator
#\u4F5C\u8005
author=lym
#Email
email=
#\u8868\u524D\u7F00(\u7C7B\u540D\u4E0D\u4F1A\u5305\u542B\u8868\u524D\u7F00)
tablePrefix=tb_
#\u7C7B\u578B\u8F6C\u6362\uFF0C\u914D\u7F6E\u4FE1\u606F
tinyint=Integer
smallint=Integer
mediumint=Integer
int=Integer
integer=Integer
bigint=Long
float=Float
double=Double
decimal=BigDecimal
bit=Boolean
char=String
varchar=String
tinytext=String
text=String
mediumtext=String
longtext=String
date=Date
datetime=Date
timestamp=Date

View File

@@ -0,0 +1,76 @@
package ${package}.${pkgName}.controller;
import java.util.Map;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.security.access.prepost.PreAuthorize;
import org.shoulder.core.dto.response.PageResult;
import org.shoulder.core.dto.response.RestResult;
import ${package}.${pkgName}.entity.${className};
import ${package}.${pkgName}.service.${className}Service;
/**
* ${comments}
*
* @author ${author}
* @date ${datetime}
*/
@RestController
@RequestMapping("${pathName}")
@Api(tags = "${comments}")
public class ${className}Controller {
@Autowired
private ${className}Service ${lowClassName}Service;
/**
* 列表
*/
@RequestMapping("/list")
@PreAuthorize("hasAnyAuthority('${tableName}:${pathName}:list')")
public PageResult list(@RequestParam Map<String, Object> params){
PageResult pageResult = ${lowClassName}Service.findAll(params);
return pageResult;
}
/**
* 保存
*/
@RequestMapping("/save")
@PreAuthorize("hasAnyAuthority('generator:sysroleuser:save')")
public Result save(@RequestBody ${className} ${lowClassName}){
${lowClassName}Service.save(${lowClassName});
return Result.succeed("保存成功");
}
/**
* 修改
*/
@RequestMapping("/update")
@PreAuthorize("hasAnyAuthority('generator:sysroleuser:update')")
public Result update(@RequestBody ${className} ${lowClassName}){
${lowClassName}Service.update(${lowClassName});
return Result.succeed("修改成功");
}
/**
* 删除
*/
@RequestMapping("/delete/{id}")
@PreAuthorize("hasAnyAuthority('generator:sysroleuser:delete')")
public Result delete(@PathVariable Long ${pk.attrname}){
${lowClassName}Service.delete(${pk.attrname});
return Result.succeed("删除成功");
}
}

View File

@@ -0,0 +1,27 @@
package ${package}.${pkgName}.dao;
import ${package}.${pkgName}.entity.${className};
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.Map;
/**
* ${comments}
*
* @author ${author}
* @date ${datetime}
*/
@Mapper
public interface ${className}Dao {
int save(${className} ${lowClassName});
int update(${className} ${lowClassName});
int delete(Long id);
List<${className}> findAll(Map<String, Object> params);
}

View File

@@ -0,0 +1,62 @@
<?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}.${pkgName}.dao.${className}Dao">
<!-- 可根据自己的需求,是否要使用 -->
<resultMap type="${package}.${pkgName}.entity.${className}" id="${lowClassName}Map">
#foreach($column in $columns)
<result property="${column.attrname}" column="${column.columnName}"/>
#end
</resultMap>
<insert id="save">
insert into ${tableName}(
#foreach($column in $columns)
#if( $!{velocityCount} == $!{columns.size()})
${column.columnName}
#else
${column.columnName},
#end
#end
) values (
#foreach($column in $columns)
#if( $!{velocityCount} == $!{columns.size()})
#{${column.columnName}}
#else
#{ ${column.columnName}},
#end
#end
)
</insert>
<update id="update">
update ${tableName}
<set>
#foreach($column in $columns)
<if test="${column.columnName} != null">
${column.columnName} = #{${column.columnName}},
</if>
#end
</set>
where id = #{id}
</update>
<delete id="delete" parameterType="long" flushCache="true">
delete from ${tableName} where id = #{id}
</delete>
<select id="findAll" resultType="${package}.${pkgName}.entity.${className}">
select * from ${tableName} t
<include refid="where"/>
</select>
<sql id="where">
<where>
#foreach($column in $columns)
<if test="searchKey != null and searchKey != '' and searchKey=='${column.columnName}'">
and t.${column.columnName} like concat('%', #{searchValue}, '%')
</if>
#end
</where>
</sql>
</mapper>

View File

@@ -0,0 +1,31 @@
package ${package}.${pkgName}.entity;
import lombok.Data;
import lombok.NoArgsConstructor;
#if(${hasBigDecimal})
import java.math.BigDecimal;
#end
import java.io.Serializable;
import java.util.Date;
/**
* ${comments}
*
* @author ${author}
* @date ${datetime}
*/
@Data
@NoArgsConstructor
public class ${className} implements Serializable {
private static final long serialVersionUID = 1L;
#foreach ($column in $columns)
#if($column.columnName == $pk.columnName)
#end
private $column.attrType $column.attrname;
#end
}

View File

@@ -0,0 +1,43 @@
package ${package}.${pkgName}.service;
import ${package}.${pkgName}.entity.${className};
import org.shoulder.core.dto.response.PageResult;
import java.util.Map;
/**
* ${comments}
*
* @author ${author}
* @date ${datetime}
*/
public interface ${className}Service {
/**
* 添加
* @param ${lowClassName}
*/
int save(${className} ${lowClassName});
/**
* 修改
* @param ${lowClassName}
*/
int update(${className} ${lowClassName});
/**
* 删除
* @param id
*/
int delete(Long id);
/**
* 列表
* @param params
* @return
*/
PageResult<${className}> findAll(Map<String, Object> params);
}

View File

@@ -0,0 +1,66 @@
package ${package}.${pkgName}.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.shoulder.core.dto.response.PageResult;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections4.MapUtils;
import ${package}.${pkgName}.entity.${className};
import ${package}.${pkgName}.dao.${className}Dao;
import ${package}.${pkgName}.service.${className}Service;
@Service
public class ${className}ServiceImpl implements ${className}Service {
@Autowired
private ${className}Dao ${lowClassName}Dao;
/**
* 添加
* @param ${lowClassName}
*/
public int save(${className} ${lowClassName}){
return ${lowClassName}Dao.save(${lowClassName});
}
/**
* 修改
* @param ${lowClassName}
*/
public int update(${className} ${lowClassName}){
return ${lowClassName}Dao.update(${lowClassName});
}
/**
* 删除
* @param id
*/
public int delete(Long id){
return ${lowClassName}Dao.delete(id);
}
/**
* 列表
* @param params
* @return
*/
public PageResult<${className}> findAll(Map<String, Object> params){
//设置分页信息分别是当前页数和每页显示的总记录数【记住必须在mapper接口中的方法执行之前设置该分页信息】
if (MapUtils.getInteger(params, "page")!=null && MapUtils.getInteger(params, "limit")!=null)
PageHelper.startPage(MapUtils.getInteger(params, "page"),MapUtils.getInteger(params, "limit"),true);
List<${className}> list = ${lowClassName}Dao.findAll(params);
PageInfo<${className}> pageInfo = new PageInfo(list);
return PageResult.<${className}>builder().data(pageInfo.getList()).code(0).count(pageInfo.getTotal()).build();
}
}

View File

@@ -0,0 +1,169 @@
<div class="layui-card">
<div class="layui-card-header">
<h2 class="header-title">${comments}</h2>
<span class="layui-breadcrumb pull-right">
<a href="#!console">首页</a>
<a><cite>${comments}</cite></a>
</span>
</div>
<div class="layui-card-body">
<div class="layui-form toolbar">
搜索:
<select id="${lowClassName}-search-key">
<option value="">-请选择-</option>
<option value="name">名称</option>
</select>&emsp;
<input id="${lowClassName}-edit-value" class="layui-input search-input" type="text" placeholder="输入关键字"/>&emsp;
<button id="${lowClassName}-btn-search" class="layui-btn icon-btn"><i class="layui-icon">&#xe615;</i>搜索
</button>
<button id="${lowClassName}-btn-add" class="layui-btn icon-btn"><i class="layui-icon">&#xe654;</i>添加</button>
</div>
<!-- 数据表格 -->
<table class="layui-table" id="${lowClassName}-table" lay-filter="${lowClassName}-table"></table>
</div>
</div>
<!-- 表单弹窗 -->
<script type="text/html" id="${lowClassName}-model">
<form id="${lowClassName}-form" lay-filter="${lowClassName}-form" class="layui-form model-form">
#foreach($column in $columns)
<div class="layui-form-item">
<label class="layui-form-label">${column.comments}</label>
<div class="layui-input-block">
<input name="${column.columnName}" placeholder="请输入${column.comments}" type="text"
class="layui-input" maxlength="20"
lay-verify="required" required/>
</div>
</div>
#end
<div class="layui-form-item model-form-footer">
<button class="layui-btn layui-btn-primary" ew-event="closeDialog" type="button">取消</button>
<button class="layui-btn" lay-filter="${lowClassName}-form-submit" lay-submit>保存</button>
</div>
</form>
</script>
<!-- 表格操作列 -->
<script type="text/html" id="${lowClassName}-table-bar">
<a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="edit">修改</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
<script>
layui.use(['form', 'table', 'util', 'config', 'admin', 'formSelects'], function () {
var form = layui.form;
var table = layui.table;
var config = layui.config;
var layer = layui.layer;
var util = layui.util;
var admin = layui.admin;
var formSelects = layui.formSelects;
//渲染表格
table.render({
elem: '#${lowClassName}-table',
url: config.base_server + 'api-${moduleName}/${pathName}/list',
where: {
access_token: config.getToken().access_token
},
page: true,
cols: [[
{type: 'numbers'},
{field: 'id', sort: true, title: 'ID', width: 80},
#foreach($column in $columns)
{field: '${column.columnName}', sort: true, title: '${column.comments}'},
#end
{
field: 'createTime', sort: true, templet: function (d) {
return util.toDateString(d.createTime);
}, title: '创建时间'
},
{align: 'center', toolbar: '#${lowClassName}-table-bar', title: '操作', width: 250}
]]
});
// 添加按钮点击事件
$('#${lowClassName}-btn-add').click(function () {
showEditModel();
});
// 表单提交事件
form.on('submit(${lowClassName}-form-submit)', function (data) {
layer.load(2);
var url = "";
if (data.field.id == '') {
url = 'api-${moduleName}/${pathName}/' + 'save';
} else {
url = 'api-${moduleName}/${pathName}/' + 'update';
}
admin.req(url, JSON.stringify(data.field), function (data) {
layer.closeAll('loading');
if (data.resp_code == 0) {
layer.msg(data.resp_msg, {icon: 1, time: 500});
table.reload('${lowClassName}-table');
layer.closeAll('page');
} else {
layer.msg(data.resp_msg, {icon: 2, time: 500});
}
}, $('#${lowClassName}-form').attr('method'));
return false;
});
// 工具条点击事件
table.on('tool(${lowClassName}-table)', function (obj) {
var data = obj.data;
if (obj.event === 'edit') { //修改
showEditModel(data);
} else if (obj.event === 'del') { //删除
doDelete(obj);
}
});
// 搜索按钮点击事件
$('#${lowClassName}-btn-search').click(function () {
var key = $('#${lowClassName}-search-key').val();
var value = $('#${lowClassName}-edit-value').val();
table.reload('${lowClassName}-table', {where: {searchKey: key, searchValue: value}});
});
// 显示编辑弹窗
var showEditModel = function (data) {
layer.open({
type: 1,
title: data ? '修改${comments}' : '添加${comments}',
area: '450px',
offset: '120px',
content: $('#${lowClassName}-model').html(),
success: function () {
$('#${lowClassName}-form')[0].reset();
$('#${lowClassName}-form').attr('method', 'POST');
if (data) {
$("input[name='code']").attr('disabled', true);
$("input[name='code']").attr('class', "layui-input layui-disabled");
form.val('${lowClassName}-form', data);
$('#${lowClassName}-form').attr('method', 'POST');
}
}
});
};
// 删除
var doDelete = function (obj) {
layer.confirm('确定要删除吗?', function (i) {
layer.close(i);
layer.load(2);
admin.req('api-${moduleName}/${pathName}/delete/' + obj.data.id, {}, function (data) {
layer.closeAll('loading');
if (data.resp_code == 0) {
layer.msg(data.resp_msg, {icon: 1, time: 500});
obj.del();
} else {
layer.msg(data.resp_msg, {icon: 2, time: 500});
}
}, 'DELETE');
});
};
});
</script>

View File

@@ -0,0 +1,28 @@
# shoulder-通知推送中心
包含
- 邮件推送
- 短信推送
- 站内消息推送
- xx 推送...
## 系统架构
如图 ![消息中心架构](arch.png)
改进:
-`业务服务``消息中心` 采用 API 方式通信,可以提供`SDK`,简化调用,另推荐在此之上应添加业务网关,对上层业务屏蔽底层实现。
-`业务服务``消息中心` 采用 MQ 方式通信,一般来说,无需再使用网关隔离。
TODO考虑将各种推送方式集成提供单体服务
- 降低初学者的理解难度
- 减少小型场景的资源占用
----
站内消息https://www.jianshu.com/p/c180e1510639

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -0,0 +1,56 @@
# 邮件推送服务
## 需求分析
希望邮件推送服务能提供的功能
- 可编程、可扩展(通过程序对接、改造)
- 能够通过开发在程序逻辑或者管理界面中自动触发发送
- 支持邮件发送验证码和邮件营销推送
- 能够支持HTML的邮件内容而HTML内容能够随时随地进行修改方便美工和开发去调整
- 可观测
- 验证类邮件能够支持IP统计、次数统计能够进行时间限制、防止恶意发送
- 推送类邮件能够支持统计发送数量、发送成功率等反馈数据
- 推送效率
- 验证邮件要能在5-10秒内发送成功到达率高
- 用户友好
- 用户可以退订推送类邮件
## 功能设计
1. 邮件模板模块。通过模板功能来支持HTML邮件内容以及随时可更新替换的要求通过模板里的关键词参数设计来达到验证码、用户名、营销内容等动态输入。
2. ~~验证码模块。用来支撑邮件验证码校验、请求限制等功能~~ (由调用方来限制,放入认证服务中)。
3. 记录发送记录相关信息模块。记录所有发送记录,用于统计和分析。
4. 管理后台模块。包括:用户管理、邮件模板配置、发送记录查询等基本支撑功能,在后台尽量以界面化实现管理。
5. 将企业邮局和邮件推送服务分离;企业内外网邮件域隔离。
以域名 `XXX.com` 为例,一般企业邮局地为`zhangsan@XXX.com`
邮件服务不适合也以`XXX.com`为域,因为这会和企业邮局服务相互干扰(也不是没有规避方式,只是配置起来很麻烦)
隔离方案:增加一个二级域名,例如`mail.XXX.com`,邮件服务地址就是 `service@mail.XXX.com`
-------------
不建议自己搭邮件服务器十分容易被拦截即使使用ip池
----------
参考https://www.cnblogs.com/chuma/p/5694744.html
https://www.cnblogs.com/tellerfuliye/articles/13156469.html
----
shoulder 以外的其他方案
https://gitee.com/52itstyle/spring-boot-mail

View File

@@ -0,0 +1,22 @@
<?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>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform</artifactId>
<version>1.0-SNAPSHOT</version><!-- shoulder-platform-version -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itlym.platform.notify</groupId>
<artifactId>shoulder-notify-center</artifactId>
<packaging>pom</packaging>
<modules>
<module>shoulder-sms</module>
</modules>
</project>

View File

@@ -0,0 +1,8 @@
# shoulder-短信推送中心
- sms-api
- shoulder-短信推送中心接口、DTO
- sms-center
-
- shoulder-sms-client
- 快速开发工具包,使用者只需引入该 jar 包,即可获得与 `shoulder-短信推送中心` 通信的能力

View File

@@ -0,0 +1,48 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform-parent</artifactId>
<version>1.0-SNAPSHOT</version><!-- shoulder-platform-version -->
<relativePath>../../shoulder-platform-common/shoulder-platform-parent/pom.xml</relativePath>
</parent>
<groupId>cn.itlym.platform.notify</groupId>
<artifactId>shoulder-sms</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>sms-center</module>
<module>shoulder-sms-client</module>
<module>sms-api</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>cn.itlym.platform.notify</groupId>
<artifactId>sms-api</artifactId>
<version>${shoulder-platform.version}</version>
</dependency>
<dependency>
<groupId>cn.itlym.platform.notify</groupId>
<artifactId>sms-center</artifactId>
<version>${shoulder-platform.version}</version>
</dependency>
<!-- sdk -->
<dependency>
<groupId>cn.itlym.platform.notify</groupId>
<artifactId>shoulder-sms-client</artifactId>
<version>${shoulder-platform.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@@ -0,0 +1,33 @@
<?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>
<groupId>cn.itlym.platform.notify</groupId>
<artifactId>shoulder-sms</artifactId>
<version>1.0-SNAPSHOT</version><!-- shoulder-platform-version -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itlym.platform.notify</groupId>
<artifactId>shoulder-sms-client</artifactId>
<description>功能向sms-center发送请求。供微服务内部其他服务使用</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<!-- IDE tip -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,11 @@
package cn.itlym.shoulder.platform.notify.sms.sdk.client;
/**
* 供内部服务使用的 client
*
* @author lym
*/
public class SmsClient {
}

View File

@@ -0,0 +1,7 @@
/**
* 短信推送服务中心的 SDK
* 供其他微服务引用,便于调用短信推送服务接口
*
* @author lym
*/
package cn.itlym.shoulder.platform.notify.sms.sdk;

View File

@@ -0,0 +1,16 @@
<?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>
<groupId>cn.itlym.platform.notify</groupId>
<artifactId>shoulder-sms</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itlym.platform.notify</groupId>
<artifactId>sms-api</artifactId>
</project>

View File

@@ -0,0 +1,89 @@
<?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>
<groupId>cn.itlym.platform.notify</groupId>
<artifactId>shoulder-sms</artifactId>
<version>1.0-SNAPSHOT</version><!-- shoulder-platform-version -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itlym.platform.notify</groupId>
<artifactId>sms-center</artifactId>
<name>${artifactId}</name>
<description>Shoulder平台-短信推送服务</description>
<properties>
<javax.mail.version>1.6.2</javax.mail.version>
</properties>
<dependencies>
<!-- sms-api -->
<dependency>
<groupId>cn.itlym.platform.notify</groupId>
<artifactId>sms-api</artifactId>
</dependency>
<!-- aliyun-sms -->
<dependency>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-sms-aliyun-spring-boot-starter</artifactId>
</dependency>
<!-- WEB/db/mq/discovery/config/monitor/trace -->
<dependency>
<groupId>cn.itlym.platform</groupId>
<artifactId>shoulder-platform-starter-micro</artifactId>
</dependency>
<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- mail -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>${javax.mail.version}</version>
</dependency>
<!-- freemarker 模版 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
</dependencies>
<build>
<!-- maven 默认使用 artifactId+version拼接 -->
<finalName>${project.artifactId}</finalName>
<!-- 动态打包环境配置源文件 -->
<filters>
<filter>../../../dynamicConfig/config-${profile.active}.properties</filter>
</filters>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>

View File

@@ -0,0 +1,31 @@
package cn.itlym.shoulder.platform.notify.sms;
import cn.itlym.shoulder.platform.notify.sms.enums.EnumDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
/**
* 微服务的短信中心
*
* @author lym
*/
@SpringBootApplication
public class SmsCenterStarter {
public static void main(String[] args) {
SpringApplication.run(SmsCenterStarter.class, args);
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Enum.class, new EnumDeserializer());
objectMapper.registerModule(module);
return objectMapper;
}
}

View File

@@ -0,0 +1,19 @@
package cn.itlym.shoulder.platform.notify.sms.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.jms.core.JmsTemplate;
/**
* @author lym
*/
//@Configuration
public class JmsConfig {
@Bean
@ConditionalOnMissingBean
public JmsTemplate jmsTemplate() {
return new JmsTemplate();
}
}

View File

@@ -0,0 +1,41 @@
package cn.itlym.shoulder.platform.notify.sms.constant;
/**
* @author lym
*/
public enum EmailTemplateTypeEnum {
/**
* 纯文本
*/
TEXT(1),
/**
* 网页
*/
HTML(2),
/**
* themeleaf
*/
THYMELEAF(3),
/**
* freeMarker
*/
FREEMARKER(4),
;
private int value;
EmailTemplateTypeEnum(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}

View File

@@ -0,0 +1,46 @@
package cn.itlym.shoulder.platform.notify.sms.controller;
import cn.itlym.shoulder.platform.notify.sms.dto.EmailDTO;
import cn.itlym.shoulder.platform.notify.sms.dto.UiResult;
import cn.itlym.shoulder.platform.notify.sms.service.EmailSender;
import cn.itlym.shoulder.platform.notify.sms.service.EmailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/email")
public class EmailController {
@Value("${test.email.receiver-address}")
public String receiverAddress;
//@Autowired
private EmailService emailService;
@Autowired
private EmailSender emailSender;
@GetMapping("testSend")
public UiResult send() {
EmailDTO emailDTO = new EmailDTO();
emailDTO.setReceiver_emails(new String[]{receiverAddress});
emailDTO.setContent("test");
emailDTO.setContent("test");
try {
emailSender.send(emailDTO);
} catch (Exception e) {
e.printStackTrace();
return UiResult.error();
}
return UiResult.ok();
}
@PostMapping("list")
public UiResult list(EmailDTO mail) {
return emailService.listMail(mail);
}
}

View File

@@ -0,0 +1,48 @@
package cn.itlym.shoulder.platform.notify.sms.controller;
import cn.itlym.shoulder.platform.notify.sms.param.SmsParam;
import cn.itlym.shoulder.platform.notify.sms.service.SmsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lym
*/
@RestController
@RequestMapping("sms")
public class SmsController {
@Autowired
private SmsService smsService;
@Value("${test.sms.phoneNumber}")
private String phoneNumber;
@Value("${test.sms.template-code}")
private String templateCode;
@PostMapping("send")
public boolean send(SmsParam param) {
return smsService.sendSms(param);
}
@RequestMapping("test")
public String test() {
if (!"true".equals(System.getProperty("ebug"))) {
throw new IllegalStateException("please add jvm launch param(-Debug=true)");
}
SmsParam param = SmsParam.newBuilder()
.phoneNumber(phoneNumber)
.templateCode(templateCode)
.addTemplateParam("code", "test")
.build();
boolean result = smsService.sendSms(param);
return "your are testing now, and result=" + result;
}
}

View File

@@ -0,0 +1,32 @@
package cn.itlym.shoulder.platform.notify.sms.controller;
import cn.itlym.shoulder.platform.notify.sms.enums.MyEnum;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
ObjectMapper objectMapper;
@RequestMapping("ein")
public Object ein(MyEnum v) {
System.out.println(v);
return v;
}
@RequestMapping("eout")
public MyEnum eout() {
return MyEnum.V1;
}
@RequestMapping("de")
public MyEnum de() throws JsonProcessingException {
return objectMapper.readValue("{\"name\":\"1\"}", MyEnum.class);
}
}

View File

@@ -0,0 +1,74 @@
package cn.itlym.shoulder.platform.notify.sms.dto;
import java.io.Serializable;
import java.util.HashMap;
/**
* EmailDTO
*/
public class EmailDTO implements Serializable {
private static final long serialVersionUID = 1L;
//必填参数
private String[] receiver_emails;//接收方邮件
private String subject;//主题
private String content;//邮件内容
//选填
private String template;//模板
private HashMap<String, String> templateParam;// 自定义参数
public EmailDTO() {
super();
}
public EmailDTO(String[] receiver_emails, String subject, String content, String template,
HashMap<String, String> templateParam) {
super();
this.receiver_emails = receiver_emails;
this.subject = subject;
this.content = content;
this.template = template;
this.templateParam = templateParam;
}
public String[] getReceiver_emails() {
return receiver_emails;
}
public void setReceiver_emails(String[] receiver_emails) {
this.receiver_emails = receiver_emails;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getTemplate() {
return template;
}
public void setTemplate(String template) {
this.template = template;
}
public HashMap<String, String> getTemplateParam() {
return templateParam;
}
public void setTemplateParam(HashMap<String, String> templateParam) {
this.templateParam = templateParam;
}
}

View File

@@ -0,0 +1,54 @@
package cn.itlym.shoulder.platform.notify.sms.dto;
import java.util.HashMap;
import java.util.Map;
/**
* 页面响应 DTO
*/
public class UiResult extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public UiResult() {
put("code", 0);
}
public static UiResult error() {
return error(500, "未知异常,请联系管理员");
}
public static UiResult error(String msg) {
return error(500, msg);
}
public static UiResult error(int code, String msg) {
UiResult r = new UiResult();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static UiResult ok(Object msg) {
UiResult r = new UiResult();
r.put("msg", msg);
return r;
}
public static UiResult ok(Map<String, Object> map) {
UiResult r = new UiResult();
r.putAll(map);
return r;
}
public static UiResult ok() {
return new UiResult();
}
@Override
public UiResult put(String key, Object value) {
super.put(key, value);
return this;
}
}

View File

@@ -0,0 +1,33 @@
package cn.itlym.shoulder.platform.notify.sms.entity;
import java.util.Date;
/**
* @author lym
*/
public class EmailContentTemplate {
private String id;
/**
* 内容较长
*/
private String template;
/**
* text\html\thymeleaf\freemarker
*/
private String type;
/**
* 参数名,如 "a,b,c"
*/
private String param;
private boolean delete;
private Date createTime;
private String creator;
}

View File

@@ -0,0 +1,116 @@
package cn.itlym.shoulder.platform.notify.sms.entity;
import cn.itlym.shoulder.platform.notify.sms.dto.EmailDTO;
import javax.persistence.*;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Date;
//@Entity
@Table(name = "tb_email")
public class EmailEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 自增主键
*/
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", unique = true, nullable = false)
private Long id;
/**
* 接收人邮箱(多个逗号分开)
*/
@Column(name = "receive_email", nullable = false, length = 500)
private String receiveEmail;
/**
* 主题
*/
@Column(name = "subject", nullable = false, length = 100)
private String subject;
/**
* 发送内容
*/
@Column(name = "content", nullable = false, length = 65535)
private String content;
/**
* 模板
*/
@Column(name = "template", nullable = false, length = 100)
private String template;
/**
* 发送时间
*/
@Column(name = "send_time", nullable = false, length = 19)
private Timestamp sendTime;
public EmailEntity() {
super();
}
public EmailEntity(EmailDTO mail) {
this.receiveEmail = Arrays.toString(mail.getReceiver_emails());
this.subject = mail.getSubject();
this.content = mail.getContent();
this.template = mail.getTemplate();
this.sendTime = new Timestamp(new Date().getTime());
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getReceiveEmail() {
return receiveEmail;
}
public void setReceiveEmail(String receiveEmail) {
this.receiveEmail = receiveEmail;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getTemplate() {
return template;
}
public void setTemplate(String template) {
this.template = template;
}
public Timestamp getSendTime() {
return sendTime;
}
public void setSendTime(Timestamp sendTime) {
this.sendTime = sendTime;
}
}

View File

@@ -0,0 +1,37 @@
package cn.itlym.shoulder.platform.notify.sms.enums;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
public class EnumConverter implements Converter<String, Enum<?>> {
@SuppressWarnings("unchecked")
private Class<? extends Enum> enumType;
public EnumConverter(Class<? extends Enum> enumType) {
this.enumType = enumType;
}
@Override
public Enum convert(@NonNull String source) {
if (source.isBlank()) {
return null;
}
// 尝试用名称匹配。忽略大小写
Enum[] enums = enumType.getEnumConstants();
for (Enum e : enums) {
if (e.name().equalsIgnoreCase(source)) {
return e;
}
}
return null;
// 尝试使用 public static T of(String source) 方法
// 尝试使用标识字段匹配
// 尝试使用标识方法匹配
}
}

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