Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/mail-1.6.1

 Conflicts:
	yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/mail/MailSendApiImpl.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailAccountController.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailLogController.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/account/MailAccountBaseVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/account/MailAccountCreateReqVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/account/MailAccountPageReqVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/account/MailAccountUpdateReqVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogBaseVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogExcelVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogExportReqVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogPageReqVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/send/MailReqVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateBaseVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateCreateReqVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplatePageReqVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateUpdateReqVO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/mail/MailAccountConvert.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/mail/MailLogConvert.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/mail/MailTemplateConvert.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailAccountDO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailLogDO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailTemplateDO.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailAccountMapper.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailLogMapper.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailTemplateMapper.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailAccountService.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateService.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/impl/MailAccountServiceImpl.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/impl/MailLogServiceImpl.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/impl/MailSendServiceImpl.java
	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/impl/MailTemplateServiceImpl.java
This commit is contained in:
YunaiV 2022-05-02 23:05:34 +08:00
commit 0189993a2a
1231 changed files with 41509 additions and 25197 deletions

4
Jenkinsfile vendored
View File

@ -21,7 +21,7 @@ pipeline {
// GitHub 账号名
GITHUB_ACCOUNT = 'https://gitee.com/zhijiantianya/ruoyi-vue-pro'
// 应用名称
APP_NAME = 'yudao-admin-server'
APP_NAME = 'yudao-server'
// 应用部署路径
APP_DEPLOY_BASE_DIR = '/media/pi/KINGTON/data/work/projects/'
}
@ -57,4 +57,4 @@ pipeline {
}
}
}
}
}

View File

@ -5,21 +5,28 @@
**「我喜欢写代码,乐此不疲」**
**「我喜欢做开源,以此为乐」**
我 🐶 在上海艰苦奋斗,早中晚在 top3 大厂认真搬砖,夜里为开源做贡献。
如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。
## 🐯 平台简介
**芋道**,一套**全部开源**的**企业级**的快速开发平台,毫无保留给个人及企业免费使用。
**芋道**以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。
> 有任何问题,或者想要的功能,可以在 _Issues_ 中提给艿艿。
>
> 😜 给项目点点 Star 吧,这对我们真的很重要!
* 前端采用 [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) ,正在支持 Vue 3 + ElementUI Plus 最新方案。
* 后端采用 Spring Boot、MySQL + MyBatis Plus、Redis + Redisson。
* 数据库可使用 MySQL、Oracle、PostgreSQL、SQL Server、MariaDB、国产达梦 DM、TiDB 等
* 权限认证使用 Spring Security & Token & Redis支持多终端、多种用户的认证系统。
* 支持加载动态权限菜单,按钮级别权限控制,本地缓存提升性能。
* 支持 SaaS 多租户系统,可自定义每个租户的权限,提供透明化的多租户底层封装。
* 工作流使用 Activiti ,支持动态表单、在线设计流程、多种任务分配方式。
* 工作流使用 Activiti + Flowable,支持动态表单、在线设计流程、多种任务分配方式。
* 高效率开发,使用代码生成器可以一键生成前后端代码 + 单元测试 + Swagger 接口文档 + Validator 参数校验。
* 集成微信小程序、微信公众号、企业微信、钉钉等三方登陆,集成支付宝、微信等支付与退款。
* 集成阿里云、腾讯云、云片等短信渠道,集成阿里云、腾讯云、七牛云等云存储服务。
* 集成阿里云、腾讯云、云片等短信渠道,集成 MinIO、阿里云、腾讯云、七牛云等云存储服务。
| 项目名 | 说明 | 传说门 |
|--------------------|------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
@ -150,7 +157,7 @@ ps核心功能已经实现正在对接微信小程序中...
| 框架 | 说明 | 版本 | 学习指南 |
|---------------------------------------------------------------------------------------------|------------------|----------|----------------------------------------------------------------|
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.5.10 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.5.12 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 | |
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.8 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.1 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |

View File

