Merge remote-tracking branch 'yudao/feature/mall_product' into feature/mall_product

This commit is contained in:
puhui999 2023-07-31 18:37:13 +08:00
commit 92b10e2ecf
173 changed files with 2224 additions and 4061 deletions

View File

@ -1,23 +1,24 @@
# Docker Build & Up
目标: 快速部署体验系统,帮助了解系统之间的依赖关系。
依赖docker compose v2删除`name: yudao-system`,降低`version`版本为`3.3`以下,支持`docker-compose`。
## 功能文件列表
```text
.
├── Docker-HOWTO.md
├── docker-compose.yml
├── docker.env
├── Docker-HOWTO.md
├── docker-compose.yml
├── docker.env <-- 提供docker-compose环境变量配置
├── yudao-server
│ ├── Dockerfile
│ └── nginx.conf
│ └── Dockerfile
└── yudao-ui-admin
├── .dockerignore
└── Dockerfile
├── Dockerfile
└── nginx.conf <-- 提供基础配置gzip压缩api转发
```
## Maven build (Optional)
## 构建 jar 包
```shell
# 创建maven缓存volume
@ -30,29 +31,19 @@ docker run -it --rm --name yudao-maven \
maven mvn clean install package '-Dmaven.test.skip=true'
```
## Docker Compose Build
```shell
docker compose --env-file docker.env build
```
## Docker Compose Up
## 构建启动服务
```shell
docker compose --env-file docker.env up -d
```
第一次执行由于数据库未初始化因此yudao-server容器会运行失败。执行如下命令初始化数据库
首次运行会自动构建容器。可以通过`docker compose build [service]`来手动构建所有或某个docker镜像
```shell
docker compose exec -T mysql \
sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --default-character-set=utf8mb4 ruoyi-vue-pro' \
< ./sql/mysql/ruoyi-vue-pro.sql
```
`--env-file docker.env`为可选参数,只是展示了通过`.env`文件配置容器启动的环境变量,`docker-compose.yml`本身已经提供足够的默认参数来正常运行系统。
## Server:Port
## 服务器的宿主机端口映射
- admin: http://localhost:8080
- API: http://localhost:48080
- mysql: root/123456, port: 3308
- admin ui: http://localhost:8080
- api server: http://localhost:48080
- mysql: root/123456, port: 3306
- redis: port: 6379

View File

@ -102,7 +102,7 @@
系统内置多种多种业务功能,可以用于快速你的业务系统:
![功能分层](https://static.iocoder.cn/ruoyi-vue-pro-biz.png)
![功能分层](https://static.iocoder.cn/ruoyi-vue-pro-biz.png?imageView2/2/format/webp)
* 系统功能
* 基础设施

View File

@ -1,4 +1,4 @@
version: "3.8"
version: "3.4"
name: yudao-system
@ -9,25 +9,22 @@ services:
restart: unless-stopped
tty: true
ports:
- 13306:3306
- "3306:3306"
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE:-ruoyi-vue-pro}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456}
volumes:
- mysql:/var/lib/mysql/
networks:
- yudao-network
- ./sql/mysql/ruoyi-vue-pro.sql:/docker-entrypoint-initdb.d/ruoyi-vue-pro.sql:ro
redis:
container_name: yudao-redis
image: redis:6-alpine
restart: unless-stopped
ports:
- 16379:6379
- "6379:6379"
volumes:
- redis:/data
networks:
- yudao-network
server:
container_name: yudao-server
@ -36,7 +33,7 @@ services:
image: yudao-server
restart: unless-stopped
ports:
- 48080:48080
- "48080:48080"
environment:
# https://github.com/polovyivan/docker-pass-configs-to-container
SPRING_PROFILES_ACTIVE: local
@ -54,8 +51,6 @@ services:
--spring.datasource.dynamic.datasource.slave.username=${SLAVE_DATASOURCE_USERNAME:-root}
--spring.datasource.dynamic.datasource.slave.password=${SLAVE_DATASOURCE_PASSWORD:-123456}
--spring.redis.host=${REDIS_HOST:-yudao-redis}
networks:
- yudao-network
depends_on:
- mysql
- redis
@ -78,16 +73,10 @@ services:
image: yudao-admin
restart: unless-stopped
ports:
- 8080:80
networks:
- yudao-network
- "8080:80"
depends_on:
- server
networks:
yudao-network:
driver: bridge
volumes:
mysql:
driver: local

View File

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

View File

@ -14,7 +14,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>1.7.3-snapshot</revision>
<revision>1.8.0-snapshot</revision>
<!-- 统一依赖管理 -->
<spring.boot.version>2.7.13</spring.boot.version>
<!-- Web 相关 -->
@ -71,7 +71,7 @@
<justauth.version>1.0.1</justauth.version>
<jimureport.version>1.5.8</jimureport.version>
<xercesImpl.version>2.12.2</xercesImpl.version>
<wx-java-mp.version>4.5.0</wx-java-mp.version>
<weixin-java.version>4.5.0</weixin-java.version>
</properties>
<dependencyManagement>
@ -216,10 +216,9 @@
<artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
<version>${dynamic-datasource.version}</version>
</dependency>
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId>
<artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 -->
<version>${mybatis-plus-join-boot-starter.version}</version>
</dependency>
@ -599,10 +598,25 @@
<version>${justauth.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>${wx-java-mp.version}</version>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<!-- 积木报表-->

View File

@ -204,7 +204,7 @@ public class CollectionUtils {
return from.stream().filter(predicate).findFirst().orElse(null);
}
public static <T, V extends Comparable<? super V>> V getMaxValue(List<T> from, Function<T, V> valueFunc) {
public static <T, V extends Comparable<? super V>> V getMaxValue(Collection<T> from, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) {
return null;
}

View File

@ -4,11 +4,11 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import org.junit.jupiter.api.BeforeEach;
@ -23,6 +23,7 @@ import static cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataP
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
@ -73,6 +74,8 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
.setUserType(UserTypeEnum.ADMIN.getValue()));
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
// mock 方法permissionApi 返回 null
when(permissionApi.getDeptDataPermission(eq(loginUser.getId()))).thenReturn(null);
// 调用
NullPointerException exception = assertThrows(NullPointerException.class,

View File

@ -63,9 +63,7 @@
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.5.0</version>
</dependency>
<!-- TODO 芋艿:清理 -->
<!-- Test 测试相关 -->
<dependency>

View File

@ -70,6 +70,7 @@ public class PayClientFactoryImpl implements PayClientFactory {
case ALIPAY_APP: return (AbstractPayClient<Config>) new AlipayAppPayClient(channelId, (AlipayPayClientConfig) config);
case ALIPAY_PC: return (AbstractPayClient<Config>) new AlipayPcPayClient(channelId, (AlipayPayClientConfig) config);
case ALIPAY_BAR: return (AbstractPayClient<Config>) new AlipayBarPayClient(channelId, (AlipayPayClientConfig) config);
// 其它支付
case MOCK: return (AbstractPayClient<Config>) new MockPayClient(channelId, (MockPayClientConfig) config);
}
// 创建失败错误日志 + 抛出异常

View File

@ -11,7 +11,9 @@ import java.time.LocalDateTime;
import java.util.Map;
/**
* 模拟支付的 PayClient 实现类, 模拟支付返回结果都是成功
* 模拟支付的 PayClient 实现类
*
* 模拟支付返回结果都是成功方便大家日常流畅
*
* @author jason
*/
@ -25,31 +27,30 @@ public class MockPayClient extends AbstractPayClient<MockPayClientConfig> {
@Override
protected void doInit() {
}
@Override
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
// 模拟支付渠道订单号为空
return PayOrderRespDTO.successOf("", "", LocalDateTime.now(), reqDTO.getOutTradeNo(), MOCK_RESP_SUCCESS_DATA);
return PayOrderRespDTO.successOf("MOCK-P-" + reqDTO.getOutTradeNo(), "", LocalDateTime.now(),
reqDTO.getOutTradeNo(), MOCK_RESP_SUCCESS_DATA);
}
@Override
protected PayOrderRespDTO doGetOrder(String outTradeNo) {
// 模拟支付渠道订单号为空
return PayOrderRespDTO.successOf("", "", LocalDateTime.now(), outTradeNo, MOCK_RESP_SUCCESS_DATA);
return PayOrderRespDTO.successOf("MOCK-P-" + outTradeNo, "", LocalDateTime.now(),
outTradeNo, MOCK_RESP_SUCCESS_DATA);
}
@Override
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
// 模拟支付渠道退款单号为空
return PayRefundRespDTO.successOf("", LocalDateTime.now(), reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA);
return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(),
reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA);
}
@Override
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) {
// 模拟支付渠道退款单号为空
return PayRefundRespDTO.successOf("", LocalDateTime.now(), outRefundNo, MOCK_RESP_SUCCESS_DATA);
return PayRefundRespDTO.successOf("MOCK-R-" + outRefundNo, LocalDateTime.now(),
outRefundNo, MOCK_RESP_SUCCESS_DATA);
}
@Override
@ -61,4 +62,5 @@ public class MockPayClient extends AbstractPayClient<MockPayClientConfig> {
protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) {
throw new UnsupportedOperationException("模拟支付无支付回调");
}
}
}

View File

@ -14,7 +14,9 @@ import javax.validation.Validator;
public class MockPayClientConfig implements PayClientConfig {
/**
* 配置名称,如果不加任何属性, JsonUtils.parseObject2 解析会报错. 暂时加个名称
* 配置名称
*
* 如果不加任何属性JsonUtils.parseObject2 解析会报错所以暂时加个名称
*/
private String name;
@ -22,4 +24,5 @@ public class MockPayClientConfig implements PayClientConfig {
public void validate(Validator validator) {
// 模拟支付配置无需校验
}
}
}

View File

