# Conflicts:
#	yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateService.java
#	yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java
This commit is contained in:
YunaiV
2025-12-28 10:22:38 +08:00
41 changed files with 2853 additions and 1396 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +1,58 @@
package cn.iocoder.yudao.framework.common.util.json.databind; package cn.iocoder.yudao.framework.common.util.json.databind;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.SerializerProvider;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* 基于时间戳的 LocalDateTime 序列化器 * 基于时间戳的 LocalDateTime 序列化器
* *
* @author 老五 * @author 老五
*/ */
@Slf4j
public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> { public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer(); public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
private static final Map<Class<?>, Map<String, Field>> FIELD_CACHE = new ConcurrentHashMap<>();
@Override @Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 情况一:有 JsonFormat 自定义注解则使用它。https://github.com/YunaiV/ruoyi-vue-pro/pull/1019 // 情况一:有 JsonFormat 自定义注解则使用它。https://github.com/YunaiV/ruoyi-vue-pro/pull/1019
String fieldName = gen.getOutputContext().getCurrentName(); String fieldName = gen.getOutputContext().getCurrentName();
if (fieldName != null) { if (fieldName != null) {
Class<?> clazz = gen.getOutputContext().getCurrentValue().getClass(); Object currentValue = gen.getOutputContext().getCurrentValue();
Field field = ReflectUtil.getField(clazz, fieldName); if (currentValue != null) {
JsonFormat[] jsonFormats = field.getAnnotationsByType(JsonFormat.class); Class<?> clazz = currentValue.getClass();
if (jsonFormats.length > 0) { Map<String, Field> fieldMap = FIELD_CACHE.computeIfAbsent(clazz, this::buildFieldMap);
String pattern = jsonFormats[0].pattern(); Field field = fieldMap.get(fieldName);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); // 进一步修复https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1480
gen.writeString(formatter.format(value)); if (field != null && field.isAnnotationPresent(JsonFormat.class)) {
return; JsonFormat jsonFormat = field.getAnnotation(JsonFormat.class);
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(jsonFormat.pattern());
gen.writeString(formatter.format(value));
return;
} catch (Exception ex) {
log.warn("[serialize][({}#{}) 使用 JsonFormat pattern 失败,尝试使用默认的 Long 时间戳]",
clazz.getName(), fieldName, ex);
}
}
} }
} }
@@ -41,4 +60,26 @@ public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTi
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
} }
/**
* 构建字段映射(缓存)
*
* @param clazz 类
* @return 字段映射
*/
private Map<String, Field> buildFieldMap(Class<?> clazz) {
Map<String, Field> fieldMap = new HashMap<>();
for (Field field : ReflectUtil.getFields(clazz)) {
String fieldName = field.getName();
JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class);
if (jsonProperty != null) {
String value = jsonProperty.value();
if (StrUtil.isNotEmpty(value) && ObjUtil.notEqual("\u0000", value)) {
fieldName = value;
}
}
fieldMap.put(fieldName, field);
}
return fieldMap;
}
} }

View File