@ -1,20 +1,15 @@
#!/bin/bash
set -e
# 基础
# export JAVA_HOME=/work/programs/jdk/jdk1.8.0_181
# export PATH=PATH=$PATH:$JAVA_HOME/bin
# export CLASSPATH=$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
DATE=$(date +%Y%m%d%H%M)
# 基础路径
BASE_PATH=/media/pi/KINGTON/data/work/projects/yudao-admin-server
BASE_PATH=/work/projects/yudao-server
# 编译后 jar 的地址。部署时Jenkins 会上传 jar 包到该目录下
SOURCE_PATH=$BASE_PATH/build
# 服务名称。同时约定部署服务的 jar 包名字也为它。
SERVER_NAME=yudao-admin-server
SERVER_NAME=yudao-server
# 环境
PROFILES_ACTIVE=dev
PROFILES_ACTIVE=development
# 健康检查 URL
HEALTH_CHECK_URL=http://127.0.0.1:48080/actuator/health/
@ -62,7 +57,7 @@ function transfer() {
echo "[transfer] 转移 $SERVER_NAME.jar 完成"
}
# 停止
# 停止:优雅关闭之前已经启动的服务
function stop() {
echo "[stop] 开始停止 $BASE_PATH/$SERVER_NAME"
PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}')
@ -71,8 +66,8 @@ function stop() {
# 正常关闭
echo "[stop] $BASE_PATH/$SERVER_NAME 运行中,开始 kill [$PID]"
kill -15 $PID
# 等待最大 60 秒,直到关闭完成。
for ((i = 0; i < 60; i++))
# 等待最大 120 秒,直到关闭完成。
for ((i = 0; i < 120; i++))
do
sleep 1
PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}')
@ -95,7 +90,7 @@ function stop() {
fi
}
# 启动
# 启动:启动后端项目
function start() {
# 开启启动前,打印启动参数
echo "[start] 开始启动 $BASE_PATH/$SERVER_NAME"
@ -108,13 +103,13 @@ function start() {
echo "[start] 启动 $BASE_PATH/$SERVER_NAME 完成"
}
# 健康检查
# 健康检查:自动判断后端项目是否正常启动
function healthCheck() {
# 如果配置健康检查,则进行健康检查
if [ -n "$HEALTH_CHECK_URL" ]; then
# 健康检查最大 60 秒,直到健康检查通过
# 健康检查最大 120 秒,直到健康检查通过
echo "[healthCheck] 开始通过 $HEALTH_CHECK_URL 地址,进行健康检查";
for ((i = 0; i < 60; i++))
for ((i = 0; i < 120; i++))
do
# 请求健康检查地址,只获取状态码。
result=`curl -I -m 10 -o /dev/null -s -w %{http_code} $HEALTH_CHECK_URL || echo "000"`
@ -138,11 +133,11 @@ function healthCheck() {
else
tail -n 10 nohup.out
fi
# 如果未配置健康检查,则 slepp 60 秒,人工看日志是否部署成功。
# 如果未配置健康检查,则 sleep 120 秒,人工看日志是否部署成功。
else
echo "[healthCheck] HEALTH_CHECK_URL 未配置,开始 sleep 60 秒";
sleep 60
echo "[healthCheck] sleep 60 秒完成,查看日志,自行判断是否启动成功";
echo "[healthCheck] HEALTH_CHECK_URL 未配置,开始 sleep 120 秒";
sleep 120
echo "[healthCheck] sleep 120 秒完成,查看日志,自行判断是否启动成功";
tail -n 50 nohup.out
fi
}
@ -159,7 +154,7 @@ function deploy() {
# 启动 Java 服务
start
# 健康检查
# healthCheck
healthCheck
}
deploy

View File

@ -25,7 +25,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>1.6.0-snapshot</revision>
<revision>1.6.2-snapshot</revision>
<!-- Maven 相关 -->
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
sql/db2/README.md Normal file
View File

@ -0,0 +1,3 @@
暂未适配 IBM DB2 数据库,如果你有需要,可以微信联系 wangwenbin-server 一起建设。
你需要把表结构与数据导入到 DM 数据库,我来测试与适配代码。

3
sql/dm/README.md Normal file
View File

@ -0,0 +1,3 @@
暂未适配国产 DM 数据库,如果你有需要,可以微信联系 wangwenbin-server 一起建设。
你需要把表结构与数据导入到 DM 数据库,我来测试与适配代码。

2471
sql/mysql/ruoyi-vue-pro.sql Normal file

File diff suppressed because it is too large Load Diff

5560
sql/oracle/ruoyi-vue-pro.sql Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,288 +0,0 @@
/*
Navicat Premium Data Transfer
Source Server : 127.0.0.1
Source Server Type : MySQL
Source Server Version : 80026
Source Host : localhost:3306
Source Schema : ruoyi-vue-pro
Target Server Type : MySQL
Target Server Version : 80026
File Encoding : 65001
Date: 05/02/2022 00:50:30
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for QRTZ_BLOB_TRIGGERS
-- ----------------------------
DROP TABLE IF EXISTS `QRTZ_BLOB_TRIGGERS`;
CREATE TABLE `QRTZ_BLOB_TRIGGERS` (
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`BLOB_DATA` blob,
PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
KEY `SCHED_NAME` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
CONSTRAINT `qrtz_blob_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of QRTZ_BLOB_TRIGGERS
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for QRTZ_CALENDARS
-- ----------------------------
DROP TABLE IF EXISTS `QRTZ_CALENDARS`;
CREATE TABLE `QRTZ_CALENDARS` (
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL,
`CALENDAR_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`CALENDAR` blob NOT NULL,
PRIMARY KEY (`SCHED_NAME`,`CALENDAR_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of QRTZ_CALENDARS
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for QRTZ_CRON_TRIGGERS
-- ----------------------------
DROP TABLE IF EXISTS `QRTZ_CRON_TRIGGERS`;
CREATE TABLE `QRTZ_CRON_TRIGGERS` (
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`CRON_EXPRESSION` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL,
`TIME_ZONE_ID` varchar(80) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
CONSTRAINT `qrtz_cron_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of QRTZ_CRON_TRIGGERS
-- ----------------------------
BEGIN;
INSERT INTO `QRTZ_CRON_TRIGGERS` VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', '* * * * * ?', 'Asia/Shanghai');
INSERT INTO `QRTZ_CRON_TRIGGERS` VALUES ('schedulerName', 'userSessionTimeoutJob', 'DEFAULT', '0 * * * * ? *', 'Asia/Shanghai');
COMMIT;
-- ----------------------------
-- Table structure for QRTZ_FIRED_TRIGGERS
-- ----------------------------
DROP TABLE IF EXISTS `QRTZ_FIRED_TRIGGERS`;
CREATE TABLE `QRTZ_FIRED_TRIGGERS` (
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL,
`ENTRY_ID` varchar(95) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`INSTANCE_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`FIRED_TIME` bigint NOT NULL,
`SCHED_TIME` bigint NOT NULL,
`PRIORITY` int NOT NULL,
`STATE` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
`JOB_NAME` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`JOB_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`IS_NONCONCURRENT` varchar(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`REQUESTS_RECOVERY` varchar(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`SCHED_NAME`,`ENTRY_ID`),
KEY `IDX_QRTZ_FT_TRIG_INST_NAME` (`SCHED_NAME`,`INSTANCE_NAME`),
KEY `IDX_QRTZ_FT_INST_JOB_REQ_RCVRY` (`SCHED_NAME`,`INSTANCE_NAME`,`REQUESTS_RECOVERY`),
KEY `IDX_QRTZ_FT_J_G` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`),
KEY `IDX_QRTZ_FT_JG` (`SCHED_NAME`,`JOB_GROUP`),
KEY `IDX_QRTZ_FT_T_G` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
KEY `IDX_QRTZ_FT_TG` (`SCHED_NAME`,`TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of QRTZ_FIRED_TRIGGERS
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for QRTZ_JOB_DETAILS
-- ----------------------------
DROP TABLE IF EXISTS `QRTZ_JOB_DETAILS`;
CREATE TABLE `QRTZ_JOB_DETAILS` (
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL,
`JOB_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`JOB_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`DESCRIPTION` varchar(250) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`JOB_CLASS_NAME` varchar(250) COLLATE utf8mb4_unicode_ci NOT NULL,
`IS_DURABLE` varchar(1) COLLATE utf8mb4_unicode_ci NOT NULL,
`IS_NONCONCURRENT` varchar(1) COLLATE utf8mb4_unicode_ci NOT NULL,
`IS_UPDATE_DATA` varchar(1) COLLATE utf8mb4_unicode_ci NOT NULL,
`REQUESTS_RECOVERY` varchar(1) COLLATE utf8mb4_unicode_ci NOT NULL,
`JOB_DATA` blob,
PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`),
KEY `IDX_QRTZ_J_REQ_RECOVERY` (`SCHED_NAME`,`REQUESTS_RECOVERY`),
KEY `IDX_QRTZ_J_GRP` (`SCHED_NAME`,`JOB_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of QRTZ_JOB_DETAILS
-- ----------------------------
BEGIN;
INSERT INTO `QRTZ_JOB_DETAILS` VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', NULL, 'cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000057400104A4F425F48414E444C45525F4E414D4574000C7061794E6F746966794A6F627800);
INSERT INTO `QRTZ_JOB_DETAILS` VALUES ('schedulerName', 'userSessionTimeoutJob', 'DEFAULT', NULL, 'cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B0200007870000000000000000D7400104A4F425F48414E444C45525F4E414D457400157573657253657373696F6E54696D656F75744A6F627800);
COMMIT;
-- ----------------------------
-- Table structure for QRTZ_LOCKS
-- ----------------------------
DROP TABLE IF EXISTS `QRTZ_LOCKS`;
CREATE TABLE `QRTZ_LOCKS` (
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL,
`LOCK_NAME` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`SCHED_NAME`,`LOCK_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of QRTZ_LOCKS
-- ----------------------------
BEGIN;
INSERT INTO `QRTZ_LOCKS` VALUES ('schedulerName', 'STATE_ACCESS');
INSERT INTO `QRTZ_LOCKS` VALUES ('schedulerName', 'TRIGGER_ACCESS');
COMMIT;
-- ----------------------------
-- Table structure for QRTZ_PAUSED_TRIGGER_GRPS
-- ----------------------------
DROP TABLE IF EXISTS `QRTZ_PAUSED_TRIGGER_GRPS`;
CREATE TABLE `QRTZ_PAUSED_TRIGGER_GRPS` (
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`SCHED_NAME`,`TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of QRTZ_PAUSED_TRIGGER_GRPS
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for QRTZ_SCHEDULER_STATE
-- ----------------------------
DROP TABLE IF EXISTS `QRTZ_SCHEDULER_STATE`;
CREATE TABLE `QRTZ_SCHEDULER_STATE` (
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL,
`INSTANCE_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`LAST_CHECKIN_TIME` bigint NOT NULL,
`CHECKIN_INTERVAL` bigint NOT NULL,
PRIMARY KEY (`SCHED_NAME`,`INSTANCE_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of QRTZ_SCHEDULER_STATE
-- ----------------------------
BEGIN;
INSERT INTO `QRTZ_SCHEDULER_STATE` VALUES ('schedulerName', 'Yunai.local1635571630493', 1635572537879, 15000);
COMMIT;
-- ----------------------------
-- Table structure for QRTZ_SIMPLE_TRIGGERS
-- ----------------------------
DROP TABLE IF EXISTS `QRTZ_SIMPLE_TRIGGERS`;
CREATE TABLE `QRTZ_SIMPLE_TRIGGERS` (
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`REPEAT_COUNT` bigint NOT NULL,
`REPEAT_INTERVAL` bigint NOT NULL,
`TIMES_TRIGGERED` bigint NOT NULL,
PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
CONSTRAINT `qrtz_simple_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of QRTZ_SIMPLE_TRIGGERS
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for QRTZ_SIMPROP_TRIGGERS
-- ----------------------------
DROP TABLE IF EXISTS `QRTZ_SIMPROP_TRIGGERS`;
CREATE TABLE `QRTZ_SIMPROP_TRIGGERS` (
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`STR_PROP_1` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`STR_PROP_2` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`STR_PROP_3` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`INT_PROP_1` int DEFAULT NULL,
`INT_PROP_2` int DEFAULT NULL,
`LONG_PROP_1` bigint DEFAULT NULL,
`LONG_PROP_2` bigint DEFAULT NULL,
`DEC_PROP_1` decimal(13,4) DEFAULT NULL,
`DEC_PROP_2` decimal(13,4) DEFAULT NULL,
`BOOL_PROP_1` varchar(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`BOOL_PROP_2` varchar(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
CONSTRAINT `qrtz_simprop_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of QRTZ_SIMPROP_TRIGGERS
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for QRTZ_TRIGGERS
-- ----------------------------
DROP TABLE IF EXISTS `QRTZ_TRIGGERS`;
CREATE TABLE `QRTZ_TRIGGERS` (
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`JOB_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`JOB_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`DESCRIPTION` varchar(250) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`NEXT_FIRE_TIME` bigint DEFAULT NULL,
`PREV_FIRE_TIME` bigint DEFAULT NULL,
`PRIORITY` int DEFAULT NULL,
`TRIGGER_STATE` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
`TRIGGER_TYPE` varchar(8) COLLATE utf8mb4_unicode_ci NOT NULL,
`START_TIME` bigint NOT NULL,
`END_TIME` bigint DEFAULT NULL,
`CALENDAR_NAME` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`MISFIRE_INSTR` smallint DEFAULT NULL,
`JOB_DATA` blob,
PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
KEY `IDX_QRTZ_T_J` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`),
KEY `IDX_QRTZ_T_JG` (`SCHED_NAME`,`JOB_GROUP`),
KEY `IDX_QRTZ_T_C` (`SCHED_NAME`,`CALENDAR_NAME`),
KEY `IDX_QRTZ_T_G` (`SCHED_NAME`,`TRIGGER_GROUP`),
KEY `IDX_QRTZ_T_STATE` (`SCHED_NAME`,`TRIGGER_STATE`),
KEY `IDX_QRTZ_T_N_STATE` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`,`TRIGGER_STATE`),
KEY `IDX_QRTZ_T_N_G_STATE` (`SCHED_NAME`,`TRIGGER_GROUP`,`TRIGGER_STATE`),
KEY `IDX_QRTZ_T_NEXT_FIRE_TIME` (`SCHED_NAME`,`NEXT_FIRE_TIME`),
KEY `IDX_QRTZ_T_NFT_ST` (`SCHED_NAME`,`TRIGGER_STATE`,`NEXT_FIRE_TIME`),
KEY `IDX_QRTZ_T_NFT_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`),
KEY `IDX_QRTZ_T_NFT_ST_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_STATE`),
KEY `IDX_QRTZ_T_NFT_ST_MISFIRE_GRP` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_GROUP`,`TRIGGER_STATE`),
CONSTRAINT `qrtz_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of QRTZ_TRIGGERS
-- ----------------------------
BEGIN;
INSERT INTO `QRTZ_TRIGGERS` VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', 'payNotifyJob', 'DEFAULT', NULL, 1635572540000, 1635572539000, 5, 'WAITING', 'CRON', 1635294882000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000074000F4A4F425F52455452595F434F554E5471007E000B7800);
INSERT INTO `QRTZ_TRIGGERS` VALUES ('schedulerName', 'userSessionTimeoutJob', 'DEFAULT', 'userSessionTimeoutJob', 'DEFAULT', NULL, 1643993400000, -1, 5, 'WAITING', 'CRON', 1643993386000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B0200007870000007D074000F4A4F425F52455452595F434F554E547371007E0009000000037800);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -14,9 +14,9 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>1.6.0-snapshot</revision>
<revision>1.6.2-snapshot</revision>
<!-- 统一依赖管理 -->
<spring.boot.version>2.5.10</spring.boot.version>
<spring.boot.version>2.5.12</spring.boot.version>
<!-- Web 相关 -->
<knife4j.version>3.0.2</knife4j.version>
<swagger-annotations.version>1.5.22</swagger-annotations.version>
@ -25,10 +25,11 @@
<mysql.version>5.1.46</mysql.version>
<druid.version>1.2.8</druid.version>
<mybatis-plus.version>3.4.3.4</mybatis-plus.version>
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
<dynamic-datasource.version>3.5.0</dynamic-datasource.version>
<redisson.version>3.16.6</redisson.version>
<redisson.version>3.17.0</redisson.version>
<!-- Config 配置中心相关 -->
<apollo.version>1.7.0</apollo.version>
<apollo.version>1.9.2</apollo.version>
<!-- Job 定时任务相关 -->
<!-- 服务保障相关 -->
<lock4j.version>2.2.0</lock4j.version>
@ -45,6 +46,7 @@
<activiti.version>7.1.0.M6</activiti.version>
<flowable.version>6.7.0</flowable.version>
<!-- 工具类相关 -->
<jasypt-spring-boot-starter.version>3.0.4</jasypt-spring-boot-starter.version>
<lombok.version>1.18.20</lombok.version>
<mapstruct.version>1.4.1.Final</mapstruct.version>
<hutool.version>5.6.1</hutool.version>
@ -52,13 +54,15 @@
<velocity.version>2.2</velocity.version>
<screw.version>1.0.5</screw.version>
<guava.version>30.1.1-jre</guava.version>
<guice.version>5.1.0</guice.version>
<transmittable-thread-local.version>2.12.2</transmittable-thread-local.version>
<commons-net.version>3.8.0</commons-net.version>
<jsch.version>0.1.55</jsch.version>
<!-- 三方云服务相关 -->
<s3.version>2.17.147</s3.version>
<minio.version>8.2.2</minio.version>
<aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.version>
<aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version>
<tencentcloud-sdk-java.version>3.1.471</tencentcloud-sdk-java.version>
<yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
<justauth.version>1.4.0</justauth.version>
</properties>
@ -190,6 +194,11 @@
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 -->
<version>${mybatis-plus-generator.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
@ -426,6 +435,12 @@
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId> <!-- 加解密 -->
<version>${jasypt-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-excel</artifactId>
@ -490,6 +505,12 @@
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>${guice.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId> <!-- 解决 ThreadLocal 父子线程的传值问题 -->
@ -514,9 +535,9 @@
<version>${revision}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${s3.version}</version>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<!-- SMS SDK begin -->
@ -545,6 +566,11 @@
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>${aliyun-java-sdk-dysmsapi.version}</version>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>${tencentcloud-sdk-java.version}</version>
</dependency>
<!-- SMS SDK end -->
<dependency>

View File

@ -1,8 +1,19 @@
package cn.iocoder.yudao.framework.common.util.collection;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.TypeUtil;
import org.springframework.cglib.core.TypeUtils;
import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* Array 工具类
@ -30,4 +41,16 @@ public class ArrayUtils {
return result;
}
public static <T, V> V[] toArray(Collection<T> from, Function<T, V> mapper) {
return toArray(convertList(from, mapper));
}
@SuppressWarnings("unchecked")
public static <T> T[] toArray(Collection<T> from) {
if (CollectionUtil.isEmpty(from)) {
return (T[]) (new Object[0]);
}
return ArrayUtil.toArray(from, (Class<T>) CollectionUtil.getElementType(from.iterator()));
}
}

View File

@ -115,7 +115,7 @@ public class CollectionUtils {
return new HashMap<>();
}
return from.stream()
.collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
.collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
}
// 暂时没想好名字先以 2 结尾噶
@ -169,4 +169,5 @@ public class CollectionUtils {
public static <T> Collection<T> singleton(T deptId) {
return deptId == null ? Collections.emptyList() : Collections.singleton(deptId);
}
}

View File

@ -1,9 +1,7 @@
package cn.iocoder.yudao.framework.common.util.http;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.map.TableMap;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.ReferenceUtil;
import cn.hutool.core.util.ReflectUtil;
import java.nio.charset.Charset;

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.common.util.io;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import java.io.InputStream;

View File

@ -35,7 +35,7 @@ import java.util.Set;
* 注意使用 DeptDataPermissionRule 需要保证表中有 dept_id 部门编号的字段可自定义
*
* 实际业务场景下会存在一个经典的问题当用户修改部门时冗余的 dept_id 是否需要修改
* 1. 一般情况下dept_id 不进行修改则会导致用户看到之前的数据yudao-admin-server 采用该方案
* 1. 一般情况下dept_id 不进行修改则会导致用户看到之前的数据yudao-server 采用该方案
* 2. 部分情况下希望该用户还是能看到之前的数据则有两种方式解决需要你改造该 DeptDataPermissionRule 的实现代码
* 1编写洗数据的脚本 dept_id 修改成新部门的编号建议
* 最终过滤条件是 WHERE dept_id = ?

View File

@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayQrPayClient;
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayWapPayClient;
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXNativePayClient;
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPubPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
@ -63,6 +64,7 @@ public class PayClientFactoryImpl implements PayClientFactory {
case WX_PUB: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
case WX_LITE: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
case WX_APP: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
case WX_NATIVE: return (AbstractPayClient<Config>) new WXNativePayClient(channelId, (WXPayClientConfig) config);
case ALIPAY_WAP: return (AbstractPayClient<Config>) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config);
case ALIPAY_QR: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
case ALIPAY_APP: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);

View File

@ -18,6 +18,7 @@ import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
@ -54,7 +55,8 @@ public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayCl
*/
@Override
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
Map<String, String> params = data.getParams();
Map<String, String> params = strToMap(data.getBody());
return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no"))
.channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
.tradeStatus(params.get("trade_status"))
@ -64,7 +66,7 @@ public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayCl
@Override
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
Map<String, String> params = notifyData.getParams();
Map<String, String> params = strToMap(notifyData.getBody());
PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no"))
.tradeNo(params.get("out_trade_no"))
.reqNo(params.get("out_biz_no"))
@ -128,4 +130,17 @@ public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayCl
}
}
public static Map<String, String> strToMap(String s) {
Map<String, String> stringStringMap = new HashMap<>();
//调整时间格式
String s3 = s.replaceAll("%3A", ":");
//获取map
String s4 = s3.replace("+", " ");
String[] split = s4.split("&");
for (String s1 : split) {
String[] split1 = s1.split("=");
stringStringMap.put(split1[0], split1[1]);
}
return stringStringMap;
}
}

