diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java index baefa50015..4e422feefd 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java @@ -1,39 +1,58 @@ 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.StrUtil; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; +import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.lang.reflect.Field; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * 基于时间戳的 LocalDateTime 序列化器 * * @author 老五 */ +@Slf4j public class TimestampLocalDateTimeSerializer extends JsonSerializer { public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer(); + private static final Map, Map> FIELD_CACHE = new ConcurrentHashMap<>(); + @Override public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { // 情况一:有 JsonFormat 自定义注解,则使用它。https://github.com/YunaiV/ruoyi-vue-pro/pull/1019 String fieldName = gen.getOutputContext().getCurrentName(); if (fieldName != null) { - Class clazz = gen.getOutputContext().getCurrentValue().getClass(); - Field field = ReflectUtil.getField(clazz, fieldName); - JsonFormat[] jsonFormats = field.getAnnotationsByType(JsonFormat.class); - if (jsonFormats.length > 0) { - String pattern = jsonFormats[0].pattern(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); - gen.writeString(formatter.format(value)); - return; + Object currentValue = gen.getOutputContext().getCurrentValue(); + if (currentValue != null) { + Class clazz = currentValue.getClass(); + Map fieldMap = FIELD_CACHE.computeIfAbsent(clazz, this::buildFieldMap); + Field field = fieldMap.get(fieldName); + // 进一步修复:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1480 + if (field != null && field.isAnnotationPresent(JsonFormat.class)) { + 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 buildFieldMap(Class clazz) { + Map 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; + } + } diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml index d295f72ff6..51e6825966 100644 --- a/yudao-module-ai/pom.xml +++ b/yudao-module-ai/pom.xml @@ -19,9 +19,9 @@ 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno - 1.1.0 + 1.1.2 - 1.1.0.0-RC1 + 1.1.0.0-RC2 1.2.6 diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java index 7b05690bb2..ff856314d6 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java @@ -100,7 +100,7 @@ public class BpmTaskController { @GetMapping("manager-page") @Operation(summary = "获取全部任务的分页", description = "用于【流程任务】菜单") - @PreAuthorize("@ss.hasPermission('bpm:task:mananger-query')") + @PreAuthorize("@ss.hasPermission('bpm:task:manager-query')") public CommonResult> getTaskManagerPage(@Valid BpmTaskPageReqVO pageVO) { PageResult pageResult = taskService.getTaskPage(getLoginUserId(), pageVO); if (CollUtil.isEmpty(pageResult.getList())) { diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java index 57f4d393f3..8d824d401b 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java @@ -34,6 +34,12 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav public BpmParallelMultiInstanceBehavior(Activity activity, AbstractBpmnActivityBehavior innerActivityBehavior) { super(activity, innerActivityBehavior); + // 关联 Pull Request:https://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()); } /** @@ -50,14 +56,7 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav protected int resolveNrOfInstances(DelegateExecution execution) { // 情况一:UserTask 节点 if (execution.getCurrentFlowElement() instanceof UserTask) { - // 第一步,设置 collectionVariable 和 CollectionVariable - // 从 execution.getVariable() 读取所有任务处理人的 key - super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 - super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); - // 从 execution.getVariable() 读取当前所有任务处理的人的 key - super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); - - // 第二步,获取任务的所有处理人 + // 获取任务的所有处理人 @SuppressWarnings("unchecked") Set assigneeUserIds = (Set) execution.getVariable(super.collectionVariable, Set.class); if (assigneeUserIds == null) { diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java index e48b1f95d5..75582a0541 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java @@ -30,6 +30,12 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB public BpmSequentialMultiInstanceBehavior(Activity activity, AbstractBpmnActivityBehavior innerActivityBehavior) { super(activity, innerActivityBehavior); + // 关联 Pull Request:https://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()); } /** @@ -41,14 +47,7 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB protected int resolveNrOfInstances(DelegateExecution execution) { // 情况一:UserTask 节点 if (execution.getCurrentFlowElement() instanceof UserTask) { - // 第一步,设置 collectionVariable 和 CollectionVariable - // 从 execution.getVariable() 读取所有任务处理人的 key - super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 - super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); - // 从 execution.getVariable() 读取当前所有任务处理的人的 key - super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); - - // 第二步,获取任务的所有处理人 + // 获取任务的所有处理人 // 不使用 execution.getVariable 原因:目前依次审批任务回退后 collectionVariable 变量没有清理, 如果重新进入该任务不会重新分配审批人 @SuppressWarnings("unchecked") Set assigneeUserIds = (Set) execution.getVariableLocal(super.collectionVariable, Set.class); @@ -88,10 +87,6 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter); 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); } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java index df8b0d5fd5..cf9f1b329a 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java @@ -125,6 +125,7 @@ public class BpmTaskCandidateInvoker { }); } + @DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致找不到候选人 public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, Long startUserId, String processDefinitionId, Map processVariables) { // 如果是 CallActivity 子流程,不进行计算候选人 diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java index 1160613f0d..62b4c81928 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener; 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.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService; import org.flowable.bpmn.model.FlowElement; import org.flowable.engine.delegate.DelegateExecution; @@ -40,8 +41,9 @@ public class BpmCopyTaskDelegate implements JavaDelegate { } // 2. 执行抄送 FlowElement currentFlowElement = execution.getCurrentFlowElement(); - processInstanceCopyService.createProcessInstanceCopy(userIds, null, execution.getProcessInstanceId(), - currentFlowElement.getId(), currentFlowElement.getName(), null); + FlowableUtils.execute(execution.getTenantId(), () -> + processInstanceCopyService.createProcessInstanceCopy(userIds, null, execution.getProcessInstanceId(), + currentFlowElement.getId(), currentFlowElement.getName(), null)); } } diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java index 8081aa04a3..e40cb68065 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java @@ -72,6 +72,14 @@ public class FileController { return success(fileService.createFile(createReqVO)); } + @GetMapping("/get") + @Operation(summary = "获得文件") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:file:query')") + public CommonResult getFile(@RequestParam("id") Long id) { + return success(BeanUtils.toBean(fileService.getFile(id), FileRespVO.class)); + } + @DeleteMapping("/delete") @Operation(summary = "删除文件") @Parameter(name = "id", description = "编号", required = true) diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java index 7f55507e42..b3c8116cd6 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java @@ -23,6 +23,8 @@ public enum CodegenFrontTypeEnum { VUE3_VBEN5_EP_SCHEMA(50), // Vue3 VBEN5 + EP + schema 模版 VUE3_VBEN5_EP_GENERAL(51), // Vue3 VBEN5 + EP 标准模版 + + VUE3_ADMIN_UNIAPP_WOT(60), // Vue3 Admin + Uniapp + WOT 标准模版 ; /** diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java index c4ce19ed64..9f2bcb2da2 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java @@ -96,7 +96,7 @@ public class CodegenEngine { .build(); /** - * 后端的配置模版 + * 前端的配置模版 * * key1:UI 模版的类型 {@link CodegenFrontTypeEnum#getType()} * key2:模板在 resources 的地址 @@ -137,6 +137,16 @@ public class CodegenEngine { vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) .put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("api/api.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 .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"), vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) @@ -387,8 +397,8 @@ public class CodegenEngine { * @return 格式化后的代码 */ private String prettyCode(String content, String vmPath) { - // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(需要排除 vben5) - if (!StrUtil.contains(vmPath, "vben5")) { + // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(需要排除 vben5、vue3_admin_uniapp) + if (!StrUtil.containsAny(vmPath, "vben5", "vue3_admin_uniapp")) { content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }"); } // Vue 界面:去除多的 dateFormatter,只有一个的情况下,说明没使用到 @@ -617,6 +627,15 @@ public class CodegenEngine { "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) { return "yudao-ui-${sceneEnum.basePackage}-vben/" + // 顶级目录 "src/" + path; diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java index 9e3f42680b..496df77df4 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java @@ -61,6 +61,7 @@ public interface FileService { * @return 编号 */ Long createFile(FileCreateReqVO createReqVO); + FileDO getFile(Long id); /** * 删除文件 diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java index 36965b70d9..ede9c83cfb 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java @@ -152,6 +152,11 @@ public class FileServiceImpl implements FileService { return file.getId(); } + @Override + public FileDO getFile(Long id) { + return validateFileExists(id); + } + @Override public void deleteFile(Long id) throws Exception { // 校验存在 diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/api/api.ts.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/api/api.ts.vm new file mode 100644 index 0000000000..2ae596e273 --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/api/api.ts.vm @@ -0,0 +1,56 @@ +import type { PageParam, PageResult } from '@/http/types' +import { http } from '@/http/http' + +#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 + +/** ${table.classComment}信息 */ +export interface ${simpleClassName} { +#foreach ($column in $columns) + #if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult) + #set ($javaType = $column.javaType.toLowerCase()) + #set ($javaFieldLower = $column.javaField.toLowerCase()) + #set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime") + #if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte") + ${column.javaField}#if($optional)?#end: number + #elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime") + ${column.javaField}#if($optional)?#end: Date + #elseif(${javaType} == "boolean") + ${column.javaField}#if($optional)?#end: boolean + #else + ${column.javaField}#if($optional)?#end: string + #end + #end +#end +} + +// TODO @AI:`/system/operate-log/get?id=${id}` 类似这种 +/** 获取${table.classComment}分页列表 */ +export function get${simpleClassName}Page(params: PageParam) { + return http.get>('/${table.moduleName}/${simpleClassName_strikeCase}/page', params) +} + +/** 获取${table.classComment}详情 */ +export function get${simpleClassName}(id: ${primaryTsType}) { + return http.get<${simpleClassName}>('/${table.moduleName}/${simpleClassName_strikeCase}/get?id=' + id) +} + +/** 创建${table.classComment} */ +export function create${simpleClassName}(data: ${simpleClassName}) { + return http.post('/${table.moduleName}/${simpleClassName_strikeCase}/create', data) +} + +/** 更新${table.classComment} */ +export function update${simpleClassName}(data: ${simpleClassName}) { + return http.put('/${table.moduleName}/${simpleClassName_strikeCase}/update', data) +} + +// TODO @AI:`/system/operate-log/get?id=${id}` 类似这种 +/** 删除${table.classComment} */ +export function delete${simpleClassName}(id: ${primaryTsType}) { + return http.delete('/${table.moduleName}/${simpleClassName_strikeCase}/delete?id=' + id) +} diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/search-form.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/search-form.vue.vm new file mode 100644 index 0000000000..194f172908 --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/search-form.vue.vm @@ -0,0 +1,297 @@ + + + + diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail/index.vue.vm new file mode 100644 index 0000000000..b3302e521d --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail/index.vue.vm @@ -0,0 +1,151 @@ + + + + + diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm new file mode 100644 index 0000000000..60c844381e --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm @@ -0,0 +1,253 @@ + + + + + diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/index.vue.vm new file mode 100644 index 0000000000..2d7daaba14 --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/index.vue.vm @@ -0,0 +1,211 @@ + + + + + diff --git a/yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java b/yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java index 6ff70e2e4c..0d6e23c827 100644 --- a/yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java +++ b/yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java @@ -82,7 +82,17 @@ public class MemberUserController { @PreAuthorize("@ss.hasPermission('member:user:query')") public CommonResult getUser(@RequestParam("id") Long 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") diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java index 808c465049..9f9c1995d0 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java @@ -85,7 +85,7 @@ public class SocialClientApiImpl implements SocialClientApi { // 2. 获得社交用户 SocialUserRespDTO socialUser = socialUserService.getSocialUserByUserId(reqDTO.getUserType(), reqDTO.getUserId(), SocialTypeEnum.WECHAT_MINI_PROGRAM.getType()); - if (StrUtil.isBlankIfStr(socialUser.getOpenid())) { + if (ObjUtil.isNull(socialUser) || StrUtil.isBlankIfStr(socialUser.getOpenid())) { log.warn("[sendWxaSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:会员 openid 缺失]", reqDTO); return; } diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/LoginLogController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/LoginLogController.java index 8fe089a742..ac51e48867 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/LoginLogController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/LoginLogController.java @@ -36,6 +36,14 @@ public class LoginLogController { @Resource private LoginLogService loginLogService; + @GetMapping("/get") + @Operation(summary = "获得登录日志") + @PreAuthorize("@ss.hasPermission('system:login-log:query')") + public CommonResult getLoginLog(Long id) { + LoginLogDO loginLog = loginLogService.getLoginLog(id); + return success(BeanUtils.toBean(loginLog, LoginLogRespVO.class)); + } + @GetMapping("/page") @Operation(summary = "获得登录日志分页列表") @PreAuthorize("@ss.hasPermission('system:login-log:query')") diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java index 8ed643c021..a6d04d82fe 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java @@ -1,8 +1,10 @@ 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.idev.excel.annotation.ExcelIgnoreUnannotated; 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.constant.TransType; import com.fhs.core.trans.vo.VO; @@ -31,6 +33,11 @@ public class OperateLogRespVO implements VO { @ExcelProperty("操作人") 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 = "订单") @ExcelProperty("操作模块类型") private String type; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsLogController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsLogController.java index 10594a8006..17e1653aac 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsLogController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsLogController.java @@ -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.service.sms.SmsLogService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; 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.RestController; @@ -44,6 +46,15 @@ public class SmsLogController { 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 getSmsLog(@RequestParam("id") Long id) { + SmsLogDO smsLog = smsLogService.getSmsLog(id); + return success(BeanUtils.toBean(smsLog, SmsLogRespVO.class)); + } + @GetMapping("/export-excel") @Operation(summary = "导出短信日志 Excel") @PreAuthorize("@ss.hasPermission('system:sms-log:export')") diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogService.java index 64cd07b74f..78bf55c4d1 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogService.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogService.java @@ -12,6 +12,14 @@ import javax.validation.Valid; */ public interface LoginLogService { + /** + * 获得登录日志 + * + * @param id 编号 + * @return 登录日志 + */ + LoginLogDO getLoginLog(Long id); + /** * 获得登录日志分页 * diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogServiceImpl.java index e1de978c8b..b299e3f77d 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/logger/LoginLogServiceImpl.java @@ -21,6 +21,11 @@ public class LoginLogServiceImpl implements LoginLogService { @Resource private LoginLogMapper loginLogMapper; + @Override + public LoginLogDO getLoginLog(Long id) { + return loginLogMapper.selectById(id); + } + @Override public PageResult getLoginLogPage(LoginLogPageReqVO pageReqVO) { return loginLogMapper.selectPage(pageReqVO); diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java index e53a2fc03a..2ee46dbf75 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java @@ -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.redis.RedisKeyConstants; import com.google.common.annotations.VisibleForTesting; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import javax.annotation.Resource; -import javax.validation.Valid; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; import java.util.regex.Pattern; 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) - .setParams(parseTemplateContentParams(createReqVO.getContent())); + .setParams(parseTemplateTitleAndContentParams(createReqVO.getTitle(), createReqVO.getContent())); mailTemplateMapper.insert(template); return template.getId(); } @@ -69,7 +71,7 @@ public class MailTemplateServiceImpl implements MailTemplateService { // 更新 MailTemplateDO updateObj = BeanUtils.toBean(updateReqVO, MailTemplateDO.class) - .setParams(parseTemplateContentParams(updateReqVO.getContent())); + .setParams(parseTemplateTitleAndContentParams(updateReqVO.getTitle(), updateReqVO.getContent())); mailTemplateMapper.updateById(updateObj); } @@ -129,7 +131,77 @@ public class MailTemplateServiceImpl implements MailTemplateService { @Override public String formatMailTemplateContent(String content, Map params) { - return StrUtil.format(content, params); + // 1. 先替换模板变量 + String formattedContent = StrUtil.format(content, params); + + // 关联 Pull Request:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1461 讨论 + // 2.1 反转义HTML特殊字符 + formattedContent = unescapeHtml(formattedContent); + // 2.2 处理代码块(确保
标签格式正确)
+        formattedContent = formatHtmlCodeBlocks(formattedContent);
+        // 2.3 将最外层的 pre 标签替换为 div 标签
+        formattedContent = replaceOuterPreWithDiv(formattedContent);
+        return formattedContent;
+    }
+
+    private String replaceOuterPreWithDiv(String content) {
+        if (StrUtil.isEmpty(content)) {
+            return content;
+        }
+        // 使用正则表达式匹配所有的 
 标签,包括嵌套的  标签
+        String regex = "(?s)]*>(.*?)
"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(content); + StringBuilder sb = new StringBuilder(); + while (matcher.find()) { + // 提取
 标签内的内容
+            String innerContent = matcher.group(1);
+            // 返回 div 标签包裹的内容
+            matcher.appendReplacement(sb, "
" + innerContent + "
"); + } + matcher.appendTail(sb); + return sb.toString(); + } + + /** + * 反转义 HTML 特殊字符 + * + * @param input 输入字符串 + * @return 反转义后的字符串 + */ + private String unescapeHtml(String input) { + if (StrUtil.isEmpty(input)) { + return input; + } + return input + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace(""", "\"") + .replace("'", "'") + .replace(" ", " "); + } + + /** + * 格式化 HTML 中的代码块 + * + * @param content 邮件内容 + * @return 格式化后的邮件内容 + */ + private String formatHtmlCodeBlocks(String content) { + // 匹配
 标签的代码块
+        Pattern codeBlockPattern = Pattern.compile("(.*?)
", Pattern.DOTALL); + Matcher matcher = codeBlockPattern.matcher(content); + StringBuilder sb = new StringBuilder(); + while (matcher.find()) { + // 获取代码块内容 + String codeBlock = matcher.group(1); + // 为代码块添加样式 + String replacement = "
" + codeBlock + "
"; + matcher.appendReplacement(sb, replacement); + } + matcher.appendTail(sb); + return sb.toString(); } @Override @@ -137,14 +209,31 @@ public class MailTemplateServiceImpl implements MailTemplateService { return mailTemplateMapper.selectCountByAccountId(accountId); } + /** + * 解析标题和内容中的参数 + */ + @VisibleForTesting + public List parseTemplateTitleAndContentParams(String title, String content) { + List titleParams = ReUtil.findAllGroup1(PATTERN_PARAMS, title); + List contentParams = ReUtil.findAllGroup1(PATTERN_PARAMS, content); + // 合并参数并去重 + List allParams = new ArrayList<>(titleParams); + for (String param : contentParams) { + if (!allParams.contains(param)) { + allParams.add(param); + } + } + return allParams; + } + /** * 获得邮件模板中的参数,形如 {key} * * @param content 内容 * @return 参数列表 */ - private List parseTemplateContentParams(String content) { + List parseTemplateContentParams(String content) { return ReUtil.findAllGroup1(PATTERN_PARAMS, content); } -} +} \ No newline at end of file diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java index bdc461639b..ad0c20e7cb 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java @@ -255,6 +255,9 @@ public class MenuServiceImpl implements MenuService { return; } // 如果 id 为空,说明不用比较是否为相同 id 的菜单 + if (id == null) { + throw exception(MENU_NAME_DUPLICATE); + } if (!menu.getId().equals(id)) { throw exception(MENU_NAME_DUPLICATE); } @@ -277,7 +280,7 @@ public class MenuServiceImpl implements MenuService { } // 如果 id 为空,说明不用比较是否为相同 id 的菜单 if (id == null) { - return; + throw exception(MENU_COMPONENT_NAME_DUPLICATE); } if (!menu.getId().equals(id)) { throw exception(MENU_COMPONENT_NAME_DUPLICATE); diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java index 49ec93aaca..d40a0e9fd6 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java @@ -58,6 +58,14 @@ public interface SmsLogService { void updateSmsReceiveResult(Long id, String apiSerialNo, Boolean success, LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg); + /** + * 获得短信日志 + * + * @param id 日志编号 + * @return 短信日志 + */ + SmsLogDO getSmsLog(Long id); + /** * 获得短信日志分页 * diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java index 92bf9c501c..139a3b9aa8 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java @@ -78,6 +78,11 @@ public class SmsLogServiceImpl implements SmsLogService { .receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build()); } + @Override + public SmsLogDO getSmsLog(Long id) { + return smsLogMapper.selectById(id); + } + @Override public PageResult getSmsLogPage(SmsLogPageReqVO pageReqVO) { return smsLogMapper.selectPage(pageReqVO); diff --git a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java index 7f76570bb7..d40671dd0c 100755 --- a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java +++ b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java @@ -196,6 +196,119 @@ public class MailTemplateServiceImplTest extends BaseDbUnitTest { mailTemplateService.formatMailTemplateContent("{name},你好,{what}吃了吗?", params)); } + @Test + public void testFormatMailTemplateContent_htmlUnescape() { + // 准备参数 + Map params = new HashMap<>(); + params.put("title", "测试标题"); + + // 测试HTML反转义 + String content = "

{title}

<p>这是一个测试</p>&nbsp;空格"; + String expected = "

测试标题

这是一个测试

空格"; + // 调用,并断言 + assertEquals(expected, + mailTemplateService.formatMailTemplateContent(content, params)); + } + + @Test + public void testFormatMailTemplateContent_codeBlockFormatting() { + // 准备参数 + Map params = new HashMap<>(); + params.put("name", "测试"); + + // 测试代码块格式化 + String content = "
public class Test {\n    public static void main(String[] args) {\n        System.out.println(\"Hello {name}\"));\n    }\n}
"; + + // 调用,并断言结果 + String result = mailTemplateService.formatMailTemplateContent(content, params); + // 断言 pre 标签被替换为 div 标签 + assertTrue(result.contains("
public class Test {")); + assertTrue(result.contains("System.out.println(\"Hello 测试\"")); + assertTrue(result.contains("
")); + } + + @Test + public void testFormatMailTemplateContent_preToDiv() { + // 准备参数 + Map params = new HashMap<>(); + params.put("content", "测试内容"); + + // 测试 pre 标签替换为 div 标签 + String content = "
{content}
"; + String result = mailTemplateService.formatMailTemplateContent(content, params); + // 断言结果中包含 div 标签,而不包含 pre 标签 + assertTrue(result.contains("
测试内容
")); + } + + @Test + public void testFormatMailTemplateContent_completeHtml() { + // 准备参数 + Map params = new HashMap<>(); + params.put("username", "testuser"); + params.put("company", "测试公司"); + + // 测试完整的 HTML 邮件模板 + String content = "\n \n \n \n \n Title\n \n \n
\n \n
\n
\n
\n
\n \n
\n
\n \n \n \n \n \n \n \n \n
\n
\n \n \n \n
\n
此邮件由系统发出,请勿直接回复或转发他人
\n

\n
\n \n \n \n \n \n \n \n \n
\n

\n 尊敬的 {username},\n

\n
\n
\n

\n 内容
\n 内容
\n 内容123

\n\n 如果您在使用过程中遇到任何问题或者有任何建议,都可以随时联系我们的客户团队,我们将竭诚为您服务。\n\n\n\n\n
\n
\n {company}
\n 地址:xxxxx
\n 邮箱:lambc77@163.com\n

\n
\n
\n
\n
\n
\n 声明:本邮件含有保密信息,仅限于收件人所用。禁止任何人未经发件人许可,以任何形式(包括但不限于部分的泄露、复制或散发)不当的使用本邮件中的信息。如果您错收了本邮件,请您立即电话或邮件通知发件人并删除本邮件,谢谢!\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n \n "; + + // 调用,并断言成功处理 + String result = mailTemplateService.formatMailTemplateContent(content, params); + // 断言结果中包含替换后的变量 + assertTrue(result.contains("尊敬的 testuser")); + assertTrue(result.contains("测试公司")); + // 断言结果是有效的 HTML + assertTrue(result.startsWith("")); + } + + @Test + public void testFormatMailTemplateContent_emptyContent() { + // 准备参数 + Map params = new HashMap<>(); + + // 测试空内容 + String result = mailTemplateService.formatMailTemplateContent("", params); + assertEquals("", result); + } + + @Test + public void testFormatMailTemplateContent_noParams() { + // 准备参数 + Map params = new HashMap<>(); + + // 测试没有参数需要替换的情况 + String content = "
System.out.println(\"Hello World\");
"; + String result = mailTemplateService.formatMailTemplateContent(content, params); + assertTrue(result.contains("
System.out.println(\"Hello World\");
")); + } + + @Test + public void testFormatMailTemplateContent_multiplePreTags() { + // 准备参数 + Map params = new HashMap<>(); + params.put("param1", "value1"); + params.put("param2", "value2"); + + // 测试多个 pre 标签的情况 + String content = "
First code block: {param1}
\n" + + "

Some text between code blocks

\n" + + "
Second code block: {param2}
"; + String result = mailTemplateService.formatMailTemplateContent(content, params); + // 断言两个pre标签都被替换为div标签 + assertTrue(result.contains("
First code block: value1
")); + assertTrue(result.contains("
Second code block: value2
")); + } + + @Test + public void testFormatMailTemplateContent_specialCharacters() { + // 准备参数 + Map params = new HashMap<>(); + + // 简化测试,只测试基本的 HTML 特殊字符 + String content = "<div>测试 & 特殊字符</div>"; + String result = mailTemplateService.formatMailTemplateContent(content, params); + // 断言特殊字符被正确反转义 + assertTrue(result.contains("
测试 & 特殊字符
")); + } + @Test public void testCountByAccountId() { // mock 数据 @@ -212,4 +325,60 @@ public class MailTemplateServiceImplTest extends BaseDbUnitTest { assertEquals(1, count); } + @Test + public void testDifferenceWithHtmlContent() { + // 准备包含 HTML 格式的模板内容 + String content = "
" + + "

Welcome, {username}!

" + + "

Your account has been created successfully.

" + + "
" + + "Account Details:
" + + "Username: {username}
" + + "Email: {email}
" + + "Role: {role}
" + + "
" + + "

Please click here to activate your account.

" + + "
public class WelcomeMessage {\n    public static void main(String[] args) {\n        System.out.println(\"Hello {username}!\");\n    }\n}
" + + "
"; + + Map params = new HashMap<>(); + params.put("username", "testuser"); + params.put("email", "test@163.com"); + params.put("role", "admin"); + params.put("activationLink", "https://example.com/activate?code=12345"); + + // 1. 使用 parseTemplateContentParams:只提取参数名称,忽略了 HTML 格式 + List parsedParams = mailTemplateService.parseTemplateContentParams(content); + System.out.println("parseTemplateContentParams结果:" + parsedParams); + + // 断言:只提取了纯参数名称,没有 HTML 格式 + assertEquals(6, parsedParams.size()); + // 检查所有参数类型 + assertEquals(3, parsedParams.stream().filter("username"::equals).count()); + assertEquals(1, parsedParams.stream().filter("email"::equals).count()); + assertEquals(1, parsedParams.stream().filter("role"::equals).count()); + assertEquals(1, parsedParams.stream().filter("activationLink"::equals).count()); + // 断言:没有包含任何 HTML 标签 + for (String param : parsedParams) { + assertFalse(param.contains("<")); + assertFalse(param.contains(">")); + } + + // 2. 使用 formatMailTemplateContent:处理 HTML 格式,生成最终内容 + String formattedContent = mailTemplateService.formatMailTemplateContent(content, params); + System.out.println("formatMailTemplateContent结果:" + formattedContent); + + // 断言:HTML 格式被保留并处理 + assertTrue(formattedContent.contains("
")); + assertTrue(formattedContent.contains("

Welcome, testuser!

")); + assertTrue(formattedContent.contains("here")); + assertTrue(formattedContent.contains("
public class WelcomeMessage {")); + assertTrue(formattedContent.contains("
")); + // 断言:所有参数都被正确替换 + assertFalse(formattedContent.contains("{username}")); + assertFalse(formattedContent.contains("{email}")); + assertFalse(formattedContent.contains("{role}")); + assertFalse(formattedContent.contains("{activationLink}")); + } + }