@@ -100,7 +100,7 @@ public class BpmTaskController {
@GetMapping("manager-page") @GetMapping("manager-page")
@Operation(summary = "获取全部任务的分页", description = "用于【流程任务】菜单") @Operation(summary = "获取全部任务的分页", description = "用于【流程任务】菜单")
@PreAuthorize("@ss.hasPermission('bpm:task:mananger-query')") @PreAuthorize("@ss.hasPermission('bpm:task:manager-query')")
public CommonResult<PageResult<BpmTaskRespVO>> getTaskManagerPage(@Valid BpmTaskPageReqVO pageVO) { public CommonResult<PageResult<BpmTaskRespVO>> getTaskManagerPage(@Valid BpmTaskPageReqVO pageVO) {
PageResult<HistoricTaskInstance> pageResult = taskService.getTaskPage(getLoginUserId(), pageVO); PageResult<HistoricTaskInstance> pageResult = taskService.getTaskPage(getLoginUserId(), pageVO);
if (CollUtil.isEmpty(pageResult.getList())) { if (CollUtil.isEmpty(pageResult.getList())) {

View File

@@ -34,6 +34,12 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
public BpmParallelMultiInstanceBehavior(Activity activity, public BpmParallelMultiInstanceBehavior(Activity activity,
AbstractBpmnActivityBehavior innerActivityBehavior) { AbstractBpmnActivityBehavior innerActivityBehavior) {
super(activity, innerActivityBehavior); super(activity, innerActivityBehavior);
// 关联 Pull Requesthttps://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1483
// 在解析/构造阶段基于 activityId 初始化与 activity 绑定且不变的字段,避免在运行期修改 Behavior 实例状态
super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(activity.getId());
// 从 execution.getVariable() 读取当前所有任务处理的人的 key
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(activity.getId());
} }
/** /**

View File

@@ -30,6 +30,12 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
public BpmSequentialMultiInstanceBehavior(Activity activity, AbstractBpmnActivityBehavior innerActivityBehavior) { public BpmSequentialMultiInstanceBehavior(Activity activity, AbstractBpmnActivityBehavior innerActivityBehavior) {
super(activity, innerActivityBehavior); super(activity, innerActivityBehavior);
// 关联 Pull Requesthttps://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1483
// 在解析/构造阶段基于 activityId 初始化与 activity 绑定且不变的字段,避免在运行期修改 Behavior 实例状态
super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(activity.getId());
// 从 execution.getVariable() 读取当前所有任务处理的人的 key
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(activity.getId());
} }
/** /**
@@ -88,10 +94,6 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter); super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter);
return; return;
} }
// 参见 https://gitee.com/zhijiantianya/yudao-cloud/issues/IC239F
super.collectionExpression = null;
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter); super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter);
} }

View File

@@ -125,6 +125,7 @@ public class BpmTaskCandidateInvoker {
}); });
} }
@DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致找不到候选人
public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId, public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId,
Long startUserId, String processDefinitionId, Map<String, Object> processVariables) { Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
// 如果是 CallActivity 子流程,不进行计算候选人 // 如果是 CallActivity 子流程,不进行计算候选人

View File

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.FlowElement;
import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.DelegateExecution;
@@ -40,8 +41,9 @@ public class BpmCopyTaskDelegate implements JavaDelegate {
} }
// 2. 执行抄送 // 2. 执行抄送
FlowElement currentFlowElement = execution.getCurrentFlowElement(); FlowElement currentFlowElement = execution.getCurrentFlowElement();
processInstanceCopyService.createProcessInstanceCopy(userIds, null, execution.getProcessInstanceId(), FlowableUtils.execute(execution.getTenantId(), () ->
currentFlowElement.getId(), currentFlowElement.getName(), null); processInstanceCopyService.createProcessInstanceCopy(userIds, null, execution.getProcessInstanceId(),
currentFlowElement.getId(), currentFlowElement.getName(), null));
} }
} }

View File

@@ -23,6 +23,8 @@ public enum CodegenFrontTypeEnum {
VUE3_VBEN5_EP_SCHEMA(50), // Vue3 VBEN5 + EP + schema 模版 VUE3_VBEN5_EP_SCHEMA(50), // Vue3 VBEN5 + EP + schema 模版
VUE3_VBEN5_EP_GENERAL(51), // Vue3 VBEN5 + EP 标准模版 VUE3_VBEN5_EP_GENERAL(51), // Vue3 VBEN5 + EP 标准模版
VUE3_ADMIN_UNIAPP_WOT(60), // Vue3 Admin + Uniapp + WOT 标准模版
; ;
/** /**

View File

@@ -72,6 +72,14 @@ public class FileController {
return success(fileService.createFile(createReqVO)); return success(fileService.createFile(createReqVO));
} }
@GetMapping("/get")
@Operation(summary = "获得文件")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:file:query')")
public CommonResult<FileRespVO> getFile(@RequestParam("id") Long id) {
return success(BeanUtils.toBean(fileService.getFile(id), FileRespVO.class));
}
@DeleteMapping("/delete") @DeleteMapping("/delete")
@Operation(summary = "删除文件") @Operation(summary = "删除文件")
@Parameter(name = "id", description = "编号", required = true) @Parameter(name = "id", description = "编号", required = true)

View File

@@ -4,12 +4,14 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 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.CodegenCreateListReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; 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.CodegenTablePageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; 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.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; 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.CodegenColumnMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper; import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum; 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.framework.codegen.config.CodegenProperties;
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenBuilder; 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.codegen.inner.CodegenEngine;
import cn.iocoder.yudao.module.infra.service.db.DataSourceConfigService;
import cn.iocoder.yudao.module.infra.service.db.DatabaseTableService; 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.TableField;
import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
@@ -45,6 +49,8 @@ public class CodegenServiceImpl implements CodegenService {
@Resource @Resource
private DatabaseTableService databaseTableService; private DatabaseTableService databaseTableService;
@Resource
private DataSourceConfigService dataSourceConfigService;
@Resource @Resource
private CodegenTableMapper codegenTableMapper; 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 @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.CodegenTemplateTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenVOTypeEnum; import cn.iocoder.yudao.module.infra.enums.codegen.CodegenVOTypeEnum;
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties; 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.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableTable; import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
@@ -96,7 +97,7 @@ public class CodegenEngine {
.build(); .build();
/** /**
* 端的配置模版 * 端的配置模版
* *
* key1UI 模版的类型 {@link CodegenFrontTypeEnum#getType()} * key1UI 模版的类型 {@link CodegenFrontTypeEnum#getType()}
* key2模板在 resources 的地址 * key2模板在 resources 的地址
@@ -137,6 +138,16 @@ public class CodegenEngine {
vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("api/api.ts"), .put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("api/api.ts"),
vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("api/api.ts"),
vue3UniappFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/index.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("components/search-form.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/components/search-form.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/form/index.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/form/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail/index.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
// VUE3_VBEN2_ANTD_SCHEMA // VUE3_VBEN2_ANTD_SCHEMA
.put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"), .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"),
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
@@ -300,16 +311,17 @@ public class CodegenEngine {
/** /**
* 生成代码 * 生成代码
* *
* @param dbType 数据库类型
* @param table 表定义 * @param table 表定义
* @param columns table 的字段定义数组 * @param columns table 的字段定义数组
* @param subTables 子表数组,当且仅当主子表时使用 * @param subTables 子表数组,当且仅当主子表时使用
* @param subColumnsList subTables 的字段定义数组 * @param subColumnsList subTables 的字段定义数组
* @return 生成的代码key 是路径value 是对应代码 * @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) { List<CodegenTableDO> subTables, List<List<CodegenColumnDO>> subColumnsList) {
// 1.1 初始化 bindMap 上下文 // 1.1 初始化 bindMap 上下文
Map<String, Object> bindingMap = initBindingMap(table, columns, subTables, subColumnsList); Map<String, Object> bindingMap = initBindingMap(dbType, table, columns, subTables, subColumnsList);
// 1.2 获得模版 // 1.2 获得模版
Map<String, String> templates = getTemplates(table.getFrontType()); Map<String, String> templates = getTemplates(table.getFrontType());
@@ -387,8 +399,8 @@ public class CodegenEngine {
* @return 格式化后的代码 * @return 格式化后的代码
*/ */
private String prettyCode(String content, String vmPath) { private String prettyCode(String content, String vmPath) {
// Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(需要排除 vben5 // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(需要排除 vben5、vue3_admin_uniapp
if (!StrUtil.contains(vmPath, "vben5")) { if (!StrUtil.containsAny(vmPath, "vben5", "vue3_admin_uniapp")) {
content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }"); content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }");
} }
// Vue 界面:去除多的 dateFormatter只有一个的情况下说明没使用到 // Vue 界面:去除多的 dateFormatter只有一个的情况下说明没使用到
@@ -415,10 +427,11 @@ public class CodegenEngine {
return content; 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) { List<CodegenTableDO> subTables, List<List<CodegenColumnDO>> subColumnsList) {
// 创建 bindingMap // 创建 bindingMap
Map<String, Object> bindingMap = new HashMap<>(globalBindingMap); Map<String, Object> bindingMap = new HashMap<>(globalBindingMap);
bindingMap.put("dbType", dbType);
bindingMap.put("table", table); bindingMap.put("table", table);
bindingMap.put("columns", columns); bindingMap.put("columns", columns);
bindingMap.put("primaryColumn", CollectionUtils.findFirst(columns, CodegenColumnDO::getPrimaryKey)); // 主键字段 bindingMap.put("primaryColumn", CollectionUtils.findFirst(columns, CodegenColumnDO::getPrimaryKey)); // 主键字段
@@ -617,6 +630,15 @@ public class CodegenEngine {
"src/" + path; "src/" + path;
} }
private static String vue3AdminUniappTemplatePath(String path) {
return "codegen/vue3_admin_uniapp/" + path + ".vm";
}
private static String vue3UniappFilePath(String path) {
return "yudao-ui-${sceneEnum.basePackage}-uniapp/" + // 顶级目录
"src/" + path;
}
private static String vue3VbenFilePath(String path) { private static String vue3VbenFilePath(String path) {
return "yudao-ui-${sceneEnum.basePackage}-vben/" + // 顶级目录 return "yudao-ui-${sceneEnum.basePackage}-vben/" + // 顶级目录
"src/" + path; "src/" + path;

View File

@@ -62,6 +62,7 @@ public interface FileService {
* @return 编号 * @return 编号
*/ */
Long createFile(FileCreateReqVO createReqVO); Long createFile(FileCreateReqVO createReqVO);
FileDO getFile(Long id);
/** /**
* 删除文件 * 删除文件

View File

@@ -152,6 +152,11 @@ public class FileServiceImpl implements FileService {
return file.getId(); return file.getId();
} }
@Override
public FileDO getFile(Long id) {
return validateFileExists(id);
}
@Override @Override
public void deleteFile(Long id) throws Exception { public void deleteFile(Long id) throws Exception {
// 校验存在 // 校验存在

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 -- 菜单 SQL
INSERT INTO system_menu( INSERT INTO system_menu(
name, permission, type, sort, parent_id, name, permission, type, sort, parent_id,
@@ -9,12 +30,9 @@ VALUES (
); );
-- 按钮父菜单ID -- 按钮父菜单ID
-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
SELECT @parentId := LAST_INSERT_ID(); SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL -- 按钮 SQL
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出'])
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export'])
#foreach ($functionName in $functionNames) #foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1) #set ($index = $foreach.count - 1)
INSERT INTO system_menu( INSERT INTO system_menu(
@@ -25,4 +43,139 @@ VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId, '${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId,
'', '', '', 0 '', '', '', 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

@@ -0,0 +1,297 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `#[[${]]#getNavbarHeight()#[[}]]#px` }">
#set ($hasDict = 0)
#foreach ($column in $columns)
#if ($hasDict == 0 && $column.listOperation && $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#end
#end
#foreach($column in $columns)
#if ($column.listOperation)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType)
#set ($listOperationCondition = $column.listOperationCondition)
#set ($comment = $column.columnComment)
#set ($dictMethod = "getDictOptions")
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
#set ($dictMethod = "getIntDictOptions")
#elseif ($javaType == "String")
#set ($dictMethod = "getStrDictOptions")
#elseif ($javaType == "Boolean")
#set ($dictMethod = "getBoolDictOptions")
#end
#if ($column.htmlType == "input")
<view class="yd-search-form-item">
<view class="yd-search-form-label">
${comment}
</view>
<wd-input
v-model="formData.${javaField}"
placeholder="请输入${comment}"
clearable
/>
</view>
#elseif ($column.htmlType == "datetime" && $listOperationCondition == "BETWEEN")
#set ($AttrName = $javaField.substring(0,1).toUpperCase() + ${javaField.substring(1)})
<view class="yd-search-form-item">
<view class="yd-search-form-label">
${comment}
</view>
<view class="yd-search-form-date-range-container">
<view @click="visible${AttrName}[0] = true">
<view class="yd-search-form-date-range-picker">
{{ formatDate(formData.${javaField}?.[0]) || '开始日期' }}
</view>
</view>
-
<view @click="visible${AttrName}[1] = true">
<view class="yd-search-form-date-range-picker">
{{ formatDate(formData.${javaField}?.[1]) || '结束日期' }}
</view>
</view>
</view>
<wd-datetime-picker-view v-if="visible${AttrName}[0]" v-model="temp${AttrName}[0]" type="date" />
<view v-if="visible${AttrName}[0]" class="yd-search-form-date-range-actions">
<wd-button size="small" plain @click="visible${AttrName}[0] = false">
取消
</wd-button>
<wd-button size="small" type="primary" @click="handle${AttrName}0Confirm">
确定
</wd-button>
</view>
<wd-datetime-picker-view v-if="visible${AttrName}[1]" v-model="temp${AttrName}[1]" type="date" />
<view v-if="visible${AttrName}[1]" class="yd-search-form-date-range-actions">
<wd-button size="small" plain @click="visible${AttrName}[1] = false">
取消
</wd-button>
<wd-button size="small" type="primary" @click="handle${AttrName}1Confirm">
确定
</wd-button>
</view>
</view>
#elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $dictType && "" != $dictType)
<view class="yd-search-form-item">
<view class="yd-search-form-label">
${comment}
</view>
<wd-radio-group v-model="formData.${javaField}" shape="button">
<wd-radio :value="-1">
全部
</wd-radio>
<wd-radio
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</view>
#else
<view class="yd-search-form-item">
<view class="yd-search-form-label">
${comment}
</view>
<wd-input
v-model="formData.${javaField}"
placeholder="请输入${comment}"
clearable
/>
</view>
#end
#end
#end
<view class="yd-search-form-actions">
<wd-button class="flex-1" plain @click="handleReset">
重置
</wd-button>
<wd-button class="flex-1" type="primary" @click="handleSearch">
搜索
</wd-button>
</view>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
#set ($hasDict = 0)
#set ($hasGetDictOptions = 0)
#set ($hasGetIntDictOptions = 0)
#set ($hasGetStrDictOptions = 0)
#set ($hasGetBoolDictOptions = 0)
#set ($hasDateTimeBetween = 0)
#foreach($column in $columns)
#if ($column.listOperation && $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#if ($column.htmlType == "select" || $column.htmlType == "radio")
#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
#set ($hasGetIntDictOptions = 1)
#elseif ($column.javaType == "String")
#set ($hasGetStrDictOptions = 1)
#elseif ($column.javaType == "Boolean")
#set ($hasGetBoolDictOptions = 1)
#else
#set ($hasGetDictOptions = 1)
#end
#end
#end
#if ($hasDateTimeBetween == 0 && $column.listOperation && $column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
#set ($hasDateTimeBetween = 1)
#end
#end
import { computed, reactive, ref } from 'vue'
import { getNavbarHeight } from '@/utils'
#if ($hasDateTimeBetween == 1)
import { formatDate, formatDateRange } from '@/utils/date'
#end
#if ($hasDict == 1)
#set ($dictImportNames = "getDictLabel, ")
#if ($hasGetDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getDictOptions, ")
#end
#if ($hasGetIntDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getIntDictOptions, ")
#end
#if ($hasGetStrDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getStrDictOptions, ")
#end
#if ($hasGetBoolDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getBoolDictOptions, ")
#end
#set ($dictImportNames = $dictImportNames.trim())
#set ($dictImportNames = $dictImportNames.substring(0, $dictImportNames.length() - 1))
import { $dictImportNames } from '@/hooks/useDict'
import { DICT_TYPE } from '@/utils/constants'
#end
const emit = defineEmits<{
search: [data: Record<string, any>]
reset: []
}>()
const visible = ref(false)
const formData = reactive({
#foreach($column in $columns)
#if ($column.listOperation)
#set ($javaType = $column.javaType.toLowerCase())
#if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
${column.javaField}: [undefined, undefined] as [number | undefined, number | undefined],
#elseif ($column.dictType && "" != $column.dictType)
#if(${javaType} == "string")
${column.javaField}: -1 as -1 | string, // -1 表示全部
#elseif(${javaType} == "boolean")
${column.javaField}: -1 as -1 | boolean, // -1 表示全部
#else
${column.javaField}: -1, // -1 表示全部
#end
#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
${column.javaField}: undefined as number | undefined,
#elseif(${javaType} == "boolean")
${column.javaField}: undefined as boolean | undefined,
#else
${column.javaField}: undefined as string | undefined,
#end
#end
#end
})
/** 搜索条件 placeholder 拼接 */
const placeholder = computed(() => {
const conditions: string[] = []
#foreach($column in $columns)
#if ($column.listOperation)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType.toLowerCase())
#set ($comment = $column.columnComment)
#if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
if (formData.${javaField}?.[0] && formData.${javaField}?.[1]) {
conditions.push(`${comment}:#[[${]]#formatDate(formData.${javaField}[0])#[[}]]#~#[[${]]#formatDate(formData.${javaField}[1])#[[}]]#`)
}
#elseif ($dictType && "" != $dictType)
if (formData.${javaField} !== -1) {
conditions.push(`${comment}:#[[${]]#getDictLabel(DICT_TYPE.${dictType.toUpperCase()}, formData.${javaField})#[[}]]#`)
}
#else
#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte" || ${javaType} == "boolean")
if (formData.${javaField} !== undefined) {
conditions.push(`${comment}:#[[${]]#formData.${javaField}#[[}]]#`)
}
#else
if (formData.${javaField}) {
conditions.push(`${comment}:#[[${]]#formData.${javaField}#[[}]]#`)
}
#end
#end
#end
#end
return conditions.length > 0 ? conditions.join(' | ') : '搜索${table.classComment}'
})
#if ($hasDateTimeBetween == 1)
// 时间范围选择器状态
#foreach($column in $columns)
#if ($column.listOperation && $column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
#set ($javaField = $column.javaField)
#set ($AttrName = $javaField.substring(0,1).toUpperCase() + ${javaField.substring(1)})
const visible${AttrName} = ref<[boolean, boolean]>([false, false])
const temp${AttrName} = ref<[number, number]>([Date.now(), Date.now()])
/** ${column.columnComment}[0]确认 */
function handle${AttrName}0Confirm() {
formData.${javaField} = [temp${AttrName}.value[0], formData.${javaField}?.[1]]
visible${AttrName}.value[0] = false
}
/** ${column.columnComment}[1]确认 */
function handle${AttrName}1Confirm() {
formData.${javaField} = [formData.${javaField}?.[0], temp${AttrName}.value[1]]
visible${AttrName}.value[1] = false
}
#end
#end
#end
/** 搜索 */
function handleSearch() {
visible.value = false
emit('search', {
...formData,
#foreach($column in $columns)
#if ($column.listOperation)
#if ($column.dictType && "" != $column.dictType)
${column.javaField}: formData.${column.javaField} === -1 ? undefined : formData.${column.javaField},
#elseif ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
${column.javaField}: formatDateRange(formData.${column.javaField}),
#end
#end
#end
})
}
/** 重置 */
function handleReset() {
#foreach($column in $columns)
#if ($column.listOperation)
#if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
formData.${column.javaField} = [undefined, undefined]
#elseif ($column.dictType && "" != $column.dictType)
formData.${column.javaField} = -1
#else
formData.${column.javaField} = undefined
#end
#end
#end
visible.value = false
emit('reset')
}
</script>

View File

@@ -0,0 +1,151 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="${table.classComment}详情"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 详情内容 -->
<view>
<wd-cell-group border>
#foreach($column in $columns)
#if ($column.primaryKey || $column.listOperationResult || $column.createOperation || $column.updateOperation)
#set ($javaField = $column.javaField)
#set ($comment = $column.columnComment)
#if ($column.dictType && "" != $column.dictType)
<wd-cell title="${comment}">
<dict-tag :type="DICT_TYPE.${column.dictType.toUpperCase()}" :value="formData?.${javaField}" />
</wd-cell>
#elseif ($column.javaType == "LocalDateTime")
<wd-cell title="${comment}" :value="formatDateTime(formData?.${javaField}) || '-'" />
#else
<wd-cell title="${comment}" :value="formData?.${javaField} ?? '-'" />
#end
#end
#end
</wd-cell-group>
</view>
<!-- 底部操作按钮 -->
<view class="yd-detail-footer">
<view class="yd-detail-footer-actions">
<wd-button
v-if="hasAccessByCodes(['${permissionPrefix}:update'])"
class="flex-1" type="warning" @click="handleEdit"
>
编辑
</wd-button>
<wd-button
v-if="hasAccessByCodes(['${permissionPrefix}:delete'])"
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
>
删除
</wd-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
#set ($hasDict = 0)
#foreach($column in $columns)
#if ($hasDict == 0 && $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#end
#end
#set ($hasDateTime = 0)
#foreach($column in $columns)
#if ($hasDateTime == 0 && $column.javaType == "LocalDateTime")
#set ($hasDateTime = 1)
#end
#end
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { delete${simpleClassName}, get${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
#if ($hasDict == 1)
import { DICT_TYPE } from '@/utils/constants'
#end
#if ($hasDateTime == 1)
import { formatDateTime } from '@/utils/date'
#end
const props = defineProps<{
id?: number | any
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const toast = useToast()
const formData = ref<${simpleClassName}>()
const deleting = ref(false)
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
}
/** 加载${table.classComment}详情 */
async function getDetail() {
if (!props.id) {
return
}
try {
toast.loading('加载中...')
formData.value = await get${simpleClassName}(props.id)
} finally {
toast.close()
}
}
/** 编辑${table.classComment} */
function handleEdit() {
uni.navigateTo({
url: `/pages-${table.moduleName}/${table.businessName}/form/index?id=#[[${]]#props.id#[[}]]#`,
})
}
/** 删除${table.classComment} */
function handleDelete() {
if (!props.id) {
return
}
uni.showModal({
title: '提示',
content: '确定要删除该${table.classComment}吗?',
success: async (res) => {
if (!res.confirm) {
return
}
deleting.value = true
try {
await delete${simpleClassName}(props.id)
toast.success('删除成功')
setTimeout(() => {
handleBack()
}, 500)
} finally {
deleting.value = false
}
},
})
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,253 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
:title="getTitle"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 表单区域 -->
<view>
<wd-form ref="formRef" :model="formData" :rules="formRules">
<wd-cell-group border>
#foreach($column in $columns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType)
#set ($comment = $column.columnComment)
#set ($dictMethod = "getDictOptions")
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
#set ($dictMethod = "getIntDictOptions")
#elseif ($javaType == "String")
#set ($dictMethod = "getStrDictOptions")
#elseif ($javaType == "Boolean")
#set ($dictMethod = "getBoolDictOptions")
#end
## 优先判断是否有字典,有字典则使用 radio-group
#if (($column.htmlType == "select" || $column.htmlType == "radio") && $dictType && "" != $dictType)
<wd-cell title="${comment}" title-width="180rpx" prop="${javaField}" center>
<wd-radio-group v-model="formData.${javaField}" shape="button">
<wd-radio
v-for="dict in $dictMethod(DICT_TYPE.${dictType.toUpperCase()})"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-cell>
## 数字类型(无字典)
#elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte")
<wd-cell title="${comment}" title-width="180rpx" prop="${javaField}" center>
<wd-input-number
v-model="formData.${javaField}"
:min="0"
/>
</wd-cell>
## 布尔类型
#elseif (${javaType.toLowerCase()} == "boolean")
<wd-cell title="${comment}" title-width="180rpx" prop="${javaField}" center>
<wd-switch v-model="formData.${javaField}" />
</wd-cell>
## 日期时间类型
#elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime")
#set ($pickerType = "date")
#if (${javaType.toLowerCase()} == "localdatetime")
#set ($pickerType = "datetime")
#end
<wd-datetime-picker
v-model="formData.${javaField}"
type="${pickerType}"
label="${comment}"
label-width="180rpx"
prop="${javaField}"
/>
## 文本域
#elseif ($column.htmlType == "textarea")
<wd-textarea
v-model="formData.${javaField}"
label="${comment}"
label-width="180rpx"
placeholder="请输入${comment}"
:maxlength="200"
show-word-limit
clearable
/>
## 默认:文本输入
#else
<wd-input
v-model="formData.${javaField}"
label="${comment}"
label-width="180rpx"
prop="${javaField}"
clearable
placeholder="请输入${comment}"
/>
#end
#end
#end
</wd-cell-group>
</wd-form>
</view>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button
type="primary"
block
:loading="formLoading"
@click="handleSubmit"
>
保存
</wd-button>
</view>
</view>
</template>
<script lang="ts" setup>
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
#set ($hasDict = 0)
#set ($hasGetDictOptions = 0)
#set ($hasGetIntDictOptions = 0)
#set ($hasGetStrDictOptions = 0)
#set ($hasGetBoolDictOptions = 0)
#foreach ($column in $columns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& ($column.htmlType == "select" || $column.htmlType == "radio")
&& $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
#set ($hasGetIntDictOptions = 1)
#elseif ($column.javaType == "String")
#set ($hasGetStrDictOptions = 1)
#elseif ($column.javaType == "Boolean")
#set ($hasGetBoolDictOptions = 1)
#else
#set ($hasGetDictOptions = 1)
#end
#end
#end
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { computed, onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
#if ($hasDict == 1)
#set ($dictImportNames = "")
#if ($hasGetDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getDictOptions, ")
#end
#if ($hasGetIntDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getIntDictOptions, ")
#end
#if ($hasGetStrDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getStrDictOptions, ")
#end
#if ($hasGetBoolDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getBoolDictOptions, ")
#end
#set ($dictImportNames = $dictImportNames.trim())
#set ($dictImportNames = $dictImportNames.substring(0, $dictImportNames.length() - 1))
import { $dictImportNames } from '@/hooks/useDict'
#end
import { navigateBackPlus } from '@/utils'
#if ($hasDict == 1)
import { DICT_TYPE } from '@/utils/constants'
#end
const props = defineProps<{
id?: ${primaryTsType} | any
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑${table.classComment}' : '新增${table.classComment}')
const formLoading = ref(false)
const formData = ref<${simpleClassName}>({
#foreach($column in $columns)
#if (($column.createOperation || $column.updateOperation) || $column.primaryKey)
#set ($javaType = $column.javaType.toLowerCase())
#set ($javaFieldLower = $column.javaField.toLowerCase())
#set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")
#if ($column.primaryKey)
${column.javaField}: undefined,
#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
${column.javaField}: 0,
#elseif(${javaType} == "boolean")
${column.javaField}: false,
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
${column.javaField}: undefined,
#else
${column.javaField}: '',
#end
#end
#end
})
const formRules = {
#foreach($column in $columns)
#set ($javaFieldLower = $column.javaField.toLowerCase())
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
&& $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
${column.javaField}: [{ required: true, message: '${column.columnComment}不能为空' }],
#end
#end
}
const formRef = ref()
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
}
/** 加载${table.classComment}详情 */
async function getDetail() {
if (!props.id) {
return
}
formData.value = await get${simpleClassName}(props.id)
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
formLoading.value = true
try {
if (props.id) {
await update${simpleClassName}(formData.value)
toast.success('修改成功')
} else {
await create${simpleClassName}(formData.value)
toast.success('新增成功')
}
setTimeout(() => {
handleBack()
}, 500)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,211 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="${table.classComment}管理"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 搜索组件 -->
<SearchForm @search="handleQuery" @reset="handleReset" />
<!-- ${table.classComment}列表 -->
<view class="p-24rpx">
<view
v-for="item in list"
:key="item.${primaryColumn.javaField}"
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
@click="handleDetail(item)"
>
<view class="p-24rpx">
#set ($titleField = "")
#set ($statusField = "")
#set ($statusDictType = "")
#foreach($column in $columns)
#if ($titleField == "" && !$column.primaryKey && $column.listOperationResult)
#set ($titleField = $column.javaField)
#set ($titleComment = $column.columnComment)
#end
#if ($statusField == "" && $column.listOperationResult && $column.dictType && "" != $column.dictType)
#set ($statusField = $column.javaField)
#set ($statusDictType = $column.dictType)
#end
#end
#if ($titleField == "")
#set ($titleField = $primaryColumn.javaField)
#end
<view class="mb-16rpx flex items-center justify-between">
<view class="text-32rpx text-[#333] font-semibold">
{{ item.${titleField} }}
</view>
#if($statusField != "")
<dict-tag :type="DICT_TYPE.${statusDictType.toUpperCase()}" :value="item.${statusField}" />
#end
</view>
#foreach($column in $columns)
#if ($column.listOperationResult && !$column.primaryKey && $column.javaField != $titleField && $column.javaField != $statusField)
#set ($javaField = $column.javaField)
#set ($comment = $column.columnComment)
#set ($dictType = $column.dictType)
#set ($javaType = $column.javaType)
#if ($dictType && "" != $dictType)
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">${comment}</text>
<dict-tag :type="DICT_TYPE.${dictType.toUpperCase()}" :value="item.${javaField}" />
</view>
#elseif ($javaType == "LocalDateTime")
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">${comment}</text>
<text class="line-clamp-1">{{ formatDateTime(item.${javaField}) || '-' }}</text>
</view>
#else
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">${comment}</text>
<text class="line-clamp-1">{{ item.${javaField} }}</text>
</view>
#end
#end
#end
</view>
</view>
<!-- 加载更多 -->
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
<wd-status-tip image="content" tip="暂无${table.classComment}数据" />
</view>
<wd-loadmore
v-if="list.length > 0"
:state="loadMoreState"
@reload="loadMore"
/>
</view>
<!-- 新增按钮 -->
<wd-fab
v-if="hasAccessByCodes(['${permissionPrefix}:create'])"
position="right-bottom"
type="primary"
:expandable="false"
@click="handleAdd"
/>
</view>
</template>
<script lang="ts" setup>
#set ($hasDict = 0)
#foreach($column in $columns)
#if ($hasDict == 0 && $column.listOperationResult && $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#end
#end
#set ($hasDateTime = 0)
#foreach($column in $columns)
#if ($column.listOperationResult)
#if ($hasDateTime == 0 && $column.javaType == "LocalDateTime")
#set ($hasDateTime = 1)
#end
#end
#end
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import type { LoadMoreState } from '@/http/types'
import { onReachBottom } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue'
import { get${simpleClassName}Page } from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
#if ($hasDict == 1)
import { DICT_TYPE } from '@/utils/constants'
#end
#if ($hasDateTime == 1)
import { formatDateTime } from '@/utils/date'
#end
import SearchForm from './components/search-form.vue'
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const total = ref(0)
const list = ref<${simpleClassName}[]>([])
const loadMoreState = ref<LoadMoreState>('loading')
const queryParams = ref({
pageNo: 1,
pageSize: 10,
})
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 查询${table.classComment}列表 */
async function getList() {
loadMoreState.value = 'loading'
try {
const data = await get${simpleClassName}Page(queryParams.value)
list.value = [...list.value, ...data.list]
total.value = data.total
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
} catch {
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
loadMoreState.value = 'error'
}
}
/** 搜索按钮操作 */
function handleQuery(data?: Record<string, any>) {
queryParams.value = {
...data,
pageNo: 1,
pageSize: queryParams.value.pageSize,
}
list.value = []
getList()
}
/** 重置按钮操作 */
function handleReset() {
handleQuery()
}
/** 加载更多 */
function loadMore() {
if (loadMoreState.value === 'finished') {
return
}
queryParams.value.pageNo++
getList()
}
/** 新增${table.classComment} */
function handleAdd() {
uni.navigateTo({
url: '/pages-${table.moduleName}/${table.businessName}/form/index',
})
}
/** 查看详情 */
function handleDetail(item: ${simpleClassName}) {
uni.navigateTo({
url: `/pages-${table.moduleName}/${table.businessName}/detail/index?id=#[[${]]#item.${primaryColumn.javaField}#[[}]]#`,
})
}
/** 触底加载更多 */
onReachBottom(() => {
loadMore()
})
/** 初始化 */
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
</style>

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.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.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; 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.CodegenColumnMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper; import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum; 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.framework.codegen.config.CodegenProperties;
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenBuilder; 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.codegen.inner.CodegenEngine;
import cn.iocoder.yudao.module.infra.service.db.DataSourceConfigService;
import cn.iocoder.yudao.module.infra.service.db.DatabaseTableService; 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.TableField;
import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -62,6 +65,8 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
@MockBean @MockBean
private DatabaseTableService databaseTableService; private DatabaseTableService databaseTableService;
@MockitoBean
private DataSourceConfigService dataSourceConfigService;
@MockBean @MockBean
private CodegenBuilder codegenBuilder; private CodegenBuilder codegenBuilder;
@@ -453,9 +458,12 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId()) CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setOrdinalPosition(2)); .setOrdinalPosition(2));
codegenColumnMapper.insert(column02); codegenColumnMapper.insert(column02);
// mock 数据DataSourceConfigDO
when(dataSourceConfigService.getDataSourceConfig(eq(table.getDataSourceConfigId())))
.thenReturn(randomPojo(DataSourceConfigDO.class, o -> o.setUrl("jdbc:mysql://")));
// mock 执行生成 // mock 执行生成
Map<String, String> codes = MapUtil.of(randomString(), randomString()); 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(2, columns.size());
assertEquals(column01, columns.get(0)); assertEquals(column01, columns.get(0));
assertEquals(column02, columns.get(1)); assertEquals(column02, columns.get(1));
@@ -494,9 +502,12 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
// mock 数据sub CodegenColumnDO // mock 数据sub CodegenColumnDO
CodegenColumnDO subColumn01 = randomPojo(CodegenColumnDO.class, o -> o.setId(1024L).setTableId(subTable.getId())); CodegenColumnDO subColumn01 = randomPojo(CodegenColumnDO.class, o -> o.setId(1024L).setTableId(subTable.getId()));
codegenColumnMapper.insert(subColumn01); codegenColumnMapper.insert(subColumn01);
// mock 数据DataSourceConfigDO
when(dataSourceConfigService.getDataSourceConfig(eq(table.getDataSourceConfigId())))
.thenReturn(randomPojo(DataSourceConfigDO.class, o -> o.setUrl("jdbc:mysql://")));
// mock 执行生成 // mock 执行生成
Map<String, String> codes = MapUtil.of(randomString(), randomString()); 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(2, columns.size());
assertEquals(column01, columns.get(0)); assertEquals(column01, columns.get(0));
assertEquals(column02, columns.get(1)); 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.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum; import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum; 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.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -28,7 +29,7 @@ public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
List<CodegenColumnDO> columns = getColumnList("student"); 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"); //writeResult(result, resourcesPath + "/vue2_one");
// 断言 // 断言
@@ -44,7 +45,7 @@ public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
List<CodegenColumnDO> columns = getColumnList("category"); 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"); //writeResult(result, resourcesPath + "/vue2_tree");
// 断言 // 断言
@@ -88,7 +89,7 @@ public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
List<CodegenColumnDO> teacherColumns = getColumnList("teacher"); 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)); Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
// 生成测试文件 // 生成测试文件
//writeResult(result, resourcesPath + path); //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.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum; import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum; 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.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -28,7 +29,7 @@ public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
List<CodegenColumnDO> columns = getColumnList("student"); 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"); //writeResult(result, resourcesPath + "/vue3_one");
// 断言 // 断言
@@ -44,7 +45,7 @@ public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
List<CodegenColumnDO> columns = getColumnList("category"); 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"); //writeResult(result, resourcesPath + "/vue3_tree");
// 断言 // 断言
@@ -88,7 +89,7 @@ public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
List<CodegenColumnDO> teacherColumns = getColumnList("teacher"); 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)); Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
// 生成测试文件 // 生成测试文件
//writeResult(result, resourcesPath + path); //writeResult(result, resourcesPath + path);

View File

@@ -76,7 +76,7 @@ public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
.in(CouponTemplateDO::getTakeType, canTakeTypes) // 2. 领取方式一致 .in(CouponTemplateDO::getTakeType, canTakeTypes) // 2. 领取方式一致
.and(ww -> ww.gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now()) // 3.1 未过期 .and(ww -> ww.gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now()) // 3.1 未过期
.or().eq(CouponTemplateDO::getValidityType, CouponTemplateValidityTypeEnum.TERM.getType())) // 3.2 领取之后 .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; return canTakeConsumer;
} }

View File

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

View File

@@ -7,7 +7,7 @@ import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.Cou
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
import javax.validation.Valid; import jakarta.validation.Valid;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -18,6 +18,23 @@ import java.util.List;
*/ */
public interface CouponTemplateService { 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; 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.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi; import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi;
@@ -41,6 +40,16 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
@Resource @Resource
private ProductSpuApi productSpuApi; 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 @Override
public Long createCouponTemplate(CouponTemplateCreateReqVO createReqVO) { public Long createCouponTemplate(CouponTemplateCreateReqVO createReqVO) {
// 校验商品范围 // 校验商品范围
@@ -59,7 +68,7 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId()); CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId());
// 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时)
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
&& ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制 && !isTotalCountUnlimited(updateReqVO.getTotalCount()) // 非不限制总发放数量
&& updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) { && updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) {
throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount()); throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount());
} }

View File

@@ -82,7 +82,17 @@ public class MemberUserController {
@PreAuthorize("@ss.hasPermission('member:user:query')") @PreAuthorize("@ss.hasPermission('member:user:query')")
public CommonResult<MemberUserRespVO> getUser(@RequestParam("id") Long id) { public CommonResult<MemberUserRespVO> getUser(@RequestParam("id") Long id) {
MemberUserDO user = memberUserService.getUser(id); MemberUserDO user = memberUserService.getUser(id);
return success(MemberUserConvert.INSTANCE.convert03(user)); if (user == null) {
return success(null);
}
MemberUserRespVO userVO = MemberUserConvert.INSTANCE.convert03(user);
if (user.getLevelId() != null) {
MemberLevelDO level = memberLevelService.getLevel(userVO.getId());
if (level != null) {
userVO.setLevelName(level.getName());
}
}
return success(userVO);
} }
@GetMapping("/page") @GetMapping("/page")

View File

@@ -36,6 +36,14 @@ public class LoginLogController {
@Resource @Resource
private LoginLogService loginLogService; private LoginLogService loginLogService;
@GetMapping("/get")
@Operation(summary = "获得登录日志")
@PreAuthorize("@ss.hasPermission('system:login-log:query')")
public CommonResult<LoginLogRespVO> getLoginLog(Long id) {
LoginLogDO loginLog = loginLogService.getLoginLog(id);
return success(BeanUtils.toBean(loginLog, LoginLogRespVO.class));
}
@GetMapping("/page") @GetMapping("/page")
@Operation(summary = "获得登录日志分页列表") @Operation(summary = "获得登录日志分页列表")
@PreAuthorize("@ss.hasPermission('system:login-log:query')") @PreAuthorize("@ss.hasPermission('system:login-log:query')")

View File

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

View File

@@ -1,8 +1,10 @@
package cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog; package cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
import com.fhs.core.trans.anno.Trans; import com.fhs.core.trans.anno.Trans;
import com.fhs.core.trans.constant.TransType; import com.fhs.core.trans.constant.TransType;
import com.fhs.core.trans.vo.VO; import com.fhs.core.trans.vo.VO;
@@ -31,6 +33,11 @@ public class OperateLogRespVO implements VO {
@ExcelProperty("操作人") @ExcelProperty("操作人")
private String userName; private String userName;
@Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1", implementation = Integer.class)
@ExcelProperty("用户类型")
@DictFormat(DictTypeConstants.USER_TYPE)
private Integer userType;
@Schema(description = "操作模块类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单") @Schema(description = "操作模块类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单")
@ExcelProperty("操作模块类型") @ExcelProperty("操作模块类型")
private String type; private String type;

View File

@@ -11,10 +11,12 @@ import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogRespVO;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsLogDO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsLogDO;
import cn.iocoder.yudao.module.system.service.sms.SmsLogService; import cn.iocoder.yudao.module.system.service.sms.SmsLogService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -44,6 +46,15 @@ public class SmsLogController {
return success(BeanUtils.toBean(pageResult, SmsLogRespVO.class)); return success(BeanUtils.toBean(pageResult, SmsLogRespVO.class));
} }
@GetMapping("/get")
@Operation(summary = "获得短信日志")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:sms-log:query')")
public CommonResult<SmsLogRespVO> getSmsLog(@RequestParam("id") Long id) {
SmsLogDO smsLog = smsLogService.getSmsLog(id);
return success(BeanUtils.toBean(smsLog, SmsLogRespVO.class));
}
@GetMapping("/export-excel") @GetMapping("/export-excel")
@Operation(summary = "导出短信日志 Excel") @Operation(summary = "导出短信日志 Excel")
@PreAuthorize("@ss.hasPermission('system:sms-log:export')") @PreAuthorize("@ss.hasPermission('system:sms-log:export')")

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

View File

@@ -91,10 +91,10 @@ public class AliyunSmsClient extends AbstractSmsClient {
@Override @Override
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
// 1. 执行请求 // 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<>(); TreeMap<String, Object> queryParam = new TreeMap<>();
queryParam.put("TemplateCode", apiTemplateId); queryParam.put("TemplateCode", apiTemplateId);
JSONObject response = request("QuerySmsTemplate", queryParam); JSONObject response = request("GetSmsTemplate", queryParam);
// 2.1 请求失败 // 2.1 请求失败
String code = response.getStr("Code"); 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.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; 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.servlet.ServletUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
@@ -216,13 +217,13 @@ public class AdminAuthServiceImpl implements AdminAuthService {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(), OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
OAuth2ClientConstants.CLIENT_ID_DEFAULT, null); OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);
// 构建返回结果 // 构建返回结果
return AuthConvert.INSTANCE.convert(accessTokenDO); return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
} }
@Override @Override
public AuthLoginRespVO refreshToken(String refreshToken) { public AuthLoginRespVO refreshToken(String refreshToken) {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT); OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);
return AuthConvert.INSTANCE.convert(accessTokenDO); return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
} }
@Override @Override