@ -1,47 +0,0 @@
package cn.iocoder.yudao.framework.tenant.core.redis;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import java.time.Duration;
/**
* 多租户拓展的 RedisKeyDefine 实现类
*
* 由于 Redis 不同于 MySQL column 字段无法通过类似 WHERE tenant_id = ? 的方式过滤
* 所以需要通过在 Redis Key 上增加后缀的方式进行租户之间的隔离具体的步骤是
* 1. 假设 Redis Key user:%d示例是 user:1对应到多租户的 Redis Key user:%d:%d
* 2. Redis DAO 需要使用 {@link #formatKey(Object...)} 方法进行 Redis Key 的格式化
*
* 注意大多数情况下并不用使用 TenantRedisKeyDefine 实现主要的使用场景还是 Redis Key 可能存在冲突的情况
* 例如说租户 1 2 都有一个手机号作为 Key则他们会存在冲突的问题
*
* @author 芋道源码
*/
public class TenantRedisKeyDefine extends RedisKeyDefine {
/**
* 多租户的 KEY 模板
*/
private static final String KEY_TEMPLATE_SUFFIX = ":%d";
public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeout);
}
public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) {
super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeoutType);
}
private static String buildKeyTemplate(String keyTemplate) {
return keyTemplate + KEY_TEMPLATE_SUFFIX;
}
@Override
public String formatKey(Object... args) {
args = ArrayUtil.append(args, TenantContextHolder.getRequiredTenantId());
return super.formatKey(args);
}
}

View File

@ -1,27 +0,0 @@
package cn.iocoder.yudao.framework.tenant.core.redis;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TenantRedisKeyDefineTest {
@Test
public void testFormatKey() {
Long tenantId = 30L;
TenantContextHolder.setTenantId(tenantId);
// 准备参数
TenantRedisKeyDefine define = new TenantRedisKeyDefine("", "user:%d:%d", RedisKeyDefine.KeyTypeEnum.HASH,
Object.class, RedisKeyDefine.TimeoutTypeEnum.FIXED);
Long userId = 10L;
Integer userType = 1;
// 调用
String key = define.formatKey(userId, userType);
// 断言
assertEquals("user:10:1:30", key);
}
}

View File

@ -34,16 +34,12 @@
<!-- 三方云服务相关 -->
<dependency>
<groupId>com.github.binarywang</groupId>
<!-- <artifactId>weixin-java-mp</artifactId>-->
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
<!-- TODO 芋艿:清理 -->
</dependencies>
</project>

View File

@ -1,7 +1,5 @@
package cn.iocoder.yudao.framework.captcha.config;
import cn.hutool.core.util.ClassUtil;
import cn.iocoder.yudao.framework.captcha.core.enums.CaptchaRedisKeyConstants;
import cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl;
import com.xingyuv.captcha.properties.AjCaptchaProperties;
import com.xingyuv.captcha.service.CaptchaCacheService;
@ -15,12 +13,6 @@ import javax.annotation.Resource;
@AutoConfiguration
public class YudaoCaptchaConfiguration {
static {
// 手动加载 Lock4jRedisKeyConstants 因为它不会被使用到
// 如果不加载会导致 Redis 监控看到它的 Redis Key 枚举
ClassUtil.loadClass(CaptchaRedisKeyConstants.class.getName());
}
@Resource
private StringRedisTemplate stringRedisTemplate;

View File

@ -1,12 +1,5 @@
package cn.iocoder.yudao.framework.captcha.core.enums;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import com.xingyuv.captcha.model.vo.PointVO;
import java.time.Duration;
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
/**
* 验证码 Redis Key 枚举类
*
@ -14,12 +7,22 @@ import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.S
*/
public interface CaptchaRedisKeyConstants {
RedisKeyDefine AJ_CAPTCHA_REQ_LIMIT = new RedisKeyDefine("验证码的请求限流",
"AJ.CAPTCHA.REQ.LIMIT-%s-%s",
STRING, Integer.class, Duration.ofSeconds(60)); // 例如说验证失败 5 get 接口锁定
/**
* 验证码的请求限流
*
* KEY 格式AJ.CAPTCHA.REQ.LIMIT-%s-%s
* VALUE 数据类型String // 例如说验证失败 5 get 接口锁定
* 过期时间60
*/
String AJ_CAPTCHA_REQ_LIMIT = "AJ.CAPTCHA.REQ.LIMIT-%s-%s";
RedisKeyDefine AJ_CAPTCHA_RUNNING = new RedisKeyDefine("验证码的坐标",
"RUNNING:CAPTCHA:%s", // AbstractCaptchaService.REDIS_CAPTCHA_KEY
STRING, PointVO.class, Duration.ofSeconds(120)); // {"secretKey":"PP1w2Frr2KEejD2m","x":162,"y":5}
/**
* 验证码的坐标
*
* KEY 格式RUNNING:CAPTCHA:%s // AbstractCaptchaService.REDIS_CAPTCHA_KEY
* VALUE 数据类型String // PointVO.class {"secretKey":"PP1w2Frr2KEejD2m","x":162,"y":5}
* 过期时间120
*/
String AJ_CAPTCHA_RUNNING = "RUNNING:CAPTCHA:%s";
}

View File

@ -64,9 +64,8 @@
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId>
<artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 -->
</dependency>
</dependencies>
</project>

View File

@ -6,10 +6,10 @@ import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
@ -17,10 +17,8 @@ import java.util.List;
/**
* MyBatis Plus BaseMapper 的基础上拓展提供更多的能力
* <p>
* 为什么继承 MPJBaseMapper 接口支持 MyBatis Plus 多表 Join 的能力
*/
public interface BaseMapperX<T> extends MPJBaseMapper<T> {
public interface BaseMapperX<T> extends BaseMapper<T> {
default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
// MyBatis Plus 查询
@ -46,18 +44,6 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
}
default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2,
SFunction<T, ?> field3, Object value3) {
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2)
.eq(field3, value3));
}
default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2,
SFunction<T, ?> field3, Object value3, SFunction<T, ?> field4, Object value4) {
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2)
.eq(field3, value3).eq(field4, value4));
}
default Long selectCount() {
return selectCount(new QueryWrapper<T>());
}
@ -117,11 +103,6 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
update(update, new QueryWrapper<>());
}
/**
* 根据ID 批量更新适合大量数据更新
*
* @param entities 实体们
*/
default void updateBatch(Collection<T> entities) {
Db.updateBatchById(entities);
}
@ -130,13 +111,8 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
Db.updateBatchById(entities, size);
}
/**
* 批量修改插入, 会根据实体的主键是否为空更新还是修改默认为 1000
*
* @param entities 实体们
*/
default void saveOrUpdateBatch(Collection<T> entities){
Db.saveOrUpdateBatch(entities);
default void saveOrUpdateBatch(Collection<T> collection) {
Db.saveOrUpdateBatch(collection);
}
}

View File

@ -1,13 +1,10 @@
package cn.iocoder.yudao.framework.idempotent.core.redis;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import lombok.AllArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
/**
* 幂等 Redis DAO
*
@ -16,9 +13,14 @@ import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.S
@AllArgsConstructor
public class IdempotentRedisDAO {
private static final RedisKeyDefine IDEMPOTENT = new RedisKeyDefine("幂等操作",
"idempotent:%s", // 参数为 uuid
STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
/**
* 幂等操作
*
* KEY 格式idempotent:%s // 参数为 uuid
* VALUE 格式String
* 过期时间不固定
*/
private static final String IDEMPOTENT = "idempotent:%s";
private final StringRedisTemplate redisTemplate;
@ -28,7 +30,7 @@ public class IdempotentRedisDAO {
}
private static String formatKey(String key) {
return String.format(IDEMPOTENT.getKeyTemplate(), key);
return String.format(IDEMPOTENT, key);
}
}

View File

@ -1,21 +1,13 @@
package cn.iocoder.yudao.framework.lock4j.config;
import cn.hutool.core.util.ClassUtil;
import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration;
import cn.iocoder.yudao.framework.lock4j.core.DefaultLockFailureStrategy;
import cn.iocoder.yudao.framework.lock4j.core.Lock4jRedisKeyConstants;
import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
@AutoConfiguration(before = LockAutoConfiguration.class)
public class YudaoLock4jConfiguration {
static {
// 手动加载 Lock4jRedisKeyConstants 因为它不会被使用到
// 如果不加载会导致 Redis 监控看到它的 Redis Key 枚举
ClassUtil.loadClass(Lock4jRedisKeyConstants.class.getName());
}
@Bean
public DefaultLockFailureStrategy lockFailureStrategy() {
return new DefaultLockFailureStrategy();

View File

@ -1,10 +1,5 @@
package cn.iocoder.yudao.framework.lock4j.core;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import org.redisson.api.RLock;
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH;
/**
* Lock4j Redis Key 枚举类
*
@ -12,8 +7,13 @@ import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.H
*/
public interface Lock4jRedisKeyConstants {
RedisKeyDefine LOCK4J = new RedisKeyDefine("分布式锁",
"lock4j:%s", // 参数来自 DefaultLockKeyBuilder
HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson Lock 使用 Hash 数据结构
/**
* 分布式锁
*
* KEY 格式lock4j:%s // 参数来自 DefaultLockKeyBuilder
* VALUE 数据格式HASH // RLock.classRedisson Lock 使用 Hash 数据结构
* 过期时间不固定
*/
String LOCK4J = "lock4j:%s";
}

View File

@ -37,6 +37,11 @@
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.framework.redis.config;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@ -7,8 +9,15 @@ import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.util.Objects;
import static cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration.buildRedisSerializer;
/**
* Cache 配置类基于 Redis 实现
@ -20,15 +29,19 @@ public class YudaoCacheAutoConfiguration {
/**
* RedisCacheConfiguration Bean
*
* <p>
* 参考 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()));
// 设置使用 : 单冒号而不是双 :: 冒号避免 Redis Desktop Manager 多余空格
// 详细可见 https://blog.csdn.net/chuixue24/article/details/103928965 博客
config = config.computePrefixWith(cacheName -> cacheName + StrUtil.COLON);
// 设置使用 JSON 序列化方式
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(buildRedisSerializer()));
// 设置 CacheProperties.Redis 的属性
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
@ -47,4 +60,14 @@ public class YudaoCacheAutoConfiguration {
return config;
}
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate,
RedisCacheConfiguration redisCacheConfiguration) {
// 创建 RedisCacheWriter 对象
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
// 创建 TenantRedisCacheManager 对象
return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
}
}

View File

@ -1,5 +1,8 @@
package cn.iocoder.yudao.framework.redis.config;
import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
@ -25,9 +28,17 @@ public class YudaoRedisAutoConfiguration {
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 使用 JSON 序列化方式库是 Jackson 序列化 VALUE
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());
template.setValueSerializer(buildRedisSerializer());
template.setHashValueSerializer(buildRedisSerializer());
return template;
}
public static RedisSerializer<?> buildRedisSerializer() {
RedisSerializer<Object> json = RedisSerializer.json();
// 解决 LocalDateTime 的序列化
ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper");
objectMapper.registerModules(new JavaTimeModule());
return json;
}
}

View File

@ -1,113 +0,0 @@
package cn.iocoder.yudao.framework.redis.core;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import java.time.Duration;
/**
* Redis Key 定义类
*
* @author 芋道源码
*/
@Data
public class RedisKeyDefine {
@Getter
@AllArgsConstructor
public enum KeyTypeEnum {
STRING("String"),
LIST("List"),
HASH("Hash"),
SET("Set"),
ZSET("Sorted Set"),
STREAM("Stream"),
PUBSUB("Pub/Sub");
/**
* 类型
*/
@JsonValue
private final String type;
}
@Getter
@AllArgsConstructor
public enum TimeoutTypeEnum {
FOREVER(1), // 永不超时
DYNAMIC(2), // 动态超时
FIXED(3); // 固定超时
/**
* 类型
*/
@JsonValue
private final Integer type;
}
/**
* Key 模板
*/
private final String keyTemplate;
/**
* Key 类型的枚举
*/
private final KeyTypeEnum keyType;
/**
* Value 类型
*
* 如果是使用分布式锁设置为 {@link java.util.concurrent.locks.Lock} 类型
*/
private final Class<?> valueType;
/**
* 超时类型
*/
private final TimeoutTypeEnum timeoutType;
/**
* 过期时间
*/
private final Duration timeout;
/**
* 备注
*/
private final String memo;
private RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType,
TimeoutTypeEnum timeoutType, Duration timeout) {
this.memo = memo;
this.keyTemplate = keyTemplate;
this.keyType = keyType;
this.valueType = valueType;
this.timeout = timeout;
this.timeoutType = timeoutType;
// 添加注册表
RedisKeyRegistry.add(this);
}
public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
this(memo, keyTemplate, keyType, valueType, TimeoutTypeEnum.FIXED, timeout);
}
public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) {
this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO);
}
/**
* 格式化 Key
*
* 注意内部采用 {@link String#format(String, Object...)} 实现
*
* @param args 格式化的参数
* @return Key
*/
public String formatKey(Object... args) {
return String.format(keyTemplate, args);
}
}