View File

@ -0,0 +1,141 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS;
import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS;
@Slf4j
public class WXNativePayClient extends AbstractPayClient<WXPayClientConfig> {
private WxPayService client;
public WXNativePayClient(Long channelId, WXPayClientConfig config) {
super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config, new WXCodeMapping());
}
@Override
protected void doInit() {
WxPayConfig payConfig = new WxPayConfig();
BeanUtil.copyProperties(config, payConfig, "keyContent");
payConfig.setTradeType(WxPayConstants.TradeType.NATIVE); // 设置使用 native 支付方式
// if (StrUtil.isNotEmpty(config.getKeyContent())) {
// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
// }
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
// weixin-pay-java 存在 BUG无法直接设置内容所以创建临时文件来解决
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
}
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
// weixin-pay-java 存在 BUG无法直接设置内容所以创建临时文件来解决
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
}
// 真实客户端
this.client = new WxPayServiceImpl();
client.setConfig(payConfig);
}
@Override
public PayCommonResult<String> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
// 这里原生的返回的是支付的 url 所以直接使用string接收
//"invokeResponse": "weixin://wxpay/bizpayurl?pr=EGYAem7zz"
String responseV3;
try {
switch (config.getApiVersion()) {
case WXPayClientConfig.API_VERSION_V2:
responseV3 = unifiedOrderV2(reqDTO).getCodeUrl();
break;
case WXPayClientConfig.API_VERSION_V3:
responseV3 = this.unifiedOrderV3(reqDTO);
break;
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"),
ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()), null, codeMapping);
}
return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, responseV3, codeMapping);
}
private WxPayNativeOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
//前端
String trade_type = reqDTO.getChannelExtras().get("trade_type");
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest
.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId())
.body(reqDTO.getBody())
.totalFee(reqDTO.getAmount().intValue()) // 单位分
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
.spbillCreateIp(reqDTO.getUserIp())
.notifyUrl(reqDTO.getNotifyUrl())
.productId(trade_type)
.build();
// 执行请求
return client.createOrder(request);
}
private String unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(reqDTO.getMerchantOrderId());
request.setDescription(reqDTO.getBody());
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
request.setNotifyUrl(reqDTO.getNotifyUrl());
// 执行请求
// log.info("支付字段request:{}",request.getTimeExpire());
return client.createOrderV3(TradeTypeEnum.NATIVE, request);
}
@Override
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException {
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody());
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
// 转换结果
return PayOrderNotifyRespDTO.builder().orderExtensionNo(notifyResult.getOutTradeNo())
.channelOrderNo(notifyResult.getTransactionId()).channelUserId(notifyResult.getOpenid())
.successTime(DateUtil.parse(notifyResult.getTimeEnd(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
.data(data.getBody()).build();
}
@Override
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
//TODO 需要实现
throw new UnsupportedOperationException("需要实现");
}
@Override
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
//TODO 需要实现
throw new UnsupportedOperationException();
}
}

View File

@ -82,9 +82,9 @@ public class WXPayClientConfig implements PayClientConfig {
@NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
private String privateCertContent;
/**
* apiV3 钥值
* apiV3 钥值
*/
@NotBlank(message = "apiV3 钥值 不能为空", groups = V3.class)
@NotBlank(message = "apiV3 钥值 不能为空", groups = V3.class)
private String apiV3Key;
/**

View File

@ -98,7 +98,7 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
// TODO 芋艿貌似没 title
.body(reqDTO.getBody())
.totalFee(reqDTO.getAmount().intValue()) // 单位分
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss"))
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
.spbillCreateIp(reqDTO.getUserIp())
.openid(getOpenid(reqDTO))
.notifyUrl(reqDTO.getNotifyUrl())
@ -114,7 +114,7 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
// TODO 芋艿貌似没 title
request.setDescription(reqDTO.getBody());
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分
request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss"));
request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"));
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
request.setNotifyUrl(reqDTO.getNotifyUrl());

View File

@ -20,6 +20,8 @@ public enum PayChannelEnum {
WX_PUB("wx_pub", "微信 JSAPI 支付", WXPayClientConfig.class), // 公众号网页
WX_LITE("wx_lite", "微信小程序支付", WXPayClientConfig.class),
WX_APP("wx_app", "微信 App 支付", WXPayClientConfig.class),
WX_NATIVE("wx_native", "微信 native 支付", WXPayClientConfig.class),
ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class),
ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class),

View File

@ -12,7 +12,7 @@
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>短信拓展,支持阿里云、云片</description>
<description>短信拓展,支持阿里云、云片、腾讯云</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies>
@ -77,6 +77,10 @@
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
</dependency>
<!-- SMS SDK end -->
</dependencies>

View File

@ -11,7 +11,7 @@ import java.util.List;
* 短信客户端用于对接各短信平台的 SDK实现短信发送等功能
*
* @author zzf
* @date 2021/1/25 14:14
* @since 2021/1/25 14:14
*/
public interface SmsClient {

View File

@ -6,7 +6,7 @@ import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
* 短信客户端的工厂接口
*
* @author zzf
* @date 2021/1/28 14:01
* @since 2021/1/28 14:01
*/
public interface SmsClientFactory {

View File

@ -16,7 +16,7 @@ import java.util.List;
* 短信客户端的抽象类提供模板方法减少子类的冗余代码
*
* @author zzf
* @date 2021/2/1 9:28
* @since 2021/2/1 9:28
*/
@Slf4j
public abstract class AbstractSmsClient implements SmsClient {
@ -31,7 +31,7 @@ public abstract class AbstractSmsClient implements SmsClient {
protected final SmsCodeMapping codeMapping;
public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) {
this.properties = properties;
this.properties = prepareProperties(properties);
this.codeMapping = codeMapping;
}
@ -54,11 +54,21 @@ public abstract class AbstractSmsClient implements SmsClient {
return;
}
log.info("[refresh][配置({})发生变化,重新初始化]", properties);
this.properties = properties;
this.properties = prepareProperties(properties);
// 初始化
this.init();
}
/**
* 在赋值给{@link this#properties}子类可根据需要预处理短信渠道配置
*
* @param properties 数据库中存储的短信渠道配置
* @return 满足子类实现的短信渠道配置
*/
protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) {
return properties;
}
@Override
public Long getId() {
return properties.getId();

View File

@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.sms.core.client.SmsClient;
import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory;
import cn.iocoder.yudao.framework.sms.core.client.impl.aliyun.AliyunSmsClient;
import cn.iocoder.yudao.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient;
import cn.iocoder.yudao.framework.sms.core.client.impl.tencent.TencentSmsClient;
import cn.iocoder.yudao.framework.sms.core.client.impl.yunpian.YunpianSmsClient;
import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum;
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
@ -44,7 +45,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
Arrays.stream(SmsChannelEnum.values()).forEach(channel -> {
// 创建一个空的 SmsChannelProperties 对象
SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode())
.setApiKey("default").setApiSecret("default");
.setApiKey("default default").setApiSecret("default");
// 创建 Sms 客户端
AbstractSmsClient smsClient = createSmsClient(properties);
channelCodeClients.put(channel.getCode(), smsClient);
@ -81,6 +82,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
case ALIYUN: return new AliyunSmsClient(properties);
case YUN_PIAN: return new YunpianSmsClient(properties);
case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties);
case TENCENT: return new TencentSmsClient(properties);
}
// 创建失败错误日志 + 抛出异常
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);

View File

@ -41,7 +41,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
* 阿里短信客户端的实现类
*
* @author zzf
* @date 2021/1/25 14:17
* @since 2021/1/25 14:17
*/
@Slf4j
public class AliyunSmsClient extends AbstractSmsClient {

View File

@ -0,0 +1,41 @@
package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
import lombok.Data;
/**
* 腾讯云短信配置实现类
* 腾讯云发送短信时需要额外的参数 sdkAppId,
*
* @author shiwp
*/
@Data
public class TencentSmsChannelProperties extends SmsChannelProperties {
/**
* 应用 id
*/
private String sdkAppId;
/**
* 考虑到不破坏原有的 apiKey + apiSecret 的结构
* 所以腾讯云短信存储时 secretId 拼接到 apiKey 字段中格式为 "secretId sdkAppId"
* 因此在使用时需要将 secretId sdkAppId 解析出来分别存储到对应字段中
*/
public static TencentSmsChannelProperties build(SmsChannelProperties properties) {
if (properties instanceof TencentSmsChannelProperties) {
return (TencentSmsChannelProperties) properties;
}
TencentSmsChannelProperties result = BeanUtil.toBean(properties, TencentSmsChannelProperties.class);
String combineKey = properties.getApiKey();
Assert.notEmpty(combineKey, "apiKey 不能为空");
String[] keys = combineKey.trim().split(" ");
Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]");
Assert.notBlank(keys[0], "腾讯云短信 secretId 不能为空");
Assert.notBlank(keys[1], "腾讯云短信 sdkAppId 不能为空");
result.setSdkAppId(keys[1]).setApiKey(keys[0]);
return result;
}
}

View File

@ -0,0 +1,302 @@
package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.impl.AbstractSmsClient;
import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.*;
import lombok.Data;
import java.util.Date;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
/**
* 腾讯云短信功能实现
* <p>
* 参见 https://cloud.tencent.com/document/product/382/52077
*
* @author shiwp
*/
public class TencentSmsClient extends AbstractSmsClient {
/**
* 调用成功 code
*/
public static final String API_SUCCESS_CODE = "Ok";
/**
* REGION使用南京
*/
private static final String ENDPOINT = "ap-nanjing";
/**
* 是否国际/港澳台短信
* 0表示国内短信
* 1表示国际/港澳台短信
*/
private static final long INTERNATIONAL = 0L;
private SmsClient client;
public TencentSmsClient(SmsChannelProperties properties) {
super(properties, new TencentSmsCodeMapping());
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
}
@Override
protected void doInit() {
// 实例化一个认证对象入参需要传入腾讯云账户密钥对 secretIdsecretKey
Credential credential = new Credential(properties.getApiKey(), properties.getApiSecret());
client = new SmsClient(credential, ENDPOINT);
}
@Override
protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId,
String mobile,
String apiTemplateId,
List<KeyValue<String, Object>> templateParams) throws Throwable {
return invoke(() -> buildSendSmsRequest(sendLogId, mobile, apiTemplateId, templateParams),
this::doSendSms0,
response -> {
SendStatus sendStatus = response.getSendStatusSet()[0];
return SmsCommonResult.build(sendStatus.getCode(), sendStatus.getMessage(), response.getRequestId(),
new SmsSendRespDTO().setSerialNo(sendStatus.getSerialNo()), codeMapping);
});
}
/**
* 腾讯云发放短信的时候需要额外的参数 sdkAppId
* 考虑到不破坏原有的 apiKey + apiSecret 的结构所以将 secretId 拼接到 apiKey 字段中格式为 "secretId sdkAppId"
* 因此这边需要使用 TencentSmsChannelProperties 做拆分重新封装到 properties
*
* @param properties 数据库中存储的短信渠道配置
* @return TencentSmsChannelProperties
*/
@Override
protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) {
return TencentSmsChannelProperties.build(properties);
}
/**
* 调用腾讯云 SDK 发送短信
*
* @param request 发送短信请求
* @return 发送短信响应
* @throws TencentCloudSDKException SDK 用来封装发送短信失败
*/
private SendSmsResponse doSendSms0(SendSmsRequest request) throws TencentCloudSDKException {
return client.SendSms(request);
}
/**
* 封装腾讯云发送短信请求
*
* @param sendLogId 日志编号
* @param mobile 手机号
* @param apiTemplateId 短信 API 的模板编号
* @param templateParams 短信模板参数通过 List 数组保证参数的顺序
* @return 腾讯云发送短信请求
*/
private SendSmsRequest buildSendSmsRequest(Long sendLogId,
String mobile,
String apiTemplateId,
List<KeyValue<String, Object>> templateParams) {
SendSmsRequest request = new SendSmsRequest();
request.setSmsSdkAppId(((TencentSmsChannelProperties) properties).getSdkAppId());
request.setPhoneNumberSet(new String[]{mobile});
request.setSignName(properties.getSignature());
request.setTemplateId(apiTemplateId);
request.setTemplateParamSet(ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue())));
request.setSessionContext(JsonUtils.toJsonString(new SessionContext().setLogId(sendLogId)));
return request;
}
@Override
protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
List<SmsReceiveStatus> callback = JsonUtils.parseArray(text, SmsReceiveStatus.class);
return CollectionUtils.convertList(callback, status -> {
SmsReceiveRespDTO data = new SmsReceiveRespDTO();
data.setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription());
data.setReceiveTime(status.getReceiveTime()).setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus()));
data.setMobile(status.getMobile()).setSerialNo(status.getSerialNo());
SessionContext context;
Long logId;
Assert.notNull(context = status.getSessionContext(), "回执信息中未解析出 context请联系腾讯云小助手");
Assert.notNull(logId = context.getLogId(), "回执信息中未解析出 logId请联系腾讯云小助手");
data.setLogId(logId);
return data;
});
}
@Override
protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable {
return invoke(() -> this.buildSmsTemplateStatusRequest(apiTemplateId),
this::doGetSmsTemplate0,
response -> {
SmsTemplateRespDTO data = convertTemplateStatusDTO(response.getDescribeTemplateStatusSet()[0]);
return SmsCommonResult.build(API_SUCCESS_CODE, null, response.getRequestId(), data, codeMapping);
});
}
@VisibleForTesting
SmsTemplateRespDTO convertTemplateStatusDTO(DescribeTemplateListStatus templateStatus) {
if (templateStatus == null) {
return null;
}
SmsTemplateAuditStatusEnum auditStatus;
Assert.notNull(templateStatus.getStatusCode(),
StrUtil.format("短信模版审核状态为 null模版 id{}", templateStatus.getTemplateId()));
switch (templateStatus.getStatusCode().intValue()) {
case -1:
auditStatus = SmsTemplateAuditStatusEnum.FAIL;
break;
case 0:
auditStatus = SmsTemplateAuditStatusEnum.SUCCESS;
break;
case 1:
auditStatus = SmsTemplateAuditStatusEnum.CHECKING;
break;
default:
throw new IllegalStateException(StrUtil.format("不能解析短信模版审核状态{},模版 id{}",
templateStatus.getStatusCode(), templateStatus.getTemplateId()));
}
SmsTemplateRespDTO data = new SmsTemplateRespDTO();
data.setId(String.valueOf(templateStatus.getTemplateId())).setContent(templateStatus.getTemplateContent());
data.setAuditStatus(auditStatus.getStatus()).setAuditReason(templateStatus.getReviewReply());
return data;
}
/**
* 封装查询模版审核状态请求
* @param apiTemplateId api 的模版 id
* @return 查询模版审核状态请求
*/
private DescribeSmsTemplateListRequest buildSmsTemplateStatusRequest(String apiTemplateId) {
DescribeSmsTemplateListRequest request = new DescribeSmsTemplateListRequest();
request.setTemplateIdSet(new Long[]{Long.parseLong(apiTemplateId)});
// 地区 0表示国内短信1表示国际/港澳台短信
request.setInternational(INTERNATIONAL);
return request;
}
/**
* 调用腾讯云 SDK 查询短信模版状态
*
* @param request 查询短信模版状态请求
* @return 查询短信模版状态响应
* @throws TencentCloudSDKException SDK 用来封装查询短信模版状态失败
*/
private DescribeSmsTemplateListResponse doGetSmsTemplate0(DescribeSmsTemplateListRequest request) throws TencentCloudSDKException {
return client.DescribeSmsTemplateList(request);
}
<Q, P, R> SmsCommonResult<R> invoke(Supplier<Q> requestSupplier,
SdkFunction<Q, P> responseSupplier,
Function<P, SmsCommonResult<R>> resultGen) {
// 构建请求body
Q request = requestSupplier.get();
P response;
// 调用腾讯云发送短信
try {
response = responseSupplier.apply(request);
} catch (TencentCloudSDKException e) {
// 调用异常封装结果
return SmsCommonResult.build(e.getErrorCode(), e.getMessage(), e.getRequestId(), null, codeMapping);
}
return resultGen.apply(response);
}
@Data
private static class SmsReceiveStatus {
/**
* 短信接受成功 code
*/
public static final String SUCCESS_CODE = "SUCCESS";
/**
* 用户实际接收到短信的时间
*/
@JsonProperty("user_receive_time")
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
private Date receiveTime;
/**
* 国家或地区
*/
@JsonProperty("nationcode")
private String nationCode;
/**
* 手机号码
*/
private String mobile;
/**
* 实际是否收到短信接收状态SUCCESS成功FAIL失败
*/
@JsonProperty("report_status")
private String status;
/**
* 用户接收短信状态码错误信息
*/
@JsonProperty("errmsg")
private String errCode;
/**
* 用户接收短信状态描述
*/
@JsonProperty("description")
private String description;
/**
* 本次发送标识 ID与发送接口返回的SerialNo对应
*/
@JsonProperty("sid")
private String serialNo;
/**
* 用户的 session 内容与发送接口的请求参数SessionContext一致
*/
@JsonProperty("ext")
private SessionContext sessionContext;
}
@VisibleForTesting
@Data
static class SessionContext {
/**
* 发送短信记录id
*/
private Long logId;
}
private interface SdkFunction<T, R> {
R apply(T t) throws TencentCloudSDKException;
}
}

