# Conflicts:
#	yudao-dependencies/pom.xml
This commit is contained in:
YunaiV 2025-12-27 21:38:07 +08:00
commit d0293ea2fa
21 changed files with 1552 additions and 1386 deletions

File diff suppressed because it is too large Load Diff

View File

@ -77,6 +77,7 @@
<jimureport.version>2.1.3</jimureport.version>
<jimubi.version>2.3.0</jimubi.version>
<weixin-java.version>4.7.9-20251224.161447</weixin-java.version>
<alipay-sdk-java.version>4.40.607.ALL</alipay-sdk-java.version>
<!-- 专属于 JDK8 安全漏洞升级 -->
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
</properties>
@ -596,6 +597,18 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>${alipay-sdk-java.version}</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>

View File

@ -59,6 +59,8 @@ import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -231,20 +233,24 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
// 4.3 流式返回
StringBuffer contentBuffer = new StringBuffer();
StringBuffer reasoningContentBuffer = new StringBuffer();
// 防止执行多次知识库和联网搜索
AtomicBoolean firstExecuteFlag = new AtomicBoolean(true);
AtomicReference<List<AiChatMessageRespVO.KnowledgeSegment>> cacheSegments = new AtomicReference<>();
AtomicReference<List<AiWebSearchResponse.WebPage>> cacheWebSearchPages = new AtomicReference<>();
return streamResponse.map(chunk -> {
// 仅首次返回知识库联网搜索
List<AiChatMessageRespVO.KnowledgeSegment> segments = null;
List<AiWebSearchResponse.WebPage> webSearchPages = null;
if (StrUtil.isEmpty(contentBuffer)) {
Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() ->
knowledgeDocumentService.getKnowledgeDocumentMap(
convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)));
segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> {
AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
segment.setDocumentName(document != null ? document.getName() : null);
});
if (webSearchResponse != null) {
webSearchPages = webSearchResponse.getLists();
if (firstExecuteFlag.compareAndSet(true, false)) { // CAS 操作确保仅执行一次
Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() -> knowledgeDocumentService.getKnowledgeDocumentMap(
convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)));
cacheSegments.set(BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> {
AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
segment.setDocumentName(document != null ? document.getName() : null);
}));
if (webSearchResponse != null) {
cacheWebSearchPages.set(webSearchResponse.getLists());
}
}
}
// 响应结果
@ -261,7 +267,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
.setContent(StrUtil.nullToDefault(newContent, "")) // 避免 null 情况
.setReasoningContent(StrUtil.nullToDefault(newReasoningContent, "")) // 避免 null 情况
.setSegments(segments).setWebSearchPages(webSearchPages))); // 知识库 + 联网搜索
.setSegments(cacheSegments.get()).setWebSearchPages(cacheWebSearchPages.get()))); // 知识库 + 联网搜索
}).doOnComplete(() -> {
// 忽略租户因为 Flux 异步无法透传租户
TenantUtils.executeIgnore(() -> chatMessageMapper.updateById(

View File

@ -4,12 +4,14 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO;
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenColumnMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum;
@ -17,7 +19,9 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenBuilder;
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine;
import cn.iocoder.yudao.module.infra.service.db.DataSourceConfigService;
import cn.iocoder.yudao.module.infra.service.db.DatabaseTableService;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.config.po.TableField;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.google.common.annotations.VisibleForTesting;
@ -45,6 +49,8 @@ public class CodegenServiceImpl implements CodegenService {
@Resource
private DatabaseTableService databaseTableService;
@Resource
private DataSourceConfigService dataSourceConfigService;
@Resource
private CodegenTableMapper codegenTableMapper;
@ -284,8 +290,11 @@ public class CodegenServiceImpl implements CodegenService {
}
}
// 获取数据源对应的数据库类型
DataSourceConfigDO dataSourceConfig = dataSourceConfigService.getDataSourceConfig(table.getDataSourceConfigId());
DbType dbType = JdbcUtils.getDbType(dataSourceConfig.getUrl());
// 执行生成
return codegenEngine.execute(table, columns, subTables, subColumnsList);
return codegenEngine.execute(dbType, table, columns, subTables, subColumnsList);
}
@Override

View File

@ -33,6 +33,7 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenVOTypeEnum;
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
import com.baomidou.mybatisplus.annotation.DbType;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Maps;
@ -310,16 +311,17 @@ public class CodegenEngine {
/**
* 生成代码
*
* @param dbType 数据库类型
* @param table 表定义
* @param columns table 的字段定义数组
* @param subTables 子表数组当且仅当主子表时使用
* @param subColumnsList subTables 的字段定义数组
* @return 生成的代码key 是路径value 是对应代码
*/
public Map<String, String> execute(CodegenTableDO table, List<CodegenColumnDO> columns,
public Map<String, String> execute(DbType dbType, CodegenTableDO table, List<CodegenColumnDO> columns,
List<CodegenTableDO> subTables, List<List<CodegenColumnDO>> subColumnsList) {
// 1.1 初始化 bindMap 上下文
Map<String, Object> bindingMap = initBindingMap(table, columns, subTables, subColumnsList);
Map<String, Object> bindingMap = initBindingMap(dbType, table, columns, subTables, subColumnsList);
// 1.2 获得模版
Map<String, String> templates = getTemplates(table.getFrontType());
@ -425,10 +427,11 @@ public class CodegenEngine {
return content;
}
private Map<String, Object> initBindingMap(CodegenTableDO table, List<CodegenColumnDO> columns,
private Map<String, Object> initBindingMap(DbType dbType, CodegenTableDO table, List<CodegenColumnDO> columns,
List<CodegenTableDO> subTables, List<List<CodegenColumnDO>> subColumnsList) {
// 创建 bindingMap
Map<String, Object> bindingMap = new HashMap<>(globalBindingMap);
bindingMap.put("dbType", dbType);
bindingMap.put("table", table);
bindingMap.put("columns", columns);
bindingMap.put("primaryColumn", CollectionUtils.findFirst(columns, CodegenColumnDO::getPrimaryKey)); // 主键字段

View File

@ -1,3 +1,24 @@
## 通用变量定义
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出'])
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export'])
##
## 宏定义:生成按钮 SQL通用部分
#macro(insertButtonSql $parentIdVar)
#foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1)
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, ${parentIdVar},
'', '', '', 0
);
#end
#end
##
## ======================= MySQL / OceanBase =======================
#if ($dbType.name() == 'MYSQL' || $dbType.name() == 'OCEAN_BASE')
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
@ -9,12 +30,9 @@ VALUES (
);
-- 按钮父菜单ID
-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出'])
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export'])
#foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1)
INSERT INTO system_menu(
@ -25,4 +43,139 @@ VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId,
'', '', '', 0
);
#end
#end
##
## ======================= Oracle / 达梦 DM =======================
#elseif ($dbType.name() == 'ORACLE' || $dbType.name() == 'DM')
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status, component_name
)
VALUES (
'${table.classComment}管理', '', 2, 0, ${table.parentMenuId},
'${simpleClassName_strikeCase}', '', '${table.moduleName}/${table.businessName}/index', 0, '${table.className}'
);
-- 按钮父菜单ID
-- 说明Oracle/达梦 使用序列获取上一个插入的 ID
DECLARE
v_parent_id NUMBER;
BEGIN
SELECT system_menu_seq.CURRVAL INTO v_parent_id FROM DUAL;
-- 按钮 SQL
#foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1)
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, v_parent_id,
'', '', '', 0
);
#end
END;
/
##
## ======================= PostgreSQL / 人大金仓 KingbaseES =======================
#elseif ($dbType.name() == 'POSTGRE_SQL' || $dbType.name() == 'KINGBASE_ES')
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status, component_name
)
VALUES (
'${table.classComment}管理', '', 2, 0, ${table.parentMenuId},
'${simpleClassName_strikeCase}', '', '${table.moduleName}/${table.businessName}/index', 0, '${table.className}'
);
-- 按钮父菜单ID
-- 说明PostgreSQL/KingbaseES 使用 lastval() 获取上一个插入的序列值
DO $$
DECLARE
v_parent_id BIGINT;
BEGIN
v_parent_id := lastval();
-- 按钮 SQL
#foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1)
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, v_parent_id,
'', '', '', 0
);
#end
END $$;
##
## ======================= SQL Server =======================
#elseif ($dbType.name() == 'SQL_SERVER' || $dbType.name() == 'SQL_SERVER2005')
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status, component_name
)
VALUES (
'${table.classComment}管理', '', 2, 0, ${table.parentMenuId},
'${simpleClassName_strikeCase}', '', '${table.moduleName}/${table.businessName}/index', 0, '${table.className}'
);
-- 按钮父菜单ID
-- 说明SQL Server 使用 SCOPE_IDENTITY() 获取上一个插入的自增 ID
DECLARE @parentId BIGINT;
SET @parentId = SCOPE_IDENTITY();
-- 按钮 SQL
#foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1)
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId,
'', '', '', 0
);
#end
##
## ======================= 不支持的数据库类型 =======================
#else
-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-- 注意:当前数据库类型 ${dbType.name()} 暂不支持自动生成菜单 SQL
-- 请参考以下 MySQL 语法,手动修改为您的数据库对应的语法
-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status, component_name
)
VALUES (
'${table.classComment}管理', '', 2, 0, ${table.parentMenuId},
'${simpleClassName_strikeCase}', '', '${table.moduleName}/${table.businessName}/index', 0, '${table.className}'
);
-- 按钮父菜单ID
-- TODO: 请根据您的数据库类型,修改获取上一个插入 ID 的方式
-- MySQL: SELECT @parentId := LAST_INSERT_ID();
-- Oracle: SELECT system_menu_seq.CURRVAL INTO v_parent_id FROM DUAL;
-- PostgreSQL: SELECT lastval() INTO v_parent_id;
-- SQL Server: SET @parentId = SCOPE_IDENTITY();
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
#foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1)
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId,
'', '', '', 0
);
#end
#end