View File

@@ -12,6 +12,14 @@ import javax.validation.Valid;
*/ */
public interface LoginLogService { public interface LoginLogService {
/**
* 获得登录日志
*
* @param id 编号
* @return 登录日志
*/
LoginLogDO getLoginLog(Long id);
/** /**
* 获得登录日志分页 * 获得登录日志分页
* *

View File

@@ -21,6 +21,11 @@ public class LoginLogServiceImpl implements LoginLogService {
@Resource @Resource
private LoginLogMapper loginLogMapper; private LoginLogMapper loginLogMapper;
@Override
public LoginLogDO getLoginLog(Long id) {
return loginLogMapper.selectById(id);
}
@Override @Override
public PageResult<LoginLogDO> getLoginLogPage(LoginLogPageReqVO pageReqVO) { public PageResult<LoginLogDO> getLoginLogPage(LoginLogPageReqVO pageReqVO) {
return loginLogMapper.selectPage(pageReqVO); return loginLogMapper.selectPage(pageReqVO);

View File

@@ -11,16 +11,18 @@ import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO;
import cn.iocoder.yudao.module.system.dal.mysql.mail.MailTemplateMapper; import cn.iocoder.yudao.module.system.dal.mysql.mail.MailTemplateMapper;
import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants; import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource; import java.util.ArrayList;
import javax.validation.Valid;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -53,7 +55,7 @@ public class MailTemplateServiceImpl implements MailTemplateService {
// 插入 // 插入
MailTemplateDO template = BeanUtils.toBean(createReqVO, MailTemplateDO.class) MailTemplateDO template = BeanUtils.toBean(createReqVO, MailTemplateDO.class)
.setParams(parseTemplateContentParams(createReqVO.getContent())); .setParams(parseTemplateTitleAndContentParams(createReqVO.getTitle(), createReqVO.getContent()));
mailTemplateMapper.insert(template); mailTemplateMapper.insert(template);
return template.getId(); return template.getId();
} }
@@ -69,7 +71,7 @@ public class MailTemplateServiceImpl implements MailTemplateService {
// 更新 // 更新
MailTemplateDO updateObj = BeanUtils.toBean(updateReqVO, MailTemplateDO.class) MailTemplateDO updateObj = BeanUtils.toBean(updateReqVO, MailTemplateDO.class)
.setParams(parseTemplateContentParams(updateReqVO.getContent())); .setParams(parseTemplateTitleAndContentParams(updateReqVO.getTitle(), updateReqVO.getContent()));
mailTemplateMapper.updateById(updateObj); mailTemplateMapper.updateById(updateObj);
} }
@@ -129,7 +131,77 @@ public class MailTemplateServiceImpl implements MailTemplateService {
@Override @Override
public String formatMailTemplateContent(String content, Map<String, Object> params) { public String formatMailTemplateContent(String content, Map<String, Object> params) {
return StrUtil.format(content, params); // 1. 先替换模板变量
String formattedContent = StrUtil.format(content, params);
// 关联 Pull Requesthttps://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1461 讨论
// 2.1 反转义HTML特殊字符
formattedContent = unescapeHtml(formattedContent);
// 2.2 处理代码块(确保<pre><code>标签格式正确)
formattedContent = formatHtmlCodeBlocks(formattedContent);
// 2.3 将最外层的 pre 标签替换为 div 标签
formattedContent = replaceOuterPreWithDiv(formattedContent);
return formattedContent;
}
private String replaceOuterPreWithDiv(String content) {
if (StrUtil.isEmpty(content)) {
return content;
}
// 使用正则表达式匹配所有的 <pre> 标签,包括嵌套的 <code> 标签
String regex = "(?s)<pre[^>]*>(.*?)</pre>";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(content);
StringBuilder sb = new StringBuilder();
while (matcher.find()) {
// 提取 <pre> 标签内的内容
String innerContent = matcher.group(1);
// 返回 div 标签包裹的内容
matcher.appendReplacement(sb, "<div>" + innerContent + "</div>");
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 反转义 HTML 特殊字符
*
* @param input 输入字符串
* @return 反转义后的字符串
*/
private String unescapeHtml(String input) {
if (StrUtil.isEmpty(input)) {
return input;
}
return input
.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"")
.replace("&#39;", "'")
.replace("&nbsp;", " ");
}
/**
* 格式化 HTML 中的代码块
*
* @param content 邮件内容
* @return 格式化后的邮件内容
*/
private String formatHtmlCodeBlocks(String content) {
// 匹配 <pre><code> 标签的代码块
Pattern codeBlockPattern = Pattern.compile("<pre\\s*.*?><code\\s*.*?>(.*?)</code></pre>", Pattern.DOTALL);
Matcher matcher = codeBlockPattern.matcher(content);
StringBuilder sb = new StringBuilder();
while (matcher.find()) {
// 获取代码块内容
String codeBlock = matcher.group(1);
// 为代码块添加样式
String replacement = "<pre style=\"background-color: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto;\"><code>" + codeBlock + "</code></pre>";
matcher.appendReplacement(sb, replacement);
}
matcher.appendTail(sb);
return sb.toString();
} }
@Override @Override
@@ -137,14 +209,31 @@ public class MailTemplateServiceImpl implements MailTemplateService {
return mailTemplateMapper.selectCountByAccountId(accountId); return mailTemplateMapper.selectCountByAccountId(accountId);
} }
/**
* 解析标题和内容中的参数
*/
@VisibleForTesting
public List<String> parseTemplateTitleAndContentParams(String title, String content) {
List<String> titleParams = ReUtil.findAllGroup1(PATTERN_PARAMS, title);
List<String> contentParams = ReUtil.findAllGroup1(PATTERN_PARAMS, content);
// 合并参数并去重
List<String> allParams = new ArrayList<>(titleParams);
for (String param : contentParams) {
if (!allParams.contains(param)) {
allParams.add(param);
}
}
return allParams;
}
/** /**
* 获得邮件模板中的参数,形如 {key} * 获得邮件模板中的参数,形如 {key}
* *
* @param content 内容 * @param content 内容
* @return 参数列表 * @return 参数列表
*/ */
private List<String> parseTemplateContentParams(String content) { List<String> parseTemplateContentParams(String content) {
return ReUtil.findAllGroup1(PATTERN_PARAMS, content); return ReUtil.findAllGroup1(PATTERN_PARAMS, content);
} }
} }

View File

@@ -180,7 +180,13 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
.setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes()) .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes())
.setRefreshToken(refreshTokenDO.getRefreshToken()) .setRefreshToken(refreshTokenDO.getRefreshToken())
.setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds())); .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); oauth2AccessTokenMapper.insert(accessTokenDO);
// 记录到 Redis 中 // 记录到 Redis 中
oauth2AccessTokenRedisDAO.set(accessTokenDO); oauth2AccessTokenRedisDAO.set(accessTokenDO);

View File

@@ -255,6 +255,9 @@ public class MenuServiceImpl implements MenuService {
return; return;
} }
// 如果 id 为空,说明不用比较是否为相同 id 的菜单 // 如果 id 为空,说明不用比较是否为相同 id 的菜单
if (id == null) {
throw exception(MENU_NAME_DUPLICATE);
}
if (!menu.getId().equals(id)) { if (!menu.getId().equals(id)) {
throw exception(MENU_NAME_DUPLICATE); throw exception(MENU_NAME_DUPLICATE);
} }
@@ -277,7 +280,7 @@ public class MenuServiceImpl implements MenuService {
} }
// 如果 id 为空,说明不用比较是否为相同 id 的菜单 // 如果 id 为空,说明不用比较是否为相同 id 的菜单
if (id == null) { if (id == null) {
return; throw exception(MENU_COMPONENT_NAME_DUPLICATE);
} }
if (!menu.getId().equals(id)) { if (!menu.getId().equals(id)) {
throw exception(MENU_COMPONENT_NAME_DUPLICATE); throw exception(MENU_COMPONENT_NAME_DUPLICATE);

View File

@@ -58,6 +58,14 @@ public interface SmsLogService {
void updateSmsReceiveResult(Long id, String apiSerialNo, Boolean success, void updateSmsReceiveResult(Long id, String apiSerialNo, Boolean success,
LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg); LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg);
/**
* 获得短信日志
*
* @param id 日志编号
* @return 短信日志
*/
SmsLogDO getSmsLog(Long id);
/** /**
* 获得短信日志分页 * 获得短信日志分页
* *

View File

@@ -78,6 +78,11 @@ public class SmsLogServiceImpl implements SmsLogService {
.receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build()); .receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build());
} }
@Override
public SmsLogDO getSmsLog(Long id) {
return smsLogMapper.selectById(id);
}
@Override @Override
public PageResult<SmsLogDO> getSmsLogPage(SmsLogPageReqVO pageReqVO) { public PageResult<SmsLogDO> getSmsLogPage(SmsLogPageReqVO pageReqVO) {
return smsLogMapper.selectPage(pageReqVO); return smsLogMapper.selectPage(pageReqVO);