View File

@ -1,28 +0,0 @@
package cn.iocoder.yudao.framework.redis.core;
import java.util.ArrayList;
import java.util.List;
/**
* {@link RedisKeyDefine} 注册表
*/
public class RedisKeyRegistry {
/**
* Redis RedisKeyDefine 数组
*/
private static final List<RedisKeyDefine> DEFINES = new ArrayList<>();
public static void add(RedisKeyDefine define) {
DEFINES.add(define);
}
public static List<RedisKeyDefine> list() {
return DEFINES;
}
public static int size() {
return DEFINES.size();
}
}

View File

@ -0,0 +1,51 @@
package cn.iocoder.yudao.framework.redis.core;
import cn.hutool.core.util.StrUtil;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
/**
* 支持自定义过期时间的 {@link RedisCacheManager} 实现类
*
* {@link Cacheable#cacheNames()} 格式为 "key#ttl" # 后面的 ttl 为过期时间单位为秒
*
* @author 芋道源码
*/
public class TimeoutRedisCacheManager extends RedisCacheManager {
private static final String SPLIT = "#";
public TimeoutRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
if (StrUtil.isEmpty(name)) {
return super.createRedisCache(name, cacheConfig);
}
// 如果使用 # 分隔大小不为 2则说明不使用自定义过期时间
String[] names = StrUtil.splitToArray(name, SPLIT);
if (names.length != 2) {
return super.createRedisCache(name, cacheConfig);
}
// 核心通过修改 cacheConfig 的过期时间实现自定义过期时间
if (cacheConfig != null) {
// 移除 # 后面的 : 以及后面的内容避免影响解析
names[1] = StrUtil.subBefore(names[1], StrUtil.COLON, false);
// 解析时间
Duration duration = DurationStyle.detectAndParse(names[1], ChronoUnit.SECONDS);
cacheConfig = cacheConfig.entryTtl(duration);
}
return super.createRedisCache(names[0], cacheConfig);
}
}

View File