View File

@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTa
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO;
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenColumnMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
@ -19,7 +20,9 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenBuilder;
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine;
import cn.iocoder.yudao.module.infra.service.db.DataSourceConfigService;
import cn.iocoder.yudao.module.infra.service.db.DatabaseTableService;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.config.po.TableField;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import org.junit.jupiter.api.Test;
@ -62,6 +65,8 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
@MockBean
private DatabaseTableService databaseTableService;
@MockitoBean
private DataSourceConfigService dataSourceConfigService;
@MockBean
private CodegenBuilder codegenBuilder;
@ -453,9 +458,12 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setOrdinalPosition(2));
codegenColumnMapper.insert(column02);
// mock 数据DataSourceConfigDO
when(dataSourceConfigService.getDataSourceConfig(eq(table.getDataSourceConfigId())))
.thenReturn(randomPojo(DataSourceConfigDO.class, o -> o.setUrl("jdbc:mysql://")));
// mock 执行生成
Map<String, String> codes = MapUtil.of(randomString(), randomString());
when(codegenEngine.execute(eq(table), argThat(columns -> {
when(codegenEngine.execute(eq(DbType.MYSQL), eq(table), argThat(columns -> {
assertEquals(2, columns.size());
assertEquals(column01, columns.get(0));
assertEquals(column02, columns.get(1));
@ -494,9 +502,12 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
// mock 数据sub CodegenColumnDO
CodegenColumnDO subColumn01 = randomPojo(CodegenColumnDO.class, o -> o.setId(1024L).setTableId(subTable.getId()));
codegenColumnMapper.insert(subColumn01);
// mock 数据DataSourceConfigDO
when(dataSourceConfigService.getDataSourceConfig(eq(table.getDataSourceConfigId())))
.thenReturn(randomPojo(DataSourceConfigDO.class, o -> o.setUrl("jdbc:mysql://")));
// mock 执行生成
Map<String, String> codes = MapUtil.of(randomString(), randomString());
when(codegenEngine.execute(eq(table), argThat(columns -> {
when(codegenEngine.execute(eq(DbType.MYSQL), eq(table), argThat(columns -> {
assertEquals(2, columns.size());
assertEquals(column01, columns.get(0));
assertEquals(column02, columns.get(1));

View File

@ -4,6 +4,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -28,7 +29,7 @@ public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(table, columns, null, null);
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 生成测试文件
//writeResult(result, resourcesPath + "/vue2_one");
// 断言
@ -44,7 +45,7 @@ public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
List<CodegenColumnDO> columns = getColumnList("category");
// 调用
Map<String, String> result = codegenEngine.execute(table, columns, null, null);
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 生成测试文件
//writeResult(result, resourcesPath + "/vue2_tree");
// 断言
@ -88,7 +89,7 @@ public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
// 调用
Map<String, String> result = codegenEngine.execute(table, columns,
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
// 生成测试文件
//writeResult(result, resourcesPath + path);

View File

@ -4,6 +4,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -28,7 +29,7 @@ public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(table, columns, null, null);
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 生成测试文件
//writeResult(result, resourcesPath + "/vue3_one");
// 断言
@ -44,7 +45,7 @@ public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
List<CodegenColumnDO> columns = getColumnList("category");
// 调用
Map<String, String> result = codegenEngine.execute(table, columns, null, null);
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 生成测试文件
//writeResult(result, resourcesPath + "/vue3_tree");
// 断言
@ -88,7 +89,7 @@ public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
// 调用
Map<String, String> result = codegenEngine.execute(table, columns,
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
// 生成测试文件
//writeResult(result, resourcesPath + path);

View File

@ -76,7 +76,7 @@ public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
.in(CouponTemplateDO::getTakeType, canTakeTypes) // 2. 领取方式一致
.and(ww -> ww.gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now()) // 3.1 未过期
.or().eq(CouponTemplateDO::getValidityType, CouponTemplateValidityTypeEnum.TERM.getType())) // 3.2 领取之后
.apply(" (take_count < total_count OR total_count = -1)"); // 4. 剩余数量大于 0或者无限领取
.apply(" (take_count < total_count OR total_count = " + CouponTemplateDO.TOTAL_COUNT_MAX + ")"); // 4. 剩余数量大于 0或者无限领取
}
return canTakeConsumer;
}

View File

@ -285,8 +285,8 @@ public class CouponServiceImpl implements CouponService {
// 校验剩余发放数量是否充足仅在 CouponTakeTypeEnum.USER 用户领取时
// 关联案例https://t.zsxq.com/mElGQhttps://t.zsxq.com/6pLzr
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
&& ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 校验不限制领取数
&& couponTemplate.getTakeCount() > couponTemplate.getTotalCount()) { // 已领取数量 >= 总发放数量
&& !couponTemplateService.isTotalCountUnlimited(couponTemplate.getTotalCount()) // 校验不限制总发放数量
&& couponTemplate.getTakeCount() > couponTemplate.getTotalCount()) { // 已领取数量 > 总发放数量
throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
}
// 校验"固定日期"的有效期类型是否过期
@ -304,7 +304,7 @@ public class CouponServiceImpl implements CouponService {
* @param couponTemplate 优惠劵模版
*/
private void removeTakeLimitUser(Set<Long> userIds, CouponTemplateDO couponTemplate) {
if (couponTemplate.getTakeLimitCount() <= 0) {
if (couponTemplateService.isTakeLimitCountUnlimited(couponTemplate.getTakeLimitCount())) {
return;
}
// 查询已领过券的用户
@ -360,7 +360,8 @@ public class CouponServiceImpl implements CouponService {
}
// 2.1 过滤领取数量无限制的
Set<Long> templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1);
Set<Long> templateIds = convertSet(templates, CouponTemplateDO::getId,
template -> !couponTemplateService.isTakeLimitCountUnlimited(template.getTakeLimitCount()));
// 2.2 检查用户领取的数量是否超过限制
if (CollUtil.isNotEmpty(templateIds)) {
Map<Long, Integer> couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId);

View File

@ -18,6 +18,23 @@ import java.util.List;
*/
public interface CouponTemplateService {
/**
* 判断是否不限制每人领取数量
*
* @param takeLimitCount 每人限领个数
* @return 是否不限制
*/
boolean isTakeLimitCountUnlimited(Integer takeLimitCount);
/**
* 判断是否不限制总发放数量
*
* @param totalCount 发放数量
* @return 是否不限制
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean isTotalCountUnlimited(Integer totalCount);
/**
* 创建优惠劵模板
*

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.promotion.service.coupon;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi;
@ -41,6 +40,16 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
@Resource
private ProductSpuApi productSpuApi;
@Override
public boolean isTakeLimitCountUnlimited(Integer takeLimitCount) {
return CouponTemplateDO.TAKE_LIMIT_COUNT_MAX.equals(takeLimitCount);
}
@Override
public boolean isTotalCountUnlimited(Integer totalCount) {
return CouponTemplateDO.TOTAL_COUNT_MAX.equals(totalCount);
}
@Override
public Long createCouponTemplate(CouponTemplateCreateReqVO createReqVO) {
// 校验商品范围
@ -59,7 +68,7 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId());
// 校验发放数量不能过小仅在 CouponTakeTypeEnum.USER 用户领取时
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
&& ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制
&& !isTotalCountUnlimited(updateReqVO.getTotalCount()) // 非不限制总发放数量
&& updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) {
throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount());
}

View File

@ -71,13 +71,7 @@
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.35.79.ALL</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
</exclusions>
<version>4.40.607.ALL</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>

View File

@ -130,4 +130,18 @@ public class PayTransferCreateReqDTO {
return channelExtras;
}
// ========== 支付宝场景 ==========
/**
* 支付宝构建转账渠道额外参数
*
* @param sceneName 转账场景名称用于描述转账用途
* @return channelExtras
*/
public static Map<String, String> buildAlipayChannelExtra(String sceneName) {
Map<String, String> channelExtras = new HashMap<>();
channelExtras.put("sceneName", sceneName);
return channelExtras;
}
}

View File

@ -234,6 +234,10 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
if (reqDTO.getChannelExtras() != null) {
model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
}
String sceneName = MapUtil.getStr(reqDTO.getChannelExtras(), "sceneName");
if (StrUtil.isNotBlank(sceneName)) {
model.setTransferSceneName(sceneName);
}
// 个性化的参数
Participant payeeInfo = new Participant();
payeeInfo.setIdentityType("ALIPAY_LOGON_ID"); // 暂时只考虑转账到支付宝银行没有权限 https://opendocs.alipay.com/open/02byvc?scene=66dd06f5a923403393b85de68d3c0055

View File

@ -61,6 +61,7 @@ public class OperateLogController {
@Operation(summary = "导出操作日志")
@GetMapping("/export-excel")
@PreAuthorize("@ss.hasPermission('system:operate-log:export')")
@TransMethodResult
@ApiAccessLog(operateType = EXPORT)
public void exportOperateLog(HttpServletResponse response, @Valid OperateLogPageReqVO exportReqVO) throws IOException {
exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);

View File

@ -5,8 +5,10 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSmsSendReqVO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSocialLoginReqVO;
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;
@ -26,8 +28,6 @@ public interface AuthConvert {
AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class);
AuthLoginRespVO convert(OAuth2AccessTokenDO bean);
default AuthPermissionInfoRespVO convert(AdminUserDO user, List<RoleDO> roleList, List<MenuDO> menuList) {
return AuthPermissionInfoRespVO.builder()
.user(BeanUtils.toBean(user, AuthPermissionInfoRespVO.UserVO.class))

View File

@ -91,10 +91,10 @@ public class AliyunSmsClient extends AbstractSmsClient {
@Override
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
// 1. 执行请求
// 参考链接 https://api.aliyun.com/document/Dysmsapi/2017-05-25/QuerySmsTemplate
// 参考链接 https://api.aliyun.com/document/Dysmsapi/2017-05-25/GetSmsTemplate
TreeMap<String, Object> queryParam = new TreeMap<>();
queryParam.put("TemplateCode", apiTemplateId);
JSONObject response = request("QuerySmsTemplate", queryParam);
JSONObject response = request("GetSmsTemplate", queryParam);
// 2.1 请求失败
String code = response.getStr("Code");

View File

@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
@ -215,13 +216,13 @@ public class AdminAuthServiceImpl implements AdminAuthService {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);
// 构建返回结果
return AuthConvert.INSTANCE.convert(accessTokenDO);
return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
}
@Override
public AuthLoginRespVO refreshToken(String refreshToken) {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);
return AuthConvert.INSTANCE.convert(accessTokenDO);
return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
}
@Override

View File

@ -180,7 +180,13 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
.setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes())
.setRefreshToken(refreshTokenDO.getRefreshToken())
.setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds()));
accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号避免缓存到 Redis 的时候无对应的租户编号
// 优先从 refreshToken 获取租户编号避免 ThreadLocal 被污染时导致 tenantId null
// 可能关联的 issuehttps://t.zsxq.com/JIi5G
Long tenantId = refreshTokenDO.getTenantId();
if (tenantId == null) {
tenantId = TenantContextHolder.getTenantId();
}
accessTokenDO.setTenantId(tenantId);
oauth2AccessTokenMapper.insert(accessTokenDO);
// 记录到 Redis
oauth2AccessTokenRedisDAO.set(accessTokenDO);