Merge remote-tracking branch 'origin/develop' into develop

# Conflicts:
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/article/AppArticleController.java
This commit is contained in:
owen
2024-01-05 15:40:29 +08:00
206 changed files with 2100 additions and 1493 deletions

View File

@@ -15,6 +15,7 @@ import java.util.Arrays;
@Getter
public enum TerminalEnum implements IntArrayValuable {
UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它
WECHAT_MINI_PROGRAM(10, "微信小程序"),
WECHAT_WAP(11, "微信公众号"),
H5(20, "H5 网页"),

View File

@@ -1,10 +1,12 @@
package cn.iocoder.yudao.framework.common.util.cache;
import com.alibaba.ttl.threadpool.TtlExecutors;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.time.Duration;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
@@ -15,11 +17,13 @@ import java.util.concurrent.Executors;
public class CacheUtils {
public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
Executor executor = Executors.newCachedThreadPool( // TODO 芋艿:可能要思考下,未来要不要做成可配置
TtlExecutors.getDefaultDisableInheritableThreadFactory()); // TTL 保证 ThreadLocal 可以透传
return CacheBuilder.newBuilder()
// 只阻塞当前数据加载线程,其他线程返回旧值
.refreshAfterWrite(duration)
// 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
.build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO 芋艿:可能要思考下,未来要不要做成可配置
.build(CacheLoader.asyncReloading(loader, executor));
}
}

View File

@@ -88,8 +88,6 @@ public class ServletUtils {
return JakartaServletUtil.getClientIP(request);
}
// TODO @疯狂terminal 还是从 ServletUtils 里拿,更容易全局治理;
public static boolean isJsonRequest(ServletRequest request) {
return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
}

View File

@@ -490,6 +490,37 @@ id,name,type,parentId
441700,阳江市,3,440000
441800,清远市,3,440000
441900,东莞市,3,440000
441901,莞城区,4,441900
441902,南城区,4,441900
441904,万江区,4,441900
441905,石碣镇,4,441900
441906,石龙镇,4,441900
441907,茶山镇,4,441900
441908,石排镇,4,441900
441909,企石镇,4,441900
441910,横沥镇,4,441900
441911,桥头镇,4,441900
441912,谢岗镇,4,441900
441913,东坑镇,4,441900
441914,常平镇,4,441900
441915,寮步镇,4,441900
441916,大朗镇,4,441900
441917,麻涌镇,4,441900
441918,中堂镇,4,441900
441919,高埗镇,4,441900
441920,樟木头镇,4,441900
441921,大岭山镇,4,441900
441922,望牛墩镇,4,441900
441923,黄江镇,4,441900
441924,洪梅镇,4,441900
441925,清溪镇,4,441900
441926,沙田镇,4,441900
441927,道滘镇,4,441900
441928,塘厦镇,4,441900
441929,虎门镇,4,441900
441930,厚街镇,4,441900
441931,凤岗镇,4,441900
441932,长安镇,4,441900
442000,中山市,3,440000
445100,潮州市,3,440000
445200,揭阳市,3,440000
@@ -3605,4 +3636,4 @@ id,name,type,parentId
659008,可克达拉市,4,659000
659009,昆玉市,4,659000
659010,胡杨河市,4,659000
659011,新星市,4,659000
659011,新星市,4,659000
1 id name type parentId
490 441700 阳江市 3 440000
491 441800 清远市 3 440000
492 441900 东莞市 3 440000
493 441901 莞城区 4 441900
494 441902 南城区 4 441900
495 441904 万江区 4 441900
496 441905 石碣镇 4 441900
497 441906 石龙镇 4 441900
498 441907 茶山镇 4 441900
499 441908 石排镇 4 441900
500 441909 企石镇 4 441900
501 441910 横沥镇 4 441900
502 441911 桥头镇 4 441900
503 441912 谢岗镇 4 441900
504 441913 东坑镇 4 441900
505 441914 常平镇 4 441900
506 441915 寮步镇 4 441900
507 441916 大朗镇 4 441900
508 441917 麻涌镇 4 441900
509 441918 中堂镇 4 441900
510 441919 高埗镇 4 441900
511 441920 樟木头镇 4 441900
512 441921 大岭山镇 4 441900
513 441922 望牛墩镇 4 441900
514 441923 黄江镇 4 441900
515 441924 洪梅镇 4 441900
516 441925 清溪镇 4 441900
517 441926 沙田镇 4 441900
518 441927 道滘镇 4 441900
519 441928 塘厦镇 4 441900
520 441929 虎门镇 4 441900
521 441930 厚街镇 4 441900
522 441931 凤岗镇 4 441900
523 441932 长安镇 4 441900
524 442000 中山市 3 440000
525 445100 潮州市 3 440000
526 445200 揭阳市 3 440000
3636 659008 可克达拉市 4 659000
3637 659009 昆玉市 4 659000
3638 659010 胡杨河市 4 659000
3639 659011 新星市 4 659000

