# Conflicts:
#	yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java
#	yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java
This commit is contained in:
YunaiV
2025-08-18 08:42:59 +08:00
24 changed files with 250 additions and 121 deletions

View File

@@ -49,8 +49,15 @@ public class HttpUtils {
return builder.build();
}
private String append(String base, Map<String, ?> 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();
}
/**

View File

@@ -111,9 +111,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);
}
@@ -308,6 +305,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) {

View File

@@ -42,4 +42,14 @@ public interface FileApi {
String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
String name, String directory, String type);
/**
* 生成文件预签名地址,用于读取
*
* @param url 完整的文件访问地址
* @param expirationSeconds 访问有效期,单位秒
* @return 文件预签名地址
*/
String presignGetUrl(@NotEmpty(message = "URL 不能为空") String url,
Integer expirationSeconds);
}

View File

@@ -23,4 +23,9 @@ public class FileApiImpl implements FileApi {
return fileService.createFile(content, name, directory, type);
}
@Override
public String presignGetUrl(String url, Integer expirationSeconds) {
return fileService.presignGetUrl(url, expirationSeconds);
}
}

View File

@@ -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<FilePresignedUrlRespVO> 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")

View File

@@ -42,7 +42,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 = "文件目录")
@@ -50,7 +50,7 @@ public class AppFileController {
public CommonResult<FilePresignedUrlRespVO> 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")

View File

@@ -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("不支持的操作");
}

View File

@@ -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<LocalFileClientConfig> {
@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) {

View File

@@ -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;
}

View File

@@ -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<S3FileClientConfig> {
private static final Duration EXPIRATION_DEFAULT = Duration.ofHours(24);
private S3Client client;
private S3Presigner presigner;
@@ -75,7 +81,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
// 上传文件
client.putObject(putRequest, RequestBody.fromBytes(content));
// 拼接返回路径
return config.getDomain() + "/" + path;
return presignGetUrl(path, null);
}
@Override
@@ -97,23 +103,33 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
}
@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();
return signedUrl.toString();
}
/**

View File

@@ -73,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

View File

@@ -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));

View File

@@ -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);
/**
* 创建文件

View File

@@ -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 lombok.SneakyThrows;
@@ -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();

View File

@@ -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();
}
}

View File

@@ -5,11 +5,11 @@ import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClient;
import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig;
import jakarta.validation.Validation;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import javax.validation.Validation;
@SuppressWarnings("resource")
public class S3FileClientTest {
@Test
@@ -71,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");
@@ -78,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 {
@@ -94,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

View File

@@ -541,14 +541,23 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
* @see <a href="https://github.com/binarywang/weixin-java-pay-demo/blob/master/src/main/java/com/github/binarywang/demo/wx/pay/controller/WxPayV3Controller.java#L202-L221">官方示例</a>
*/
private SignatureHeader getRequestHeader(Map<String, String> headers) {
// 参见 https://gitee.com/zhijiantianya/yudao-cloud/issues/ICSFL6
return SignatureHeader.builder()
.signature(headers.get("wechatpay-signature"))
.nonce(headers.get("wechatpay-nonce"))
.serial(headers.get("wechatpay-serial"))
.timeStamp(headers.get("wechatpay-timestamp"))
.signature(getHeaderValue(headers, "Wechatpay-Signature", "wechatpay-signature"))
.nonce(getHeaderValue(headers, "Wechatpay-Nonce", "wechatpay-nonce"))
.serial(getHeaderValue(headers, "Wechatpay-Serial", "wechatpay-serial"))
.timeStamp(getHeaderValue(headers, "Wechatpay-Timestamp", "wechatpay-timestamp"))
.build();
}
private String getHeaderValue(Map<String, String> headers, String capitalizedKey, String lowercaseKey) {
String value = headers.get(capitalizedKey);
if (value != null) {
return value;
}
return headers.get(lowercaseKey);
}
// TODO @芋艿:可能是 wxjava 的 bughttps://github.com/binarywang/WxJava/issues/1557
private void fixV3HttpClientConnectionPoolShutDown() {
client.getConfig().setApiV3HttpClient(null);

View File

@@ -22,4 +22,8 @@ public interface SmsLogMapper extends BaseMapperX<SmsLogDO> {
.orderByDesc(SmsLogDO::getId));
}
default SmsLogDO selectByApiSerialNo(String apiSerialNo) {
return selectOne(SmsLogDO::getApiSerialNo, apiSerialNo);
}
}

View File

@@ -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")); // 发送序列号

View File

@@ -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,12 +49,13 @@ public interface SmsLogService {
* 更新日志的接收结果
*
* @param id 日志编号
* @param apiSerialNo 发送编号
* @param success 是否接收成功
* @param receiveTime 用户接收时间
* @param apiReceiveCode API 接收结果的编码
* @param apiReceiveMsg API 接收结果的说明
*/
void updateSmsReceiveResult(Long id, Boolean success,
void updateSmsReceiveResult(Long id, String apiSerialNo, Boolean success,
LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg);
/**

View File

@@ -10,7 +10,8 @@ import cn.iocoder.yudao.module.system.enums.sms.SmsSendStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Objects;
@@ -63,10 +64,17 @@ public class SmsLogServiceImpl implements SmsLogService {
}
@Override
public void updateSmsReceiveResult(Long id, 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;
if (id == null || id == 0) {
SmsLogDO log = smsLogMapper.selectByApiSerialNo(apiSerialNo);
if (log == null) {
return;
}
id = log.getId();
}
smsLogMapper.updateById(SmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus())
.receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build());
}

View File

@@ -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()));
}

View File

@@ -41,47 +41,47 @@ public class SmsLogServiceImplTest extends BaseDbUnitTest {
@Test
public void testGetSmsLogPage() {
// mock 数据
SmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到
o.setChannelId(1L);
o.setTemplateId(10L);
o.setMobile("15601691300");
o.setSendStatus(SmsSendStatusEnum.INIT.getStatus());
o.setSendTime(buildTime(2020, 11, 11));
o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus());
o.setReceiveTime(buildTime(2021, 11, 11));
});
smsLogMapper.insert(dbSmsLog);
// 测试 channelId 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setChannelId(2L)));
// 测试 templateId 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setTemplateId(20L)));
// 测试 mobile 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setMobile("18818260999")));
// 测试 sendStatus 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus())));
// 测试 sendTime 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12))));
// 测试 receiveStatus 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveStatus(SmsReceiveStatusEnum.SUCCESS.getStatus())));
// 测试 receiveTime 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12))));
// 准备参数
SmsLogPageReqVO reqVO = new SmsLogPageReqVO();
reqVO.setChannelId(1L);
reqVO.setTemplateId(10L);
reqVO.setMobile("156");
reqVO.setSendStatus(SmsSendStatusEnum.INIT.getStatus());
reqVO.setSendTime(buildBetweenTime(2020, 11, 1, 2020, 11, 30));
reqVO.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus());
reqVO.setReceiveTime(buildBetweenTime(2021, 11, 1, 2021, 11, 30));
// mock 数据
SmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到
o.setChannelId(1L);
o.setTemplateId(10L);
o.setMobile("15601691300");
o.setSendStatus(SmsSendStatusEnum.INIT.getStatus());
o.setSendTime(buildTime(2020, 11, 11));
o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus());
o.setReceiveTime(buildTime(2021, 11, 11));
});
smsLogMapper.insert(dbSmsLog);
// 测试 channelId 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setChannelId(2L)));
// 测试 templateId 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setTemplateId(20L)));
// 测试 mobile 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setMobile("18818260999")));
// 测试 sendStatus 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus())));
// 测试 sendTime 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12))));
// 测试 receiveStatus 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveStatus(SmsReceiveStatusEnum.SUCCESS.getStatus())));
// 测试 receiveTime 不匹配
smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12))));
// 准备参数
SmsLogPageReqVO reqVO = new SmsLogPageReqVO();
reqVO.setChannelId(1L);
reqVO.setTemplateId(10L);
reqVO.setMobile("156");
reqVO.setSendStatus(SmsSendStatusEnum.INIT.getStatus());
reqVO.setSendTime(buildBetweenTime(2020, 11, 1, 2020, 11, 30));
reqVO.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus());
reqVO.setReceiveTime(buildBetweenTime(2021, 11, 1, 2021, 11, 30));
// 调用
PageResult<SmsLogDO> pageResult = smsLogService.getSmsLogPage(reqVO);
// 断言
assertEquals(1, pageResult.getTotal());
assertEquals(1, pageResult.getList().size());
assertPojoEquals(dbSmsLog, pageResult.getList().get(0));
// 调用
PageResult<SmsLogDO> pageResult = smsLogService.getSmsLogPage(reqVO);
// 断言
assertEquals(1, pageResult.getTotal());
assertEquals(1, pageResult.getList().size());
assertPojoEquals(dbSmsLog, pageResult.getList().get(0));
}
@Test
@@ -153,13 +153,14 @@ public class SmsLogServiceImplTest extends BaseDbUnitTest {
smsLogMapper.insert(dbSmsLog);
// 准备参数
Long id = dbSmsLog.getId();
String apiSerialNo = dbSmsLog.getApiSerialNo();
Boolean success = randomBoolean();
LocalDateTime receiveTime = randomLocalDateTime();
String apiReceiveCode = randomString();
String apiReceiveMsg = randomString();
// 调用
smsLogService.updateSmsReceiveResult(id, success, receiveTime, apiReceiveCode, apiReceiveMsg);
smsLogService.updateSmsReceiveResult(id, apiSerialNo, success, receiveTime, apiReceiveCode, apiReceiveMsg);
// 断言
dbSmsLog = smsLogMapper.selectById(id);
assertEquals(success ? SmsReceiveStatusEnum.SUCCESS.getStatus()

View File

@@ -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())));
}