Merge branch 'master-jdk17' into feature-project

# Conflicts:
#	README.md
#	yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/BeanUtils.java
#	yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java
#	yudao-server/src/main/resources/application-dev.yaml
#	yudao-server/src/main/resources/application-local.yaml
This commit is contained in:
2024-10-08 09:24:42 +08:00
738 changed files with 21737 additions and 9166 deletions

View File

@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.infra.api.config;
/**
* 参数配置 API 接口
*
* @author 芋道源码
*/
public interface ConfigApi {
/**
* 根据参数键查询参数值
*
* @param key 参数键
* @return 参数值
*/
String getConfigValueByKey(String key);
}

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.api.logger;
import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
import jakarta.validation.Valid;
import org.springframework.scheduling.annotation.Async;
/**
* API 访问日志的 API 接口
@ -18,4 +19,14 @@ public interface ApiAccessLogApi {
*/
void createApiAccessLog(@Valid ApiAccessLogCreateReqDTO createDTO);
/**
* 【异步】创建 API 访问日志
*
* @param createDTO 访问日志 DTO
*/
@Async
default void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) {
createApiAccessLog(createDTO);
}
}

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.api.logger;
import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
import jakarta.validation.Valid;
import org.springframework.scheduling.annotation.Async;
/**
* API 错误日志的 API 接口
@ -18,4 +19,14 @@ public interface ApiErrorLogApi {
*/
void createApiErrorLog(@Valid ApiErrorLogCreateReqDTO createDTO);
/**
* 【异步】创建 API 异常日志
*
* @param createDTO 异常日志 DTO
*/
@Async
default void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) {
createApiErrorLog(createDTO);
}
}

View File

@ -22,7 +22,7 @@ public interface ErrorCodeConstants {
ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1_001_001_003, "定时任务已经处于该状态,无需修改");
ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1_001_001_004, "只有开启状态的任务,才可以修改");
ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1_001_001_005, "CRON 表达式不正确");
ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在");
ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在,注意 Bean 默认首字母小写");
ErrorCode JOB_HANDLER_BEAN_TYPE_ERROR = new ErrorCode(1_001_001_007, "定时任务的处理器 Bean 类型不正确,未实现 JobHandler 接口");
// ========== API 错误日志 1-001-002-000 ==========

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.infra.api.config;
import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO;
import cn.iocoder.yudao.module.infra.service.config.ConfigService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
/**
* 参数配置 API 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class ConfigApiImpl implements ConfigApi {
@Resource
private ConfigService configService;
@Override
public String getConfigValueByKey(String key) {
ConfigDO config = configService.getConfigByKey(key);
return config != null ? config.getValue() : null;
}
}

View File

@ -94,7 +94,7 @@ public class ConfigController {
@Operation(summary = "导出参数配置")
@PreAuthorize("@ss.hasPermission('infra:config:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportConfig(@Valid ConfigPageReqVO exportReqVO,
public void exportConfig(ConfigPageReqVO exportReqVO,
HttpServletResponse response) throws IOException {
exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<ConfigDO> list = configService.getConfigPage(exportReqVO).getList();

View File

@ -2,19 +2,20 @@ package cn.iocoder.yudao.module.infra.controller.app.file;
import cn.hutool.core.io.IoUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
import cn.iocoder.yudao.module.infra.controller.app.file.vo.AppFileUploadReqVO;
import cn.iocoder.yudao.module.infra.service.file.FileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "用户 App - 文件存储")
@ -29,10 +30,25 @@ public class AppFileController {
@PostMapping("/upload")
@Operation(summary = "上传文件")
@PermitAll
public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile();
String path = uploadReqVO.getPath();
return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));
}
@GetMapping("/presigned-url")
@Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
@PermitAll
public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception {
return success(fileService.getFilePresignedUrl(path));
}
@PostMapping("/create")
@Operation(summary = "创建文件", description = "模式二:前端上传文件:配合 presigned-url 接口,记录上传了上传的文件")
@PermitAll
public CommonResult<Long> createFile(@Valid @RequestBody FileCreateReqVO createReqVO) {
return success(fileService.createFile(createReqVO));
}
}

View File

@ -13,7 +13,7 @@ public interface CodegenColumnMapper extends BaseMapperX<CodegenColumnDO> {
default List<CodegenColumnDO> selectListByTableId(Long tableId) {
return selectList(new LambdaQueryWrapperX<CodegenColumnDO>()
.eq(CodegenColumnDO::getTableId, tableId)
.orderByAsc(CodegenColumnDO::getId));
.orderByAsc(CodegenColumnDO::getOrdinalPosition));
}
default void deleteListByTableId(Long tableId) {

View File

@ -14,7 +14,6 @@ public enum CodegenFrontTypeEnum {
VUE2(10), // Vue2 Element UI 标准模版
VUE3(20), // Vue3 Element Plus 标准模版
VUE3_SCHEMA(21), // Vue3 Element Plus Schema 模版
VUE3_VBEN(30), // Vue3 VBEN 模版
;

View File

@ -34,4 +34,10 @@ public class CodegenProperties {
@NotNull(message = "代码生成的前端类型不能为空")
private Integer frontType;
/**
* 是否生成单元测试
*/
@NotNull(message = "是否生成单元测试不能为空")
private Boolean unitTestEnable;
}