View File

@@ -46,6 +46,7 @@
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -71,6 +71,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
}
// payConfig.setCertSerialNo();
// 创建 client 客户端
client = new WxPayServiceImpl();

View File

@@ -41,6 +41,7 @@ public abstract class AbstractSmsClient implements SmsClient {
return;
}
log.info("[refresh][配置({})发生变化,重新初始化]", properties);
this.properties = properties;
// 初始化
this.init();
}

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.tenant.core.context;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
import com.alibaba.ttl.TransmittableThreadLocal;
@@ -21,7 +22,7 @@ public class TenantContextHolder {
private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
/**
* 获得租户编号
* 获得租户编号
*
* @return 租户编号
*/
@@ -29,6 +30,16 @@ public class TenantContextHolder {
return TENANT_ID.get();
}
/**
* 获得租户编号 String
*
* @return 租户编号
*/
public static String getTenantIdStr() {
Long tenantId = getTenantId();
return StrUtil.toStringOrNull(tenantId);
}
/**
* 获得租户编号。如果不存在,则抛出 NullPointerException 异常
*

View File

@@ -1,12 +1,14 @@
package cn.iocoder.yudao.framework.excel.core.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.converters.longconverter.LongStringConverter;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
@@ -33,9 +35,10 @@ public class ExcelUtils {
EasyExcel.write(response.getOutputStream(), head)
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
.registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
.sheet(sheetName).doWrite(data);
// 设置 header 和 contentType。写在最后的原因是避免报错时响应 contentType 已经被修改了
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8));
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
}

View File

@@ -81,7 +81,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
}
// 腾讯云必须有 region否则会报错
if (config.getEndpoint().contains(ENDPOINT_TENCENT)) {
return StrUtil.subAfter(config.getEndpoint(), ".cos.", false)
return StrUtil.subAfter(config.getEndpoint(), "cos.", false)
.replaceAll("." + ENDPOINT_TENCENT, ""); // 去除 Endpoint
}
return null;

View File

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.flowable.config;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.flowable.core.web.FlowableWebFilter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.AsyncListenableTaskExecutor;
@@ -17,6 +18,7 @@ public class YudaoFlowableConfiguration {
* 如果不创建会导致项目启动时Flowable 报错的问题
*/
@Bean
@ConditionalOnMissingBean
public AsyncListenableTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
@@ -40,4 +42,5 @@ public class YudaoFlowableConfiguration {
registrationBean.setOrder(WebFilterOrderEnum.FLOWABLE_FILTER);
return registrationBean;
}
}

View File

@@ -65,6 +65,12 @@
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <!-- 多数据源 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>

View File