View File

@ -0,0 +1,50 @@
package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping;
import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import static cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*;
/**
* 腾讯云的 SmsCodeMapping 实现类
*
* 参见 https://cloud.tencent.com/document/api/382/52075#.E5.85.AC.E5.85.B1.E9.94.99.E8.AF.AF.E7.A0.81
*
* @author : shiwp
*/
public class TencentSmsCodeMapping implements SmsCodeMapping {
@Override
public ErrorCode apply(String apiCode) {
switch (apiCode) {
case TencentSmsClient.API_SUCCESS_CODE: return GlobalErrorCodeConstants.SUCCESS;
case "FailedOperation.ContainSensitiveWord": return SMS_SEND_CONTENT_INVALID;
case "FailedOperation.JsonParseFail":
case "MissingParameter.EmptyPhoneNumberSet":
case "LimitExceeded.PhoneNumberCountLimit":
case "FailedOperation.FailResolvePacket": return GlobalErrorCodeConstants.BAD_REQUEST;
case "FailedOperation.InsufficientBalanceInSmsPackage": return SMS_ACCOUNT_MONEY_NOT_ENOUGH;
case "FailedOperation.MarketingSendTimeConstraint": return SMS_SEND_MARKET_LIMIT_CONTROL;
case "FailedOperation.PhoneNumberInBlacklist": return SMS_MOBILE_BLACK;
case "FailedOperation.SignatureIncorrectOrUnapproved": return SMS_SIGN_INVALID;
case "FailedOperation.MissingTemplateToModify":
case "FailedOperation.TemplateIncorrectOrUnapproved": return SMS_TEMPLATE_INVALID;
case "InvalidParameterValue.IncorrectPhoneNumber": return SMS_MOBILE_INVALID;
case "InvalidParameterValue.SdkAppIdNotExist": return SMS_APP_ID_INVALID;
case "InvalidParameterValue.TemplateParameterLengthLimit":
case "InvalidParameterValue.TemplateParameterFormatError": return SMS_TEMPLATE_PARAM_ERROR;
case "LimitExceeded.PhoneNumberDailyLimit": return SMS_SEND_DAY_LIMIT_CONTROL;
case "LimitExceeded.PhoneNumberThirtySecondLimit":
case "LimitExceeded.PhoneNumberOneHourLimit": return SMS_SEND_BUSINESS_LIMIT_CONTROL;
case "UnauthorizedOperation.RequestPermissionDeny":
case "FailedOperation.ForbidAddMarketingTemplates":
case "FailedOperation.NotEnterpriseCertification":
case "UnauthorizedOperation.IndividualUserMarketingSmsPermissionDeny": return SMS_PERMISSION_DENY;
case "UnauthorizedOperation.RequestIpNotInWhitelist": return SMS_IP_DENY;
case "AuthFailure.SecretIdNotFound": return SMS_ACCOUNT_INVALID;
}
return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
}
}

View File