View File

@ -37,6 +37,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
.region(buildRegion()) // Region
.credentials(config.getAccessKey(), config.getAccessSecret()) // 认证密钥
.build();
enableVirtualStyleEndpoint();
}
/**
@ -86,6 +87,17 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
return null;
}
/**
* 开启 VirtualStyle 模式
*/
private void enableVirtualStyleEndpoint() {
if (StrUtil.containsAny(config.getEndpoint(),
S3FileClientConfig.ENDPOINT_TENCENT, // 腾讯云 https://cloud.tencent.com/document/product/436/41284
S3FileClientConfig.ENDPOINT_VOLCES)) { // 火山云 https://www.volcengine.com/docs/6349/1288493
client.enableVirtualStyleEndpoint();
}
}
@Override
public String upload(byte[] content, String path, String type) throws Exception {
// 执行上传

View File

@ -20,6 +20,7 @@ public class S3FileClientConfig implements FileClientConfig {
public static final String ENDPOINT_QINIU = "qiniucs.com";
public static final String ENDPOINT_ALIYUN = "aliyuncs.com";
public static final String ENDPOINT_TENCENT = "myqcloud.com";
public static final String ENDPOINT_VOLCES = "volces.com"; // 火山云(字节)
/**
* 节点地址

View File

@ -22,13 +22,14 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import com.baomidou.mybatisplus.generator.config.po.TableField;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
@ -179,11 +180,18 @@ public class CodegenServiceImpl implements CodegenService {
&& tableField.getMetaInfo().isNullable() == codegenColumn.getNullable()
&& tableField.isKeyFlag() == codegenColumn.getPrimaryKey()
&& tableField.getComment().equals(codegenColumn.getColumnComment());
Set<String> modifyFieldNames = tableFields.stream()
.filter(tableField -> codegenColumnDOMap.get(tableField.getColumnName()) != null
&& !primaryKeyPredicate.test(tableField, codegenColumnDOMap.get(tableField.getColumnName())))
.map(TableField::getColumnName)
.collect(Collectors.toSet());
Set<String> modifyFieldNames = IntStream.range(0, tableFields.size()).mapToObj(index -> {
TableField tableField = tableFields.get(index);
String columnName = tableField.getColumnName();
CodegenColumnDO codegenColumn = codegenColumnDOMap.get(columnName);
if (codegenColumn == null) {
return null;
}
if (!primaryKeyPredicate.test(tableField, codegenColumn) || codegenColumn.getOrdinalPosition() != index) {
return columnName;
}
return null;
}).filter(Objects::nonNull).collect(Collectors.toSet());
// 3.2 计算需要【删除】的字段
Set<String> tableFieldNames = convertSet(tableFields, TableField::getName);
Set<Long> deleteColumnIds = codegenColumns.stream()

View File

@ -135,15 +135,6 @@ public class CodegenEngine {
vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
.put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("api/api.ts"),
vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
// Vue3 Schema 模版
.put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/data.ts"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
.put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/index.vue"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/form.vue"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
.put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("api/api.ts"),
vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
// Vue3 vben 模版
.put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
@ -407,6 +398,11 @@ public class CodegenEngine {
Map<String, String> templates = new LinkedHashMap<>();
templates.putAll(SERVER_TEMPLATES);
templates.putAll(FRONT_TEMPLATES.row(frontType));
// 如果禁用单元测试,则移除对应的模版
if (Boolean.FALSE.equals(codegenProperties.getUnitTestEnable())) {
templates.remove(javaTemplatePath("test/serviceTest"));
templates.remove("codegen/sql/h2.vm");
}
return templates;
}
@ -496,10 +492,6 @@ public class CodegenEngine {
"src/" + path;
}
private static String vue3SchemaTemplatePath(String path) {
return "codegen/vue3_schema/" + path + ".vm";
}
private static String vue3VbenTemplatePath(String path) {
return "codegen/vue3_vben/" + path + ".vm";
}

View File

@ -58,6 +58,6 @@ public interface ConfigService {
* @param reqVO 分页条件
* @return 分页列表
*/
PageResult<ConfigDO> getConfigPage(@Valid ConfigPageReqVO reqVO);
PageResult<ConfigDO> getConfigPage(ConfigPageReqVO reqVO);
}

View File

@ -5,7 +5,6 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
@ -18,7 +17,6 @@ import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
@ -49,12 +47,11 @@ public class DatabaseTableServiceImpl implements DatabaseTableService {
// 获得数据源配置
DataSourceConfigDO config = dataSourceConfigService.getDataSourceConfig(dataSourceConfigId);
Assert.notNull(config, "数据源({}) 不存在!", dataSourceConfigId);
DbType dbType = JdbcUtils.getDbType(config.getUrl());
// 使用 MyBatis Plus Generator 解析表结构
DataSourceConfig.Builder dataSourceConfigBuilder = new DataSourceConfig.Builder(config.getUrl(), config.getUsername(),
config.getPassword());
if (Objects.equals(dbType, DbType.SQL_SERVER)) { // 特殊SQLServer jdbc 非标准,参见 https://github.com/baomidou/mybatis-plus/issues/5419
if (JdbcUtils.isSQLServer(config.getUrl())) { // 特殊SQLServer jdbc 非标准,参见 https://github.com/baomidou/mybatis-plus/issues/5419
dataSourceConfigBuilder.databaseQueryClass(SQLQuery.class);
}
StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder().enableSkipView(); // 忽略视图,业务上一般用不到

View File

@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@ -91,13 +92,15 @@ public class JobServiceImpl implements JobService {
}
private void validateJobHandlerExists(String handlerName) {
Object handler = SpringUtil.getBean(handlerName);
if (handler == null) {
try {
Object handler = SpringUtil.getBean(handlerName);
assert handler != null;
if (!(handler instanceof JobHandler)) {
throw exception(JOB_HANDLER_BEAN_TYPE_ERROR);
}
} catch (NoSuchBeanDefinitionException e) {
throw exception(JOB_HANDLER_BEAN_NOT_EXISTS);
}
if (!(handler instanceof JobHandler)) {
throw exception(JOB_HANDLER_BEAN_TYPE_ERROR);
}
}
@Override

View File

@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.infra.service.logger;
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.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO;
@ -35,7 +37,12 @@ public class ApiAccessLogServiceImpl implements ApiAccessLogService {
ApiAccessLogDO apiAccessLog = BeanUtils.toBean(createDTO, ApiAccessLogDO.class);
apiAccessLog.setRequestParams(StrUtil.maxLength(apiAccessLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH));
apiAccessLog.setResultMsg(StrUtil.maxLength(apiAccessLog.getResultMsg(), RESULT_MSG_MAX_LENGTH));
apiAccessLogMapper.insert(apiAccessLog);
if (TenantContextHolder.getTenantId() != null) {
apiAccessLogMapper.insert(apiAccessLog);
} else {
// 极端情况下,上下文中没有租户时,此时忽略租户上下文,避免插入失败!
TenantUtils.executeIgnore(() -> apiAccessLogMapper.insert(apiAccessLog));
}
}
@Override

View File

@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.infra.service.logger;
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.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO;
@ -25,9 +27,9 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.API_ERROR_L
*
* @author 芋道源码
*/
@Slf4j
@Service
@Validated
@Slf4j
public class ApiErrorLogServiceImpl implements ApiErrorLogService {
@Resource
@ -38,7 +40,12 @@ public class ApiErrorLogServiceImpl implements ApiErrorLogService {
ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class)
.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus());
apiErrorLog.setRequestParams(StrUtil.maxLength(apiErrorLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH));
apiErrorLogMapper.insert(apiErrorLog);
if (TenantContextHolder.getTenantId() != null) {
apiErrorLogMapper.insert(apiErrorLog);
} else {
// 极端情况下,上下文中没有租户时,此时忽略租户上下文,避免插入失败!
TenantUtils.executeIgnore(() -> apiErrorLogMapper.insert(apiErrorLog));
}
}
@Override

View File

@ -286,6 +286,7 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
// 校验存在
validate${subSimpleClassName}Exists(${subClassNameVar}.getId());
// 更新
${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下updateTime 不更新
${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar});
}

View File

@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS "${table.tableName.toLowerCase()}" (
"${column.columnName}" ${dataType} DEFAULT '',
#elseif (${column.columnName} == 'deleted')
"deleted" bit NOT NULL DEFAULT FALSE,
#elseif (${column.columnName} == 'tenantId')
#elseif (${column.columnName} == 'tenant_id')
"tenant_id" bigint NOT NULL DEFAULT 0,
#else
"${column.columnName.toLowerCase()}" ${dataType}#if (${column.nullable} == false) NOT NULL#end,

View File

@ -306,8 +306,8 @@ export default {
await this.#[[$modal]]#.confirm('是否确认导出所有${table.classComment}数据项?');
try {
this.exportLoading = true;
const res = await ${simpleClassName}Api.export${simpleClassName}Excel(this.queryParams);
this.#[[$]]#download.excel(res.data, '${table.classComment}.xls');
const data = await ${simpleClassName}Api.export${simpleClassName}Excel(this.queryParams);
this.#[[$]]#download.excel(data, '${table.classComment}.xls');
} catch {
} finally {
this.exportLoading = false;

View File

@ -64,12 +64,11 @@
<el-checkbox
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-checkbox>
:label="dict.label"
:value="dict.value"
/>
#else##没数据字典
<el-checkbox>请选择字典生成</el-checkbox>
<el-checkbox label="请选择字典生成" />
#end
</el-checkbox-group>
</el-form-item>
@ -85,7 +84,7 @@
{{ dict.label }}
</el-radio>
#else##没数据字典
<el-radio label="1">请选择字典生成</el-radio>
<el-radio value="1">请选择字典生成</el-radio>
#end
</el-radio-group>
</el-form-item>

View File

@ -92,12 +92,11 @@
<el-checkbox
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-checkbox>
:label="dict.label"
:value="dict.value"
/>
#else##没数据字典
<el-checkbox>请选择字典生成</el-checkbox>
<el-checkbox label="请选择字典生成" />
#end
</el-checkbox-group>
</el-form-item>
@ -117,7 +116,7 @@
{{ dict.label }}
</el-radio>
#else##没数据字典
<el-radio label="1">请选择字典生成</el-radio>
<el-radio value="1">请选择字典生成</el-radio>
#end
</el-radio-group>
</el-form-item>
@ -219,12 +218,11 @@
<el-checkbox
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-checkbox>
:label="dict.label"
:value="dict.value"
/>
#else##没数据字典
<el-checkbox>请选择字典生成</el-checkbox>
<el-checkbox label="请选择字典生成" />
#end
</el-checkbox-group>
</el-form-item>
@ -240,7 +238,7 @@
{{ dict.label }}
</el-radio>
#else##没数据字典
<el-radio label="1">请选择字典生成</el-radio>
<el-radio value="1">请选择字典生成</el-radio>
#end
</el-radio-group>
</el-form-item>

View File

@ -75,12 +75,11 @@
<el-checkbox
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-checkbox>
:label="dict.label"
:value="dict.value"
/>
#else##没数据字典
<el-checkbox>请选择字典生成</el-checkbox>
<el-checkbox label="请选择字典生成" />
#end
</el-checkbox-group>
</el-form-item>
@ -96,7 +95,7 @@
{{ dict.label }}
</el-radio>
#else##没数据字典
<el-radio label="1">请选择字典生成</el-radio>
<el-radio value="1">请选择字典生成</el-radio>
#end
</el-radio-group>
</el-form-item>

View File

@ -74,7 +74,7 @@
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
class="!w-220px"
/>
</el-form-item>
#end
@ -181,7 +181,7 @@
#end
#end
#end
<el-table-column label="操作" align="center">
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link

View File

@ -1,46 +0,0 @@
import request from '@/config/axios'
#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
export interface ${simpleClassName}VO {
#foreach ($column in $columns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}: number
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}: Date
#else
${column.javaField}: ${column.javaType.toLowerCase()}
#end
#end
#end
}
// 查询${table.classComment}列表
export const get${simpleClassName}Page = async (params) => {
return await request.get({ url: '${baseURL}/page', params })
}
// 查询${table.classComment}详情
export const get${simpleClassName} = async (id: number) => {
return await request.get({ url: '${baseURL}/get?id=' + id })
}
// 新增${table.classComment}
export const create${simpleClassName} = async (data: ${simpleClassName}VO) => {
return await request.post({ url: '${baseURL}/create', data })
}
// 修改${table.classComment}
export const update${simpleClassName} = async (data: ${simpleClassName}VO) => {
return await request.put({ url: '${baseURL}/update', data })
}
// 删除${table.classComment}
export const delete${simpleClassName} = async (id: number) => {
return await request.delete({ url: '${baseURL}/delete?id=' + id })
}
// 导出${table.classComment} Excel
export const export${simpleClassName}Api = async (params) => {
return await request.download({ url: '${baseURL}/export-excel', params })
}

View File

@ -1,124 +0,0 @@
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
import { dateFormatter } from '@/utils/formatTime'
// 表单校验
export const rules = reactive({
#foreach ($column in $columns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
#set($comment=$column.columnComment)
$column.javaField: [required],
#end
#end
})
// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
const crudSchemas = reactive<CrudSchema[]>([
#foreach($column in $columns)
#if ($column.listOperation || $column.listOperationResult || $column.createOperation || $column.updateOperation)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType)
{
label: '${column.columnComment}',
field: '${column.javaField}',
## ========= 字典部分 =========
#if ("" != $dictType)## 有数据字典
dictType: DICT_TYPE.$dictType.toUpperCase(),
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
dictClass: 'number',
#elseif ($javaType == "String")
dictClass: 'string',
#elseif ($javaType == "Boolean")
dictClass: 'boolean',
#end
#end
## ========= Table 表格部分 =========
#if (!$column.listOperationResult)
isTable: false,
#else
#if ($column.htmlType == "datetime")
formatter: dateFormatter,
#end
#end
## ========= Search 表格部分 =========
#if ($column.listOperation)
isSearch: true,
#if ($column.htmlType == "datetime")
search: {
component: 'DatePicker',
componentProps: {
valueFormat: 'YYYY-MM-DD HH:mm:ss',
type: 'daterange',
defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
}
},
#end
#end
## ========= Form 表单部分 =========
#if ((!$column.createOperation && !$column.updateOperation) || $column.primaryKey)
isForm: false,
#else
#if($column.htmlType == "imageUpload")## 图片上传
form: {
component: 'UploadImg'
},
#elseif($column.htmlType == "fileUpload")## 文件上传
form: {
component: 'UploadFile'
},
#elseif($column.htmlType == "editor")## 文本编辑器
form: {
component: 'Editor',
componentProps: {
valueHtml: '',
height: 200
}
},
#elseif($column.htmlType == "select")## 下拉框
form: {
component: 'SelectV2'
},
#elseif($column.htmlType == "checkbox")## 多选框
form: {
component: 'Checkbox'
},
#elseif($column.htmlType == "radio")## 单选框
form: {
component: 'Radio'
},
#elseif($column.htmlType == "datetime")## 时间框
form: {
component: 'DatePicker',
componentProps: {
type: 'datetime',
valueFormat: 'x'
}
},
#elseif($column.htmlType == "textarea")## 文本框
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
},
#elseif(${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer")## 文本框
form: {
component: 'InputNumber',
value: 0
},
#end
#end
},
#end
#end
{
label: '操作',
field: 'action',
isForm: false
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@ -1,65 +0,0 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<Form ref="formRef" :schema="allSchemas.formSchema" :rules="rules" v-loading="formLoading" />
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
import { rules, allSchemas } from './${classNameVar}.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
const data = await ${simpleClassName}Api.get${simpleClassName}(id)
formRef.value.setValues(data)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.getElFormRef().validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formRef.value.formModel as ${simpleClassName}Api.${simpleClassName}VO
if (formType.value === 'create') {
await ${simpleClassName}Api.create${simpleClassName}(data)
message.success(t('common.createSuccess'))
} else {
await ${simpleClassName}Api.update${simpleClassName}(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
</script>

View File

@ -1,85 +0,0 @@
<template>
<!-- 搜索工作栏 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams">
<!-- 新增等操作按钮 -->
<template #actionMore>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['${permissionPrefix}:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</template>
</Search>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<Table
:columns="allSchemas.tableColumns"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
>
<template #action="{ row }">
<el-button
link
type="primary"
@click="openForm('update', row.id)"
v-hasPermi="['${permissionPrefix}:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
v-hasPermi="['${permissionPrefix}:delete']"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</Table>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<${simpleClassName}Form ref="formRef" @success="getList" />
</template>
<script setup lang="ts" name="${table.className}">
import { allSchemas } from './${classNameVar}.data'
import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
import ${simpleClassName}Form from './${simpleClassName}Form.vue'
// tableObject表格的属性对象可获得分页大小、条数等属性
// tableMethods表格的操作对象可进行获得分页、删除记录等操作
// 详细可见https://doc.iocoder.cn/vue3/crud-schema/
const { tableObject, tableMethods } = useTable({
getListApi: ${simpleClassName}Api.get${simpleClassName}Page, // 分页接口
delListApi: ${simpleClassName}Api.delete${simpleClassName} // 删除接口
})
// 获得表格的各种操作
const { getList, setSearchParams } = tableMethods
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = (id: number) => {
tableMethods.delList(id, false)
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@ -42,9 +42,17 @@ export const searchFormSchema: FormSchema[] = [
#foreach($column in $columns)
#if ($column.listOperation)
#set ($dictType=$column.dictType)
#set ($javaType = $column.javaType)
#set ($javaField = $column.javaField)
#set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#set ($comment=$column.columnComment)
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
#set ($dictMethod = "number")
#elseif ($javaType == "String")
#set ($dictMethod = "string")
#elseif ($javaType == "Boolean")
#set ($dictMethod = "boolean")
#end
{
label: '${comment}',
field: '${javaField}',
@ -54,16 +62,16 @@ export const searchFormSchema: FormSchema[] = [
component: 'Select',
componentProps: {
#if ("" != $dictType)## 设置了 dictType 数据字典的情况
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase()),
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else## 未设置 dictType 数据字典的情况
options: [],
#end
},
#elseif ($column.htmlType == "radio")
component: 'Radio',
component: 'RadioButtonGroup',
componentProps: {
#if ("" != $dictType)## 设置了 dictType 数据字典的情况
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase()),
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else## 未设置 dictType 数据字典的情况
options: [],
#end
@ -87,9 +95,17 @@ export const createFormSchema: FormSchema[] = [
#foreach($column in $columns)
#if ($column.createOperation)
#set ($dictType = $column.dictType)
#set ($javaType = $column.javaType)
#set ($javaField = $column.javaField)
#set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#set ($comment = $column.columnComment)
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
#set ($dictMethod = "number")
#elseif ($javaType == "String")
#set ($dictMethod = "string")
#elseif ($javaType == "Boolean")
#set ($dictMethod = "boolean")
#end
#if (!$column.primaryKey)## 忽略主键,不用在表单里
{
label: '${comment}',
@ -117,7 +133,7 @@ export const createFormSchema: FormSchema[] = [
component: 'Select',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else##没数据字典
options:[],
#end
@ -126,7 +142,7 @@ export const createFormSchema: FormSchema[] = [
component: 'Checkbox',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else##没数据字典
options:[],
#end
@ -135,7 +151,7 @@ export const createFormSchema: FormSchema[] = [
component: 'RadioButtonGroup',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else##没数据字典
options:[],
#end
@ -166,9 +182,17 @@ export const updateFormSchema: FormSchema[] = [
#foreach($column in $columns)
#if ($column.updateOperation)
#set ($dictType = $column.dictType)
#set ($javaType = $column.javaType)
#set ($javaField = $column.javaField)
#set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#set ($comment = $column.columnComment)
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
#set ($dictMethod = "number")
#elseif ($javaType == "String")
#set ($dictMethod = "string")
#elseif ($javaType == "Boolean")
#set ($dictMethod = "boolean")
#end
#if (!$column.primaryKey)## 忽略主键,不用在表单里
{
label: '${comment}',
@ -196,7 +220,7 @@ export const updateFormSchema: FormSchema[] = [
component: 'Select',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else##没数据字典
options:[],
#end
@ -205,7 +229,7 @@ export const updateFormSchema: FormSchema[] = [
component: 'Checkbox',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else##没数据字典
options:[],
#end
@ -214,7 +238,7 @@ export const updateFormSchema: FormSchema[] = [
component: 'RadioButtonGroup',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else##没数据字典
options:[],
#end

View File

@ -4,7 +4,8 @@ import { columns, searchFormSchema } from './${classNameVar}.data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal'
import { useTable } from '@/components/Table'
import { IconEnum } from '@/enums/appEnum'
import { BasicTable, TableAction, useTable } from '@/components/Table'
import { delete${simpleClassName}, export${simpleClassName}, get${simpleClassName}Page } from '@/api/${table.moduleName}/${table.businessName}'
defineOptions({ name: '${table.className}' })

View File

@ -24,12 +24,11 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.baomidou.mybatisplus.generator.config.po.TableField;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import org.junit.jupiter.api.Disabled;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import jakarta.annotation.Resource;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -234,17 +233,16 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
}
@Test
@Disabled // TODO @芋艿:这个单测会随机性失败,需要定位下;
public void testSyncCodegenFromDB() {
// mock 数据CodegenTableDO
CodegenTableDO table = randomPojo(CodegenTableDO.class, o -> o.setTableName("t_yunai")
.setDataSourceConfigId(1L).setScene(CodegenSceneEnum.ADMIN.getScene()));
codegenTableMapper.insert(table);
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setColumnName("id"));
.setColumnName("id").setPrimaryKey(true).setOrdinalPosition(0));
codegenColumnMapper.insert(column01);
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setColumnName("name"));
.setColumnName("name").setOrdinalPosition(1));
codegenColumnMapper.insert(column02);
// 准备参数
Long tableId = table.getId();
@ -263,7 +261,7 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
when(databaseTableService.getTable(eq(1L), eq("t_yunai")))
.thenReturn(tableInfo);
// mock 方法CodegenTableDO
List<CodegenColumnDO> newColumns = randomPojoList(CodegenColumnDO.class);
List<CodegenColumnDO> newColumns = randomPojoList(CodegenColumnDO.class, 2);
when(codegenBuilder.buildColumns(eq(table.getId()), argThat(tableFields -> {
assertEquals(2, tableFields.size());
assertSame(tableInfo.getFields(), tableFields);
@ -457,9 +455,11 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType()));
codegenTableMapper.insert(table);
// mock 数据CodegenColumnDO
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId()));
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setOrdinalPosition(1));
codegenColumnMapper.insert(column01);
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId()));
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setOrdinalPosition(2));
codegenColumnMapper.insert(column02);
// mock 执行生成
Map<String, String> codes = MapUtil.of(randomString(), randomString());
@ -486,9 +486,11 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
.setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType()));
codegenTableMapper.insert(table);
// mock 数据CodegenColumnDO
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId()));
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setOrdinalPosition(1));
codegenColumnMapper.insert(column01);
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId()));
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setOrdinalPosition(2));
codegenColumnMapper.insert(column02);
// mock 数据sub CodegenTableDO
CodegenTableDO subTable = randomPojo(CodegenTableDO.class,

View File

@ -114,7 +114,8 @@ public class JobLogServiceImplTest extends BaseDbUnitTest {
assertEquals(1, count);
List<JobLogDO> logs = jobLogMapper.selectList();
assertEquals(1, logs.size());
assertEquals(log02, logs.get(0));
// TODO @芋艿createTime updateTime 被屏蔽,仅 win11 会复现,建议后续修复。
assertPojoEquals(log02, logs.get(0), "createTime", "updateTime");
}
@Test

View File

@ -91,7 +91,8 @@ public class ApiAccessLogServiceImplTest extends BaseDbUnitTest {
assertEquals(1, count);
List<ApiAccessLogDO> logs = apiAccessLogMapper.selectList();
assertEquals(1, logs.size());
assertEquals(log02, logs.get(0));
// TODO @芋艿createTime updateTime 被屏蔽,仅 win11 会复现,建议后续修复。
assertPojoEquals(log02, logs.get(0), "createTime", "updateTime");
}
@Test

View File

@ -157,7 +157,8 @@ public class ApiErrorLogServiceImplTest extends BaseDbUnitTest {
assertEquals(1, count);
List<ApiErrorLogDO> logs = apiErrorLogMapper.selectList();
assertEquals(1, logs.size());
assertEquals(log02, logs.get(0));
// TODO @芋艿createTime updateTime 被屏蔽,仅 win11 会复现,建议后续修复。
assertPojoEquals(log02, logs.get(0), "createTime", "updateTime");
}
}

View File

@ -19,6 +19,7 @@ spring:
sql:
init:
schema-locations: classpath:/sql/create_tables.sql
encoding: UTF-8
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
data: