From 2de710190ef085256a6792bb8b89fb629ef738a8 Mon Sep 17 00:00:00 2001 From: yubaolin Date: Thu, 7 Aug 2025 17:29:51 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E8=85=BE=E8=AE=AF=E4=BA=91=E7=9F=AD?= =?UTF-8?q?=E4=BF=A1=E5=9B=9E=E8=B0=83=E6=B2=A1=E6=9C=89logId=20=E9=9C=80?= =?UTF-8?q?=E8=A6=81serialNo=E6=9D=A5=E6=9B=B4=E6=96=B0=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=20https://gitee.com/zhijiantianya/yudao-clou?= =?UTF-8?q?d/issues/ICNG6O?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sms/core/client/impl/TencentSmsClient.java | 1 + .../module/system/service/sms/SmsLogService.java | 3 ++- .../system/service/sms/SmsLogServiceImpl.java | 13 ++++++++++--- .../system/service/sms/SmsSendServiceImpl.java | 2 +- .../system/service/sms/SmsLogServiceImplTest.java | 3 ++- .../system/service/sms/SmsSendServiceImplTest.java | 2 +- 6 files changed, 17 insertions(+), 7 deletions(-) diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java index 19cde8c26..653458f46 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java @@ -119,6 +119,7 @@ public class TencentSmsClient extends AbstractSmsClient { return new SmsReceiveRespDTO() .setSuccess("SUCCESS".equals(statusObj.getStr("report_status"))) // 是否接收成功 .setErrorCode(statusObj.getStr("errmsg")) // 状态报告编码 + .setErrorMsg(statusObj.getStr("description")) // 状态报告描述 .setMobile(statusObj.getStr("mobile")) // 手机号 .setReceiveTime(statusObj.getLocalDateTime("user_receive_time", null)) // 状态报告时间 .setSerialNo(statusObj.getStr("sid")); // 发送序列号 diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java index 0c86c0f07..a63415620 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java @@ -49,12 +49,13 @@ public interface SmsLogService { * 更新日志的接收结果 * * @param id 日志编号 + * @param serialNo 发送编号 * @param success 是否接收成功 * @param receiveTime 用户接收时间 * @param apiReceiveCode API 接收结果的编码 * @param apiReceiveMsg API 接收结果的说明 */ - void updateSmsReceiveResult(Long id, Boolean success, + void updateSmsReceiveResult(Long id, String serialNo, Boolean success, LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg); /** diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java index 4f969cedf..8285f5b94 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.system.service.sms; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsLogDO; @@ -7,6 +8,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsLogMapper; import cn.iocoder.yudao.module.system.enums.sms.SmsReceiveStatusEnum; import cn.iocoder.yudao.module.system.enums.sms.SmsSendStatusEnum; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -63,12 +65,17 @@ public class SmsLogServiceImpl implements SmsLogService { } @Override - public void updateSmsReceiveResult(Long id, Boolean success, LocalDateTime receiveTime, + public void updateSmsReceiveResult(Long id, String serialNo, Boolean success, LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg) { SmsReceiveStatusEnum receiveStatus = Objects.equals(success, true) ? SmsReceiveStatusEnum.SUCCESS : SmsReceiveStatusEnum.FAILURE; - smsLogMapper.updateById(SmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus()) - .receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build()); + smsLogMapper.update(new LambdaUpdateWrapper() + .set(SmsLogDO::getReceiveStatus, receiveStatus.getStatus()) + .set(SmsLogDO::getReceiveTime, receiveTime) + .set(SmsLogDO::getApiReceiveCode, apiReceiveCode) + .set(SmsLogDO::getApiReceiveMsg, apiReceiveMsg) + .eq(id != null && id > 0, SmsLogDO::getId, id) + .eq(!StrUtil.isEmptyIfStr(serialNo), SmsLogDO::getApiSerialNo, serialNo)); } @Override diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java index 41f429eca..bd5068f7b 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java @@ -184,7 +184,7 @@ public class SmsSendServiceImpl implements SmsSendService { return; } // 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新 - receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(), + receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(), result.getSerialNo(), result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorMsg())); } diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java index 570e8ceea..4f7cf59ab 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java @@ -153,13 +153,14 @@ public class SmsLogServiceImplTest extends BaseDbUnitTest { smsLogMapper.insert(dbSmsLog); // 准备参数 Long id = dbSmsLog.getId(); + String serialNo = dbSmsLog.getApiSerialNo(); Boolean success = randomBoolean(); LocalDateTime receiveTime = randomLocalDateTime(); String apiReceiveCode = randomString(); String apiReceiveMsg = randomString(); // 调用 - smsLogService.updateSmsReceiveResult(id, success, receiveTime, apiReceiveCode, apiReceiveMsg); + smsLogService.updateSmsReceiveResult(id, serialNo, success, receiveTime, apiReceiveCode, apiReceiveMsg); // 断言 dbSmsLog = smsLogMapper.selectById(id); assertEquals(success ? SmsReceiveStatusEnum.SUCCESS.getStatus() diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java index 487c6f7fe..ffcc0587d 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java @@ -291,7 +291,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { // 调用 smsSendService.receiveSmsStatus(channelCode, text); // 断言 - receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()), + receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSerialNo()), eq(result.getSuccess()), eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorCode()))); } From fa02450c56d09eb7026cae8f501d9a0181fe7ce3 Mon Sep 17 00:00:00 2001 From: baolin Date: Sun, 10 Aug 2025 11:04:42 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E8=85=BE=E8=AE=AF=E4=BA=91=E7=9F=AD?= =?UTF-8?q?=E4=BF=A1=E5=9B=9E=E8=B0=83=E6=B2=A1=E6=9C=89logId=20=E6=A0=B9?= =?UTF-8?q?=E6=8D=AEserialNo=E6=9F=A5=E8=AF=A2id=E6=9D=A5=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/service/sms/SmsLogServiceImpl.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java index 8285f5b94..be661c995 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.system.service.sms; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsLogDO; @@ -8,11 +7,11 @@ import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsLogMapper; import cn.iocoder.yudao.module.system.enums.sms.SmsReceiveStatusEnum; import cn.iocoder.yudao.module.system.enums.sms.SmsSendStatusEnum; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; + import java.time.LocalDateTime; import java.util.Map; import java.util.Objects; @@ -69,13 +68,16 @@ public class SmsLogServiceImpl implements SmsLogService { String apiReceiveCode, String apiReceiveMsg) { SmsReceiveStatusEnum receiveStatus = Objects.equals(success, true) ? SmsReceiveStatusEnum.SUCCESS : SmsReceiveStatusEnum.FAILURE; - smsLogMapper.update(new LambdaUpdateWrapper() - .set(SmsLogDO::getReceiveStatus, receiveStatus.getStatus()) - .set(SmsLogDO::getReceiveTime, receiveTime) - .set(SmsLogDO::getApiReceiveCode, apiReceiveCode) - .set(SmsLogDO::getApiReceiveMsg, apiReceiveMsg) - .eq(id != null && id > 0, SmsLogDO::getId, id) - .eq(!StrUtil.isEmptyIfStr(serialNo), SmsLogDO::getApiSerialNo, serialNo)); + Long logId = id; + if (logId == null || logId == 0) { + SmsLogDO log = smsLogMapper.selectOne(SmsLogDO::getApiSerialNo, serialNo); + if (log == null) { // 这里查不到,有可能短信不是在本系统发送,故不抛异常 + return; + } + logId = log.getId(); + } + smsLogMapper.updateById(SmsLogDO.builder().id(logId).receiveStatus(receiveStatus.getStatus()) + .receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build()); } @Override From 37d120b19355f46c66ad72f2ad02ba9d676e1089 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 17 Aug 2025 15:25:21 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix=EF=BC=9A=E3=80=90framework=20=E6=A1=86?= =?UTF-8?q?=E6=9E=B6=E3=80=91GlobalExceptionHandler=20=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=9B=B4=E5=A4=9A=20ServiceException=20=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/handler/GlobalExceptionHandler.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index 22234ba3b..3f3a871a1 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -36,6 +36,7 @@ import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException; @@ -93,6 +94,9 @@ public class GlobalExceptionHandler { if (ex instanceof ValidationException) { return validationException((ValidationException) ex); } + if (ex instanceof MaxUploadSizeExceededException) { + return maxUploadSizeExceededExceptionHandler((MaxUploadSizeExceededException) ex); + } if (ex instanceof NoHandlerFoundException) { return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex); } @@ -111,9 +115,6 @@ public class GlobalExceptionHandler { if (ex instanceof AccessDeniedException) { return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); } - if (ex instanceof UncheckedExecutionException && ex.getCause() != ex) { - return allExceptionHandler(request, ex.getCause()); - } return defaultExceptionHandler(request, ex); } @@ -213,6 +214,14 @@ public class GlobalExceptionHandler { return CommonResult.error(BAD_REQUEST); } + /** + * 处理上传文件过大异常 + */ + @ExceptionHandler(MaxUploadSizeExceededException.class) + public CommonResult maxUploadSizeExceededExceptionHandler(MaxUploadSizeExceededException ex) { + return CommonResult.error(BAD_REQUEST.getCode(), "上传文件过大,请调整后重试"); + } + /** * 处理 SpringMVC 请求地址不存在 * @@ -309,6 +318,12 @@ public class GlobalExceptionHandler { */ @ExceptionHandler(value = Exception.class) public CommonResult defaultExceptionHandler(HttpServletRequest req, Throwable ex) { + // 特殊:如果是 ServiceException 的异常,则直接返回 + // 例如说:https://gitee.com/zhijiantianya/yudao-cloud/issues/ICSSRM、https://gitee.com/zhijiantianya/yudao-cloud/issues/ICT6FM + if (ex.getCause() != null && ex.getCause() instanceof ServiceException) { + return serviceExceptionHandler((ServiceException) ex.getCause()); + } + // 情况一:处理表不存在的异常 CommonResult tableNotExistsResult = handleTableNotExists(ex); if (tableNotExistsResult != null) { From 49572b29948d80fa9c1e788f7b0a73aed4b5cc26 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 17 Aug 2025 17:01:06 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix=EF=BC=9A=E3=80=90system=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=8A=9F=E8=83=BD=E3=80=91=E8=85=BE=E8=AE=AF=E4=BA=91?= =?UTF-8?q?=E7=9F=AD=E4=BF=A1=E5=9B=9E=E8=B0=83=E6=B2=A1=E6=9C=89=20logId?= =?UTF-8?q?=20=E9=9C=80=E8=A6=81=20serialNo=20=E6=9D=A5=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/system/dal/mysql/sms/SmsLogMapper.java | 4 ++++ .../module/system/service/sms/SmsLogService.java | 6 +++--- .../system/service/sms/SmsLogServiceImpl.java | 13 ++++++------- .../system/service/sms/SmsLogServiceImplTest.java | 4 ++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsLogMapper.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsLogMapper.java index f2388711a..31245fd0d 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsLogMapper.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsLogMapper.java @@ -22,4 +22,8 @@ public interface SmsLogMapper extends BaseMapperX { .orderByDesc(SmsLogDO::getId)); } + default SmsLogDO selectByApiSerialNo(String apiSerialNo) { + return selectOne(SmsLogDO::getApiSerialNo, apiSerialNo); + } + } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java index a63415620..49ec93aac 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java @@ -12,7 +12,7 @@ import java.util.Map; * 短信日志 Service 接口 * * @author zzf - * @date 13:48 2021/3/2 + * @since 13:48 2021/3/2 */ public interface SmsLogService { @@ -49,13 +49,13 @@ public interface SmsLogService { * 更新日志的接收结果 * * @param id 日志编号 - * @param serialNo 发送编号 + * @param apiSerialNo 发送编号 * @param success 是否接收成功 * @param receiveTime 用户接收时间 * @param apiReceiveCode API 接收结果的编码 * @param apiReceiveMsg API 接收结果的说明 */ - void updateSmsReceiveResult(Long id, String serialNo, Boolean success, + void updateSmsReceiveResult(Long id, String apiSerialNo, Boolean success, LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg); /** diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java index be661c995..45660e60e 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java @@ -64,19 +64,18 @@ public class SmsLogServiceImpl implements SmsLogService { } @Override - public void updateSmsReceiveResult(Long id, String serialNo, Boolean success, LocalDateTime receiveTime, + public void updateSmsReceiveResult(Long id, String apiSerialNo, Boolean success, LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg) { SmsReceiveStatusEnum receiveStatus = Objects.equals(success, true) ? SmsReceiveStatusEnum.SUCCESS : SmsReceiveStatusEnum.FAILURE; - Long logId = id; - if (logId == null || logId == 0) { - SmsLogDO log = smsLogMapper.selectOne(SmsLogDO::getApiSerialNo, serialNo); - if (log == null) { // 这里查不到,有可能短信不是在本系统发送,故不抛异常 + if (id == null || id == 0) { + SmsLogDO log = smsLogMapper.selectByApiSerialNo(apiSerialNo); + if (log == null) { return; } - logId = log.getId(); + id = log.getId(); } - smsLogMapper.updateById(SmsLogDO.builder().id(logId).receiveStatus(receiveStatus.getStatus()) + smsLogMapper.updateById(SmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus()) .receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build()); } diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java index 4f7cf59ab..fa979ce4a 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java @@ -153,14 +153,14 @@ public class SmsLogServiceImplTest extends BaseDbUnitTest { smsLogMapper.insert(dbSmsLog); // 准备参数 Long id = dbSmsLog.getId(); - String serialNo = dbSmsLog.getApiSerialNo(); + String apiSerialNo = dbSmsLog.getApiSerialNo(); Boolean success = randomBoolean(); LocalDateTime receiveTime = randomLocalDateTime(); String apiReceiveCode = randomString(); String apiReceiveMsg = randomString(); // 调用 - smsLogService.updateSmsReceiveResult(id, serialNo, success, receiveTime, apiReceiveCode, apiReceiveMsg); + smsLogService.updateSmsReceiveResult(id, apiSerialNo, success, receiveTime, apiReceiveCode, apiReceiveMsg); // 断言 dbSmsLog = smsLogMapper.selectById(id); assertEquals(success ? SmsReceiveStatusEnum.SUCCESS.getStatus() From 8e45cf5787ca4a93a2fe11611fbc31af2fc97b4d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 18 Aug 2025 00:26:28 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT=20?= =?UTF-8?q?=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/common/util/http/HttpUtils.java | 11 +++- .../mybatis/core/util/MyBatisUtils.java | 31 +++++++++-- .../encrypt/core/filter/ApiEncryptFilter.java | 4 +- .../yudao/module/infra/api/file/FileApi.java | 14 +++++ .../module/infra/api/file/FileApiImpl.java | 5 ++ .../controller/admin/file/FileController.java | 4 +- .../app/file/AppFileController.java | 4 +- .../file/core/client/FileClient.java | 19 +++++-- .../core/client/local/LocalFileClient.java | 10 +++- .../client/s3/FilePresignedUrlRespDTO.java | 29 ---------- .../file/core/client/s3/S3FileClient.java | 51 ++++++++++++------ .../core/client/s3/S3FileClientConfig.java | 14 ++++- .../file/core/utils/FileTypeUtils.java | 8 ++- .../infra/service/file/FileService.java | 14 +++-- .../infra/service/file/FileServiceImpl.java | 18 +++++-- .../file/core/local/LocalFileClientTest.java | 16 ++++++ .../file/core/s3/S3FileClientTest.java | 30 ++++++++++- .../convert/coupon/CouponConvert.java | 12 ++--- .../TradeStatusSyncToWxaOrderHandler.java | 53 ++++++++++++------- 19 files changed, 250 insertions(+), 97 deletions(-) delete mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/FilePresignedUrlRespDTO.java diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java index e1e0ac08e..85a644f1f 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java @@ -47,8 +47,15 @@ public class HttpUtils { return builder.build(); } - private String append(String base, Map query, boolean fragment) { - return append(base, query, null, fragment); + public static String removeUrlQuery(String url) { + if (!StrUtil.contains(url, '?')) { + return url; + } + UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); + // 移除 query、fragment + builder.setQuery(null); + builder.setFragment(null); + return builder.build(); } /** diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java index ac33ba8ef..56f51d91d 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java @@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.SortingField; import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.core.toolkit.StringPool; @@ -47,16 +48,36 @@ public class MyBatisUtils { return page; } + @SuppressWarnings("PatternVariableCanBeUsed") public static void addOrder(Wrapper wrapper, Collection sortingFields) { if (CollUtil.isEmpty(sortingFields)) { return; } - QueryWrapper query = (QueryWrapper) wrapper; - for (SortingField sortingField : sortingFields) { - query.orderBy(true, - SortingField.ORDER_ASC.equals(sortingField.getOrder()), - StrUtil.toUnderlineCase(sortingField.getField())); + if (wrapper instanceof QueryWrapper) { + QueryWrapper query = (QueryWrapper) wrapper; + for (SortingField sortingField : sortingFields) { + query.orderBy(true, + SortingField.ORDER_ASC.equals(sortingField.getOrder()), + StrUtil.toUnderlineCase(sortingField.getField())); + } + } else if (wrapper instanceof LambdaQueryWrapper) { + // LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY + LambdaQueryWrapper lambdaQuery = (LambdaQueryWrapper) wrapper; + StringBuilder orderBy = new StringBuilder(); + for (SortingField sortingField : sortingFields) { + if (StrUtil.isNotEmpty(orderBy)) { + orderBy.append(", "); + } + orderBy.append(StrUtil.toUnderlineCase(sortingField.getField())) + .append(" ") + .append(SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? "ASC" : "DESC"); + } + lambdaQuery.last("ORDER BY " + orderBy); + // 另外个思路:https://blog.csdn.net/m0_59084856/article/details/138450913 + } else { + throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName()); } + } /** diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java index e6d03ba32..1216ffeb6 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java @@ -128,6 +128,7 @@ public class ApiEncryptFilter extends ApiRequestFilter { * * @param request 请求 */ + @SuppressWarnings("PatternVariableCanBeUsed") private ApiEncrypt getApiEncrypt(HttpServletRequest request) { try { HandlerExecutionChain mappingHandler = requestMappingHandlerMapping.getHandler(request); @@ -135,7 +136,8 @@ public class ApiEncryptFilter extends ApiRequestFilter { return null; } Object handler = mappingHandler.getHandler(); - if (handler instanceof HandlerMethod handlerMethod) { + if (handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; ApiEncrypt annotation = handlerMethod.getMethodAnnotation(ApiEncrypt.class); if (annotation == null) { annotation = handlerMethod.getBeanType().getAnnotation(ApiEncrypt.class); diff --git a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java index 9c6f0f4a3..bb22b817f 100644 --- a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java +++ b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java @@ -8,8 +8,10 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; @FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory = @Tag(name = "RPC 服务 - 文件") @@ -56,4 +58,16 @@ public interface FileApi { @Operation(summary = "保存文件,并返回文件的访问路径") CommonResult createFile(@Valid @RequestBody FileCreateReqDTO createReqDTO); + /** + * 生成文件预签名地址,用于读取 + * + * @param url 完整的文件访问地址 + * @param expirationSeconds 访问有效期,单位秒 + * @return 文件预签名地址 + */ + @GetMapping(PREFIX + "/presigned-url") + @Operation(summary = "生成文件预签名地址,用于读取") + CommonResult presignGetUrl(@NotEmpty(message = "URL 不能为空") @RequestParam("url") String url, + Integer expirationSeconds); + } diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java index 7f299c3eb..0e308386b 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java @@ -23,4 +23,9 @@ public class FileApiImpl implements FileApi { createReqDTO.getDirectory(), createReqDTO.getType())); } + @Override + public CommonResult presignGetUrl(String url, Integer expirationSeconds) { + return success(fileService.presignGetUrl(url, expirationSeconds)); + } + } diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java index afcf71623..f21e79a18 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java @@ -51,7 +51,7 @@ public class FileController { } @GetMapping("/presigned-url") - @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") + @Operation(summary = "获取文件预签名地址(上传)", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") @Parameters({ @Parameter(name = "name", description = "文件名称", required = true), @Parameter(name = "directory", description = "文件目录") @@ -59,7 +59,7 @@ public class FileController { public CommonResult getFilePresignedUrl( @RequestParam("name") String name, @RequestParam(value = "directory", required = false) String directory) { - return success(fileService.getFilePresignedUrl(name, directory)); + return success(fileService.presignPutUrl(name, directory)); } @PostMapping("/create") diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java index 7f85e996d..a4c1d202e 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java @@ -41,7 +41,7 @@ public class AppFileController { } @GetMapping("/presigned-url") - @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") + @Operation(summary = "获取文件预签名地址(上传)", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") @Parameters({ @Parameter(name = "name", description = "文件名称", required = true), @Parameter(name = "directory", description = "文件目录") @@ -49,7 +49,7 @@ public class AppFileController { public CommonResult getFilePresignedUrl( @RequestParam("name") String name, @RequestParam(value = "directory", required = false) String directory) { - return success(fileService.getFilePresignedUrl(name, directory)); + return success(fileService.presignPutUrl(name, directory)); } @PostMapping("/create") diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java index 053b3c510..cf1cd620a 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java @@ -1,7 +1,5 @@ package cn.iocoder.yudao.module.infra.framework.file.core.client; -import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; - /** * 文件客户端 * @@ -42,13 +40,26 @@ public interface FileClient { */ byte[] getContent(String path) throws Exception; + // ========== 文件签名,目前仅 S3 支持 ========== + /** - * 获得文件预签名地址 + * 获得文件预签名地址,用于上传 * * @param path 相对路径 * @return 文件预签名地址 */ - default FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception { + default String presignPutUrl(String path) { + throw new UnsupportedOperationException("不支持的操作"); + } + + /** + * 生成文件预签名地址,用于读取 + * + * @param url 完整的文件访问地址 + * @param expirationSeconds 访问有效期,单位秒 + * @return 文件预签名地址 + */ + default String presignGetUrl(String url, Integer expirationSeconds) { throw new UnsupportedOperationException("不支持的操作"); } diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java index 7fa2a7ea9..6e5c0229b 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.infra.framework.file.core.client.local; import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; import java.io.File; @@ -38,7 +39,14 @@ public class LocalFileClient extends AbstractFileClient { @Override public byte[] getContent(String path) { String filePath = getFilePath(path); - return FileUtil.readBytes(filePath); + try { + return FileUtil.readBytes(filePath); + } catch (IORuntimeException ex) { + if (ex.getMessage().startsWith("File not exist:")) { + return null; + } + throw ex; + } } private String getFilePath(String path) { diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/FilePresignedUrlRespDTO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/FilePresignedUrlRespDTO.java deleted file mode 100644 index 6a1258e9e..000000000 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/FilePresignedUrlRespDTO.java +++ /dev/null @@ -1,29 +0,0 @@ -package cn.iocoder.yudao.module.infra.framework.file.core.client.s3; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 文件预签名地址 Response DTO - * - * @author owen - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class FilePresignedUrlRespDTO { - - /** - * 文件上传 URL(用于上传) - * - * 例如说: - */ - private String uploadUrl; - - /** - * 文件 URL(用于读取、下载等) - */ - private String url; - -} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java index a33f0d738..8e21c76f4 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java @@ -1,8 +1,10 @@ package cn.iocoder.yudao.module.infra.framework.file.core.client.s3; import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; @@ -15,9 +17,11 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; import java.net.URI; +import java.net.URL; import java.time.Duration; /** @@ -27,6 +31,8 @@ import java.time.Duration; */ public class S3FileClient extends AbstractFileClient { + private static final Duration EXPIRATION_DEFAULT = Duration.ofHours(24); + private S3Client client; private S3Presigner presigner; @@ -75,7 +81,7 @@ public class S3FileClient extends AbstractFileClient { // 上传文件 client.putObject(putRequest, RequestBody.fromBytes(content)); // 拼接返回路径 - return config.getDomain() + "/" + path; + return presignGetUrl(path, null); } @Override @@ -97,23 +103,38 @@ public class S3FileClient extends AbstractFileClient { } @Override - public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) { - Duration expiration = Duration.ofHours(24); - return new FilePresignedUrlRespDTO(getPresignedUrl(path, expiration), config.getDomain() + "/" + path); + public String presignPutUrl(String path) { + return presigner.presignPutObject(PutObjectPresignRequest.builder() + .signatureDuration(EXPIRATION_DEFAULT) + .putObjectRequest(b -> b.bucket(config.getBucket()).key(path)).build()) + .url().toString(); } - /** - * 生成动态的预签名上传 URL - * - * @param path 相对路径 - * @param expiration 过期时间 - * @return 生成的上传 URL - */ - private String getPresignedUrl(String path, Duration expiration) { - return presigner.presignPutObject(PutObjectPresignRequest.builder() + @Override + public String presignGetUrl(String url, Integer expirationSeconds) { + // 1. 将 url 转换为 path + String path = StrUtil.removePrefix(url, config.getDomain() + "/"); + path = HttpUtils.removeUrlQuery(path); + + // 2.1 情况一:公开访问:无需签名 + // 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名 + if (!BooleanUtil.isFalse(config.getEnablePublicAccess())) { + return config.getDomain() + "/" + path; + } + + // 2.2 情况二:私有访问:生成 GET 预签名 URL + String finalPath = path; + Duration expiration = expirationSeconds != null ? Duration.ofSeconds(expirationSeconds) : EXPIRATION_DEFAULT; + URL signedUrl = presigner.presignGetObject(GetObjectPresignRequest.builder() .signatureDuration(expiration) - .putObjectRequest(b -> b.bucket(config.getBucket()).key(path)) - .build()).url().toString(); + .getObjectRequest(b -> b.bucket(config.getBucket()).key(finalPath)).build()) + .url(); + // 特殊:适配未使用 domain 返回的情况!!! + String signedUrlStr = signedUrl.toString(); + if (!signedUrlStr.startsWith(config.getDomain())) { + signedUrlStr = signedUrlStr.replaceFirst(signedUrl.getProtocol() + "://" + signedUrl.getHost(), config.getDomain()); + } + return signedUrlStr; } /** diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java index 1e1b900d8..216197964 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java @@ -3,11 +3,12 @@ package cn.iocoder.yudao.module.infra.framework.file.core.client.s3; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig; import com.fasterxml.jackson.annotation.JsonIgnore; -import jakarta.validation.constraints.AssertTrue; -import jakarta.validation.constraints.NotNull; import lombok.Data; import org.hibernate.validator.constraints.URL; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; + /** * S3 文件客户端的配置类 * @@ -72,6 +73,15 @@ public class S3FileClientConfig implements FileClientConfig { @NotNull(message = "enablePathStyleAccess 不能为空") private Boolean enablePathStyleAccess; + /** + * 是否公开访问 + * + * true:公开访问,所有人都可以访问 + * false:私有访问,只有配置的 accessKey 才可以访问 + */ + @NotNull(message = "是否公开访问不能为空") + private Boolean enablePublicAccess; + @SuppressWarnings("RedundantIfStatement") @AssertTrue(message = "domain 不能为空") @JsonIgnore diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java index 9cc417588..d8a13e953 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java @@ -80,9 +80,15 @@ public class FileTypeUtils { */ public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { // 设置 header 和 contentType - response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); String contentType = getMineType(content, filename); response.setContentType(contentType); + // 设置内容显示、下载文件名:https://www.cnblogs.com/wq-9/articles/12165056.html + if (StrUtil.containsIgnoreCase(contentType, "image/")) { + // 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论 + response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename)); + } else { + response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); + } // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 if (StrUtil.containsIgnoreCase(contentType, "video")) { response.setHeader("Content-Length", String.valueOf(content.length)); diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java index 5b15ad873..5e3448b0f 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java @@ -37,14 +37,22 @@ public interface FileService { String name, String directory, String type); /** - * 生成文件预签名地址信息 + * 生成文件预签名地址信息,用于上传 * * @param name 文件名 * @param directory 目录 * @return 预签名地址信息 */ - FilePresignedUrlRespVO getFilePresignedUrl(@NotEmpty(message = "文件名不能为空") String name, - String directory); + FilePresignedUrlRespVO presignPutUrl(@NotEmpty(message = "文件名不能为空") String name, + String directory); + /** + * 生成文件预签名地址信息,用于读取 + * + * @param url 完整的文件访问地址 + * @param expirationSeconds 访问有效期,单位秒 + * @return 文件预签名地址 + */ + String presignGetUrl(String url, Integer expirationSeconds); /** * 创建文件 diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java index 98447fb37..f47275d33 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java @@ -6,6 +6,7 @@ import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; @@ -13,7 +14,6 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresigned import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; -import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils; import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.Resource; @@ -126,19 +126,27 @@ public class FileServiceImpl implements FileService { @Override @SneakyThrows - public FilePresignedUrlRespVO getFilePresignedUrl(String name, String directory) { + public FilePresignedUrlRespVO presignPutUrl(String name, String directory) { // 1. 生成上传的 path,需要保证唯一 String path = generateUploadPath(name, directory); // 2. 获取文件预签名地址 FileClient fileClient = fileConfigService.getMasterFileClient(); - FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path); - return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class, - object -> object.setConfigId(fileClient.getId()).setPath(path)); + String uploadUrl = fileClient.presignPutUrl(path); + String visitUrl = fileClient.presignGetUrl(path, null); + return new FilePresignedUrlRespVO().setConfigId(fileClient.getId()) + .setPath(path).setUploadUrl(uploadUrl).setUrl(visitUrl); + } + + @Override + public String presignGetUrl(String url, Integer expirationSeconds) { + FileClient fileClient = fileConfigService.getMasterFileClient(); + return fileClient.presignGetUrl(url, expirationSeconds); } @Override public Long createFile(FileCreateReqVO createReqVO) { + createReqVO.setUrl(HttpUtils.removeUrlQuery(createReqVO.getUrl())); // 目的:移除私有桶情况下,URL 的签名参数 FileDO file = BeanUtils.toBean(createReqVO, FileDO.class); fileMapper.insert(file); return file.getId(); diff --git a/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java b/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java index 7c622a530..6bc8c7bfe 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java +++ b/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileC import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; + public class LocalFileClientTest { @Test @@ -26,4 +28,18 @@ public class LocalFileClientTest { client.delete(path); } + @Test + @Disabled + public void testGetContent_notFound() { + // 创建客户端 + LocalFileClientConfig config = new LocalFileClientConfig(); + config.setDomain("http://127.0.0.1:48080"); + config.setBasePath("/Users/yunai/file_test"); + LocalFileClient client = new LocalFileClient(0L, config); + client.init(); + // 上传文件 + byte[] content = client.getContent(randomString()); + System.out.println(); + } + } diff --git a/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java b/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java index 3c40ce23e..981971b9f 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java +++ b/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java @@ -9,6 +9,7 @@ import jakarta.validation.Validation; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +@SuppressWarnings("resource") public class S3FileClientTest { @Test @@ -70,6 +71,7 @@ public class S3FileClientTest { config.setAccessSecret("kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP"); config.setBucket("ruoyi-vue-pro"); config.setDomain("http://test.yudao.iocoder.cn"); // 如果有自定义域名,则可以设置。http://static.yudao.iocoder.cn + config.setEnablePathStyleAccess(false); // 默认上海的 endpoint config.setEndpoint("s3-cn-south-1.qiniucs.com"); @@ -77,6 +79,32 @@ public class S3FileClientTest { testExecuteUpload(config); } + @Test + @Disabled // 七牛云存储(读私有桶),如果要集成测试,可以注释本行 + public void testQiniu_privateGet() { + S3FileClientConfig config = new S3FileClientConfig(); + // 配置成你自己的 +// config.setAccessKey(System.getenv("QINIU_ACCESS_KEY")); +// config.setAccessSecret(System.getenv("QINIU_SECRET_KEY")); + config.setAccessKey("b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8"); + config.setAccessSecret("kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP"); + config.setBucket("ruoyi-vue-pro-private"); + config.setDomain("http://t151glocd.hn-bkt.clouddn.com"); // 如果有自定义域名,则可以设置。http://static.yudao.iocoder.cn + config.setEnablePathStyleAccess(false); + // 默认上海的 endpoint + config.setEndpoint("s3-cn-south-1.qiniucs.com"); + + // 校验配置 + ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config); + // 创建 Client + S3FileClient client = new S3FileClient(0L, config); + client.init(); + // 执行生成 URL 签名 + String path = "output.png"; + String presignedUrl = client.presignGetUrl(path, 300); + System.out.println(presignedUrl); + } + @Test @Disabled // 华为云存储,如果要集成测试,可以注释本行 public void testHuaweiCloud() throws Exception { @@ -93,7 +121,7 @@ public class S3FileClientTest { testExecuteUpload(config); } - private void testExecuteUpload(S3FileClientConfig config) throws Exception { + private void testExecuteUpload(S3FileClientConfig config) { // 校验配置 ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config); // 创建 Client diff --git a/yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java b/yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java index 0ac9c58da..6af2f65cb 100755 --- a/yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java +++ b/yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java @@ -30,7 +30,7 @@ public interface CouponConvert { CouponRespDTO convert(CouponDO bean); default CouponDO convert(CouponTemplateDO template, Long userId) { - CouponDO couponDO = new CouponDO() + CouponDO coupon = new CouponDO() .setTemplateId(template.getId()) .setName(template.getName()) .setTakeType(template.getTakeType()) @@ -44,13 +44,13 @@ public interface CouponConvert { .setStatus(CouponStatusEnum.UNUSED.getStatus()) .setUserId(userId); if (CouponTemplateValidityTypeEnum.DATE.getType().equals(template.getValidityType())) { - couponDO.setValidStartTime(template.getValidStartTime()); - couponDO.setValidEndTime(template.getValidEndTime()); + coupon.setValidStartTime(template.getValidStartTime()); + coupon.setValidEndTime(template.getValidEndTime()); } else if (CouponTemplateValidityTypeEnum.TERM.getType().equals(template.getValidityType())) { - couponDO.setValidStartTime(LocalDateTime.now().plusDays(template.getFixedStartTerm())); - couponDO.setValidEndTime(LocalDateTime.now().plusDays(template.getFixedEndTerm())); + coupon.setValidStartTime(LocalDateTime.now().plusDays(template.getFixedStartTerm())); + coupon.setValidEndTime(coupon.getValidStartTime().plusDays(template.getFixedEndTerm())); } - return couponDO; + return coupon; } CouponPageReqVO convert(AppCouponPageReqVO pageReqVO, Collection userIds); diff --git a/yudao-module-mall/yudao-module-trade-server/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeStatusSyncToWxaOrderHandler.java b/yudao-module-mall/yudao-module-trade-server/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeStatusSyncToWxaOrderHandler.java index 605e2dc30..c5005ee3c 100644 --- a/yudao-module-mall/yudao-module-trade-server/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeStatusSyncToWxaOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-server/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeStatusSyncToWxaOrderHandler.java @@ -42,6 +42,40 @@ public class TradeStatusSyncToWxaOrderHandler implements TradeOrderHandler { if (ObjUtil.notEqual(order.getPayChannelCode(), PayChannelEnum.WX_LITE.getCode())) { return; } + + // 上传订单物流信息到微信小程序 + uploadWxaOrderShippingInfo(order); + } + + @Override + public void afterReceiveOrder(TradeOrderDO order) { + // 注意:只有微信小程序支付的订单,才需要同步 + if (ObjUtil.notEqual(order.getPayChannelCode(), PayChannelEnum.WX_LITE.getCode())) { + return; + } + PayOrderRespDTO payOrder = payOrderApi.getOrder(order.getPayOrderId()).getCheckedData(); + SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO = new SocialWxaOrderNotifyConfirmReceiveReqDTO() + .setTransactionId(payOrder.getChannelOrderNo()) + .setReceivedTime(order.getReceiveTime()); + try { + socialClientApi.notifyWxaOrderConfirmReceive(UserTypeEnum.MEMBER.getValue(), reqDTO); + } catch (Exception ex) { + log.error("[afterReceiveOrder][订单({}) 通知订单收货到微信小程序失败]", order, ex); + } + + // 如果是门店自提订单,上传订单物流信息到微信小程序 + // 原因是,门店自提订单没有 “afterDeliveryOrder” 阶段。可见 https://t.zsxq.com/KWD3u 反馈 + if (DeliveryTypeEnum.PICK_UP.getType().equals(order.getDeliveryType())) { + uploadWxaOrderShippingInfo(order); + } + } + + /** + * 上传订单物流信息到微信小程序 + * + * @param order 订单 + */ + private void uploadWxaOrderShippingInfo(TradeOrderDO order) { PayOrderRespDTO payOrder = payOrderApi.getOrder(order.getPayOrderId()).getCheckedData(); SocialWxaOrderUploadShippingInfoReqDTO reqDTO = new SocialWxaOrderUploadShippingInfoReqDTO() .setTransactionId(payOrder.getChannelOrderNo()) @@ -58,29 +92,12 @@ public class TradeStatusSyncToWxaOrderHandler implements TradeOrderHandler { reqDTO.setLogisticsType(SocialWxaOrderUploadShippingInfoReqDTO.LOGISTICS_TYPE_VIRTUAL); } try { - socialClientApi.uploadWxaOrderShippingInfo(UserTypeEnum.MEMBER.getValue(), reqDTO).checkError(); + socialClientApi.uploadWxaOrderShippingInfo(UserTypeEnum.MEMBER.getValue(), reqDTO); } catch (Exception ex) { log.error("[afterDeliveryOrder][订单({}) 上传订单物流信息到微信小程序失败]", order, ex); } } - @Override - public void afterReceiveOrder(TradeOrderDO order) { - // 注意:只有微信小程序支付的订单,才需要同步 - if (ObjUtil.notEqual(order.getPayChannelCode(), PayChannelEnum.WX_LITE.getCode())) { - return; - } - PayOrderRespDTO payOrder = payOrderApi.getOrder(order.getPayOrderId()).getCheckedData(); - SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO = new SocialWxaOrderNotifyConfirmReceiveReqDTO() - .setTransactionId(payOrder.getChannelOrderNo()) - .setReceivedTime(order.getReceiveTime()); - try { - socialClientApi.notifyWxaOrderConfirmReceive(UserTypeEnum.MEMBER.getValue(), reqDTO).getCheckedData(); - } catch (Exception ex) { - log.error("[afterReceiveOrder][订单({}) 通知订单收货到微信小程序失败]", order, ex); - } - } - // TODO @芋艿:【设置路径】 https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html#%E5%85%AD%E3%80%81%E6%B6%88%E6%81%AF%E8%B7%B3%E8%BD%AC%E8%B7%AF%E5%BE%84%E8%AE%BE%E7%BD%AE%E6%8E%A5%E5%8F%A3 } From 816e1fbf9ba8eababd78a48786078fee9e0e4e44 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 18 Aug 2025 08:42:16 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT=20?= =?UTF-8?q?=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../file/core/client/s3/S3FileClient.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java index 8e21c76f4..8e7f74611 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java @@ -105,8 +105,8 @@ public class S3FileClient extends AbstractFileClient { @Override public String presignPutUrl(String path) { return presigner.presignPutObject(PutObjectPresignRequest.builder() - .signatureDuration(EXPIRATION_DEFAULT) - .putObjectRequest(b -> b.bucket(config.getBucket()).key(path)).build()) + .signatureDuration(EXPIRATION_DEFAULT) + .putObjectRequest(b -> b.bucket(config.getBucket()).key(path)).build()) .url().toString(); } @@ -126,15 +126,10 @@ public class S3FileClient extends AbstractFileClient { String finalPath = path; Duration expiration = expirationSeconds != null ? Duration.ofSeconds(expirationSeconds) : EXPIRATION_DEFAULT; URL signedUrl = presigner.presignGetObject(GetObjectPresignRequest.builder() - .signatureDuration(expiration) - .getObjectRequest(b -> b.bucket(config.getBucket()).key(finalPath)).build()) + .signatureDuration(expiration) + .getObjectRequest(b -> b.bucket(config.getBucket()).key(finalPath)).build()) .url(); - // 特殊:适配未使用 domain 返回的情况!!! - String signedUrlStr = signedUrl.toString(); - if (!signedUrlStr.startsWith(config.getDomain())) { - signedUrlStr = signedUrlStr.replaceFirst(signedUrl.getProtocol() + "://" + signedUrl.getHost(), config.getDomain()); - } - return signedUrlStr; + return signedUrl.toString(); } /**