@ -35,7 +35,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
* 云片短信客户端的实现类
*
* @author zzf
* @date 9:48 2021/3/5
* @since 9:48 2021/3/5
*/
@Slf4j
public class YunpianSmsClient extends AbstractSmsClient {

View File

@ -8,7 +8,7 @@ import lombok.Getter;
* 短信渠道枚举
*
* @author zzf
* @date 2021/1/25 10:56
* @since 2021/1/25 10:56
*/
@Getter
@AllArgsConstructor
@ -17,7 +17,7 @@ public enum SmsChannelEnum {
DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"),
YUN_PIAN("YUN_PIAN", "云片"),
ALIYUN("ALIYUN", "阿里云"),
// TENCENT("TENCENT", "腾讯云"),
TENCENT("TENCENT", "腾讯云"),
// HUA_WEI("HUA_WEI", "华为云"),
;

View File

@ -26,6 +26,9 @@ public interface SmsFrameworkErrorCodeConstants {
ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词");
// 腾讯云为避免骚扰用户营销短信只允许在8点到22点发送
ErrorCode SMS_SEND_MARKET_LIMIT_CONTROL = new ErrorCode(2001000105, "营销短信发送时间限制");
// ========== 模板相关 2001000200 ==========
ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在
ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确");
@ -41,6 +44,7 @@ public interface SmsFrameworkErrorCodeConstants {
ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失");
ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确");
ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中");
ErrorCode SMS_APP_ID_INVALID = new ErrorCode(2001000903, "SdkAppId不合法");
ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常");

View File

@ -11,7 +11,7 @@ import javax.validation.constraints.NotNull;
* 短信渠道配置类
*
* @author zzf
* @date 2021/1/25 17:01
* @since 2021/1/25 17:01
*/
@Data
@Validated
@ -40,9 +40,9 @@ public class SmsChannelProperties {
@NotEmpty(message = "短信 API 的账号不能为空")
private String apiKey;
/**
* 短信 API
* 短信 API
*/
@NotEmpty(message = "短信 API 的钥不能为空")
@NotEmpty(message = "短信 API 的钥不能为空")
private String apiSecret;
/**
* 短信发送回调 URL

View File

@ -0,0 +1,222 @@
package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import com.google.common.collect.Lists;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.DescribeSmsTemplateListResponse;
import com.tencentcloudapi.sms.v20210111.models.DescribeTemplateListStatus;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.ArrayList;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.when;
/**
* {@link TencentSmsClient} 的单元测试
*
* @author shiwp
*/
public class TencentSmsClientTest extends BaseMockitoUnitTest {
private final SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey避免构建报错
.setApiSecret(randomString()) // 随机一个 apiSecret避免构建报错
.setSignature("芋道源码");
@InjectMocks
private TencentSmsClient smsClient = new TencentSmsClient(properties);
@Mock
private SmsClient client;
@Test
public void testDoInit() {
// 准备参数
// mock 方法
// 调用
smsClient.doInit();
// 断言
assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client"));
}
@Test
public void testRefresh() {
// 准备参数
SmsChannelProperties p = new SmsChannelProperties()
.setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey避免构建报错
.setApiSecret(randomString()) // 随机一个 apiSecret避免构建报错
.setSignature("芋道源码");
// 调用
smsClient.refresh(p);
// 断言
assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client"));
}
@Test
public void testDoSendSms() throws Throwable {
// 准备参数
Long sendLogId = randomLongId();
String mobile = randomString();
String apiTemplateId = randomString();
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
String requestId = randomString();
String serialNo = randomString();
// mock 方法
SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> {
o.setRequestId(requestId);
SendStatus[] sendStatuses = new SendStatus[1];
o.setSendStatusSet(sendStatuses);
SendStatus sendStatus = new SendStatus();
sendStatuses[0] = sendStatus;
sendStatus.setCode(TencentSmsClient.API_SUCCESS_CODE);
sendStatus.setMessage("send success");
sendStatus.setSerialNo(serialNo);
});
when(client.SendSms(argThat(request -> {
assertEquals(mobile, request.getPhoneNumberSet()[0]);
assertEquals(properties.getSignature(), request.getSignName());
assertEquals(apiTemplateId, request.getTemplateId());
assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)),
toJsonString(request.getTemplateParamSet()));
assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId"));
return true;
}))).thenReturn(response);
// 调用
SmsCommonResult<SmsSendRespDTO> result = smsClient.doSendSms(sendLogId, mobile,
apiTemplateId, templateParams);
// 断言
assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
assertEquals(response.getRequestId(), result.getApiRequestId());
// 断言结果
assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getData().getSerialNo());
}
@Test
public void testDoTParseSmsReceiveStatus() throws Throwable {
// 准备参数
String text = "[\n" +
" {\n" +
" \"user_receive_time\": \"2015-10-17 08:03:04\",\n" +
" \"nationcode\": \"86\",\n" +
" \"mobile\": \"13900000001\",\n" +
" \"report_status\": \"SUCCESS\",\n" +
" \"errmsg\": \"DELIVRD\",\n" +
" \"description\": \"用户短信送达成功\",\n" +
" \"sid\": \"12345\",\n" +
" \"ext\": {\"logId\":\"67890\"}\n" +
" }\n" +
"]";
// mock 方法
// 调用
List<SmsReceiveRespDTO> statuses = smsClient.doParseSmsReceiveStatus(text);
// 断言
assertEquals(1, statuses.size());
assertTrue(statuses.get(0).getSuccess());
assertEquals("DELIVRD", statuses.get(0).getErrorCode());
assertEquals("用户短信送达成功", statuses.get(0).getErrorMsg());
assertEquals("13900000001", statuses.get(0).getMobile());
assertEquals(DateUtils.buildTime(2015, 10, 17, 8, 3, 4), statuses.get(0).getReceiveTime());
assertEquals("12345", statuses.get(0).getSerialNo());
assertEquals(67890L, statuses.get(0).getLogId());
}
@Test
public void testDoGetSmsTemplate() throws Throwable {
// 准备参数
Long apiTemplateId = randomLongId();
String requestId = randomString();
// mock 方法
DescribeSmsTemplateListResponse response = randomPojo(DescribeSmsTemplateListResponse.class, o -> {
DescribeTemplateListStatus[] describeTemplateListStatuses = new DescribeTemplateListStatus[1];
DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus();
templateStatus.setTemplateId(apiTemplateId);
templateStatus.setStatusCode(0L);// 设置模板通过
describeTemplateListStatuses[0] = templateStatus;
o.setDescribeTemplateStatusSet(describeTemplateListStatuses);
o.setRequestId(requestId);
});
when(client.DescribeSmsTemplateList(argThat(request -> {
assertEquals(apiTemplateId, request.getTemplateIdSet()[0]);
return true;
}))).thenReturn(response);
// 调用
SmsCommonResult<SmsTemplateRespDTO> result = smsClient.doGetSmsTemplate(apiTemplateId.toString());
// 断言
assertEquals(TencentSmsClient.API_SUCCESS_CODE, result.getApiCode());
assertNull(result.getApiMsg());
assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
assertEquals(response.getRequestId(), result.getApiRequestId());
// 断言结果
assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getData().getId());
assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getData().getContent());
assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getData().getAuditReason());
}
@Test
public void testConvertSuccessTemplateStatus() {
testTemplateStatus(SmsTemplateAuditStatusEnum.SUCCESS, 0L);
}
@Test
public void testConvertCheckingTemplateStatus() {
testTemplateStatus(SmsTemplateAuditStatusEnum.CHECKING, 1L);
}
@Test
public void testConvertFailTemplateStatus() {
testTemplateStatus(SmsTemplateAuditStatusEnum.FAIL, -1L);
}
@Test
public void testConvertUnknownTemplateStatus() {
DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus();
templateStatus.setStatusCode(3L);
Long templateId = randomLongId();
// 调用并断言结果
assertThrows(IllegalStateException.class, () -> smsClient.convertTemplateStatusDTO(templateStatus),
StrUtil.format("不能解析短信模版审核状态[3]模版id[{}]", templateId));
}
private void testTemplateStatus(SmsTemplateAuditStatusEnum expected, Long value) {
DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus();
templateStatus.setStatusCode(value);
SmsTemplateRespDTO result = smsClient.convertTemplateStatusDTO(templateStatus);
assertEquals(expected.getStatus(), result.getAuditStatus());
}
}

View File