@@ -146,8 +146,8 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
*
* @param entities 实体们
*/
default void insertBatch(Collection<T> entities) {
Db.saveBatch(entities);
default Boolean insertBatch(Collection<T> entities) {
return Db.saveBatch(entities);
}
/**
@@ -156,28 +156,28 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
* @param entities 实体们
* @param size 插入数量 Db.saveBatch 默认为 1000
*/
default void insertBatch(Collection<T> entities, int size) {
Db.saveBatch(entities, size);
default Boolean insertBatch(Collection<T> entities, int size) {
return Db.saveBatch(entities, size);
}
default void updateBatch(T update) {
update(update, new QueryWrapper<>());
default int updateBatch(T update) {
return update(update, new QueryWrapper<>());
}
default void updateBatch(Collection<T> entities) {
Db.updateBatchById(entities);
default Boolean updateBatch(Collection<T> entities) {
return Db.updateBatchById(entities);
}
default void updateBatch(Collection<T> entities, int size) {
Db.updateBatchById(entities, size);
default Boolean updateBatch(Collection<T> entities, int size) {
return Db.updateBatchById(entities, size);
}
default void insertOrUpdate(T entity) {
Db.saveOrUpdate(entity);
default Boolean insertOrUpdate(T entity) {
return Db.saveOrUpdate(entity);
}
default void insertOrUpdateBatch(Collection<T> collection) {
Db.saveOrUpdateBatch(collection);
default Boolean insertOrUpdateBatch(Collection<T> collection) {
return Db.saveOrUpdateBatch(collection);
}
default int delete(String field, String value) {

View File

@@ -12,7 +12,10 @@
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>用户的认证、权限的校验</description>
<description>
1. security用户的认证、权限的校验实现「谁」可以做「什么事」
2. operatelog操作日志实现「谁」在「什么时间」对「什么」做了「什么事」
</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies>
@@ -50,6 +53,13 @@
<artifactId>guava</artifactId>
</dependency>
<dependency>
<!-- Spring Boot 通用操作日志组件,基于注解实现 -->
<!-- 此组件解决的问题是:「谁」在「什么时间」对「什么」做了「什么事」 -->
<groupId>io.github.mouzt</groupId>
<artifactId>bizlog-sdk</artifactId>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>

View File

@@ -0,0 +1,27 @@
package cn.iocoder.yudao.framework.operatelog.config;
import cn.iocoder.yudao.framework.operatelog.core.service.ILogRecordServiceImpl;
import com.mzt.logapi.service.ILogRecordService;
import com.mzt.logapi.starter.annotation.EnableLogRecord;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
/**
* 操作日志配置类
*
* @author HUIHUI
*/
@EnableLogRecord(tenant = "") // 貌似用不上 tenant 这玩意给个空好啦
@AutoConfiguration
@Slf4j
public class YudaoOperateLogV2Configuration {
@Bean
@Primary
public ILogRecordService iLogRecordServiceImpl() {
return new ILogRecordServiceImpl();
}
}

View File

@@ -0,0 +1,4 @@
/**
* 占位,无特殊作用
*/
package cn.iocoder.yudao.framework.operatelog.core;

View File

@@ -0,0 +1,85 @@
package cn.iocoder.yudao.framework.operatelog.core.service;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2CreateReqDTO;
import com.mzt.logapi.beans.LogRecord;
import com.mzt.logapi.service.ILogRecordService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
// TODO @puhui999LogRecordServiceImpl 改成这个名字哈
/**
* 操作日志 ILogRecordService 实现类
*
* 基于 {@link OperateLogApi} 实现,记录操作日志
*
* @author HUIHUI
*/
@Slf4j
public class ILogRecordServiceImpl implements ILogRecordService {
@Resource
private OperateLogApi operateLogApi;
@Override
public void record(LogRecord logRecord) {
// 1. 补全通用字段
OperateLogV2CreateReqDTO reqDTO = new OperateLogV2CreateReqDTO();
reqDTO.setTraceId(TracerUtils.getTraceId());
// 补充用户信息
fillUserFields(reqDTO);
// 补全模块信息
fillModuleFields(reqDTO, logRecord);
// 补全请求信息
fillRequestFields(reqDTO);
// 2. 异步记录日志
operateLogApi.createOperateLogV2(reqDTO);
// TODO 测试结束删除或搞个开关
log.info("操作日志 ===> {}", reqDTO);
}
private static void fillUserFields(OperateLogV2CreateReqDTO reqDTO) {
// TODO @puhui999使用 SecurityFrameworkUtils。因为要考虑rpc、mq、job它其实不是 web
reqDTO.setUserId(WebFrameworkUtils.getLoginUserId());
reqDTO.setUserType(WebFrameworkUtils.getLoginUserType());
}
public static void fillModuleFields(OperateLogV2CreateReqDTO reqDTO, LogRecord logRecord) {
reqDTO.setType(logRecord.getType()); // 大模块类型例如CRM 客户
reqDTO.setSubType(logRecord.getSubType());// 操作名称,例如:转移客户
reqDTO.setBizId(Long.parseLong(logRecord.getBizNo())); // 业务编号,例如:客户编号
reqDTO.setAction(logRecord.getAction());// 操作内容,例如:修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。
reqDTO.setExtra(logRecord.getExtra()); // 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ),例如说,记录订单编号,{ orderId: "1"}
}
private static void fillRequestFields(OperateLogV2CreateReqDTO reqDTO) {
// 获得 Request 对象
HttpServletRequest request = ServletUtils.getRequest();
if (request == null) {
return;
}
// 补全请求信息
reqDTO.setRequestMethod(request.getMethod());
reqDTO.setRequestUrl(request.getRequestURI());
reqDTO.setUserIp(ServletUtils.getClientIP(request));
reqDTO.setUserAgent(ServletUtils.getUserAgent(request));
}
@Override
public List<LogRecord> queryLog(String bizNo, String type) {
throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询");
}
@Override
public List<LogRecord> queryLogByBizNo(String bizNo, String type, String subType) {
throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询");
}
}

View File

@@ -0,0 +1,7 @@
/**
* 基于 mzt-log 框架
* 实现操作日志功能
*
* @author HUIHUI
*/
package cn.iocoder.yudao.framework.operatelog;

View File

@@ -1,2 +1,3 @@
cn.iocoder.yudao.framework.security.config.YudaoSecurityAutoConfiguration
cn.iocoder.yudao.framework.security.config.YudaoWebSecurityConfigurerAdapter
cn.iocoder.yudao.framework.security.config.YudaoWebSecurityConfigurerAdapter
cn.iocoder.yudao.framework.operatelog.config.YudaoOperateLogV2Configuration

View File

@@ -1,8 +1,11 @@
package cn.iocoder.yudao.framework.web.core.util;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
@@ -25,6 +28,13 @@ public class WebFrameworkUtils {
public static final String HEADER_TENANT_ID = "tenant-id";
/**
* 终端的 Header
*
* @see cn.iocoder.yudao.framework.common.enums.TerminalEnum
*/
public static final String HEADER_TERMINAL = "terminal";
private static WebProperties properties;
public WebFrameworkUtils(WebProperties webProperties) {
@@ -107,6 +117,15 @@ public class WebFrameworkUtils {
return getLoginUserId(request);
}
public static Integer getTerminal() {
HttpServletRequest request = getRequest();
if (request == null) {
return TerminalEnum.UNKNOWN.getTerminal();
}
String terminalValue = request.getHeader(HEADER_TERMINAL);
return NumberUtil.parseInt(terminalValue, TerminalEnum.UNKNOWN.getTerminal());
}
public static void setCommonResult(ServletRequest request, CommonResult<?> result) {
request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result);
}

View File

@@ -38,17 +38,6 @@
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢?
因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。
如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。
-->
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
<scope>provided</scope>
</dependency>
<!-- 消息队列相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>

View File

@@ -66,7 +66,7 @@ public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
@Override
public void removeSession(WebSocketSession session) {
// 移除从 idSessions 中
idSessions.remove(session.getId(), session);
idSessions.remove(session.getId());
// 移除从 idSessions 中
LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
if (user == null) {