@ -60,7 +60,7 @@ public class YudaoWebSecurityConfigurerAdapter {
/**
* 自定义的权限映射 Bean
*
* @see #configure(HttpSecurity)
* @see #filterChain(HttpSecurity)
*/
@Resource
private List<AuthorizeRequestsCustomizer> authorizeRequestsCustomizers;
@ -79,7 +79,7 @@ public class YudaoWebSecurityConfigurerAdapter {
/**
* 配置 URL 的安全配置
* <p>
*
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
@ -141,7 +141,6 @@ public class YudaoWebSecurityConfigurerAdapter {
// 添加 Token Filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}

View File

@ -40,6 +40,7 @@ public class BaseDbAndRedisUnitTest {
// Redis 配置类
RedisTestConfiguration.class, // Redis 测试配置类用于启动 RedisServer
// RedisAutoConfiguration.class, // Spring Redis 自动配置类
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
RedissonAutoConfiguration.class, // Redisson 自动高配置类
})

View File

@ -52,7 +52,8 @@ public class YudaoSwaggerAutoConfiguration {
// 接口信息
.info(buildInfo(properties))
// 接口安全配置
.components(new Components().securitySchemes(securitySchemas));
.components(new Components().securitySchemes(securitySchemas))
.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION));
securitySchemas.keySet().forEach(key -> openAPI.addSecurityItem(new SecurityRequirement().addList(key)));
return openAPI;
}

View File

@ -47,7 +47,7 @@ public class BpmTaskController {
@Parameter(name = "processInstanceId", description = "流程实例的编号", required = true)
@PreAuthorize("@ss.hasPermission('bpm:task:query')")
public CommonResult<List<BpmTaskRespVO>> getTaskListByProcessInstanceId(
@RequestParam("processInstanceId") String processInstanceId) {
@RequestParam("processInstanceId") String processInstanceId) {
return success(taskService.getTaskListByProcessInstanceId(processInstanceId));
}

View File

@ -73,11 +73,13 @@ public interface BpmTaskConvert {
BpmTaskDonePageItemRespVO convert2(HistoricTaskInstance bean);
@Mappings({@Mapping(source = "processInstance.id", target = "id"),
@Mapping(source = "processInstance.name", target = "name"),
@Mapping(source = "processInstance.startUserId", target = "startUserId"),
@Mapping(source = "processInstance.processDefinitionId", target = "processDefinitionId"),
@Mapping(source = "startUser.nickname", target = "startUserNickname")})
@Mappings({
@Mapping(source = "processInstance.id", target = "id"),
@Mapping(source = "processInstance.name", target = "name"),
@Mapping(source = "processInstance.startUserId", target = "startUserId"),
@Mapping(source = "processInstance.processDefinitionId", target = "processDefinitionId"),
@Mapping(source = "startUser.nickname", target = "startUserNickname")
})
BpmTaskTodoPageItemRespVO.ProcessInstance convert(ProcessInstance processInstance, AdminUserRespDTO startUser);
default List<BpmTaskRespVO> convertList3(List<HistoricTaskInstance> tasks,

View File

@ -15,8 +15,8 @@ import cn.iocoder.yudao.module.bpm.convert.definition.BpmTaskAssignRuleConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmTaskAssignRuleMapper;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.DictTypeConstants;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.PostApi;
@ -89,7 +89,7 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
@Override
public List<BpmTaskAssignRuleDO> getTaskAssignRuleListByProcessDefinitionId(String processDefinitionId,
String taskDefinitionKey) {
String taskDefinitionKey) {
return taskRuleMapper.selectListByProcessDefinitionId(processDefinitionId, taskDefinitionKey);
}
@ -128,14 +128,14 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
validTaskAssignRuleOptions(reqVO.getType(), reqVO.getOptions());
// 校验是否已经配置
BpmTaskAssignRuleDO existRule =
taskRuleMapper.selectListByModelIdAndTaskDefinitionKey(reqVO.getModelId(), reqVO.getTaskDefinitionKey());
taskRuleMapper.selectListByModelIdAndTaskDefinitionKey(reqVO.getModelId(), reqVO.getTaskDefinitionKey());
if (existRule != null) {
throw exception(TASK_ASSIGN_RULE_EXISTS, reqVO.getModelId(), reqVO.getTaskDefinitionKey());
}
// 存储
BpmTaskAssignRuleDO rule = BpmTaskAssignRuleConvert.INSTANCE.convert(reqVO)
.setProcessDefinitionId(BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL); // 只有流程模型才允许新建
.setProcessDefinitionId(BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL); // 只有流程模型才允许新建
taskRuleMapper.insert(rule);
return rule.getId();
}
@ -169,14 +169,14 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
// 遍历匹配对应的规则
Map<String, BpmTaskAssignRuleRespVO> processInstanceRuleMap =
CollectionUtils.convertMap(processInstanceRules, BpmTaskAssignRuleRespVO::getTaskDefinitionKey);
CollectionUtils.convertMap(processInstanceRules, BpmTaskAssignRuleRespVO::getTaskDefinitionKey);
for (BpmTaskAssignRuleRespVO modelRule : modelRules) {
BpmTaskAssignRuleRespVO processInstanceRule = processInstanceRuleMap.get(modelRule.getTaskDefinitionKey());
if (processInstanceRule == null) {
return false;
}
if (!ObjectUtil.equals(modelRule.getType(), processInstanceRule.getType()) || !ObjectUtil.equal(
modelRule.getOptions(), processInstanceRule.getOptions())) {
modelRule.getOptions(), processInstanceRule.getOptions())) {
return false;
}
}
@ -192,7 +192,7 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
// 开始复制
List<BpmTaskAssignRuleDO> newRules = BpmTaskAssignRuleConvert.INSTANCE.convertList2(rules);
newRules.forEach(rule -> rule.setProcessDefinitionId(toProcessDefinitionId).setId(null).setCreateTime(null)
.setUpdateTime(null));
.setUpdateTime(null));
taskRuleMapper.insertBatch(newRules);
}
@ -215,7 +215,7 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.ROLE.getType())) {
roleApi.validRoleList(options);
} else if (ObjectUtils.equalsAny(type, BpmTaskAssignRuleTypeEnum.DEPT_MEMBER.getType(),
BpmTaskAssignRuleTypeEnum.DEPT_LEADER.getType())) {
BpmTaskAssignRuleTypeEnum.DEPT_LEADER.getType())) {
deptApi.validateDeptList(options);
} else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.POST.getType())) {
postApi.validPostList(options);
@ -225,7 +225,7 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
userGroupService.validUserGroups(options);
} else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.SCRIPT.getType())) {
dictDataApi.validateDictDataList(DictTypeConstants.TASK_ASSIGN_SCRIPT,
CollectionUtils.convertSet(options, String::valueOf));
CollectionUtils.convertSet(options, String::valueOf));
} else {
throw new IllegalArgumentException(format("未知的规则类型({})", type));
}
@ -298,7 +298,7 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
}
private Set<Long> calculateTaskCandidateUsersByPost(BpmTaskAssignRuleDO rule) {
List<AdminUserRespDTO> users = adminUserApi.getUsersByPostIds(rule.getOptions());
List<AdminUserRespDTO> users = adminUserApi.getUserListByPostIds(rule.getOptions());
return convertSet(users, AdminUserRespDTO::getId);
}

View File

@ -40,6 +40,8 @@ public class BpmTaskAssignLeaderX2ScriptTest extends BaseMockitoUnitTest {
// mock 方法(startUser)
AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L));
when(adminUserApi.getUser(eq(1L))).thenReturn(startUser);
// mock 方法(getStartUserDept)没有部门
when(deptApi.getDept(eq(10L))).thenReturn(null);
// 调用
Set<Long> result = script.calculateTaskCandidateUsers(execution);
@ -56,7 +58,9 @@ public class BpmTaskAssignLeaderX2ScriptTest extends BaseMockitoUnitTest {
when(adminUserApi.getUser(eq(1L))).thenReturn(startUser);
DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L)
.setLeaderUserId(20L));
// mock 方法getDept
when(deptApi.getDept(eq(10L))).thenReturn(startUserDept);
when(deptApi.getDept(eq(100L))).thenReturn(null);
// 调用
Set<Long> result = script.calculateTaskCandidateUsers(execution);

View File

@ -120,7 +120,7 @@ public class BpmTaskAssignRuleServiceImplTest extends BaseDbUnitTest {
// mock 方法
List<AdminUserRespDTO> users = CollectionUtils.convertList(asSet(11L, 22L),
id -> new AdminUserRespDTO().setId(id));
when(adminUserApi.getUsersByPostIds(eq(rule.getOptions()))).thenReturn(users);
when(adminUserApi.getUserListByPostIds(eq(rule.getOptions()))).thenReturn(users);
mockGetUserMap(asSet(11L, 22L));
// 调用

View File

@ -23,10 +23,10 @@ import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgn
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.USER_GROUP_NOT_EXISTS;
/**
* {@link BpmUserGroupServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
* {@link BpmUserGroupServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
@Import(BpmUserGroupServiceImpl.class)
public class BpmUserGroupServiceTest extends BaseDbUnitTest {

View File

@ -2,8 +2,3 @@
GET {{baseUrl}}/infra/redis/get-monitor-info
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}
### 请求 /infra/redis/get-key-list 接口 => 成功
GET {{baseUrl}}/infra/redis/get-key-list
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}

View File

@ -1,27 +1,20 @@
package cn.iocoder.yudao.module.infra.controller.admin.redis;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import cn.iocoder.yudao.framework.redis.core.RedisKeyRegistry;
import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisKeyDefineRespVO;
import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisKeyValueRespVO;
import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisMonitorRespVO;
import cn.iocoder.yudao.module.infra.convert.redis.RedisConvert;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.data.redis.connection.RedisServerCommands;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.*;
import java.util.Properties;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@ -47,66 +40,4 @@ public class RedisController {
return success(RedisConvert.INSTANCE.build(info, dbSize, commandStats));
}
@GetMapping("/get-key-define-list")
@Operation(summary = "获得 Redis Key 模板列表")
@PreAuthorize("@ss.hasPermission('infra:redis:get-key-list')")
public CommonResult<List<RedisKeyDefineRespVO>> getKeyDefineList() {
List<RedisKeyDefine> keyDefines = RedisKeyRegistry.list();
return success(RedisConvert.INSTANCE.convertList(keyDefines));
}
@GetMapping("/get-key-list")
@Operation(summary = "获得 Redis keys 键名列表")
@Parameter(name = "keyTemplate", description = "Redis Key 定义", example = "true")
@PreAuthorize("@ss.hasPermission('infra:redis:get-key-list')")
public CommonResult<Set<String>> getKeyDefineList(@RequestParam("keyTemplate") String keyTemplate) {
return success(getKeyDefineList0(keyTemplate));
}
private Set<String> getKeyDefineList0(String keyTemplate) {
// key 格式化
String key = StrUtil.replace(keyTemplate, "%[s|c|b|d|x|o|f|a|e|g]", parameter -> "*");
// scan 扫描 key
Set<String> keys = new LinkedHashSet<>();
stringRedisTemplate.execute((RedisCallback<Set<String>>) connection -> {
try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().match(key).count(100).build())) {
cursor.forEachRemaining(value -> keys.add(StrUtil.utf8Str(value)));
} catch (Exception e) {
throw new RuntimeException(e);
}
return keys;
});
return keys;
}
@GetMapping("/get-key-value")
@Operation(summary = "获得 Redis key 内容")
@Parameter(name = "key", description = "Redis Key", example = "oauth2_access_token:233")
@PreAuthorize("@ss.hasPermission('infra:redis:get-key-list')")
public CommonResult<RedisKeyValueRespVO> getKeyValue(@RequestParam("key") String key) {
String value = stringRedisTemplate.opsForValue().get(key);
return success(new RedisKeyValueRespVO(key, value));
}
@DeleteMapping("/delete-key")
@Operation(summary = "删除 Redis Key")
@Parameter(name = "key", description = "Redis Key", example = "oauth2_access_token:233")
@PreAuthorize("@ss.hasPermission('infra:redis:get-key-list')")
public CommonResult<Boolean> deleteKey(@RequestParam("key") String key) {
stringRedisTemplate.delete(key);
return success(Boolean.TRUE);
}
@DeleteMapping("/delete-keys")
@Operation(summary = "删除 Redis Key 根据模板")
@Parameter(name = "keyTemplate", description = "Redis Key 定义", example = "true")
@PreAuthorize("@ss.hasPermission('infra:redis:get-key-list')")
public CommonResult<Boolean> deleteKeys(@RequestParam("keyTemplate") String keyTemplate) {
Set<String> keys = getKeyDefineList0(keyTemplate);
if (CollUtil.isNotEmpty(keys)) {
stringRedisTemplate.delete(keys);
}
return success(Boolean.TRUE);
}
}

View File

@ -1,35 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.redis.vo;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.time.Duration;
@Schema(description = "管理后台 - Redis Key 信息 Response VO")
@Data
@Builder
@AllArgsConstructor
public class RedisKeyDefineRespVO {
@Schema(description = "Key 模板", requiredMode = Schema.RequiredMode.REQUIRED, example = "login_user:%s")
private String keyTemplate;
@Schema(description = "Key 类型的枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "String")
private RedisKeyDefine.KeyTypeEnum keyType;
@Schema(description = "Value 类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "java.lang.String")
private Class<?> valueType;
@Schema(description = "超时类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private RedisKeyDefine.TimeoutTypeEnum timeoutType;
@Schema(description = "过期时间,单位:毫秒", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Duration timeout;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "啦啦啦啦~")
private String memo;
}

View File

@ -1,18 +0,0 @@
package cn.iocoder.yudao.module.infra.controller.admin.redis.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@Schema(description = "管理后台 - 单个 Redis Key Value Response VO")
@Data
@AllArgsConstructor
public class RedisKeyValueRespVO {
@Schema(description = "c5f6990767804a928f4bb96ca249febf", requiredMode = Schema.RequiredMode.REQUIRED, example = "String")
private String key;
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "String")
private String value;
}

View File

@ -1,14 +1,11 @@
package cn.iocoder.yudao.module.infra.convert.redis;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisKeyDefineRespVO;
import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisMonitorRespVO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
@Mapper
@ -29,6 +26,4 @@ public interface RedisConvert {
return respVO;
}
List<RedisKeyDefineRespVO> convertList(List<RedisKeyDefine> list);
}

View File

@ -6,6 +6,9 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
@Mapper
public interface FileConfigMapper extends BaseMapperX<FileConfigDO> {
@ -18,4 +21,7 @@ public interface FileConfigMapper extends BaseMapperX<FileConfigDO> {
.orderByDesc(FileConfigDO::getId));
}
@Select("SELECT COUNT(*) FROM infra_file_config WHERE update_time > #{maxUpdateTime}")
Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime);
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.infra.mq.consumer.file;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.infra.mq.message.file.FileConfigRefreshMessage;
import cn.iocoder.yudao.module.infra.service.file.FileConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link FileConfigRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class FileConfigRefreshConsumer extends AbstractChannelMessageListener<FileConfigRefreshMessage> {
@Resource
private FileConfigService fileConfigService;
@Override
public void onMessage(FileConfigRefreshMessage message) {
log.info("[onMessage][收到 FileConfig 刷新消息]");
fileConfigService.initLocalCache();
}
}

View File

@ -1,4 +1,4 @@
/**
* 占位符避免缩进
* 消息队列的消费者
*/
package cn.iocoder.yudao.module.infra.mq.consumer;

View File

@ -1,17 +0,0 @@
package cn.iocoder.yudao.module.infra.mq.message.file;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
/**
* 文件配置数据刷新 Message
*/
@Data
public class FileConfigRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "infra.file-config.refresh";
}
}

View File