@ -0,0 +1,50 @@
package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link TencentSmsCodeMapping} 的单元测试
*
* @author : shiwp
*/
public class TencentSmsCodeMappingTest extends BaseMockitoUnitTest {
@InjectMocks
private TencentSmsCodeMapping codeMapping;
@Test
public void testApply() {
assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(TencentSmsClient.API_SUCCESS_CODE));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("FailedOperation.ContainSensitiveWord"));
assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("FailedOperation.JsonParseFail"));
assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("MissingParameter.EmptyPhoneNumberSet"));
assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("LimitExceeded.PhoneNumberCountLimit"));
assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("FailedOperation.FailResolvePacket"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("FailedOperation.InsufficientBalanceInSmsPackage"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_MARKET_LIMIT_CONTROL, codeMapping.apply("FailedOperation.MarketingSendTimeConstraint"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK, codeMapping.apply("FailedOperation.PhoneNumberInBlacklist"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("FailedOperation.SignatureIncorrectOrUnapproved"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("FailedOperation.MissingTemplateToModify"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("FailedOperation.TemplateIncorrectOrUnapproved"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("InvalidParameterValue.IncorrectPhoneNumber"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_APP_ID_INVALID, codeMapping.apply("InvalidParameterValue.SdkAppIdNotExist"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("InvalidParameterValue.TemplateParameterLengthLimit"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("InvalidParameterValue.TemplateParameterFormatError"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberDailyLimit"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberThirtySecondLimit"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberOneHourLimit"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("UnauthorizedOperation.RequestPermissionDeny"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("FailedOperation.ForbidAddMarketingTemplates"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("FailedOperation.NotEnterpriseCertification"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("UnauthorizedOperation.IndividualUserMarketingSmsPermissionDeny"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_IP_DENY, codeMapping.apply("UnauthorizedOperation.RequestIpNotInWhitelist"));
assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("AuthFailure.SecretIdNotFound"));
}
}

View File

@ -1,6 +1,9 @@
package cn.iocoder.yudao.framework.social.config;
import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.framework.social.core.YudaoAuthRequestFactory;
import com.xkcoding.http.HttpUtil;
import com.xkcoding.http.support.hutool.HutoolImpl;
import com.xkcoding.justauth.autoconfigure.JustAuthProperties;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.cache.AuthStateCache;
@ -23,6 +26,9 @@ public class YudaoSocialAutoConfiguration {
@Bean
@ConditionalOnProperty(prefix = "justauth", value = "enabled", havingValue = "true", matchIfMissing = true)
public YudaoAuthRequestFactory yudaoAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) {
// 需要修改 HttpUtil 使用的实现避免类报错
HttpUtil.setHttp(new HutoolImpl());
// 创建 YudaoAuthRequestFactory
return new YudaoAuthRequestFactory(properties, authStateCache);
}

View File

@ -4,29 +4,40 @@ import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import lombok.AllArgsConstructor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import java.util.HashSet;
import java.util.Set;
/**
* 基于 MyBatis Plus 多租户的功能实现 DB 层面的多租户的功能
*
* @author 芋道源码
*/
@AllArgsConstructor
public class TenantDatabaseInterceptor implements TenantLineHandler {
private final TenantProperties properties;
private final Set<String> ignoreTables = new HashSet<>();
public TenantDatabaseInterceptor(TenantProperties properties) {
// 不同 DB 大小写的习惯不同所以需要都添加进去
properties.getIgnoreTables().forEach(table -> {
ignoreTables.add(table.toLowerCase());
ignoreTables.add(table.toUpperCase());
});
// OracleKeyGenerator 生成主键时会查询这个表查询这个表后会自动拼接 TENANT_ID 导致报错
ignoreTables.add("DUAL");
}
@Override
public Expression getTenantId() {
return new LongValue( TenantContextHolder.getRequiredTenantId());
return new LongValue(TenantContextHolder.getRequiredTenantId());
}
@Override
public boolean ignoreTable(String tableName) {
return TenantContextHolder.isIgnore() // 情况一全局忽略多租户
|| CollUtil.contains(properties.getIgnoreTables(), tableName); // 情况二忽略多租户的表
|| CollUtil.contains(ignoreTables, tableName); // 情况二忽略多租户的表
}
}

View File

@ -75,7 +75,7 @@ public class TenantSecurityWebFilter extends ApiRequestFilter {
}
}
//检查是否是忽略的 URL, 如果是则允许访问
// 如果非允许忽略租户的 URL则校验租户是否合法
if (!isIgnoreUrl(request)) {
// 2. 如果请求未带租户的编号不允许访问
if (tenantId == null) {
@ -92,6 +92,10 @@ public class TenantSecurityWebFilter extends ApiRequestFilter {
ServletUtils.writeJSON(response, result);
return;
}
} else { // 如果是允许忽略租户的 URL若未传递租户编号则默认忽略租户编号避免报错
if (tenantId == null) {
TenantContextHolder.setIgnore(true);
}
}
// 继续过滤

View File

@ -15,12 +15,12 @@ import java.util.List;
public interface ConfigFrameworkDAO {
/**
* 查询是否存在比 maxUpdateTime 更新记录更晚的配置
* 查询是否存在比 maxUpdateTime 的更新记录数量
*
* @param maxUpdateTime 最大更新时间
* @return 是否存在
*/
boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime);
int selectCountByUpdateTimeGt(Date maxUpdateTime);
/**
* 查询配置列表

View File

@ -24,7 +24,6 @@ import java.util.List;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Predicate;
@Slf4j
public class DBConfigRepository extends AbstractConfigRepository {
@ -172,7 +171,7 @@ public class DBConfigRepository extends AbstractConfigRepository {
if (maxUpdateTime == null) { // 如果更新时间为空说明 DB 一定有新数据
log.info("[loadConfigIfUpdate][首次加载全量配置]");
} else { // 判断数据库中是否有更新的配置
if (!configFrameworkDAO.selectExistsByUpdateTimeAfter(maxUpdateTime)) {
if (configFrameworkDAO.selectCountByUpdateTimeGt(maxUpdateTime) == 0) {
return null;
}
log.info("[loadConfigIfUpdate][增量加载全量配置]");

View File

@ -63,8 +63,8 @@
<!-- 三方云服务相关 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
</dependency>
<!-- Test 测试相关 -->

View File

@ -20,15 +20,17 @@ public interface FileClient {
* @param content 文件流
* @param path 相对路径
* @return 完整路径 HTTP 访问地址
* @throws Exception 上传文件时抛出 Exception 异常
*/
String upload(byte[] content, String path);
String upload(byte[] content, String path) throws Exception;
/**
* 删除文件
*
* @param path 相对路径
* @throws Exception 删除文件时抛出 Exception 异常
*/
void delete(String path);
void delete(String path) throws Exception;
/**
* 获得文件的内容
@ -36,6 +38,6 @@ public interface FileClient {
* @param path 相对路径
* @return 文件的内容
*/
byte[] getContent(String path);
byte[] getContent(String path) throws Exception;
}

View File

@ -44,7 +44,7 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
String dir = StrUtil.removeSuffix(filePath, fileName);
boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content));
if (!success) {
throw new FtpException(StrUtil.format("文件到目标目录 ({}) 失败", filePath));
throw new FtpException(StrUtil.format("文件到目标目录 ({}) 失败", filePath));
}
// 拼接返回路径
return super.formatFileUrl(config.getDomain(), path);

View File

@ -1,19 +1,14 @@
package cn.iocoder.yudao.framework.file.core.client.s3;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import io.minio.*;
import java.net.URI;
import java.io.ByteArrayInputStream;
import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_QINIU;
import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_ALIYUN;
/**
* 基于 S3 协议的文件客户端实现 MinIO阿里云腾讯云七牛云华为云等云服务
@ -24,7 +19,7 @@ import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.
*/
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
private S3Client client;
private MinioClient client;
public S3FileClient(Long id, S3FileClientConfig config) {
super(id, config);
@ -34,34 +29,27 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
protected void doInit() {
// 补全 domain
if (StrUtil.isEmpty(config.getDomain())) {
config.setDomain(createDomain());
config.setDomain(buildDomain());
}
// 初始化客户端
client = S3Client.builder()
.serviceConfiguration(sb -> sb.pathStyleAccessEnabled(false) // 关闭路径风格
.chunkedEncodingEnabled(false)) // 禁用 chunk
.endpointOverride(createURI()) // 上传地址
.region(Region.of(config.getRegion())) // Region
.credentialsProvider(StaticCredentialsProvider.create( // 认证密钥
AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret())))
.overrideConfiguration(cb -> cb.addExecutionInterceptor(new S3ModifyPathInterceptor(config.getBucket())))
client = MinioClient.builder()
.endpoint(buildEndpointURL()) // Endpoint URL
.region(buildRegion()) // Region
.credentials(config.getAccessKey(), config.getAccessSecret()) // 认证密钥
.build();
}
/**
* 基于 endpoint 构建调用云服务的 URI 地址
* 基于 endpoint 构建调用云服务的 URL 地址
*
* @return URI 地址
*/
private URI createURI() {
String uri;
// 如果是七牛无需拼接 bucket
if (config.getEndpoint().contains(ENDPOINT_QINIU)) {
uri = StrUtil.format("https://{}", config.getEndpoint());
} else {
uri = StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
private String buildEndpointURL() {
// 如果已经是 http 或者 https则不进行拼接.主要适配 MinIO
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
return config.getEndpoint();
}
return URI.create(uri);
return StrUtil.format("https://{}", config.getEndpoint());
}
/**
@ -69,35 +57,56 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
*
* @return Domain 地址
*/
private String createDomain() {
private String buildDomain() {
// 如果已经是 http 或者 https则不进行拼接.主要适配 MinIO
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
}
// 阿里云腾讯云华为云都适合七牛云比较特殊必须有自定义域名
return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
}
/**
* 基于 bucket 构建 region 地区
*
* @return region 地区
*/
private String buildRegion() {
// 阿里云必须有 region否则会报错
if (config.getEndpoint().contains(ENDPOINT_ALIYUN)) {
return StrUtil.subBefore(config.getEndpoint(), '.', false)
.replaceAll("-internal", ""); // 去除内网 Endpoint 的后缀
}
return null;
}
@Override
public String upload(byte[] content, String path) {
public String upload(byte[] content, String path) throws Exception {
// 执行上传
PutObjectRequest.Builder request = PutObjectRequest.builder()
client.putObject(PutObjectArgs.builder()
.bucket(config.getBucket()) // bucket 必须传递
.key(path); // 相对路径作为 key
client.putObject(request.build(), RequestBody.fromBytes(content));
.object(path) // 相对路径作为 key
.stream(new ByteArrayInputStream(content), content.length, -1) // 文件内容
.build());
// 拼接返回路径
return config.getDomain() + "/" + path;
}
@Override
public void delete(String path) {
DeleteObjectRequest.Builder request = DeleteObjectRequest.builder()
public void delete(String path) throws Exception {
client.removeObject(RemoveObjectArgs.builder()
.bucket(config.getBucket()) // bucket 必须传递
.key(path); // 相对路径作为 key
client.deleteObject(request.build());
.object(path) // 相对路径作为 key
.build());
}
@Override
public byte[] getContent(String path) {
GetObjectRequest.Builder request = GetObjectRequest.builder()
public byte[] getContent(String path) throws Exception {
GetObjectResponse response = client.getObject(GetObjectArgs.builder()
.bucket(config.getBucket()) // bucket 必须传递
.key(path); // 相对路径作为 key
return client.getObjectAsBytes(request.build()).asByteArray();
.object(path) // 相对路径作为 key
.build());
return IoUtil.readBytes(response);
}
}

View File

@ -18,37 +18,28 @@ import javax.validation.constraints.NotNull;
public class S3FileClientConfig implements FileClientConfig {
public static final String ENDPOINT_QINIU = "qiniucs.com";
public static final String ENDPOINT_ALIYUN = "aliyuncs.com";
/**
* 节点地址
* 1. MinIO
* 1. MinIOhttps://www.iocoder.cn/Spring-Boot/MinIO 例如说http://127.0.0.1:9000
* 2. 阿里云https://help.aliyun.com/document_detail/31837.html
* 3. 腾讯云
* 3. 腾讯云https://cloud.tencent.com/document/product/436/6224
* 4. 七牛云https://developer.qiniu.com/kodo/4088/s3-access-domainname
* 5. 华为云
* 5. 华为云https://developer.huaweicloud.com/endpoint?OBS
*/
@NotNull(message = "endpoint 不能为空")
private String endpoint;
/**
* 自定义域名
* 1. MinIO
* 1. MinIO通过 Nginx 配置
* 2. 阿里云https://help.aliyun.com/document_detail/31836.html
* 3. 腾讯云https://cloud.tencent.com/document/product/436/11142
* 4. 七牛云https://developer.qiniu.com/kodo/8556/set-the-custom-source-domain-name
* 5. 华为云
* 5. 华为云https://support.huaweicloud.com/usermanual-obs/obs_03_0032.html
*/
@URL(message = "domain 必须是 URL 格式")
private String domain;
/**
* 区域
* 1. MinIO
* 2. 阿里云https://help.aliyun.com/document_detail/31837.html
* 3. 腾讯云
* 4. 七牛云https://developer.qiniu.com/kodo/4088/s3-access-domainname
* 5. 华为云
*/
@NotNull(message = "region 不能为空")
private String region;
/**
* 存储 Bucket
*/
@ -57,11 +48,11 @@ public class S3FileClientConfig implements FileClientConfig {
/**
* 访问 Key
* 1. MinIO
* 2. 阿里云
* 1. MinIOhttps://www.iocoder.cn/Spring-Boot/MinIO
* 2. 阿里云https://ram.console.aliyun.com/manage/ak
* 3. 腾讯云https://console.cloud.tencent.com/cam/capi
* 4. 七牛云https://portal.qiniu.com/user/key
* 5. 华为云
* 5. 华为云https://support.huaweicloud.com/qs-obs/obs_qs_0005.html
*/
@NotNull(message = "accessKey 不能为空")
private String accessKey;

View File

@ -1,36 +0,0 @@
package cn.iocoder.yudao.framework.file.core.client.s3;
import software.amazon.awssdk.core.interceptor.Context;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
import software.amazon.awssdk.http.SdkHttpRequest;
/**
* S3 修改路径的拦截器移除多余的 Bucket 前缀
* 如果不使用该拦截器希望上传的路径是 /tudou.jpg 会被添加成 /bucket/tudou.jpg
*
* @author 芋道源码
*/
public class S3ModifyPathInterceptor implements ExecutionInterceptor {
private final String bucket;
public S3ModifyPathInterceptor(String bucket) {
this.bucket = "/" + bucket;
}
@Override
public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) {
SdkHttpRequest request = context.httpRequest();
SdkHttpRequest.Builder rb = SdkHttpRequest.builder().protocol(request.protocol()).host(request.host()).port(request.port())
.method(request.method()).headers(request.headers()).rawQueryParameters(request.rawQueryParameters());
// 移除 path 前的 bucket 路径
if (request.encodedPath().startsWith(bucket)) {
rb.encodedPath(request.encodedPath().substring(bucket.length()));
} else {
rb.encodedPath(request.encodedPath());
}
return rb.build();
}
}

View File

@ -3,11 +3,13 @@ package cn.iocoder.yudao.framework.file.core.client.ftp;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.extra.ftp.FtpMode;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
public class FtpFileClientTest {
@Test
@Disabled
public void test() {
// 创建客户端
FtpFileClientConfig config = new FtpFileClientConfig();

View File

@ -2,11 +2,13 @@ package cn.iocoder.yudao.framework.file.core.client.local;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
public class LocalFileClientTest {
@Test
@Disabled
public void test() {
// 创建客户端
LocalFileClientConfig config = new LocalFileClientConfig();

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.file.core.client.s3;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -11,9 +10,25 @@ import javax.validation.Validation;
public class S3FileClientTest {
@Test
@Disabled // MinIO如果要集成测试可以注释本行
public void testMinIO() throws Exception {
S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的
config.setAccessKey("admin");
config.setAccessSecret("password");
config.setBucket("yudaoyuanma");
config.setDomain(null);
// 默认 9000 endpoint
config.setEndpoint("http://127.0.0.1:9000");
// 执行上传
testExecuteUpload(config);
}
@Test
@Disabled // 阿里云 OSS如果要集成测试可以注释本行
public void testAliyun() {
public void testAliyun() throws Exception {
S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的
config.setAccessKey(System.getenv("ALIYUN_ACCESS_KEY"));
@ -29,7 +44,7 @@ public class S3FileClientTest {
@Test
@Disabled // 腾讯云 COS如果要集成测试可以注释本行
public void testQCloud() {
public void testQCloud() throws Exception {
S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的
config.setAccessKey(System.getenv("QCLOUD_ACCESS_KEY"));
@ -38,7 +53,6 @@ public class S3FileClientTest {
config.setDomain(null); // 如果有自定义域名则可以设置http://tengxun-oss.iocoder.cn
// 默认上海的 endpoint
config.setEndpoint("cos.ap-shanghai.myqcloud.com");
config.setRegion("ap-shanghai");
// 执行上传
testExecuteUpload(config);
@ -46,7 +60,7 @@ public class S3FileClientTest {
@Test
@Disabled // 七牛云存储如果要集成测试可以注释本行
public void testQiniu() {
public void testQiniu() throws Exception {
S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的
// config.setAccessKey(System.getenv("QINIU_ACCESS_KEY"));
@ -62,11 +76,24 @@ public class S3FileClientTest {
testExecuteUpload(config);
}
private void testExecuteUpload(S3FileClientConfig config) {
// 补全配置
if (config.getRegion() == null) {
config.setRegion(StrUtil.subBefore(config.getEndpoint(), '.', false));
}
@Test
@Disabled // 华为云存储如果要集成测试可以注释本行
public void testHuaweiCloud() throws Exception {
S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的
// config.setAccessKey(System.getenv("HUAWEI_CLOUD_ACCESS_KEY"));
// config.setAccessSecret(System.getenv("HUAWEI_CLOUD_SECRET_KEY"));
config.setBucket("yudao");
config.setDomain(null); // 如果有自定义域名则可以设置
// 默认上海的 endpoint
config.setEndpoint("obs.cn-east-3.myhuaweicloud.com");
// 执行上传
testExecuteUpload(config);
}
private void testExecuteUpload(S3FileClientConfig config) throws Exception {
// 校验配置
ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config);
// 创建 Client
S3FileClient client = new S3FileClient(0L, config);
@ -77,9 +104,9 @@ public class S3FileClientTest {
String fullPath = client.upload(content, path);
System.out.println("访问地址:" + fullPath);
// 读取文件
if (false) {
if (true) {
byte[] bytes = client.getContent(path);
System.out.println("文件内容:" + bytes);
System.out.println("文件内容:" + bytes.length);
}
// 删除文件
if (false) {

View File

@ -2,11 +2,13 @@ package cn.iocoder.yudao.framework.file.core.client.sftp;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
public class SftpFileClientTest {
@Test
@Disabled
public void test() {
// 创建客户端
SftpFileClientConfig config = new SftpFileClientConfig();

View File

@ -36,7 +36,6 @@
<artifactId>jakarta.validation-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -38,6 +38,19 @@
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>

View File

@ -0,0 +1,108 @@
package cn.iocoder.yudao.framework.mybatis.config;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.framework.mybatis.core.enums.SqlConstants;
import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.Set;
/**
* IdType {@link IdType#NONE} 根据 PRIMARY 数据源所使用的数据库自动设置
*
* @author 芋道源码
*/
@Slf4j
public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor {
private static final String ID_TYPE_KEY = "mybatis-plus.global-config.db-config.id-type";
private static final String DATASOURCE_DYNAMIC_KEY = "spring.datasource.dynamic";
private static final String QUARTZ_JOB_STORE_DRIVER_KEY = "spring.quartz.properties.org.quartz.jobStore.driverDelegateClass";
private static final Set<DbType> INPUT_ID_TYPES = SetUtils.asSet(DbType.ORACLE, DbType.ORACLE_12C,
DbType.POSTGRE_SQL, DbType.KINGBASE_ES, DbType.DB2, DbType.H2);
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
// 如果获取不到 DbType则不进行处理
DbType dbType = getDbType(environment);
if (dbType == null) {
return;
}
// 设置 Quartz JobStore 对应的 Driver
// TODO 芋艿暂时没有找到特别合适的地方先放在这里
setJobStoreDriverIfPresent(environment, dbType);
// 初始化 SQL 静态变量
SqlConstants.init(dbType);
// 如果非 NONE则不进行处理
IdType idType = getIdType(environment);
if (idType != IdType.NONE) {
return;
}
// 情况一用户输入 ID适合 OraclePostgreSQLKingbaseDB2H2 数据库
if (INPUT_ID_TYPES.contains(dbType)) {
setIdType(environment, IdType.INPUT);
return;
}
// 情况二自增 ID适合 MySQL 等直接自增的数据库
setIdType(environment, IdType.AUTO);
}
public IdType getIdType(ConfigurableEnvironment environment) {
return environment.getProperty(ID_TYPE_KEY, IdType.class);
}
public void setIdType(ConfigurableEnvironment environment, IdType idType) {
environment.getSystemProperties().put(ID_TYPE_KEY, idType);
log.info("[setIdType][修改 MyBatis Plus 的 idType 为({})]", idType);
}
public void setJobStoreDriverIfPresent(ConfigurableEnvironment environment, DbType dbType) {
String driverClass = environment.getProperty(QUARTZ_JOB_STORE_DRIVER_KEY);
if (StrUtil.isNotEmpty(driverClass)) {
return;
}
// 根据 dbType 类型获取对应的 driverClass
switch (dbType) {
case POSTGRE_SQL:
driverClass = "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate";
break;
case ORACLE:
case ORACLE_12C:
driverClass = "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate";
break;
case SQL_SERVER:
case SQL_SERVER2005:
driverClass = "org.quartz.impl.jdbcjobstore.MSSQLDelegate";
break;
}
// 设置 driverClass 变量
if (StrUtil.isNotEmpty(driverClass)) {
environment.getSystemProperties().put(QUARTZ_JOB_STORE_DRIVER_KEY, driverClass);
}
}
public static DbType getDbType(ConfigurableEnvironment environment) {
String primary = environment.getProperty(DATASOURCE_DYNAMIC_KEY + "." + "primary");
if (StrUtil.isEmpty(primary)) {
return null;
}
String url = environment.getProperty(DATASOURCE_DYNAMIC_KEY + ".datasource." + primary + ".url");
if (StrUtil.isEmpty(url)) {
return null;
}
return JdbcUtils.getDbType(url);
}
}

View File

@ -1,13 +1,22 @@
package cn.iocoder.yudao.framework.mybatis.config;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
import com.baomidou.mybatisplus.extension.incrementer.H2KeyGenerator;
import com.baomidou.mybatisplus.extension.incrementer.KingbaseKeyGenerator;
import com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator;
import com.baomidou.mybatisplus.extension.incrementer.PostgreKeyGenerator;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* MyBaits 配置类
@ -31,4 +40,25 @@ public class YudaoMybatisAutoConfiguration {
return new DefaultDBFieldHandler(); // 自动填充参数类
}
@Bean
@ConditionalOnProperty(prefix = "mybatis-plus.global-config.db-config", name = "id-type", havingValue = "INPUT")
public IKeyGenerator keyGenerator(ConfigurableEnvironment environment) {
DbType dbType = IdTypeEnvironmentPostProcessor.getDbType(environment);
if (dbType != null) {
switch (dbType) {
case POSTGRE_SQL:
return new PostgreKeyGenerator();
case ORACLE:
case ORACLE_12C:
return new OracleKeyGenerator();
case H2:
return new H2KeyGenerator();
case KINGBASE_ES:
return new KingbaseKeyGenerator();
}
}
// 找不到合适的 IKeyGenerator 实现类
throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType));
}
}

View File

@ -3,8 +3,8 @@ package cn.iocoder.yudao.framework.mybatis.core.dataobject;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Builder;
import lombok.Data;
import org.apache.ibatis.type.JdbcType;
import java.io.Serializable;
import java.util.Date;
@ -32,14 +32,14 @@ public abstract class BaseDO implements Serializable {
*
* 使用 String 类型的原因是未来可能会存在非数值的情况留好拓展性
*/
@TableField(fill = FieldFill.INSERT)
@TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
private String creator;
/**
* 更新者目前使用 SysUser id 编号
*
* 使用 String 类型的原因是未来可能会存在非数值的情况留好拓展性
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
private String updater;
/**
* 是否删除

View File

@ -1,11 +1,21 @@
package cn.iocoder.yudao.framework.mybatis.core.enums;
import com.baomidou.mybatisplus.annotation.DbType;
/**
* SQL相关常量类
*
* @author 芋道源码
*/
public interface SqlConstants {
public class SqlConstants {
/**
* 数据库的类型
*/
public static DbType DB_TYPE;
String LIMIT1 = "LIMIT 1";
public static void init(DbType dbType) {
DB_TYPE = dbType;
}
}

View File

@ -75,12 +75,20 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
return selectList(new LambdaQueryWrapper<T>().in(field, values));
}
/**
* 逐条插入适合少量数据插入或者对性能要求不高的场景
*
* 如果大量请使用 {@link com.baomidou.mybatisplus.extension.service.impl.ServiceImpl#saveBatch(Collection)} 方法
* 使用示例可见 RoleMenuBatchInsertMapperUserRoleBatchInsertMapper
*
* @param entities 实体们
*/
default void insertBatch(Collection<T> entities) {
// TODO 芋艿修改成支持批量的
entities.forEach(this::insert);
}
default void updateBatch(T update) {
update(update, new QueryWrapper<>());
}
}

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.framework.mybatis.core.query;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.mybatis.core.enums.SqlConstants;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
@ -124,4 +126,24 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
return this;
}
/**
* 设置只返回最后一条
*
* TODO 芋艿不是完美解需要在思考下如果使用多数据源并且数据源是多种类型时可能会存在问题实现之返回一条的语法不同
*
* @return this
*/
public QueryWrapperX<T> limit1() {
Assert.notNull(SqlConstants.DB_TYPE, "获取不到数据库的类型");
switch (SqlConstants.DB_TYPE) {
case ORACLE:
case ORACLE_12C:
super.eq("ROWNUM", 1);
break;
default:
super.last("LIMIT 1");
}
return this;
}
}

View File

@ -0,0 +1,58 @@
package cn.iocoder.yudao.framework.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* List<String> 的类型转换器实现类对应数据库的 varchar 类型
*
* @author 永不言败
* @since 2022 3/23 12:50:15
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class StringLiSTTypeHandler implements TypeHandler<List<String>> {
private static final String COMMA = ",";
@Override
public void setParameter(PreparedStatement ps, int i, List<String> strings, JdbcType jdbcType) throws SQLException {
// 设置占位符
ps.setString(i, CollUtil.join(strings, COMMA));
}
@Override
public List<String> getResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return getResult(value);
}
@Override
public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return getResult(value);
}
@Override
public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return getResult(value);
}
private List<String> getResult(String value) {
if (value == null) {
return null;
}
return StrUtil.splitTrim(value, COMMA);
}
}

View File

@ -0,0 +1,42 @@
package cn.iocoder.yudao.framework.mybatis.core.util;
import com.baomidou.mybatisplus.annotation.DbType;
import java.sql.Connection;
import java.sql.DriverManager;
/**
* JDBC 工具类
*
* @author 芋道源码
*/
public class JdbcUtils {
/**
* 判断连接是否正确
*
* @param url 数据源连接
* @param username 账号
* @param password 密码
* @return 是否正确
*/
public static boolean isConnectionOK(String url, String username, String password) {
try (Connection ignored = DriverManager.getConnection(url, username, password)) {
return true;
} catch (Exception ex) {
return false;
}
}
/**
* 获得 URL 对应的 DB 类型
*
* @param url URL
* @return DB 类型
*/
public static DbType getDbType(String url) {
String name = com.alibaba.druid.util.JdbcUtils.getDbType(url, null);
return DbType.getDbType(name);
}
}

View File

@ -1,3 +1,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration,\
cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration
org.springframework.boot.env.EnvironmentPostProcessor=\
cn.iocoder.yudao.framework.mybatis.config.IdTypeEnvironmentPostProcessor

View File

@ -26,6 +26,12 @@
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId> <!-- 实现对 Caches 的自动化配置 -->
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,48 @@
package cn.iocoder.yudao.framework.redis.config;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* Cache 配置类基于 Redis 实现
*/
@Configuration
@EnableCaching
public class YudaoCacheAutoConfiguration {
/**
* RedisCacheConfiguration Bean
*
* 参考 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration createConfiguration 方法
*/
@Bean
@Primary
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
// 设置使用 JSON 序列化方式
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));
// 设置 CacheProperties.Redis 的属性
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.framework.redis.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
@ -11,7 +10,6 @@ import org.springframework.data.redis.serializer.RedisSerializer;
* Redis 配置类
*/
@Configuration
@Slf4j
public class YudaoRedisAutoConfiguration {
/**

View File

@ -1,2 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration
cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration,\
cn.iocoder.yudao.framework.redis.config.YudaoCacheAutoConfiguration

View File

@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/Cache/?yudao>

View File

@ -37,10 +37,10 @@ public class SecurityProperties {
@NotNull(message = "mock 模式的开关不能为空")
private Boolean mockEnable;
/**
* mock 模式的
* 一定要配置保证安全性
* mock 模式的
* 一定要配置保证安全性
*/
@NotEmpty(message = "mock 模式的钥不能为空") // 这里设置了一个默认值因为实际上只有 mockEnable true 时才需要配置
@NotEmpty(message = "mock 模式的钥不能为空") // 这里设置了一个默认值因为实际上只有 mockEnable true 时才需要配置
private String mockSecret = "yudaoyuanma";
}

View File

@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.security.config;
import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect;
import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider;
import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter;
import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl;
import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl;
@ -86,9 +86,9 @@ public class YudaoSecurityAutoConfiguration {
* Token 认证过滤器 Bean
*/
@Bean
public JWTAuthenticationTokenFilter authenticationTokenFilter(MultiUserDetailsAuthenticationProvider authenticationProvider,
GlobalExceptionHandler globalExceptionHandler) {
return new JWTAuthenticationTokenFilter(securityProperties, authenticationProvider, globalExceptionHandler);
public TokenAuthenticationFilter authenticationTokenFilter(MultiUserDetailsAuthenticationProvider authenticationProvider,
GlobalExceptionHandler globalExceptionHandler) {
return new TokenAuthenticationFilter(securityProperties, authenticationProvider, globalExceptionHandler);
}
/**

View File

@ -2,7 +2,7 @@ package cn.iocoder.yudao.framework.security.config;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.security.core.authentication.MultiUserDetailsAuthenticationProvider;
import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter;
import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
@ -55,7 +55,7 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
* Token 认证过滤器 Bean
*/
@Resource
private JWTAuthenticationTokenFilter authenticationTokenFilter;
private TokenAuthenticationFilter authenticationTokenFilter;
/**
* 自定义的权限映射 Bean

View File

@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.*;
@ -61,10 +60,6 @@ public class LoginUser implements UserDetails {
* 部门编号
*/
private Long deptId;
/**
* 所属岗位
*/
private Set<Long> postIds;
// ========== 上下文 ==========
/**

View File

@ -18,13 +18,13 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* JWT 过滤器验证 token 的有效性
* Token 过滤器验证 token 的有效性
* 验证通过后获得 {@link LoginUser} 信息并加入到 Spring Security 上下文
*
* @author 芋道源码
*/
@RequiredArgsConstructor
public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final SecurityProperties securityProperties;
@ -43,7 +43,7 @@ public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
LoginUser loginUser = authenticationProvider.verifyTokenAndRefresh(request, token);
// 模拟 Login 功能方便日常开发调试
if (loginUser == null) {
loginUser = this.mockLoginUser(request, token);
loginUser = mockLoginUser(request, token);
}
// 设置当前用户
if (loginUser != null) {

View File

@ -21,6 +21,17 @@
<artifactId>yudao-common</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-redis</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>org.mockito</groupId>

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.pay.test;
package cn.iocoder.yudao.framework.test.core.ut;
import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.infra.test;
package cn.iocoder.yudao.framework.test.core.ut;
import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.pay.test;
package cn.iocoder.yudao.framework.test.core.ut;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration;

View File

@ -2,21 +2,18 @@ package cn.iocoder.yudao.framework.apilog.core.filter;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService;
import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import lombok.RequiredArgsConstructor;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@ -26,27 +23,24 @@ import java.io.IOException;
import java.util.Date;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.*;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
* API 访问日志 Filter
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class ApiAccessLogFilter extends OncePerRequestFilter {
public class ApiAccessLogFilter extends ApiRequestFilter {
private final WebProperties webProperties;
private final String applicationName;
private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// 只过滤 API 请求的地址
return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAppApi().getPrefix(),
webProperties.getAppApi().getPrefix());
public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
super(webProperties);
this.applicationName = applicationName;
this.apiAccessLogFrameworkService = apiAccessLogFrameworkService;
}
@Override

View File

@ -11,8 +11,8 @@
<modules>
<module>yudao-module-bpm-api</module>
<module>yudao-module-bpm-base</module>
<module>yudao-module-bpm-impl-flowable</module>
<module>yudao-module-bpm-impl-activiti</module>
<module>yudao-module-bpm-biz-flowable</module>
<module>yudao-module-bpm-biz-activiti</module>
</modules>
<artifactId>yudao-module-bpm</artifactId>
<packaging>pom</packaging>
@ -24,9 +24,9 @@
bpm 解释https://baike.baidu.com/item/BPM/1933
目前提供两套实现方案:
1. 基于 Activiti 7 实现的 yudao-module-bpm-impl-activiti
2. 基于 Flowable 6 实现的 yudao-module-bpm-impl-flowable
两套实现会存在共享的逻辑,所以会继承 yudao-module-impl-base
1. 基于 Activiti 7 实现的 yudao-module-bpm-biz-activiti
2. 基于 Flowable 6 实现的 yudao-module-bpm-biz-flowable
两套实现会存在共享的逻辑,所以会继承 yudao-module-bpm-base
</description>
</project>

View File

@ -3,12 +3,8 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Map;
@ApiModel("管理后台 - 通过流程任务的 Request VO")
@Data
@ -20,6 +16,6 @@ public class BpmTaskApproveReqVO {
@ApiModelProperty(value = "审批意见", required = true, example = "不错不错!")
@NotEmpty(message = "审批意见不能为空")
private String comment;
private String reason;
}

View File

@ -22,6 +22,6 @@ public class BpmTaskDonePageItemRespVO extends BpmTaskTodoPageItemRespVO {
@ApiModelProperty(value = "任务结果", required = true, notes = "参见 bpm_process_instance_result", example = "2")
private Integer result;
@ApiModelProperty(value = "审批建议", required = true, example = "不请假了!")
private String comment;
private String reason;
}

View File

@ -16,6 +16,6 @@ public class BpmTaskRejectReqVO {
@ApiModelProperty(value = "审批意见", required = true, example = "不错不错!")
@NotEmpty(message = "审批意见不能为空")
private String comment;
private String reason;
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@ -16,6 +17,7 @@ import java.util.List;
* @author 芋道源码
*/
@TableName(value = "bpm_form", autoResultMap = true)
@KeySequence("bpm_form_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@ -17,6 +18,7 @@ import java.util.List;
* @author 芋道源码
*/
@TableName(value = "bpm_process_definition_ext", autoResultMap = true)
@KeySequence("bpm_process_definition_ext_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)

View File

@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskRuleScriptEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@ -21,6 +22,7 @@ import java.util.Set;
* @author 芋道源码
*/
@TableName(value = "bpm_task_assign_rule", autoResultMap = true)
@KeySequence("bpm_task_assign_rule_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@ -31,8 +33,9 @@ public class BpmTaskAssignRuleDO extends BaseDO {
/**
* {@link #processDefinitionId} 空串用于标识属于流程模型而不属于流程定义
* 不使用空串的原因Oracle 针对空串会处理成 null进而导致无法检索
*/
public static final String PROCESS_DEFINITION_ID_NULL = "";
public static final String PROCESS_DEFINITION_ID_NULL = "DEFAULT";
/**
* 编号
@ -64,7 +67,6 @@ public class BpmTaskAssignRuleDO extends BaseDO {
*
* 枚举 {@link BpmTaskAssignRuleTypeEnum}
*/
@TableField("`type`")
private Integer type;
/**
* 规则值数组一般关联指定表的编号

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@ -16,6 +17,7 @@ import java.util.Set;
* @author 芋道源码
*/
@TableName(value = "bpm_user_group", autoResultMap = true)
@KeySequence("bpm_user_group_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)

View File

@ -1,10 +1,13 @@
package cn.iocoder.yudao.module.bpm.dal.dataobject.oa;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import lombok.*;
import java.util.*;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.util.Date;
/**
* OA 请假申请 DO
@ -15,6 +18,7 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
* @author 芋道源码
*/
@TableName("bpm_oa_leave")
@KeySequence("bpm_oa_leave_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@ -37,8 +41,7 @@ public class BpmOALeaveDO extends BaseDO {
/**
* 请假类型
*/
@TableField("`type`")
private String type;
private Integer type;
/**
* 原因
*/

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.dal.dataobject.task;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@ -21,6 +22,7 @@ import java.util.Map;
* @author 芋道源码
*/
@TableName(value = "bpm_process_instance_ext", autoResultMap = true)
@KeySequence("bpm_process_instance_ext_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)

View File

@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.bpm.dal.dataobject.task;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@ -17,6 +19,7 @@ import java.util.Date;
* @author 芋道源码
*/
@TableName(value = "bpm_task_ext", autoResultMap = true)
@KeySequence("bpm_task_ext_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@ -60,7 +63,7 @@ public class BpmTaskExtDO extends BaseDO {
/**
* 审批建议
*/
private String comment;
private String reason;
/**
* 任务的结束时间
*

View File

@ -1,11 +1,11 @@
package cn.iocoder.yudao.module.bpm.dal.mysql.definition;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import org.apache.ibatis.annotations.Mapper;
/**
@ -17,9 +17,9 @@ import org.apache.ibatis.annotations.Mapper;
public interface BpmFormMapper extends BaseMapperX<BpmFormDO> {
default PageResult<BpmFormDO> selectPage(BpmFormPageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<BpmFormDO>()
.likeIfPresent("name", reqVO.getName())
.orderByDesc("id"));
return selectPage(reqVO, new LambdaQueryWrapperX<BpmFormDO>()
.likeIfPresent(BpmFormDO::getName, reqVO.getName())
.orderByDesc(BpmFormDO::getId));
}
}

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.bpm.dal.mysql.definition;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
@ -11,11 +11,11 @@ import java.util.List;
public interface BpmProcessDefinitionExtMapper extends BaseMapperX<BpmProcessDefinitionExtDO> {
default List<BpmProcessDefinitionExtDO> selectListByProcessDefinitionIds(Collection<String> processDefinitionIds) {
return selectList("process_definition_id", processDefinitionIds);
return selectList(BpmProcessDefinitionExtDO::getProcessDefinitionId, processDefinitionIds);
}
default BpmProcessDefinitionExtDO selectByProcessDefinitionId(String processDefinitionId) {
return selectOne("process_definition_id", processDefinitionId);
return selectOne(BpmProcessDefinitionExtDO::getProcessDefinitionId, processDefinitionId);
}
}

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.bpm.dal.mysql.definition;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.lang.Nullable;
@ -13,23 +13,23 @@ public interface BpmTaskAssignRuleMapper extends BaseMapperX<BpmTaskAssignRuleDO
default List<BpmTaskAssignRuleDO> selectListByProcessDefinitionId(String processDefinitionId,
@Nullable String taskDefinitionKey) {
return selectList(new QueryWrapperX<BpmTaskAssignRuleDO>()
.eq("process_definition_id", processDefinitionId)
.eqIfPresent("task_definition_key", taskDefinitionKey));
return selectList(new LambdaQueryWrapperX<BpmTaskAssignRuleDO>()
.eq(BpmTaskAssignRuleDO::getProcessDefinitionId, processDefinitionId)
.eqIfPresent(BpmTaskAssignRuleDO::getTaskDefinitionKey, taskDefinitionKey));
}
default List<BpmTaskAssignRuleDO> selectListByModelId(String modelId) {
return selectList(new QueryWrapperX<BpmTaskAssignRuleDO>()
.eq("model_id", modelId)
.eq("process_definition_id", BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL));
return selectList(new LambdaQueryWrapperX<BpmTaskAssignRuleDO>()
.eq(BpmTaskAssignRuleDO::getModelId, modelId)
.eq(BpmTaskAssignRuleDO::getProcessDefinitionId, BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL));
}
default BpmTaskAssignRuleDO selectListByModelIdAndTaskDefinitionKey(String modelId,
String taskDefinitionKey) {
return selectOne(new QueryWrapperX<BpmTaskAssignRuleDO>()
.eq("model_id", modelId)
.eq("process_definition_id", BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL)
.eq("task_definition_key", taskDefinitionKey));
return selectOne(new LambdaQueryWrapperX<BpmTaskAssignRuleDO>()
.eq(BpmTaskAssignRuleDO::getModelId, modelId)
.eq(BpmTaskAssignRuleDO::getProcessDefinitionId, BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL)
.eq(BpmTaskAssignRuleDO::getTaskDefinitionKey, taskDefinitionKey));
}
}

View File

@ -1,35 +1,34 @@
package cn.iocoder.yudao.module.bpm.dal.mysql.task;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceMyPageReqVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceMyPageReqVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BpmProcessInstanceExtMapper extends BaseMapperX<BpmProcessInstanceExtDO> {
default PageResult<BpmProcessInstanceExtDO> selectPage(Long userId, BpmProcessInstanceMyPageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<BpmProcessInstanceExtDO>()
.eqIfPresent("start_user_id", userId)
.likeIfPresent("name", reqVO.getName())
.eqIfPresent("process_definition_id", reqVO.getProcessDefinitionId())
.eqIfPresent("category", reqVO.getCategory())
.eqIfPresent("status", reqVO.getStatus())
.eqIfPresent("result", reqVO.getResult())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id"));
return selectPage(reqVO, new LambdaQueryWrapperX<BpmProcessInstanceExtDO>()
.eqIfPresent(BpmProcessInstanceExtDO::getStartUserId, userId)
.likeIfPresent(BpmProcessInstanceExtDO::getName, reqVO.getName())
.eqIfPresent(BpmProcessInstanceExtDO::getProcessDefinitionId, reqVO.getProcessDefinitionId())
.eqIfPresent(BpmProcessInstanceExtDO::getCategory, reqVO.getCategory())
.eqIfPresent(BpmProcessInstanceExtDO::getStatus, reqVO.getStatus())
.eqIfPresent(BpmProcessInstanceExtDO::getResult, reqVO.getResult())
.betweenIfPresent(BpmProcessInstanceExtDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc(BpmProcessInstanceExtDO::getId));
}
default BpmProcessInstanceExtDO selectByProcessInstanceId(String processDefinitionId) {
return selectOne("process_instance_id", processDefinitionId);
return selectOne(BpmProcessInstanceExtDO::getProcessInstanceId, processDefinitionId);
}
default void updateByProcessInstanceId(BpmProcessInstanceExtDO updateObj) {
update(updateObj, new QueryWrapper<BpmProcessInstanceExtDO>()
.eq("process_instance_id", updateObj.getProcessInstanceId()));
update(updateObj, new LambdaQueryWrapperX<BpmProcessInstanceExtDO>()
.eq(BpmProcessInstanceExtDO::getProcessInstanceId, updateObj.getProcessInstanceId()));
}
}

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.bpm.dal.mysql.task;
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmTaskExtDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmTaskExtDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
@ -20,6 +20,6 @@ public interface BpmTaskExtMapper extends BaseMapperX<BpmTaskExtDO> {
}
default List<BpmTaskExtDO> selectListByProcessInstanceId(String processInstanceId) {
return selectList("process_instance_id", processInstanceId);
return selectList(BpmTaskExtDO::getProcessInstanceId, processInstanceId);
}
}

View File

@ -22,7 +22,6 @@ public interface BpmMessageService {
*/
void sendMessageWhenProcessInstanceApprove(@Valid BpmMessageSendWhenProcessInstanceApproveReqDTO reqDTO);
/**
* 发送流程实例被不通过的消息
*

View File

@ -44,7 +44,7 @@ public class BpmMessageServiceImpl implements BpmMessageService {
public void sendMessageWhenProcessInstanceReject(BpmMessageSendWhenProcessInstanceRejectReqDTO reqDTO) {
Map<String, Object> templateParams = new HashMap<>();
templateParams.put("processInstanceName", reqDTO.getProcessInstanceName());
templateParams.put("comment", reqDTO.getComment());
templateParams.put("reason", reqDTO.getReason());
templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId()));
smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getStartUserId(),
BpmMessageEnum.PROCESS_INSTANCE_REJECT.getSmsTemplateCode(), templateParams));

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