@ -1,4 +1,4 @@
/**
* 占位符避免缩进
* 消息队列的消息
*/
package cn.iocoder.yudao.module.infra.mq.message;

View File

@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.infra.mq.producer.file;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import cn.iocoder.yudao.module.infra.mq.message.file.FileConfigRefreshMessage;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 文件配置相关消息的 Producer
*/
@Component
public class FileConfigProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link FileConfigRefreshMessage} 消息
*/
public void sendFileConfigRefreshMessage() {
FileConfigRefreshMessage message = new FileConfigRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -1,4 +1,4 @@
/**
* 占位符避免缩进
* 消息队列的生产者
*/
package cn.iocoder.yudao.module.infra.mq.producer;

View File

@ -16,11 +16,6 @@ import javax.validation.Valid;
*/
public interface FileConfigService {
/**
* 初始化文件客户端
*/
void initLocalCache();
/**
* 创建文件配置
*

View File

@ -1,8 +1,10 @@
package cn.iocoder.yudao.module.infra.service.file;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.file.core.client.FileClient;
@ -15,20 +17,20 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigU
import cn.iocoder.yudao.module.infra.convert.file.FileConfigConvert;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper;
import cn.iocoder.yudao.module.infra.mq.producer.file.FileConfigProducer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.validation.Validator;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_DELETE_FAIL_MASTER;
@ -46,6 +48,12 @@ public class FileConfigServiceImpl implements FileConfigService {
@Resource
private FileClientFactory fileClientFactory;
/**
* 文件配置的缓存
*/
@Getter
private List<FileConfigDO> fileConfigCache;
/**
* Master FileClient 对象有且仅有一个 {@link FileConfigDO#getMaster()} 对应的
*/
@ -55,13 +63,9 @@ public class FileConfigServiceImpl implements FileConfigService {
@Resource
private FileConfigMapper fileConfigMapper;
@Resource
private FileConfigProducer fileConfigProducer;
@Resource
private Validator validator;
@Override
@PostConstruct
public void initLocalCache() {
// 第一步查询数据
@ -76,6 +80,27 @@ public class FileConfigServiceImpl implements FileConfigService {
masterFileClient = fileClientFactory.getFileClient(config.getId());
}
});
this.fileConfigCache = configs;
}
/**
* 通过定时任务轮询刷新缓存
*
* 目的多节点部署时通过轮询通知所有节点进行刷新
*/
@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
public void refreshLocalCache() {
// 情况一如果缓存里没有数据则直接刷新缓存
if (CollUtil.isEmpty(fileConfigCache)) {
initLocalCache();
return;
}
// 情况二如果缓存里数据则通过 updateTime 判断是否有数据变更有变更则刷新缓存
LocalDateTime maxTime = CollectionUtils.getMaxValue(fileConfigCache, FileConfigDO::getUpdateTime);
if (fileConfigMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
initLocalCache();
}
}
@Override
@ -85,9 +110,9 @@ public class FileConfigServiceImpl implements FileConfigService {
.setConfig(parseClientConfig(createReqVO.getStorage(), createReqVO.getConfig()))
.setMaster(false); // 默认非 master
fileConfigMapper.insert(fileConfig);
// 发送刷新配置的消息
fileConfigProducer.sendFileConfigRefreshMessage();
// 返回
// 刷新缓存
initLocalCache();
return fileConfig.getId();
}
@ -99,8 +124,9 @@ public class FileConfigServiceImpl implements FileConfigService {
FileConfigDO updateObj = FileConfigConvert.INSTANCE.convert(updateReqVO)
.setConfig(parseClientConfig(config.getStorage(), updateReqVO.getConfig()));
fileConfigMapper.updateById(updateObj);
// 发送刷新配置的消息
fileConfigProducer.sendFileConfigRefreshMessage();
// 刷新缓存
initLocalCache();
}
@Override
@ -112,15 +138,9 @@ public class FileConfigServiceImpl implements FileConfigService {
fileConfigMapper.updateBatch(new FileConfigDO().setMaster(false));
// 更新
fileConfigMapper.updateById(new FileConfigDO().setId(id).setMaster(true));
// 发送刷新配置的消息
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
fileConfigProducer.sendFileConfigRefreshMessage();
}
});
// 刷新缓存
initLocalCache();
}
private FileClientConfig parseClientConfig(Integer storage, Map<String, Object> config) {
@ -143,8 +163,9 @@ public class FileConfigServiceImpl implements FileConfigService {
}
// 删除
fileConfigMapper.deleteById(id);
// 发送刷新配置的消息
fileConfigProducer.sendFileConfigRefreshMessage();
// 刷新缓存
initLocalCache();
}
private FileConfigDO validateFileConfigExists(Long id) {

View File

@ -16,7 +16,6 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigP
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper;
import cn.iocoder.yudao.module.infra.mq.producer.file.FileConfigProducer;
import lombok.Data;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
@ -55,8 +54,6 @@ public class FileConfigServiceImplTest extends BaseDbUnitTest {
@Resource
private FileConfigMapper fileConfigMapper;
@MockBean
private FileConfigProducer fileConfigProducer;
@MockBean
private Validator validator;
@MockBean
@ -81,6 +78,10 @@ public class FileConfigServiceImplTest extends BaseDbUnitTest {
verify(fileClientFactory).createOrUpdateFileClient(eq(2L),
eq(configDO2.getStorage()), eq(configDO2.getConfig()));
assertSame(masterFileClient, fileConfigService.getMasterFileClient());
// 断言 fileConfigCache 缓存
assertEquals(2, fileConfigService.getFileConfigCache().size());
assertEquals(configDO1, fileConfigService.getFileConfigCache().get(0));
assertEquals(configDO2, fileConfigService.getFileConfigCache().get(1));
}
@Test
@ -101,8 +102,6 @@ public class FileConfigServiceImplTest extends BaseDbUnitTest {
assertFalse(fileConfig.getMaster());
assertEquals("/yunai", ((LocalFileClientConfig) fileConfig.getConfig()).getBasePath());
assertEquals("https://www.iocoder.cn", ((LocalFileClientConfig) fileConfig.getConfig()).getDomain());
// verify 调用
verify(fileConfigProducer).sendFileConfigRefreshMessage();
}
@Test
@ -126,8 +125,6 @@ public class FileConfigServiceImplTest extends BaseDbUnitTest {
assertPojoEquals(reqVO, fileConfig, "config");
assertEquals("/yunai2", ((LocalFileClientConfig) fileConfig.getConfig()).getBasePath());
assertEquals("https://doc.iocoder.cn", ((LocalFileClientConfig) fileConfig.getConfig()).getDomain());
// verify 调用
verify(fileConfigProducer).sendFileConfigRefreshMessage();
}
@Test
@ -152,8 +149,6 @@ public class FileConfigServiceImplTest extends BaseDbUnitTest {
// 断言数据
assertTrue(fileConfigMapper.selectById(dbFileConfig.getId()).getMaster());
assertFalse(fileConfigMapper.selectById(masterFileConfig.getId()).getMaster());
// verify 调用
verify(fileConfigProducer).sendFileConfigRefreshMessage();
}
@Test
@ -174,8 +169,6 @@ public class FileConfigServiceImplTest extends BaseDbUnitTest {
fileConfigService.deleteFileConfig(id);
// 校验数据不存在了
assertNull(fileConfigMapper.selectById(id));
// verify 调用
verify(fileConfigProducer).sendFileConfigRefreshMessage();
}
@Test

View File

@ -19,10 +19,10 @@ public interface TradeCartMapper extends BaseMapperX<TradeCartDO> {
default TradeCartDO selectByUserIdAndSkuId(Long userId, Long skuId,
Boolean addStatus, Boolean orderStatus) {
return selectOne(TradeCartDO::getUserId, userId,
TradeCartDO::getSkuId, skuId,
TradeCartDO::getAddStatus, addStatus,
TradeCartDO::getOrderStatus, orderStatus);
return selectOne(new LambdaQueryWrapper<TradeCartDO>().eq(TradeCartDO::getUserId, userId)
.eq(TradeCartDO::getSkuId, skuId)
.eq(TradeCartDO::getAddStatus, addStatus)
.eq(TradeCartDO::getOrderStatus, orderStatus));
}
default Integer selectSumByUserId(Long userId) {

View File

@ -6,6 +6,9 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountPageReqVO;
import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
@Mapper
public interface MpAccountMapper extends BaseMapperX<MpAccountDO> {
@ -22,4 +25,7 @@ public interface MpAccountMapper extends BaseMapperX<MpAccountDO> {
return selectOne(MpAccountDO::getAppId, appId);
}
@Select("SELECT COUNT(*) FROM mp_account WHERE update_time > #{maxUpdateTime}")
Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime);
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.mp.mq.consumer;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.mp.mq.message.MpAccountRefreshMessage;
import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link MpAccountRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class MpAccountRefreshConsumer extends AbstractChannelMessageListener<MpAccountRefreshMessage> {
@Resource
private MpAccountService mpAccountService;
@Override
public void onMessage(MpAccountRefreshMessage message) {
log.info("[onMessage][收到 Account 刷新消息]");
mpAccountService.initLocalCache();
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.mp.mq.message;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 公众号账号刷新 Message
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MpAccountRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "mp.account.refresh";
}
}

View File

@ -1,28 +0,0 @@
package cn.iocoder.yudao.module.mp.mq.producer;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import cn.iocoder.yudao.module.mp.mq.message.MpAccountRefreshMessage;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 公众号账号 Producer
*
* @author 芋道源码
*/
@Component
public class MpAccountProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link MpAccountRefreshMessage} 消息
*/
public void sendAccountRefreshMessage() {
MpAccountRefreshMessage message = new MpAccountRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.mp.service.account;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountCreateReqVO;
import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountPageReqVO;
@ -13,7 +13,6 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
import cn.iocoder.yudao.module.mp.dal.mysql.account.MpAccountMapper;
import cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
import cn.iocoder.yudao.module.mp.mq.producer.MpAccountProducer;
import com.google.common.annotations.VisibleForTesting;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@ -21,15 +20,20 @@ import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_USERNAME_EXISTS;
/**
@ -58,9 +62,6 @@ public class MpAccountServiceImpl implements MpAccountService {
@Lazy // 延迟加载解决循环依赖的问题
private MpServiceFactory mpServiceFactory;
@Resource
private MpAccountProducer mpAccountProducer;
@Override
@PostConstruct
public void initLocalCache() {
@ -72,7 +73,30 @@ public class MpAccountServiceImpl implements MpAccountService {
// 第二步构建缓存创建或更新支付 Client
mpServiceFactory.init(accounts);
accountCache = CollectionUtils.convertMap(accounts, MpAccountDO::getAppId);
accountCache = convertMap(accounts, MpAccountDO::getAppId);
});
}
/**
* 通过定时任务轮询刷新缓存
*
* 目的多节点部署时通过轮询通知所有节点进行刷新
*/
@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
public void refreshLocalCache() {
// 注意忽略自动多租户因为要全局初始化缓存
TenantUtils.executeIgnore(() -> {
// 情况一如果缓存里没有数据则直接刷新缓存
if (CollUtil.isEmpty(accountCache)) {
initLocalCache();
return;
}
// 情况二如果缓存里数据则通过 updateTime 判断是否有数据变更有变更则刷新缓存
LocalDateTime maxTime = getMaxValue(accountCache.values(), MpAccountDO::getUpdateTime);
if (mpAccountMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
initLocalCache();
}
});
}
@ -85,8 +109,8 @@ public class MpAccountServiceImpl implements MpAccountService {
MpAccountDO account = MpAccountConvert.INSTANCE.convert(createReqVO);
mpAccountMapper.insert(account);
// 发送刷新消息
mpAccountProducer.sendAccountRefreshMessage();
// 刷新缓存
initLocalCache();
return account.getId();
}
@ -101,8 +125,8 @@ public class MpAccountServiceImpl implements MpAccountService {
MpAccountDO updateObj = MpAccountConvert.INSTANCE.convert(updateReqVO);
mpAccountMapper.updateById(updateObj);
// 发送刷新消息
mpAccountProducer.sendAccountRefreshMessage();
// 刷新缓存
initLocalCache();
}
@Override
@ -112,8 +136,8 @@ public class MpAccountServiceImpl implements MpAccountService {
// 删除
mpAccountMapper.deleteById(id);
// 发送刷新消息
mpAccountProducer.sendAccountRefreshMessage();
// 刷新缓存
initLocalCache();
}
private MpAccountDO validateAccountExists(Long id) {

View File

@ -6,8 +6,6 @@ import lombok.Data;
/**
* 支付单信息 Response DTO
*
* TODO 芋艿还没定好字段
*
* @author 芋道源码
*/
@Data

View File

@ -8,8 +8,6 @@ import java.time.LocalDateTime;
/**
* 退款单信息 Response DTO
*
* TODO 芋艿还没定好字段
*
* @author 芋道源码
*/
@Data

View File

@ -61,12 +61,6 @@
<artifactId>yudao-spring-boot-starter-job</artifactId>
</dependency>
<!-- 消息队列相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mq</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>

View File

@ -1,8 +1,5 @@
package cn.iocoder.yudao.module.pay.dal.redis;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import org.redisson.api.RLock;
/**
* 支付 Redis Key 枚举类
*
@ -10,9 +7,14 @@ import org.redisson.api.RLock;
*/
public interface RedisKeyConstants {
RedisKeyDefine PAY_NOTIFY_LOCK = new RedisKeyDefine("通知任务的分布式锁",
"pay_notify:lock:%d", // 参数来自 DefaultLockKeyBuilder
RedisKeyDefine.KeyTypeEnum.HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson Lock 使用 Hash 数据结构
/**
* 通知任务的分布式锁
*
* KEY 格式pay_notify:lock:%d // 参数来自 DefaultLockKeyBuilder
* VALUE 数据格式HASH // RLock.classRedisson Lock 使用 Hash 数据结构
* 过期时间不固定
*/
String PAY_NOTIFY_LOCK = "pay_notify:lock:%d";
/**
* 支付序号的缓存

View File

@ -33,7 +33,7 @@ public class PayNotifyLockRedisDAO {
}
private static String formatKey(Long id) {
return String.format(PAY_NOTIFY_LOCK.getKeyTemplate(), id);
return String.format(PAY_NOTIFY_LOCK, id);
}
}

View File

@ -15,7 +15,6 @@ import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelUpdateR
import cn.iocoder.yudao.module.pay.convert.channel.PayChannelConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.mysql.channel.PayChannelMapper;
import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -112,7 +111,7 @@ public class PayChannelServiceImpl implements PayChannelService {
channelMapper.insert(channel);
// 刷新缓存
refreshLocalCache();
initLocalCache();
return channel.getId();
}
@ -127,7 +126,7 @@ public class PayChannelServiceImpl implements PayChannelService {
channelMapper.updateById(channel);
// 刷新缓存
refreshLocalCache();
initLocalCache();
}
/**
@ -160,7 +159,7 @@ public class PayChannelServiceImpl implements PayChannelService {
channelMapper.deleteById(id);
// 刷新缓存
refreshLocalCache();
initLocalCache();
}
private PayChannelDO validateChannelExists(Long id) {

View File

@ -44,7 +44,7 @@ public interface AdminUserApi {
* @param postIds 岗位数组
* @return 用户数组
*/
List<AdminUserRespDTO> getUsersByPostIds(Collection<Long> postIds);
List<AdminUserRespDTO> getUserListByPostIds(Collection<Long> postIds);
/**
* 获得用户 Map

View File

@ -21,7 +21,7 @@ public class PermissionApiImpl implements PermissionApi {
@Override
public Set<Long> getUserRoleIdListByRoleIds(Collection<Long> roleIds) {
return permissionService.getUserRoleIdListByRoleIds(roleIds);
return permissionService.getUserRoleIdListByRoleId(roleIds);
}
@Override

View File

@ -40,7 +40,7 @@ public class AdminUserApiImpl implements AdminUserApi {
}
@Override
public List<AdminUserRespDTO> getUsersByPostIds(Collection<Long> postIds) {
public List<AdminUserRespDTO> getUserListByPostIds(Collection<Long> postIds) {
List<AdminUserDO> users = userService.getUserListByPostIds(postIds);
return UserConvert.INSTANCE.convertList4(users);
}

View File

@ -2,6 +2,7 @@
POST {{baseUrl}}/system/auth/login
Content-Type: application/json
tenant-id: {{adminTenentId}}
tag: Yunai.local
{
"username": "admin",

View File

@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.controller.admin.auth;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.security.config.SecurityProperties;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
@ -12,8 +11,8 @@ import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum;
import cn.iocoder.yudao.module.system.service.auth.AdminAuthService;
import cn.iocoder.yudao.module.system.service.permission.MenuService;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import cn.iocoder.yudao.module.system.service.permission.RoleService;
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
@ -34,9 +33,9 @@ import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.obtainAuthorization;
import static java.util.Collections.singleton;
@Tag(name = "管理后台 - 认证")
@RestController
@ -52,6 +51,8 @@ public class AuthController {
@Resource
private RoleService roleService;
@Resource
private MenuService menuService;
@Resource
private PermissionService permissionService;
@Resource
private SocialUserService socialUserService;
@ -91,33 +92,24 @@ public class AuthController {
@GetMapping("/get-permission-info")
@Operation(summary = "获取登录用户的权限信息")
public CommonResult<AuthPermissionInfoRespVO> getPermissionInfo() {
// 获得用户信息
// 1.1 获得用户信息
AdminUserDO user = userService.getUser(getLoginUserId());
if (user == null) {
return null;
}
// 获得角色列表
Set<Long> roleIds = permissionService.getUserRoleIdsFromCache(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus()));
List<RoleDO> roleList = roleService.getRoleListFromCache(roleIds);
// 获得菜单列表
List<MenuDO> menuList = permissionService.getRoleMenuListFromCache(roleIds,
SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType(), MenuTypeEnum.BUTTON.getType()),
singleton(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的
// 拼接结果返回
return success(AuthConvert.INSTANCE.convert(user, roleList, menuList));
}
@GetMapping("/list-menus")
@Operation(summary = "获得登录用户的菜单列表")
public CommonResult<List<AuthMenuRespVO>> getMenuList() {
// 获得角色列表
Set<Long> roleIds = permissionService.getUserRoleIdsFromCache(getLoginUserId(), singleton(CommonStatusEnum.ENABLE.getStatus()));
// 获得用户拥有的菜单列表
List<MenuDO> menuList = permissionService.getRoleMenuListFromCache(roleIds,
SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType()), // 只要目录和菜单类型
singleton(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的
// 转换成 Tree 结构返回
return success(AuthConvert.INSTANCE.buildMenuTree(menuList));
// 1.2 获得角色列表
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId());
List<RoleDO> roles = roleService.getRoleList(roleIds);
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色
// 1.3 获得菜单列表
Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId));
List<MenuDO> menuList = menuService.getMenuList(menuIds);
menuList.removeIf(menu -> !CommonStatusEnum.ENABLE.getStatus().equals(menu.getStatus())); // 移除禁用的菜单
// 2. 拼接结果返回
return success(AuthConvert.INSTANCE.convert(user, roles, menuList));
}
// ========== 短信登录相关 ==========
@ -149,7 +141,7 @@ public class AuthController {
@Parameter(name = "redirectUri", description = "回调路径")
})
public CommonResult<String> socialLogin(@RequestParam("type") Integer type,
@RequestParam("redirectUri") String redirectUri) {
@RequestParam("redirectUri") String redirectUri) {
return CommonResult.success(socialUserService.getAuthorizeUrl(type, redirectUri));
}

View File

@ -6,9 +6,10 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Set;
@Schema(description = "管理后台 - 登录用户的权限信息 Response VO,额外包括用户信息和角色列表")
@Schema(description = "管理后台 - 登录用户的权限信息 Response VO额外包括用户信息和角色列表")
@Data
@NoArgsConstructor
@AllArgsConstructor
@ -24,6 +25,9 @@ public class AuthPermissionInfoRespVO {
@Schema(description = "操作权限数组", requiredMode = Schema.RequiredMode.REQUIRED)
private Set<String> permissions;
@Schema(description = "菜单树", required = true)
private List<MenuVO> menus;
@Schema(description = "用户信息 VO")
@Data
@NoArgsConstructor
@ -37,9 +41,53 @@ public class AuthPermissionInfoRespVO {
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
private String nickname;
@Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://www.iocoder.cn/xx.jpg")
@Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.jpg")
private String avatar;
}
@Schema(description = "管理后台 - 登录用户的菜单信息 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class MenuVO {
@Schema(description = "菜单名称", required = true, example = "芋道")
private Long id;
@Schema(description = "父菜单 ID", required = true, example = "1024")
private Long parentId;
@Schema(description = "菜单名称", required = true, example = "芋道")
private String name;
@Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post")
private String path;
@Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index")
private String component;
@Schema(description = "组件名", example = "SystemUser")
private String componentName;
@Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list")
private String icon;
@Schema(description = "是否可见", required = true, example = "false")
private Boolean visible;
@Schema(description = "是否缓存", required = true, example = "false")
private Boolean keepAlive;
@Schema(description = "是否总是显示", example = "false")
private Boolean alwaysShow;
/**
* 子路由
*/
private List<MenuVO> children;
}
}

View File

@ -11,9 +11,9 @@ import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
/**
* 邮件日志 Base VO提供给添加修改详细的子 VO 使用
* 如果子 VO 存在差异的字段请不要添加到这里影响 Swagger 文档生成
*/
* 邮件日志 Base VO提供给添加修改详细的子 VO 使用
* 如果子 VO 存在差异的字段请不要添加到这里影响 Swagger 文档生成
*/
@Data
public class MailLogBaseVO {

View File

@ -37,10 +37,10 @@ public class PermissionController {
@Operation(summary = "获得角色拥有的菜单编号")
@Parameter(name = "roleId", description = "角色编号", required = true)
@GetMapping("/list-role-resources")
@GetMapping("/list-role-menus")
@PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')")
public CommonResult<Set<Long>> listRoleMenus(Long roleId) {
return success(permissionService.getRoleMenuIds(roleId));
public CommonResult<Set<Long>> getRoleMenuList(Long roleId) {
return success(permissionService.getRoleMenuListByRoleId(roleId));
}
@PostMapping("/assign-role-menu")

View File

@ -40,6 +40,3 @@ tenant-id: {{adminTenentId}}
GET {{baseUrl}}/system/role/page?pageNo=1&pageSize=10
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}
###

View File

@ -20,7 +20,7 @@ public class PermissionAssignRoleDataScopeReqVO {
// TODO 这里要多一个枚举校验
private Integer dataScope;
@Schema(description = "部门编号列表,只有范围类型为 DEPT_CUSTOM 时,该字段才需要", example = "1,3,5")
@Schema(description = "部门编号列表只有范围类型为 DEPT_CUSTOM 时,该字段才需要", example = "1,3,5")
private Set<Long> dataScopeDeptIds = Collections.emptySet(); // 兜底
}

View File

@ -7,9 +7,9 @@ import javax.validation.constraints.NotNull;
import java.util.List;
/**
* 敏感词 Base VO提供给添加修改详细的子 VO 使用
* 如果子 VO 存在差异的字段请不要添加到这里影响 Swagger 文档生成
*/
* 敏感词 Base VO提供给添加修改详细的子 VO 使用
* 如果子 VO 存在差异的字段请不要添加到这里影响 Swagger 文档生成
*/
@Data
public class SensitiveWordBaseVO {

View File

@ -7,9 +7,9 @@ import javax.validation.constraints.*;
import java.time.LocalDateTime;
/**
* 租户 Base VO提供给添加修改详细的子 VO 使用
* 如果子 VO 存在差异的字段请不要添加到这里影响 Swagger 文档生成
*/
* 租户 Base VO提供给添加修改详细的子 VO 使用
* 如果子 VO 存在差异的字段请不要添加到这里影响 Swagger 文档生成
*/
@Data
public class TenantBaseVO {

View File

@ -25,7 +25,7 @@ public class UserProfileUpdateReqVO {
@Length(min = 11, max = 11, message = "手机号长度必须 11 位")
private String mobile;
@Schema(description = "用户性别-参见 SexEnum 枚举类", example = "1")
@Schema(description = "用户性别参见 SexEnum 枚举类", example = "1")
private Integer sex;
}

View File

@ -29,7 +29,7 @@ public class UserExportReqVO {
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
@Schema(description = "部门编号,同时筛选子部门", example = "1024")
@Schema(description = "部门编号同时筛选子部门", example = "1024")
private Long deptId;
}

View File

@ -6,7 +6,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Schema(description = "管理后台 - 用户分页时的信息 Response VO,相比用户基本信息来说,会多部门信息")
@Schema(description = "管理后台 - 用户分页时的信息 Response VO相比用户基本信息来说,会多部门信息")
@Data
@NoArgsConstructor
@AllArgsConstructor

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.system.controller.admin.user.vo.user;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -13,9 +15,9 @@ public class UserUpdateStatusReqVO {
@NotNull(message = "角色编号不能为空")
private Long id;
@Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@Schema(description = "状态见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "状态不能为空")
// @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}")
@InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}")
private Integer status;
}

View File

@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import org.slf4j.LoggerFactory;
@ -27,13 +28,16 @@ public interface AuthConvert {
default AuthPermissionInfoRespVO convert(AdminUserDO user, List<RoleDO> roleList, List<MenuDO> menuList) {
return AuthPermissionInfoRespVO.builder()
.user(AuthPermissionInfoRespVO.UserVO.builder().id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).build())
.roles(convertSet(roleList, RoleDO::getCode))
.permissions(convertSet(menuList, MenuDO::getPermission))
.build();
.user(AuthPermissionInfoRespVO.UserVO.builder().id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).build())
.roles(convertSet(roleList, RoleDO::getCode))
// 权限标识信息
.permissions(convertSet(menuList, MenuDO::getPermission))
// 菜单树
.menus(buildMenuTree(menuList))
.build();
}
AuthMenuRespVO convertTreeNode(MenuDO menu);
AuthPermissionInfoRespVO.MenuVO convertTreeNode(MenuDO menu);
/**
* 将菜单列表构建成菜单树
@ -41,20 +45,23 @@ public interface AuthConvert {
* @param menuList 菜单列表
* @return 菜单树
*/
default List<AuthMenuRespVO> buildMenuTree(List<MenuDO> menuList) {
default List<AuthPermissionInfoRespVO.MenuVO> buildMenuTree(List<MenuDO> menuList) {
// 移除按钮
menuList.removeIf(menu -> menu.getType().equals(MenuTypeEnum.BUTTON.getType()));
// 排序保证菜单的有序性
menuList.sort(Comparator.comparing(MenuDO::getSort));
// 构建菜单树
// 使用 LinkedHashMap 的原因是为了排序 实际也可以用 Stream API 就是太丑了
Map<Long, AuthMenuRespVO> treeNodeMap = new LinkedHashMap<>();
Map<Long, AuthPermissionInfoRespVO.MenuVO> treeNodeMap = new LinkedHashMap<>();
menuList.forEach(menu -> treeNodeMap.put(menu.getId(), AuthConvert.INSTANCE.convertTreeNode(menu)));
// 处理父子关系
treeNodeMap.values().stream().filter(node -> !node.getParentId().equals(ID_ROOT)).forEach(childNode -> {
// 获得父节点
AuthMenuRespVO parentNode = treeNodeMap.get(childNode.getParentId());
AuthPermissionInfoRespVO.MenuVO parentNode = treeNodeMap.get(childNode.getParentId());
if (parentNode == null) {
LoggerFactory.getLogger(getClass()).error("[buildRouterTree][resource({}) 找不到父资源({})]",
childNode.getId(), childNode.getParentId());
childNode.getId(), childNode.getParentId());
return;
}
// 将自己添加到父节点中

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqV
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
@ -25,4 +26,8 @@ public interface DeptMapper extends BaseMapperX<DeptDO> {
return selectCount(DeptDO::getParentId, parentId);
}
default List<DeptDO> selectListByParentId(Collection<Long> parentIds) {
return selectList(DeptDO::getParentId, parentIds);
}
}

View File

@ -25,4 +25,7 @@ public interface MenuMapper extends BaseMapperX<MenuDO> {
.eqIfPresent(MenuDO::getStatus, reqVO.getStatus()));
}
default List<MenuDO> selectListByPermission(String permission) {
return selectList(MenuDO::getPermission, permission);
}
}

View File

@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.system.dal.mysql.permission;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.List;
@ -13,14 +11,18 @@ import java.util.List;
@Mapper
public interface RoleMenuMapper extends BaseMapperX<RoleMenuDO> {
@Repository
class BatchInsertMapper extends ServiceImpl<RoleMenuMapper, RoleMenuDO> {
}
default List<RoleMenuDO> selectListByRoleId(Long roleId) {
return selectList(RoleMenuDO::getRoleId, roleId);
}
default List<RoleMenuDO> selectListByRoleId(Collection<Long> roleIds) {
return selectList(RoleMenuDO::getRoleId, roleIds);
}
default List<RoleMenuDO> selectListByMenuId(Long menuId) {
return selectList(RoleMenuDO::getMenuId, menuId);
}
default void deleteListByRoleIdAndMenuIds(Long roleId, Collection<Long> menuIds) {
delete(new LambdaQueryWrapper<RoleMenuDO>()
.eq(RoleMenuDO::getRoleId, roleId)

View File

@ -7,7 +7,9 @@ import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.Sensitiv
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
import java.util.List;
/**
@ -40,4 +42,7 @@ public interface SensitiveWordMapper extends BaseMapperX<SensitiveWordDO> {
return selectOne(SensitiveWordDO::getName, name);
}
@Select("SELECT COUNT(*) FROM system_sensitive_word WHERE update_time > #{maxUpdateTime}")
Long selectCountByUpdateTimeGt(LocalDateTime maxTime);
}

View File

@ -6,6 +6,9 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
@Mapper
public interface SmsChannelMapper extends BaseMapperX<SmsChannelDO> {
@ -18,4 +21,7 @@ public interface SmsChannelMapper extends BaseMapperX<SmsChannelDO> {
.orderByDesc(SmsChannelDO::getId));
}
@Select("SELECT COUNT(*) FROM system_sms_channel WHERE update_time > #{maxUpdateTime}")
Long selectCountByUpdateTimeGt(LocalDateTime maxTime);
}

View File

@ -1,12 +1,7 @@
package cn.iocoder.yudao.module.system.dal.redis;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import java.time.Duration;
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
/**
* System Redis Key 枚举类
*
@ -14,16 +9,93 @@ import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.S
*/
public interface RedisKeyConstants {
RedisKeyDefine CAPTCHA_CODE = new RedisKeyDefine("验证码的缓存",
"captcha_code:%s", // 参数为 uuid
STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
/**
* 指定部门的所有子部门编号数组的缓存
* <p>
* KEY 格式dept_children_ids:{id}
* VALUE 数据类型String 子部门编号集合
*/
String DEPT_CHILDREN_ID_LIST = "dept_children_ids";
RedisKeyDefine OAUTH2_ACCESS_TOKEN = new RedisKeyDefine("访问令牌的缓存",
"oauth2_access_token:%s", // 参数为访问令牌 token
STRING, OAuth2AccessTokenDO.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
/**
* 角色的缓存
* <p>
* KEY 格式role:{id}
* VALUE 数据类型String 角色信息
*/
String ROLE = "role";
RedisKeyDefine SOCIAL_AUTH_STATE = new RedisKeyDefine("社交登陆的 state", // 注意它是被 JustAuth justauth.type.prefix 使用到
"social_auth_state:%s", // 参数为 state
STRING, String.class, Duration.ofHours(24)); // 值为 state
/**
* 用户拥有的角色编号的缓存
* <p>
* KEY 格式user_role_ids:{userId}
* VALUE 数据类型String 角色编号集合
*/
String USER_ROLE_ID_LIST = "user_role_ids";
/**
* 拥有指定菜单的角色编号的缓存
* <p>
* KEY 格式user_role_ids:{menuId}
* VALUE 数据类型String 角色编号集合
*/
String MENU_ROLE_ID_LIST = "menu_role_ids";
/**
* 拥有权限对应的菜单编号数组的缓存
* <p>
* KEY 格式permission_menu_ids:{permission}
* VALUE 数据类型String 菜单编号数组
*/
String PERMISSION_MENU_ID_LIST = "permission_menu_ids";
/**
* OAuth2 客户端的缓存
* <p>
* KEY 格式user:{id}
* VALUE 数据类型String 客户端信息
*/
String OAUTH_CLIENT = "oauth_client";
/**
* 访问令牌的缓存
* <p>
* KEY 格式oauth2_access_token:{token}
* VALUE 数据类型String 访问令牌信息 {@link OAuth2AccessTokenDO}
* <p>
* 由于动态过期时间使用 RedisTemplate 操作
*/
String OAUTH2_ACCESS_TOKEN = "oauth2_access_token:%s";
/**
* 站内信模版的缓存
* <p>
* KEY 格式notify_template:{code}
* VALUE 数据格式String 模版信息
*/
String NOTIFY_TEMPLATE = "notify_template";
/**
* 邮件账号的缓存
* <p>
* KEY 格式sms_template:{id}
* VALUE 数据格式String 账号信息
*/
String MAIL_ACCOUNT = "mail_account";
/**
* 邮件模版的缓存
* <p>
* KEY 格式mail_template:{code}
* VALUE 数据格式String 模版信息
*/
String MAIL_TEMPLATE = "mail_template";
/**
* 短信模版的缓存
* <p>
* KEY 格式sms_template:{id}
* VALUE 数据格式String 模版信息
*/
String SMS_TEMPLATE = "sms_template";
}

View File

@ -1,41 +0,0 @@
package cn.iocoder.yudao.module.system.dal.redis.common;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.time.Duration;
import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.CAPTCHA_CODE;
/**
* 验证码的 Redis DAO
*
* @author 芋道源码
*/
@Repository
public class CaptchaRedisDAO {
@Resource
private StringRedisTemplate stringRedisTemplate;
public String get(String uuid) {
String redisKey = formatKey(uuid);
return stringRedisTemplate.opsForValue().get(redisKey);
}
public void set(String uuid, String code, Duration timeout) {
String redisKey = formatKey(uuid);
stringRedisTemplate.opsForValue().set(redisKey, code, timeout);
}
public void delete(String uuid) {
String redisKey = formatKey(uuid);
stringRedisTemplate.delete(redisKey);
}
private static String formatKey(String uuid) {
return String.format(CAPTCHA_CODE.getKeyTemplate(), uuid);
}
}

View File

@ -53,7 +53,7 @@ public class OAuth2AccessTokenRedisDAO {
}
private static String formatKey(String accessToken) {
return String.format(OAUTH2_ACCESS_TOKEN.getKeyTemplate(), accessToken);
return String.format(OAUTH2_ACCESS_TOKEN, accessToken);
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.auth;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.auth.OAuth2ClientRefreshMessage;
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link OAuth2ClientRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class OAuth2ClientRefreshConsumer extends AbstractChannelMessageListener<OAuth2ClientRefreshMessage> {
@Resource
private OAuth2ClientService oauth2ClientService;
@Override
public void onMessage(OAuth2ClientRefreshMessage message) {
log.info("[onMessage][收到 OAuth2Client 刷新消息]");
oauth2ClientService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.dept;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.dept.DeptRefreshMessage;
import cn.iocoder.yudao.module.system.service.dept.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link DeptRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class DeptRefreshConsumer extends AbstractChannelMessageListener<DeptRefreshMessage> {
@Resource
private DeptService deptService;
@Override
public void onMessage(DeptRefreshMessage message) {
log.info("[onMessage][收到 Dept 刷新消息]");
deptService.initLocalCache();
}
}

View File

@ -1,31 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.mail;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.mail.MailAccountRefreshMessage;
import cn.iocoder.yudao.module.system.mq.message.mail.MailTemplateRefreshMessage;
import cn.iocoder.yudao.module.system.service.mail.MailAccountService;
import cn.iocoder.yudao.module.system.service.mail.MailTemplateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link MailAccountRefreshMessage} 的消费者
*
* @author wangjingyi
*/
@Component
@Slf4j
public class MailAccountRefreshConsumer extends AbstractChannelMessageListener<MailAccountRefreshMessage> {
@Resource
private MailAccountService mailAccountService;
@Override
public void onMessage(MailAccountRefreshMessage message) {
log.info("[onMessage][收到 Mail Account 刷新信息]");
mailAccountService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.mail;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.mail.MailTemplateRefreshMessage;
import cn.iocoder.yudao.module.system.service.mail.MailTemplateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link MailTemplateRefreshMessage} 的消费者
*
* @author wangjingyi
*/
@Component
@Slf4j
public class MailTemplateRefreshConsumer extends AbstractChannelMessageListener<MailTemplateRefreshMessage> {
@Resource
private MailTemplateService mailTemplateService;
@Override
public void onMessage(MailTemplateRefreshMessage message) {
log.info("[onMessage][收到 Mail Template 刷新信息]");
mailTemplateService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.notify;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.notify.NotifyTemplateRefreshMessage;
import cn.iocoder.yudao.module.system.service.notify.NotifyTemplateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link NotifyTemplateRefreshMessage} 的消费者
*
* @author xrcoder
*/
@Component
@Slf4j
public class NotifyTemplateRefreshConsumer extends AbstractChannelMessageListener<NotifyTemplateRefreshMessage> {
@Resource
private NotifyTemplateService notifyTemplateService;
@Override
public void onMessage(NotifyTemplateRefreshMessage message) {
log.info("[onMessage][收到 NotifyTemplate 刷新消息]");
notifyTemplateService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.permission;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.permission.MenuRefreshMessage;
import cn.iocoder.yudao.module.system.service.permission.MenuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link MenuRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class MenuRefreshConsumer extends AbstractChannelMessageListener<MenuRefreshMessage> {
@Resource
private MenuService menuService;
@Override
public void onMessage(MenuRefreshMessage message) {
log.info("[onMessage][收到 Menu 刷新消息]");
menuService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.permission;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.permission.RoleMenuRefreshMessage;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link RoleMenuRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class RoleMenuRefreshConsumer extends AbstractChannelMessageListener<RoleMenuRefreshMessage> {
@Resource
private PermissionService permissionService;
@Override
public void onMessage(RoleMenuRefreshMessage message) {
log.info("[onMessage][收到 Role 与 Menu 的关联刷新消息]");
permissionService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.permission;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.permission.RoleRefreshMessage;
import cn.iocoder.yudao.module.system.service.permission.RoleService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link RoleRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class RoleRefreshConsumer extends AbstractChannelMessageListener<RoleRefreshMessage> {
@Resource
private RoleService roleService;
@Override
public void onMessage(RoleRefreshMessage message) {
log.info("[onMessage][收到 Role 刷新消息]");
roleService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.permission;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.permission.UserRoleRefreshMessage;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link UserRoleRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class UserRoleRefreshConsumer extends AbstractChannelMessageListener<UserRoleRefreshMessage> {
@Resource
private PermissionService permissionService;
@Override
public void onMessage(UserRoleRefreshMessage message) {
log.info("[onMessage][收到 User 与 Role 的关联刷新消息]");
permissionService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.sensitiveword;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.sensitiveword.SensitiveWordRefreshMessage;
import cn.iocoder.yudao.module.system.service.sensitiveword.SensitiveWordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link SensitiveWordRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class SensitiveWordRefreshConsumer extends AbstractChannelMessageListener<SensitiveWordRefreshMessage> {
@Resource
private SensitiveWordService sensitiveWordService;
@Override
public void onMessage(SensitiveWordRefreshMessage message) {
log.info("[onMessage][收到 SensitiveWord 刷新消息]");
sensitiveWordService.initLocalCache();
}
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.sms;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.sms.SmsChannelRefreshMessage;
import cn.iocoder.yudao.module.system.service.sms.SmsChannelService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link SmsChannelRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class SmsChannelRefreshConsumer extends AbstractChannelMessageListener<SmsChannelRefreshMessage> {
@Resource
private SmsChannelService smsChannelService;
@Override
public void onMessage(SmsChannelRefreshMessage message) {
log.info("[onMessage][收到 SmsChannel 刷新消息]");
smsChannelService.initLocalCache();
}
}

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