Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro
# Conflicts: # yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java # yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java # yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java # yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java # yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java # yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java # yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppService.java # yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java # yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java # yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java # yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletTransactionServiceImpl.java # yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java # yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java
This commit is contained in:
commit
e2197551ec
|
@ -18,7 +18,7 @@
|
|||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。
|
||||
ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维导图等功能。
|
||||
目前已接入各种模型,不限于:
|
||||
国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
|
||||
国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
|
||||
|
|
|
@ -22,7 +22,7 @@ public enum AiChatRoleEnum implements IntArrayValuable {
|
|||
除此之外不需要除了正文内容外的其他回复,如标题、开头、任何解释性语句或道歉。
|
||||
"""),
|
||||
|
||||
AI_MIND_MAP_ROLE(2, "脑图助手", """
|
||||
AI_MIND_MAP_ROLE(2, "导图助手", """
|
||||
你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子:
|
||||
# Geek-AI 助手
|
||||
## 完整的开源系统
|
||||
|
|
|
@ -45,9 +45,11 @@ public interface ErrorCodeConstants {
|
|||
// ========== API 音乐 1-040-006-000 ==========
|
||||
ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!");
|
||||
|
||||
|
||||
// ========== API 写作 1-022-007-000 ==========
|
||||
ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!");
|
||||
ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "写作生成异常!");
|
||||
|
||||
// ========== API 思维导图 1-040-008-000 ==========
|
||||
ErrorCode MIND_MAP_NOT_EXISTS = new ErrorCode(1_040_008_000, "思维导图不存在!");
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。
|
||||
ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维导图等功能。
|
||||
目前已接入各种模型,不限于:
|
||||
国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
|
||||
国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
|
||||
|
|
|
@ -5,10 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
|
|||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageRespVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
|
||||
|
@ -45,6 +42,13 @@ public class AiImageController {
|
|||
return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/public-page")
|
||||
@Operation(summary = "获取公开的绘图分页")
|
||||
public CommonResult<PageResult<AiImageRespVO>> getImagePagePublic(AiImagePublicPageReqVO pageReqVO) {
|
||||
PageResult<AiImageDO> pageResult = imageService.getImagePagePublic(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/get-my")
|
||||
@Operation(summary = "获取【我的】绘图记录")
|
||||
@Parameter(name = "id", required = true, description = "绘画编号", example = "1024")
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - AI 绘画公开的分页 Request VO")
|
||||
@Data
|
||||
public class AiImagePublicPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "提示词")
|
||||
private String prompt;
|
||||
|
||||
}
|
|
@ -1,20 +1,25 @@
|
|||
package cn.iocoder.yudao.module.ai.controller.admin.mindmap;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapRespVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
|
||||
import cn.iocoder.yudao.module.ai.service.mindmap.AiMindMapService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||
|
||||
@Tag(name = "管理后台 - AI 思维导图")
|
||||
|
@ -26,10 +31,29 @@ public class AiMindMapController {
|
|||
private AiMindMapService mindMapService;
|
||||
|
||||
@PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
@Operation(summary = "脑图生成(流式)", description = "流式返回,响应较快")
|
||||
@Operation(summary = "导图生成(流式)", description = "流式返回,响应较快")
|
||||
@PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
|
||||
public Flux<CommonResult<String>> generateMindMap(@RequestBody @Valid AiMindMapGenerateReqVO generateReqVO) {
|
||||
return mindMapService.generateMindMap(generateReqVO, getLoginUserId());
|
||||
}
|
||||
|
||||
// ================ 导图管理 ================
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除思维导图")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('ai:mind-map:delete')")
|
||||
public CommonResult<Boolean> deleteMindMap(@RequestParam("id") Long id) {
|
||||
mindMapService.deleteMindMap(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得思维导图分页")
|
||||
@PreAuthorize("@ss.hasPermission('ai:mind-map:query')")
|
||||
public CommonResult<PageResult<AiMindMapRespVO>> getMindMapPage(@Valid AiMindMapPageReqVO pageReqVO) {
|
||||
PageResult<AiMindMapDO> pageResult = mindMapService.getMindMapPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, AiMindMapRespVO.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - AI 思维导图分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class AiMindMapPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "用户编号", example = "4325")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "生成内容提示", example = "Java 学习路线")
|
||||
private String prompt;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - AI 思维导图 Response VO")
|
||||
@Data
|
||||
public class AiMindMapRespVO {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3373")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4325")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "生成内容提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 学习路线")
|
||||
private String prompt;
|
||||
|
||||
@Schema(description = "生成的思维导图内容")
|
||||
private String generatedContent;
|
||||
|
||||
@Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
|
||||
private String platform;
|
||||
|
||||
@Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo-0125")
|
||||
private String model;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMessage;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
|
@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
|||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePublicPageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
|
@ -41,6 +42,13 @@ public interface AiImageMapper extends BaseMapperX<AiImageDO> {
|
|||
.orderByDesc(AiImageDO::getId));
|
||||
}
|
||||
|
||||
default PageResult<AiImageDO> selectPage(AiImagePublicPageReqVO pageReqVO) {
|
||||
return selectPage(pageReqVO, new LambdaQueryWrapperX<AiImageDO>()
|
||||
.eqIfPresent(AiImageDO::getPublicStatus, Boolean.TRUE)
|
||||
.likeIfPresent(AiImageDO::getPrompt, pageReqVO.getPrompt())
|
||||
.orderByDesc(AiImageDO::getId));
|
||||
}
|
||||
|
||||
default List<AiImageDO> selectListByStatusAndPlatform(Integer status, String platform) {
|
||||
return selectList(AiImageDO::getStatus, status,
|
||||
AiImageDO::getPlatform, platform);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package cn.iocoder.yudao.module.ai.dal.mysql.mindmap;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
|
@ -11,4 +14,13 @@ import org.apache.ibatis.annotations.Mapper;
|
|||
*/
|
||||
@Mapper
|
||||
public interface AiMindMapMapper extends BaseMapperX<AiMindMapDO> {
|
||||
|
||||
default PageResult<AiMindMapDO> selectPage(AiMindMapPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<AiMindMapDO>()
|
||||
.eqIfPresent(AiMindMapDO::getUserId, reqVO.getUserId())
|
||||
.eqIfPresent(AiMindMapDO::getPrompt, reqVO.getPrompt())
|
||||
.betweenIfPresent(AiMindMapDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(AiMindMapDO::getId));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,9 +2,7 @@ package cn.iocoder.yudao.module.ai.service.image;
|
|||
|
||||
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
|
||||
|
@ -28,6 +26,14 @@ public interface AiImageService {
|
|||
*/
|
||||
PageResult<AiImageDO> getImagePageMy(Long userId, AiImagePageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 获取公开的绘图分页
|
||||
*
|
||||
* @param pageReqVO 分页条件
|
||||
* @return 绘图分页
|
||||
*/
|
||||
PageResult<AiImageDO> getImagePagePublic(AiImagePublicPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 获得绘图记录
|
||||
*
|
||||
|
|
|
@ -12,9 +12,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
|
|||
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
|
||||
|
@ -70,6 +68,11 @@ public class AiImageServiceImpl implements AiImageService {
|
|||
return imageMapper.selectPageMy(userId, pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<AiImageDO> getImagePagePublic(AiImagePublicPageReqVO pageReqVO) {
|
||||
return imageMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiImageDO getImage(Long id) {
|
||||
return imageMapper.selectById(id);
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package cn.iocoder.yudao.module.ai.service.knowledge;
|
||||
|
||||
/**
|
||||
* AI 知识库 Service 接口
|
||||
*
|
||||
* @author xiaoxin
|
||||
*/
|
||||
public interface DocService {
|
||||
|
||||
/**
|
||||
* 向量化文档
|
||||
*/
|
||||
void embeddingDoc();
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package cn.iocoder.yudao.module.ai.service.knowledge;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.ai.reader.tika.TikaDocumentReader;
|
||||
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
|
||||
import org.springframework.ai.vectorstore.RedisVectorStore;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 知识库 Service 实现类
|
||||
*
|
||||
* @author xiaoxin
|
||||
*/
|
||||
//@Service // TODO 芋艿:临时注释,避免无法启动
|
||||
@Slf4j
|
||||
public class DocServiceImpl implements DocService {
|
||||
|
||||
@Resource
|
||||
private RedisVectorStore vectorStore;
|
||||
@Resource
|
||||
private TokenTextSplitter tokenTextSplitter;
|
||||
|
||||
// TODO @xin 临时测试用,后续删
|
||||
@Value("classpath:/webapp/test/Fel.pdf")
|
||||
private org.springframework.core.io.Resource data;
|
||||
|
||||
@Override
|
||||
public void embeddingDoc() {
|
||||
// 读取文件
|
||||
TikaDocumentReader loader = new TikaDocumentReader(data);
|
||||
List<Document> documents = loader.get();
|
||||
// 文档分段
|
||||
List<Document> segments = tokenTextSplitter.apply(documents);
|
||||
// 向量化并存储
|
||||
vectorStore.add(segments);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
package cn.iocoder.yudao.module.ai.service.mindmap;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
|
@ -20,4 +23,19 @@ public interface AiMindMapService {
|
|||
*/
|
||||
Flux<CommonResult<String>> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId);
|
||||
|
||||
/**
|
||||
* 删除思维导图
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteMindMap(Long id);
|
||||
|
||||
/**
|
||||
* 获得思维导图分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 思维导图分页
|
||||
*/
|
||||
PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO);
|
||||
|
||||
}
|
||||
|
|
|
@ -6,9 +6,11 @@ import cn.hutool.core.util.StrUtil;
|
|||
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
|
||||
import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
|
||||
|
@ -33,8 +35,10 @@ import reactor.core.publisher.Flux;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MIND_MAP_NOT_EXISTS;
|
||||
|
||||
/**
|
||||
* AI 思维导图 Service 实现类
|
||||
|
@ -57,10 +61,10 @@ public class AiMindMapServiceImpl implements AiMindMapService {
|
|||
|
||||
@Override
|
||||
public Flux<CommonResult<String>> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId) {
|
||||
// 1. 获取脑图模型。尝试获取思维导图助手角色,如果没有则使用默认模型
|
||||
// 1. 获取导图模型。尝试获取思维导图助手角色,如果没有则使用默认模型
|
||||
AiChatRoleDO role = CollUtil.getFirst(
|
||||
chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_MIND_MAP_ROLE.getName()));
|
||||
// 1.1 获取脑图执行模型
|
||||
// 1.1 获取导图执行模型
|
||||
AiChatModelDO model = getModel(role);
|
||||
// 1.2 获取角色设定消息
|
||||
String systemMessage = role != null && StrUtil.isNotBlank(role.getSystemMessage())
|
||||
|
@ -131,4 +135,23 @@ public class AiMindMapServiceImpl implements AiMindMapService {
|
|||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMindMap(Long id) {
|
||||
// 校验存在
|
||||
validateMindMapExists(id);
|
||||
// 删除
|
||||
mindMapMapper.deleteById(id);
|
||||
}
|
||||
|
||||
private void validateMindMapExists(Long id) {
|
||||
if (mindMapMapper.selectById(id) == null) {
|
||||
throw exception(MIND_MAP_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO) {
|
||||
return mindMapMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,12 +23,16 @@
|
|||
<artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
|
||||
|
@ -40,6 +44,30 @@
|
|||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 向量化,基于 Redis 存储,Tika 解析内容 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-transformers-spring-boot-starter</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-tika-document-reader</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-redis-store</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- TODO @xin:引入我们项目的 starter -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-redis</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-common</artifactId>
|
||||
|
|
|
@ -10,11 +10,20 @@ import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
|
|||
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
|
||||
import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties;
|
||||
import org.springframework.ai.document.MetadataMode;
|
||||
import org.springframework.ai.embedding.EmbeddingModel;
|
||||
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
|
||||
import org.springframework.ai.transformers.TransformersEmbeddingModel;
|
||||
import org.springframework.ai.vectorstore.RedisVectorStore;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import redis.clients.jedis.JedisPooled;
|
||||
|
||||
/**
|
||||
* 芋道 AI 自动配置
|
||||
|
@ -73,4 +82,36 @@ public class YudaoAiAutoConfiguration {
|
|||
return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl());
|
||||
}
|
||||
|
||||
// ========== rag 相关 ==========
|
||||
@Bean
|
||||
@Lazy // TODO 芋艿:临时注释,避免无法启动
|
||||
public EmbeddingModel transformersEmbeddingClient() {
|
||||
return new TransformersEmbeddingModel(MetadataMode.EMBED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 我们启动有加载很多 Embedding 模型,不晓得取哪个好,先 new 个 TransformersEmbeddingModel 跑
|
||||
*/
|
||||
@Bean
|
||||
@Lazy // TODO 芋艿:临时注释,避免无法启动
|
||||
public RedisVectorStore vectorStore(TransformersEmbeddingModel transformersEmbeddingModel, RedisVectorStoreProperties properties,
|
||||
RedisProperties redisProperties) {
|
||||
var config = RedisVectorStore.RedisVectorStoreConfig.builder()
|
||||
.withIndexName(properties.getIndex())
|
||||
.withPrefix(properties.getPrefix())
|
||||
.build();
|
||||
|
||||
RedisVectorStore redisVectorStore = new RedisVectorStore(config, transformersEmbeddingModel,
|
||||
new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
|
||||
properties.isInitializeSchema());
|
||||
redisVectorStore.afterPropertiesSet();
|
||||
return redisVectorStore;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy // TODO 芋艿:临时注释,避免无法启动
|
||||
public TokenTextSplitter tokenTextSplitter() {
|
||||
return new TokenTextSplitter(500, 100, 5, 10000, true);
|
||||
}
|
||||
|
||||
}
|
|
@ -22,7 +22,8 @@ public enum AiPlatformEnum {
|
|||
|
||||
// ========== 国外平台 ==========
|
||||
|
||||
OPENAI("OpenAI", "OpenAI"),
|
||||
OPENAI("OpenAI", "OpenAI"), // OpenAI 官方
|
||||
AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软
|
||||
OLLAMA("Ollama", "Ollama"),
|
||||
|
||||
STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI
|
||||
|
|
|
@ -21,6 +21,10 @@ import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel;
|
|||
import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties;
|
||||
import com.alibaba.dashscope.aigc.generation.Generation;
|
||||
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
|
||||
import com.azure.ai.openai.OpenAIClient;
|
||||
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties;
|
||||
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties;
|
||||
import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration;
|
||||
|
@ -31,6 +35,7 @@ import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration;
|
|||
import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiChatProperties;
|
||||
import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties;
|
||||
import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiImageProperties;
|
||||
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.image.ImageModel;
|
||||
import org.springframework.ai.model.function.FunctionCallbackContext;
|
||||
|
@ -82,6 +87,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
return buildXingHuoChatModel(apiKey);
|
||||
case OPENAI:
|
||||
return buildOpenAiChatModel(apiKey, url);
|
||||
case AZURE_OPENAI:
|
||||
return buildAzureOpenAiChatModel(apiKey, url);
|
||||
case OLLAMA:
|
||||
return buildOllamaChatModel(url);
|
||||
default:
|
||||
|
@ -106,6 +113,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
return SpringUtil.getBean(XingHuoChatModel.class);
|
||||
case OPENAI:
|
||||
return SpringUtil.getBean(OpenAiChatModel.class);
|
||||
case AZURE_OPENAI:
|
||||
return SpringUtil.getBean(AzureOpenAiChatModel.class);
|
||||
case OLLAMA:
|
||||
return SpringUtil.getBean(OllamaChatModel.class);
|
||||
default:
|
||||
|
@ -268,6 +277,21 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
return new OpenAiChatModel(openAiApi);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link AzureOpenAiAutoConfiguration}
|
||||
*/
|
||||
private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) {
|
||||
AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration();
|
||||
// 创建 OpenAIClient 对象
|
||||
AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties();
|
||||
connectionProperties.setApiKey(apiKey);
|
||||
connectionProperties.setEndpoint(url);
|
||||
OpenAIClient openAIClient = azureOpenAiAutoConfiguration.openAIClient(connectionProperties);
|
||||
// 获取 AzureOpenAiChatProperties 对象
|
||||
AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class);
|
||||
return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link OpenAiAutoConfiguration}
|
||||
*/
|
||||
|
|
|
@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
|
|||
import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions;
|
||||
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
|
||||
import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
|
||||
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
|
||||
import org.springframework.ai.chat.messages.*;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.ollama.api.OllamaOptions;
|
||||
|
@ -35,6 +36,9 @@ public class AiUtils {
|
|||
return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build();
|
||||
case OPENAI:
|
||||
return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
|
||||
case AZURE_OPENAI:
|
||||
// TODO 芋艿:貌似没 model 字段???!
|
||||
return AzureOpenAiChatOptions.builder().withDeploymentName(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
|
||||
case OLLAMA:
|
||||
return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
|
||||
default:
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2023 - 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.ai.autoconfigure.vectorstore.redis;
|
||||
|
||||
import org.springframework.ai.embedding.EmbeddingModel;
|
||||
import org.springframework.ai.vectorstore.RedisVectorStore;
|
||||
import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import redis.clients.jedis.JedisPooled;
|
||||
|
||||
/**
|
||||
* TODO @xin 先拿 spring-ai 最新代码覆盖,1.0.0-M1 跟 redis 自动配置会冲突
|
||||
*
|
||||
* TODO 这个官方,有说啥时候 fix 哇?
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Eddú Meléndez
|
||||
*/
|
||||
@AutoConfiguration(after = RedisAutoConfiguration.class)
|
||||
@ConditionalOnClass({JedisPooled.class, JedisConnectionFactory.class, RedisVectorStore.class, EmbeddingModel.class})
|
||||
//@ConditionalOnBean(JedisConnectionFactory.class)
|
||||
@EnableConfigurationProperties(RedisVectorStoreProperties.class)
|
||||
public class RedisVectorStoreAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties,
|
||||
JedisConnectionFactory jedisConnectionFactory) {
|
||||
|
||||
var config = RedisVectorStoreConfig.builder()
|
||||
.withIndexName(properties.getIndex())
|
||||
.withPrefix(properties.getPrefix())
|
||||
.build();
|
||||
|
||||
return new RedisVectorStore(config, embeddingModel,
|
||||
new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()),
|
||||
properties.isInitializeSchema());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,456 @@
|
|||
/*
|
||||
* Copyright 2023 - 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.ai.vectorstore;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.ai.embedding.EmbeddingModel;
|
||||
import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import redis.clients.jedis.JedisPooled;
|
||||
import redis.clients.jedis.Pipeline;
|
||||
import redis.clients.jedis.json.Path2;
|
||||
import redis.clients.jedis.search.*;
|
||||
import redis.clients.jedis.search.Schema.FieldType;
|
||||
import redis.clients.jedis.search.schemafields.*;
|
||||
import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The RedisVectorStore is for managing and querying vector data in a Redis database. It
|
||||
* offers functionalities like adding, deleting, and performing similarity searches on
|
||||
* documents.
|
||||
*
|
||||
* The store utilizes RedisJSON and RedisSearch to handle JSON documents and to index and
|
||||
* search vector data. It supports various vector algorithms (e.g., FLAT, HSNW) for
|
||||
* efficient similarity searches. Additionally, it allows for custom metadata fields in
|
||||
* the documents to be stored alongside the vector and content data.
|
||||
*
|
||||
* This class requires a RedisVectorStoreConfig configuration object for initialization,
|
||||
* which includes settings like Redis URI, index name, field names, and vector algorithms.
|
||||
* It also requires an EmbeddingModel to convert documents into embeddings before storing
|
||||
* them.
|
||||
*
|
||||
* @author Julien Ruaux
|
||||
* @author Christian Tzolov
|
||||
* @author Eddú Meléndez
|
||||
* @see VectorStore
|
||||
* @see RedisVectorStoreConfig
|
||||
* @see EmbeddingModel
|
||||
*/
|
||||
public class RedisVectorStore implements VectorStore, InitializingBean {
|
||||
|
||||
public enum Algorithm {
|
||||
|
||||
FLAT, HSNW
|
||||
|
||||
}
|
||||
|
||||
public record MetadataField(String name, FieldType fieldType) {
|
||||
|
||||
public static MetadataField text(String name) {
|
||||
return new MetadataField(name, FieldType.TEXT);
|
||||
}
|
||||
|
||||
public static MetadataField numeric(String name) {
|
||||
return new MetadataField(name, FieldType.NUMERIC);
|
||||
}
|
||||
|
||||
public static MetadataField tag(String name) {
|
||||
return new MetadataField(name, FieldType.TAG);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for the Redis vector store.
|
||||
*/
|
||||
public static final class RedisVectorStoreConfig {
|
||||
|
||||
private final String indexName;
|
||||
|
||||
private final String prefix;
|
||||
|
||||
private final String contentFieldName;
|
||||
|
||||
private final String embeddingFieldName;
|
||||
|
||||
private final Algorithm vectorAlgorithm;
|
||||
|
||||
private final List<MetadataField> metadataFields;
|
||||
|
||||
private RedisVectorStoreConfig() {
|
||||
this(builder());
|
||||
}
|
||||
|
||||
private RedisVectorStoreConfig(Builder builder) {
|
||||
this.indexName = builder.indexName;
|
||||
this.prefix = builder.prefix;
|
||||
this.contentFieldName = builder.contentFieldName;
|
||||
this.embeddingFieldName = builder.embeddingFieldName;
|
||||
this.vectorAlgorithm = builder.vectorAlgorithm;
|
||||
this.metadataFields = builder.metadataFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start building a new configuration.
|
||||
* @return The entry point for creating a new configuration.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the default config}
|
||||
*/
|
||||
public static RedisVectorStoreConfig defaultConfig() {
|
||||
|
||||
return builder().build();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private String indexName = DEFAULT_INDEX_NAME;
|
||||
|
||||
private String prefix = DEFAULT_PREFIX;
|
||||
|
||||
private String contentFieldName = DEFAULT_CONTENT_FIELD_NAME;
|
||||
|
||||
private String embeddingFieldName = DEFAULT_EMBEDDING_FIELD_NAME;
|
||||
|
||||
private Algorithm vectorAlgorithm = DEFAULT_VECTOR_ALGORITHM;
|
||||
|
||||
private List<MetadataField> metadataFields = new ArrayList<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the Redis index name to use.
|
||||
* @param name the index name to use
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withIndexName(String name) {
|
||||
this.indexName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the Redis key prefix to use (default: "embedding:").
|
||||
* @param prefix the prefix to use
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withPrefix(String prefix) {
|
||||
this.prefix = prefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the Redis content field name to use.
|
||||
* @param name the content field name to use
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withContentFieldName(String name) {
|
||||
this.contentFieldName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the Redis embedding field name to use.
|
||||
* @param name the embedding field name to use
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withEmbeddingFieldName(String name) {
|
||||
this.embeddingFieldName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the Redis vector algorithmto use.
|
||||
* @param algorithm the vector algorithm to use
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withVectorAlgorithm(Algorithm algorithm) {
|
||||
this.vectorAlgorithm = algorithm;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withMetadataFields(MetadataField... fields) {
|
||||
return withMetadataFields(Arrays.asList(fields));
|
||||
}
|
||||
|
||||
public Builder withMetadataFields(List<MetadataField> fields) {
|
||||
this.metadataFields = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the immutable configuration}
|
||||
*/
|
||||
public RedisVectorStoreConfig build() {
|
||||
|
||||
return new RedisVectorStoreConfig(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final boolean initializeSchema;
|
||||
|
||||
public static final String DEFAULT_INDEX_NAME = "spring-ai-index";
|
||||
|
||||
public static final String DEFAULT_CONTENT_FIELD_NAME = "content";
|
||||
|
||||
public static final String DEFAULT_EMBEDDING_FIELD_NAME = "embedding";
|
||||
|
||||
public static final String DEFAULT_PREFIX = "embedding:";
|
||||
|
||||
public static final Algorithm DEFAULT_VECTOR_ALGORITHM = Algorithm.HSNW;
|
||||
|
||||
private static final String QUERY_FORMAT = "%s=>[KNN %s @%s $%s AS %s]";
|
||||
|
||||
private static final Path2 JSON_SET_PATH = Path2.of("$");
|
||||
|
||||
private static final String JSON_PATH_PREFIX = "$.";
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RedisVectorStore.class);
|
||||
|
||||
private static final Predicate<Object> RESPONSE_OK = Predicate.isEqual("OK");
|
||||
|
||||
private static final Predicate<Object> RESPONSE_DEL_OK = Predicate.isEqual(1l);
|
||||
|
||||
private static final String VECTOR_TYPE_FLOAT32 = "FLOAT32";
|
||||
|
||||
private static final String EMBEDDING_PARAM_NAME = "BLOB";
|
||||
|
||||
public static final String DISTANCE_FIELD_NAME = "vector_score";
|
||||
|
||||
private static final String DEFAULT_DISTANCE_METRIC = "COSINE";
|
||||
|
||||
private final JedisPooled jedis;
|
||||
|
||||
private final EmbeddingModel embeddingModel;
|
||||
|
||||
private final RedisVectorStoreConfig config;
|
||||
|
||||
private FilterExpressionConverter filterExpressionConverter;
|
||||
|
||||
public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis,
|
||||
boolean initializeSchema) {
|
||||
|
||||
Assert.notNull(config, "Config must not be null");
|
||||
Assert.notNull(embeddingModel, "Embedding model must not be null");
|
||||
this.initializeSchema = initializeSchema;
|
||||
|
||||
this.jedis = jedis;
|
||||
this.embeddingModel = embeddingModel;
|
||||
this.config = config;
|
||||
this.filterExpressionConverter = new RedisFilterExpressionConverter(this.config.metadataFields);
|
||||
}
|
||||
|
||||
public JedisPooled getJedis() {
|
||||
return this.jedis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(List<Document> documents) {
|
||||
try (Pipeline pipeline = this.jedis.pipelined()) {
|
||||
for (Document document : documents) {
|
||||
var embedding = this.embeddingModel.embed(document);
|
||||
document.setEmbedding(embedding);
|
||||
|
||||
var fields = new HashMap<String, Object>();
|
||||
fields.put(this.config.embeddingFieldName, embedding);
|
||||
fields.put(this.config.contentFieldName, document.getContent());
|
||||
fields.putAll(document.getMetadata());
|
||||
pipeline.jsonSetWithEscape(key(document.getId()), JSON_SET_PATH, fields);
|
||||
}
|
||||
List<Object> responses = pipeline.syncAndReturnAll();
|
||||
Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_OK)).findAny();
|
||||
if (errResponse.isPresent()) {
|
||||
String message = MessageFormat.format("Could not add document: {0}", errResponse.get());
|
||||
if (logger.isErrorEnabled()) {
|
||||
logger.error(message);
|
||||
}
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String key(String id) {
|
||||
return this.config.prefix + id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Boolean> delete(List<String> idList) {
|
||||
try (Pipeline pipeline = this.jedis.pipelined()) {
|
||||
for (String id : idList) {
|
||||
pipeline.jsonDel(key(id));
|
||||
}
|
||||
List<Object> responses = pipeline.syncAndReturnAll();
|
||||
Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_DEL_OK)).findAny();
|
||||
if (errResponse.isPresent()) {
|
||||
if (logger.isErrorEnabled()) {
|
||||
logger.error("Could not delete document: {}", errResponse.get());
|
||||
}
|
||||
return Optional.of(false);
|
||||
}
|
||||
return Optional.of(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Document> similaritySearch(SearchRequest request) {
|
||||
|
||||
Assert.isTrue(request.getTopK() > 0, "The number of documents to returned must be greater than zero");
|
||||
Assert.isTrue(request.getSimilarityThreshold() >= 0 && request.getSimilarityThreshold() <= 1,
|
||||
"The similarity score is bounded between 0 and 1; least to most similar respectively.");
|
||||
|
||||
String filter = nativeExpressionFilter(request);
|
||||
|
||||
String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), this.config.embeddingFieldName,
|
||||
EMBEDDING_PARAM_NAME, DISTANCE_FIELD_NAME);
|
||||
|
||||
List<String> returnFields = new ArrayList<>();
|
||||
this.config.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add);
|
||||
returnFields.add(this.config.embeddingFieldName);
|
||||
returnFields.add(this.config.contentFieldName);
|
||||
returnFields.add(DISTANCE_FIELD_NAME);
|
||||
var embedding = toFloatArray(this.embeddingModel.embed(request.getQuery()));
|
||||
Query query = new Query(queryString).addParam(EMBEDDING_PARAM_NAME, RediSearchUtil.toByteArray(embedding))
|
||||
.returnFields(returnFields.toArray(new String[0]))
|
||||
.setSortBy(DISTANCE_FIELD_NAME, true)
|
||||
.dialect(2);
|
||||
|
||||
SearchResult result = this.jedis.ftSearch(this.config.indexName, query);
|
||||
return result.getDocuments()
|
||||
.stream()
|
||||
.filter(d -> similarityScore(d) >= request.getSimilarityThreshold())
|
||||
.map(this::toDocument)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Document toDocument(redis.clients.jedis.search.Document doc) {
|
||||
var id = doc.getId().substring(this.config.prefix.length());
|
||||
var content = doc.hasProperty(this.config.contentFieldName) ? doc.getString(this.config.contentFieldName)
|
||||
: null;
|
||||
Map<String, Object> metadata = this.config.metadataFields.stream()
|
||||
.map(MetadataField::name)
|
||||
.filter(doc::hasProperty)
|
||||
.collect(Collectors.toMap(Function.identity(), doc::getString));
|
||||
metadata.put(DISTANCE_FIELD_NAME, 1 - similarityScore(doc));
|
||||
return new Document(id, content, metadata);
|
||||
}
|
||||
|
||||
private float similarityScore(redis.clients.jedis.search.Document doc) {
|
||||
return (2 - Float.parseFloat(doc.getString(DISTANCE_FIELD_NAME))) / 2;
|
||||
}
|
||||
|
||||
private String nativeExpressionFilter(SearchRequest request) {
|
||||
if (request.getFilterExpression() == null) {
|
||||
return "*";
|
||||
}
|
||||
return "(" + this.filterExpressionConverter.convertExpression(request.getFilterExpression()) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
|
||||
if (!this.initializeSchema) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If index already exists don't do anything
|
||||
if (this.jedis.ftList().contains(this.config.indexName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String response = this.jedis.ftCreate(this.config.indexName,
|
||||
FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(this.config.prefix), schemaFields());
|
||||
if (!RESPONSE_OK.test(response)) {
|
||||
String message = MessageFormat.format("Could not create index: {0}", response);
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private Iterable<SchemaField> schemaFields() {
|
||||
Map<String, Object> vectorAttrs = new HashMap<>();
|
||||
vectorAttrs.put("DIM", this.embeddingModel.dimensions());
|
||||
vectorAttrs.put("DISTANCE_METRIC", DEFAULT_DISTANCE_METRIC);
|
||||
vectorAttrs.put("TYPE", VECTOR_TYPE_FLOAT32);
|
||||
List<SchemaField> fields = new ArrayList<>();
|
||||
fields.add(TextField.of(jsonPath(this.config.contentFieldName)).as(this.config.contentFieldName).weight(1.0));
|
||||
fields.add(VectorField.builder()
|
||||
.fieldName(jsonPath(this.config.embeddingFieldName))
|
||||
.algorithm(vectorAlgorithm())
|
||||
.attributes(vectorAttrs)
|
||||
.as(this.config.embeddingFieldName)
|
||||
.build());
|
||||
|
||||
if (!CollectionUtils.isEmpty(this.config.metadataFields)) {
|
||||
for (MetadataField field : this.config.metadataFields) {
|
||||
fields.add(schemaField(field));
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private SchemaField schemaField(MetadataField field) {
|
||||
String fieldName = jsonPath(field.name);
|
||||
switch (field.fieldType) {
|
||||
case NUMERIC:
|
||||
return NumericField.of(fieldName).as(field.name);
|
||||
case TAG:
|
||||
return TagField.of(fieldName).as(field.name);
|
||||
case TEXT:
|
||||
return TextField.of(fieldName).as(field.name);
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
MessageFormat.format("Field {0} has unsupported type {1}", field.name, field.fieldType));
|
||||
}
|
||||
}
|
||||
|
||||
private VectorAlgorithm vectorAlgorithm() {
|
||||
if (config.vectorAlgorithm == Algorithm.HSNW) {
|
||||
return VectorAlgorithm.HNSW;
|
||||
}
|
||||
return VectorAlgorithm.FLAT;
|
||||
}
|
||||
|
||||
private String jsonPath(String field) {
|
||||
return JSON_PATH_PREFIX + field;
|
||||
}
|
||||
|
||||
private static float[] toFloatArray(List<Double> embeddingDouble) {
|
||||
float[] embeddingFloat = new float[embeddingDouble.size()];
|
||||
int i = 0;
|
||||
for (Double d : embeddingDouble) {
|
||||
embeddingFloat[i++] = d.floatValue();
|
||||
}
|
||||
return embeddingFloat;
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,70 @@
|
|||
package cn.iocoder.yudao.framework.ai.chat;
|
||||
|
||||
import com.azure.ai.openai.OpenAIClient;
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.azure.core.credential.AzureKeyCredential;
|
||||
import com.azure.core.util.ClientOptions;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
|
||||
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME;
|
||||
|
||||
/**
|
||||
* {@link AzureOpenAiChatModel} 集成测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class AzureOpenAIChatModelTests {
|
||||
|
||||
private final OpenAIClient openAiApi = (new OpenAIClientBuilder())
|
||||
.endpoint("https://eastusprejade.openai.azure.com")
|
||||
.credential(new AzureKeyCredential("xxx"))
|
||||
.clientOptions((new ClientOptions()).setApplicationId("spring-ai"))
|
||||
.buildClient();
|
||||
private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi,
|
||||
AzureOpenAiChatOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build());
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testCall() {
|
||||
// 准备参数
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
|
||||
// 调用
|
||||
ChatResponse response = chatModel.call(new Prompt(messages));
|
||||
// 打印结果
|
||||
System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testStream() {
|
||||
// 准备参数
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package cn.iocoder.yudao.framework.ai.chat;
|
||||
|
||||
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
|
@ -17,7 +16,7 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link XingHuoChatModel} 集成测试
|
||||
* {@link OpenAiChatModel} 集成测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
|
|
|
@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.product.api.spu.dto;
|
|||
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
|
||||
import lombok.Data;
|
||||
|
||||
// TODO @LeeYan9: ProductSpuRespDTO
|
||||
/**
|
||||
* 商品 SPU 信息 Response DTO
|
||||
*
|
||||
|
|
|
@ -148,5 +148,4 @@ public class AppProductSpuController {
|
|||
return price - newPrice;
|
||||
}
|
||||
|
||||
// TODO 芋艿:商品的浏览记录;
|
||||
}
|
||||
|
|
|
@ -48,6 +48,4 @@ public class ProductBrandDO extends BaseDO {
|
|||
*/
|
||||
private Integer status;
|
||||
|
||||
// TODO 芋艿:firstLetter 首字母
|
||||
|
||||
}
|
||||
|
|
|
@ -130,11 +130,5 @@ public class ProductSkuDO extends BaseDO {
|
|||
|
||||
}
|
||||
|
||||
// TODO 芋艿:integral from y
|
||||
// TODO 芋艿:pinkPrice from y
|
||||
// TODO 芋艿:seckillPrice from y
|
||||
// TODO 芋艿:pinkStock from y
|
||||
// TODO 芋艿:seckillStock from y
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -10,23 +10,20 @@ import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.record.Ap
|
|||
import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.record.AppCombinationRecordSummaryRespVO;
|
||||
import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivityConvert;
|
||||
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
|
||||
import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
|
||||
import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordService;
|
||||
import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.Max;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
@ -43,9 +40,6 @@ public class AppCombinationRecordController {
|
|||
|
||||
@Resource
|
||||
private CombinationRecordService combinationRecordService;
|
||||
@Resource
|
||||
@Lazy
|
||||
private TradeOrderApi tradeOrderApi;
|
||||
|
||||
@GetMapping("/get-summary")
|
||||
@Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页")
|
||||
|
@ -117,26 +111,4 @@ public class AppCombinationRecordController {
|
|||
return success(CombinationActivityConvert.INSTANCE.convert(getLoginUserId(), headRecord, memberRecords));
|
||||
}
|
||||
|
||||
@GetMapping("/cancel")
|
||||
@Operation(summary = "取消拼团")
|
||||
@Parameter(name = "id", description = "拼团记录编号", required = true, example = "1024")
|
||||
public CommonResult<Boolean> cancelCombinationRecord(@RequestParam("id") Long id) {
|
||||
Long userId = getLoginUserId();
|
||||
// 1、查找这条拼团记录
|
||||
CombinationRecordDO record = combinationRecordService.getCombinationRecordByIdAndUser(userId, id);
|
||||
if (record == null) {
|
||||
return success(Boolean.FALSE);
|
||||
}
|
||||
// 1.1、需要先校验拼团记录未完成;
|
||||
if (!CombinationRecordStatusEnum.isInProgress(record.getStatus())) {
|
||||
return success(Boolean.FALSE);
|
||||
}
|
||||
|
||||
// 2. 取消已支付的订单
|
||||
tradeOrderApi.cancelPaidOrder(userId, record.getOrderId());
|
||||
// 3. 取消拼团记录
|
||||
combinationRecordService.cancelCombinationRecord(userId, record.getId(), record.getHeadId());
|
||||
return success(Boolean.TRUE);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -72,7 +72,6 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
|
|||
default PageResult<SeckillActivityDO> selectPage(AppSeckillActivityPageReqVO pageReqVO, Integer status) {
|
||||
return selectPage(pageReqVO, new LambdaQueryWrapperX<SeckillActivityDO>()
|
||||
.eqIfPresent(SeckillActivityDO::getStatus, status)
|
||||
// TODO 芋艿:对 find in set 的想法;
|
||||
.apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), "FIND_IN_SET(" + pageReqVO.getConfigId() + ",config_ids) > 0"));
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import org.springframework.stereotype.Component;
|
|||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
// TODO 芋艿:配置一个 Job
|
||||
/**
|
||||
* 优惠券过期 Job
|
||||
*
|
||||
|
|
|
@ -139,24 +139,6 @@ public interface CombinationRecordService {
|
|||
@Nullable Integer status,
|
||||
@Nullable Long headId);
|
||||
|
||||
/**
|
||||
* 获取拼团记录
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param id 拼团记录编号
|
||||
* @return 拼团记录
|
||||
*/
|
||||
CombinationRecordDO getCombinationRecordByIdAndUser(Long userId, Long id);
|
||||
|
||||
/**
|
||||
* 取消拼团
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param id 拼团记录编号
|
||||
* @param headId 团长编号
|
||||
*/
|
||||
void cancelCombinationRecord(Long userId, Long id, Long headId);
|
||||
|
||||
/**
|
||||
* 处理过期拼团
|
||||
*
|
||||
|
|
|
@ -69,7 +69,6 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
|
|||
private ProductSpuApi productSpuApi;
|
||||
@Resource
|
||||
private ProductSkuApi productSkuApi;
|
||||
|
||||
@Resource
|
||||
@Lazy // 延迟加载,避免循环依赖
|
||||
private TradeOrderApi tradeOrderApi;
|
||||
|
@ -289,61 +288,6 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
|
|||
return combinationRecordMapper.selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId(activityIds, status, headId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinationRecordDO getCombinationRecordByIdAndUser(Long userId, Long id) {
|
||||
return combinationRecordMapper.selectOne(CombinationRecordDO::getUserId, userId, CombinationRecordDO::getId, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void cancelCombinationRecord(Long userId, Long id, Long headId) {
|
||||
// 删除记录
|
||||
combinationRecordMapper.deleteById(id);
|
||||
|
||||
// 需要更新的记录
|
||||
List<CombinationRecordDO> updateRecords = new ArrayList<>();
|
||||
// 如果它是团长,则顺序(下单时间)继承
|
||||
if (Objects.equals(headId, CombinationRecordDO.HEAD_ID_GROUP)) { // 情况一:团长
|
||||
// 团员
|
||||
List<CombinationRecordDO> list = getCombinationRecordListByHeadId(id);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
// 按照创建时间升序排序
|
||||
list.sort(Comparator.comparing(CombinationRecordDO::getCreateTime)); // 影响原 list
|
||||
CombinationRecordDO newHead = list.get(0); // 新团长继位
|
||||
list.forEach(item -> {
|
||||
CombinationRecordDO recordDO = new CombinationRecordDO();
|
||||
recordDO.setId(item.getId());
|
||||
if (ObjUtil.equal(item.getId(), newHead.getId())) { // 新团长
|
||||
recordDO.setHeadId(CombinationRecordDO.HEAD_ID_GROUP);
|
||||
} else {
|
||||
recordDO.setHeadId(newHead.getId());
|
||||
}
|
||||
recordDO.setUserCount(list.size());
|
||||
updateRecords.add(recordDO);
|
||||
});
|
||||
} else { // 情况二:团员
|
||||
// 团长
|
||||
CombinationRecordDO recordHead = combinationRecordMapper.selectById(headId);
|
||||
// 团员
|
||||
List<CombinationRecordDO> records = getCombinationRecordListByHeadId(headId);
|
||||
if (CollUtil.isEmpty(records)) {
|
||||
return;
|
||||
}
|
||||
records.add(recordHead); // 加入团长,团长数据也需要更新
|
||||
records.forEach(item -> {
|
||||
CombinationRecordDO recordDO = new CombinationRecordDO();
|
||||
recordDO.setId(item.getId());
|
||||
recordDO.setUserCount(records.size());
|
||||
updateRecords.add(recordDO);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新拼团记录
|
||||
combinationRecordMapper.updateBatch(updateRecords);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyValue<Integer, Integer> expireCombinationRecord() {
|
||||
// 1. 获取所有正在进行中的过期的父拼团
|
||||
|
|
|
@ -7,12 +7,9 @@ import cn.hutool.core.util.StrUtil;
|
|||
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
|
||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
||||
import cn.iocoder.yudao.module.statistics.service.product.ProductStatisticsService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置
|
||||
|
||||
/**
|
||||
* 商品统计 Job
|
||||
*
|
||||
|
|
|
@ -11,7 +11,6 @@ import org.springframework.stereotype.Component;
|
|||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置
|
||||
/**
|
||||
* 交易统计 Job
|
||||
*
|
||||
|
|
|
@ -21,6 +21,9 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
|
|||
@ToString(callSuper = true)
|
||||
public class AfterSalePageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "用户编号", example = "1024")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "售后流水号", example = "202211190847450020500077")
|
||||
private String no;
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@ public interface TradeOrderConvert {
|
|||
default PayOrderCreateReqDTO convert(TradeOrderDO order, List<TradeOrderItemDO> orderItems,
|
||||
TradeOrderProperties orderProperties) {
|
||||
PayOrderCreateReqDTO createReqDTO = new PayOrderCreateReqDTO()
|
||||
.setAppId(orderProperties.getAppId()).setUserIp(order.getUserIp());
|
||||
.setAppKey(orderProperties.getPayAppKey()).setUserIp(order.getUserIp());
|
||||
// 商户相关字段
|
||||
createReqDTO.setMerchantOrderId(String.valueOf(order.getId()));
|
||||
String subject = orderItems.get(0).getSpuName();
|
||||
|
|
|
@ -16,6 +16,7 @@ public interface AfterSaleMapper extends BaseMapperX<AfterSaleDO> {
|
|||
|
||||
default PageResult<AfterSaleDO> selectPage(AfterSalePageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<AfterSaleDO>()
|
||||
.eqIfPresent(AfterSaleDO::getUserId, reqVO.getUserId())
|
||||
.likeIfPresent(AfterSaleDO::getNo, reqVO.getNo())
|
||||
.eqIfPresent(AfterSaleDO::getStatus, reqVO.getStatus())
|
||||
.eqIfPresent(AfterSaleDO::getType, reqVO.getType())
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
/**
|
||||
* TODO 占位
|
||||
*/
|
||||
package cn.iocoder.yudao.module.trade.dal.mysql;
|
|
@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.trade.framework.order.config;
|
|||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
// TODO @LeeYan9: 可以直接给 TradeOrderProperties 一个 @Component生效哈
|
||||
/**
|
||||
* @author LeeYan9
|
||||
* @since 2022-09-15
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package cn.iocoder.yudao.module.trade.framework.order.config;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
|
@ -18,11 +20,15 @@ import java.time.Duration;
|
|||
@Validated
|
||||
public class TradeOrderProperties {
|
||||
|
||||
private static final String PAY_APP_KEY_DEFAULT = "mall";
|
||||
|
||||
/**
|
||||
* 应用编号
|
||||
* 支付应用标识
|
||||
*
|
||||
* 在 pay 模块的 [支付管理 -> 应用信息] 里添加
|
||||
*/
|
||||
@NotNull(message = "应用编号不能为空")
|
||||
private Long appId;
|
||||
@NotEmpty(message = "Pay 应用标识不能为空")
|
||||
private String payAppKey = PAY_APP_KEY_DEFAULT;
|
||||
|
||||
/**
|
||||
* 支付超时时间
|
||||
|
|
|
@ -125,7 +125,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
|
|||
|
||||
@Override
|
||||
public BrokerageUserDO getOrCreateBrokerageUser(Long id) {
|
||||
// TODO @芋艿:这块优化下;统一到注册时处理;
|
||||
BrokerageUserDO brokerageUser = brokerageUserMapper.selectById(id);
|
||||
// 特殊:人人分销的情况下,如果分销人为空则创建分销人
|
||||
if (brokerageUser == null && ObjUtil.equal(BrokerageEnabledConditionEnum.ALL.getCondition(),
|
||||
|
|
|
@ -855,12 +855,14 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
|
|||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void cancelPaidOrder(Long userId, Long orderId) {
|
||||
// TODO 芋艿:这里实现要优化下;
|
||||
// TODO @puhui999:需要校验状态;已支付的情况下,才可以。
|
||||
TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
|
||||
if (order == null) {
|
||||
throw exception(ORDER_NOT_FOUND);
|
||||
}
|
||||
cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL);
|
||||
|
||||
// TODO @puhui999:需要退款
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package cn.iocoder.yudao.module.trade.service.price.calculator;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
||||
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
|
||||
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
|
||||
|
@ -61,7 +62,7 @@ public class TradePriceCalculatorHelper {
|
|||
orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId())
|
||||
.setDeliveryTemplateId(spu.getDeliveryTemplateId())
|
||||
.setGivePoint(spu.getGiveIntegral()).setUsePoint(0);
|
||||
if (orderItem.getPicUrl() == null) {
|
||||
if (StrUtil.isBlank(orderItem.getPicUrl())) {
|
||||
orderItem.setPicUrl(spu.getPicUrl());
|
||||
}
|
||||
});
|
||||
|
@ -240,7 +241,7 @@ public class TradePriceCalculatorHelper {
|
|||
*
|
||||
* 和 {@link #dividePrice(List, Integer)} 逻辑一致,只是传入的是 TradeOrderItemDO 对象
|
||||
*
|
||||
* @param items 订单项
|
||||
* @param items 订单项
|
||||
* @param price 订单支付金额
|
||||
* @return 分摊金额数组,和传入的 orderItems 一一对应
|
||||
*/
|
||||
|
|
|
@ -99,7 +99,7 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
|
|||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
when(tradeOrderProperties.getAppId()).thenReturn(888L);
|
||||
when(tradeOrderProperties.getPayAppKey()).thenReturn("mall");
|
||||
when(tradeOrderProperties.getPayExpireTime()).thenReturn(Duration.ofDays(1));
|
||||
when(tradeNoRedisDAO.generate(anyString())).thenReturn(IdUtil.randomUUID());
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package cn.iocoder.yudao.module.pay.api.order.dto;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
|
@ -18,10 +18,10 @@ public class PayOrderCreateReqDTO implements Serializable {
|
|||
public static final int SUBJECT_MAX_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* 应用编号
|
||||
* 应用标识
|
||||
*/
|
||||
@NotNull(message = "应用编号不能为空")
|
||||
private Long appId;
|
||||
@NotNull(message = "应用标识不能为空")
|
||||
private String appKey;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
package cn.iocoder.yudao.module.pay.api.refund.dto;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 退款单创建 Request DTO
|
||||
*
|
||||
|
@ -16,10 +15,10 @@ import javax.validation.constraints.NotNull;
|
|||
public class PayRefundCreateReqDTO {
|
||||
|
||||
/**
|
||||
* 应用编号
|
||||
* 应用标识
|
||||
*/
|
||||
@NotNull(message = "应用编号不能为空")
|
||||
private Long appId;
|
||||
@NotNull(message = "应用标识不能为空")
|
||||
private String appKey;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
|
|
|
@ -2,12 +2,12 @@ package cn.iocoder.yudao.module.pay.api.transfer.dto;
|
|||
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
@ -19,10 +19,10 @@ import java.util.Map;
|
|||
public class PayTransferCreateReqDTO {
|
||||
|
||||
/**
|
||||
* 应用编号
|
||||
* 应用标识
|
||||
*/
|
||||
@NotNull(message = "应用编号不能为空")
|
||||
private Long appId;
|
||||
@NotNull(message = "应用标识不能为空")
|
||||
private String appKey;
|
||||
|
||||
@NotEmpty(message = "转账渠道不能为空")
|
||||
private String channelCode;
|
||||
|
|
|
@ -14,6 +14,7 @@ public interface ErrorCodeConstants {
|
|||
ErrorCode APP_IS_DISABLE = new ErrorCode(1_007_000_002, "App 已经被禁用");
|
||||
ErrorCode APP_EXIST_ORDER_CANT_DELETE = new ErrorCode(1_007_000_003, "支付应用存在支付订单,无法删除");
|
||||
ErrorCode APP_EXIST_REFUND_CANT_DELETE = new ErrorCode(1_007_000_004, "支付应用存在退款订单,无法删除");
|
||||
ErrorCode APP_KEY_EXISTS = new ErrorCode(1_007_000_005, "支付应用标识已经存在");
|
||||
|
||||
// ========== CHANNEL 模块 1-007-001-000 ==========
|
||||
ErrorCode CHANNEL_NOT_FOUND = new ErrorCode(1_007_001_000, "支付渠道的配置不存在");
|
||||
|
|
|
@ -14,6 +14,10 @@ import javax.validation.constraints.*;
|
|||
@Data
|
||||
public class PayAppBaseVO {
|
||||
|
||||
@Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
|
||||
@NotEmpty(message = "应用标识不能为空")
|
||||
private String appKey;
|
||||
|
||||
@Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "小豆")
|
||||
@NotNull(message = "应用名不能为空")
|
||||
private String name;
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@Schema(description = "管理后台 - 支付应用信息创建 Request VO")
|
||||
@Data
|
||||
|
|
|
@ -20,6 +20,9 @@ public class PayAppPageReqVO extends PageParam {
|
|||
@Schema(description = "应用名", example = "小豆")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "应用标识", example = "yudao")
|
||||
private String appKey;
|
||||
|
||||
@Schema(description = "开启状态", example = "0")
|
||||
private Integer status;
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
|
@ -13,6 +16,9 @@ public class PayAppRespVO extends PayAppBaseVO {
|
|||
@Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
|
||||
private String appKey;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import javax.validation.constraints.*;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.transaction;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
|
@ -8,7 +10,14 @@ import lombok.Data;
|
|||
@Data
|
||||
public class PayWalletTransactionPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "钱包编号", example = "1")
|
||||
@Schema(description = "钱包编号", example = "888")
|
||||
private Long walletId;
|
||||
|
||||
@Schema(description = "用户编号", example = "1024")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户类型", example = "1")
|
||||
@InEnum(UserTypeEnum.class)
|
||||
private Integer userType;
|
||||
|
||||
}
|
||||
|
|
|
@ -31,6 +31,10 @@ public class PayAppDO extends BaseDO {
|
|||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 应用标识
|
||||
*/
|
||||
private String appKey;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
|
|
|
@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.pay.dal.mysql.app;
|
|||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
@ -14,9 +13,14 @@ public interface PayAppMapper extends BaseMapperX<PayAppDO> {
|
|||
default PageResult<PayAppDO> selectPage(PayAppPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<PayAppDO>()
|
||||
.likeIfPresent(PayAppDO::getName, reqVO.getName())
|
||||
.likeIfPresent(PayAppDO::getAppKey, reqVO.getAppKey())
|
||||
.eqIfPresent(PayAppDO::getStatus, reqVO.getStatus())
|
||||
.betweenIfPresent(PayAppDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(PayAppDO::getId));
|
||||
}
|
||||
|
||||
default PayAppDO selectByAppKey(String appKey) {
|
||||
return selectOne(PayAppDO::getAppKey, appKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ public class PayProperties {
|
|||
private static final String ORDER_NO_PREFIX = "P";
|
||||
private static final String REFUND_NO_PREFIX = "R";
|
||||
|
||||
private static final String WALLET_PAY_APP_KEY_DEFAULT = "wallet";
|
||||
|
||||
/**
|
||||
* 支付回调地址
|
||||
*
|
||||
|
@ -49,4 +51,10 @@ public class PayProperties {
|
|||
@NotEmpty(message = "退款订单 no 的前缀不能为空")
|
||||
private String refundNoPrefix = REFUND_NO_PREFIX;
|
||||
|
||||
/**
|
||||
* 钱包支付应用 AppKey
|
||||
*/
|
||||
@NotEmpty(message = "钱包支付应用 AppKey 不能为空")
|
||||
private String walletPayAppKey = WALLET_PAY_APP_KEY_DEFAULT;
|
||||
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
|
|||
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -88,13 +88,13 @@ public interface PayAppService {
|
|||
* @return 商户 Map
|
||||
*/
|
||||
default Map<Long, PayAppDO> getAppMap(Collection<Long> ids) {
|
||||
List<PayAppDO> list = getAppList(ids);
|
||||
List<PayAppDO> list = getAppList(ids);
|
||||
return CollectionUtils.convertMap(list, PayAppDO::getId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付应用的合法性
|
||||
*
|
||||
* <p>
|
||||
* 如果不合法,抛出 {@link ServiceException} 业务异常
|
||||
*
|
||||
* @param id 应用编号
|
||||
|
@ -102,4 +102,14 @@ public interface PayAppService {
|
|||
*/
|
||||
PayAppDO validPayApp(Long id);
|
||||
|
||||
/**
|
||||
* 支付应用的合法性
|
||||
* <p>
|
||||
* 如果不合法,抛出 {@link ServiceException} 业务异常
|
||||
*
|
||||
* @param appKey 应用标识
|
||||
* @return 应用
|
||||
*/
|
||||
PayAppDO validPayApp(String appKey);
|
||||
|
||||
}
|
||||
|
|
|
@ -11,11 +11,11 @@ import cn.iocoder.yudao.module.pay.dal.mysql.app.PayAppMapper;
|
|||
import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
|
||||
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
|
||||
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -43,6 +43,9 @@ public class PayAppServiceImpl implements PayAppService {
|
|||
|
||||
@Override
|
||||
public Long createApp(PayAppCreateReqVO createReqVO) {
|
||||
// 验证 appKey 是否重复
|
||||
validateAppKeyUnique(null, createReqVO.getAppKey());
|
||||
|
||||
// 插入
|
||||
PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO);
|
||||
appMapper.insert(app);
|
||||
|
@ -54,11 +57,28 @@ public class PayAppServiceImpl implements PayAppService {
|
|||
public void updateApp(PayAppUpdateReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
validateAppExists(updateReqVO.getId());
|
||||
// 验证 appKey 是否重复
|
||||
validateAppKeyUnique(updateReqVO.getId(), updateReqVO.getAppKey());
|
||||
|
||||
// 更新
|
||||
PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO);
|
||||
appMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
void validateAppKeyUnique(Long id, String appKey) {
|
||||
PayAppDO app = appMapper.selectByAppKey(appKey);
|
||||
if (app == null) {
|
||||
return;
|
||||
}
|
||||
// 如果 id 为空,说明不用比较是否为相同 appKey 的应用
|
||||
if (id == null) {
|
||||
throw exception(APP_KEY_EXISTS);
|
||||
}
|
||||
if (!app.getId().equals(id)) {
|
||||
throw exception(APP_KEY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAppStatus(Long id, Integer status) {
|
||||
// 校验商户存在
|
||||
|
@ -101,7 +121,7 @@ public class PayAppServiceImpl implements PayAppService {
|
|||
|
||||
@Override
|
||||
public List<PayAppDO> getAppList() {
|
||||
return appMapper.selectList();
|
||||
return appMapper.selectList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -110,14 +130,30 @@ public class PayAppServiceImpl implements PayAppService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public PayAppDO validPayApp(Long id) {
|
||||
PayAppDO app = appMapper.selectById(id);
|
||||
public PayAppDO validPayApp(Long appId) {
|
||||
PayAppDO app = appMapper.selectById(appId);
|
||||
return validatePayApp(app);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayAppDO validPayApp(String appKey) {
|
||||
PayAppDO app = appMapper.selectByAppKey(appKey);
|
||||
return validatePayApp(app);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验支付应用实体的有效性:存在 + 开启
|
||||
*
|
||||
* @param app 待校验的支付应用实体
|
||||
* @return 校验通过的支付应用实体
|
||||
*/
|
||||
private PayAppDO validatePayApp(PayAppDO app) {
|
||||
// 校验是否存在
|
||||
if (app == null) {
|
||||
throw exception(ErrorCodeConstants.APP_NOT_FOUND);
|
||||
}
|
||||
// 校验是否禁用
|
||||
if (CommonStatusEnum.DISABLE.getStatus().equals(app.getStatus())) {
|
||||
if (CommonStatusEnum.isDisable(app.getStatus())) {
|
||||
throw exception(ErrorCodeConstants.APP_IS_DISABLE);
|
||||
}
|
||||
return app;
|
||||
|
|
|
@ -43,11 +43,11 @@ import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
|
|||
public class PayDemoOrderServiceImpl implements PayDemoOrderService {
|
||||
|
||||
/**
|
||||
* 接入的实力应用编号
|
||||
* 接入的支付应用标识
|
||||
*
|
||||
* 从 [支付管理 -> 应用信息] 里添加
|
||||
*/
|
||||
private static final Long PAY_APP_ID = 7L;
|
||||
private static final String PAY_APP_KEY = "demo";
|
||||
|
||||
/**
|
||||
* 商品信息 Map
|
||||
|
@ -88,7 +88,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
|
|||
|
||||
// 2.1 创建支付单
|
||||
Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO()
|
||||
.setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
|
||||
.setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用
|
||||
.setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号
|
||||
.setSubject(spuName).setBody("").setPrice(price) // 价格信息
|
||||
.setExpireTime(addTime(Duration.ofHours(2L)))); // 支付的过期时间
|
||||
|
@ -190,7 +190,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
|
|||
String refundId = order.getId() + "-refund";
|
||||
// 2.2 创建退款单
|
||||
Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
|
||||
.setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
|
||||
.setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用
|
||||
.setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
|
||||
.setMerchantRefundId(refundId)
|
||||
.setReason("想退钱").setPrice(order.getPrice()));// 价格信息
|
||||
|
|
|
@ -111,11 +111,11 @@ public class PayOrderServiceImpl implements PayOrderService {
|
|||
@Override
|
||||
public Long createOrder(PayOrderCreateReqDTO reqDTO) {
|
||||
// 校验 App
|
||||
PayAppDO app = appService.validPayApp(reqDTO.getAppId());
|
||||
PayAppDO app = appService.validPayApp(reqDTO.getAppKey());
|
||||
|
||||
// 查询对应的支付交易单是否已经存在。如果是,则直接返回
|
||||
PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId(
|
||||
reqDTO.getAppId(), reqDTO.getMerchantOrderId());
|
||||
app.getId(), reqDTO.getMerchantOrderId());
|
||||
if (order != null) {
|
||||
log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
|
||||
order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况
|
||||
|
|
|
@ -26,12 +26,12 @@ import cn.iocoder.yudao.module.pay.service.app.PayAppService;
|
|||
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
|
||||
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
|
||||
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
|
@ -93,9 +93,9 @@ public class PayRefundServiceImpl implements PayRefundService {
|
|||
@Override
|
||||
public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
|
||||
// 1.1 校验 App
|
||||
PayAppDO app = appService.validPayApp(reqDTO.getAppId());
|
||||
PayAppDO app = appService.validPayApp(reqDTO.getAppKey());
|
||||
// 1.2 校验支付订单
|
||||
PayOrderDO order = validatePayOrderCanRefund(reqDTO);
|
||||
PayOrderDO order = validatePayOrderCanRefund(reqDTO, app.getId());
|
||||
// 1.3 校验支付渠道是否有效
|
||||
PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
|
||||
PayClient client = channelService.getPayClient(channel.getId());
|
||||
|
@ -113,7 +113,7 @@ public class PayRefundServiceImpl implements PayRefundService {
|
|||
// 2.1 插入退款单
|
||||
String no = noRedisDAO.generate(payProperties.getRefundNoPrefix());
|
||||
refund = PayRefundConvert.INSTANCE.convert(reqDTO)
|
||||
.setNo(no).setOrderId(order.getId()).setOrderNo(order.getNo())
|
||||
.setNo(no).setAppId(app.getId()).setOrderId(order.getId()).setOrderNo(order.getNo())
|
||||
.setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode())
|
||||
// 商户相关的字段
|
||||
.setNotifyUrl(app.getRefundNotifyUrl())
|
||||
|
@ -153,8 +153,8 @@ public class PayRefundServiceImpl implements PayRefundService {
|
|||
* @param reqDTO 退款申请信息
|
||||
* @return 支付订单
|
||||
*/
|
||||
private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) {
|
||||
PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId());
|
||||
private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO, Long appId) {
|
||||
PayOrderDO order = orderService.getOrder(appId, reqDTO.getMerchantOrderId());
|
||||
if (order == null) {
|
||||
throw exception(PAY_ORDER_NOT_FOUND);
|
||||
}
|
||||
|
@ -164,11 +164,11 @@ public class PayRefundServiceImpl implements PayRefundService {
|
|||
}
|
||||
|
||||
// 校验金额,退款金额不能大于原定的金额
|
||||
if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){
|
||||
if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()) {
|
||||
throw exception(REFUND_PRICE_EXCEED);
|
||||
}
|
||||
// 是否有退款中的订单
|
||||
if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(),
|
||||
if (refundMapper.selectCountByAppIdAndOrderId(appId, order.getId(),
|
||||
PayRefundStatusEnum.WAITING.getStatus()) > 0) {
|
||||
throw exception(REFUND_HAS_REFUNDING);
|
||||
}
|
||||
|
@ -197,9 +197,10 @@ public class PayRefundServiceImpl implements PayRefundService {
|
|||
* 通知并更新订单的退款结果
|
||||
*
|
||||
* @param channel 支付渠道
|
||||
* @param notify 通知
|
||||
* @param notify 通知
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class) // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效
|
||||
// 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
|
||||
// 情况一:退款成功
|
||||
if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {
|
||||
|
|
|
@ -24,12 +24,12 @@ import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
|
|||
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
|
||||
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
|
||||
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Validator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Validator;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
|
@ -79,16 +79,16 @@ public class PayTransferServiceImpl implements PayTransferService {
|
|||
@Override
|
||||
public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
|
||||
// 1.1 校验 App
|
||||
PayAppDO payApp = appService.validPayApp(reqDTO.getAppId());
|
||||
PayAppDO payApp = appService.validPayApp(reqDTO.getAppKey());
|
||||
// 1.2 校验支付渠道是否有效
|
||||
PayChannelDO channel = channelService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode());
|
||||
PayChannelDO channel = channelService.validPayChannel(payApp.getId(), reqDTO.getChannelCode());
|
||||
PayClient client = channelService.getPayClient(channel.getId());
|
||||
if (client == null) {
|
||||
log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
|
||||
throw exception(CHANNEL_NOT_FOUND);
|
||||
}
|
||||
// 1.3 校验转账单已经发起过转账。
|
||||
PayTransferDO transfer = validateTransferCanCreate(reqDTO);
|
||||
PayTransferDO transfer = validateTransferCanCreate(reqDTO, payApp.getId());
|
||||
|
||||
if (transfer == null) {
|
||||
// 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账
|
||||
|
@ -116,8 +116,8 @@ public class PayTransferServiceImpl implements PayTransferService {
|
|||
return transfer.getId();
|
||||
}
|
||||
|
||||
private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto) {
|
||||
PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(dto.getAppId(), dto.getMerchantTransferId());
|
||||
private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto, Long appId) {
|
||||
PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, dto.getMerchantTransferId());
|
||||
if (transfer != null) {
|
||||
// 已经存在,并且状态不为等待状态。说明已经调用渠道转账并返回结果.
|
||||
if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
|
||||
|
|
|
@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletRechargeMapper;
|
|||
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
|
||||
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
|
||||
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
|
||||
import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
|
||||
|
@ -51,11 +52,6 @@ import static cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum.*;
|
|||
@Slf4j
|
||||
public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
|
||||
|
||||
/**
|
||||
* TODO 芋艿:放到 payconfig
|
||||
*/
|
||||
private static final Long WALLET_PAY_APP_ID = 8L;
|
||||
|
||||
private static final String WALLET_RECHARGE_ORDER_SUBJECT = "钱包余额充值";
|
||||
|
||||
@Resource
|
||||
|
@ -68,9 +64,13 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
|
|||
private PayRefundService payRefundService;
|
||||
@Resource
|
||||
private PayWalletRechargePackageService payWalletRechargePackageService;
|
||||
|
||||
@Resource
|
||||
public SocialClientApi socialClientApi;
|
||||
|
||||
@Resource
|
||||
private PayProperties payProperties;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PayWalletRechargeDO createWalletRecharge(Long userId, Integer userType, String userIp,
|
||||
|
@ -92,7 +92,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
|
|||
|
||||
// 2.1 创建支付单
|
||||
Long payOrderId = payOrderService.createOrder(new PayOrderCreateReqDTO()
|
||||
.setAppId(WALLET_PAY_APP_ID).setUserIp(userIp)
|
||||
.setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp)
|
||||
.setMerchantOrderId(recharge.getId().toString()) // 业务的订单编号
|
||||
.setSubject(WALLET_RECHARGE_ORDER_SUBJECT).setBody("")
|
||||
.setPrice(recharge.getPayPrice())
|
||||
|
@ -174,7 +174,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
|
|||
String walletRechargeId = String.valueOf(id);
|
||||
String refundId = walletRechargeId + "-refund";
|
||||
Long payRefundId = payRefundService.createPayRefund(new PayRefundCreateReqDTO()
|
||||
.setAppId(WALLET_PAY_APP_ID).setUserIp(userIp)
|
||||
.setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp)
|
||||
.setMerchantOrderId(walletRechargeId)
|
||||
.setMerchantRefundId(refundId)
|
||||
.setReason("想退钱").setPrice(walletRecharge.getPayPrice()));
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package cn.iocoder.yudao.module.pay.service.wallet;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO;
|
||||
|
@ -11,12 +12,11 @@ import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletTransactionMapper;
|
|||
import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
|
||||
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
|
||||
import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO.TYPE_EXPENSE;
|
||||
|
@ -53,6 +53,16 @@ public class PayWalletTransactionServiceImpl implements PayWalletTransactionServ
|
|||
|
||||
@Override
|
||||
public PageResult<PayWalletTransactionDO> getWalletTransactionPage(PayWalletTransactionPageReqVO pageVO) {
|
||||
// 基于 userId + userType 查询钱包
|
||||
if (pageVO.getWalletId() == null
|
||||
&& ObjectUtil.isAllNotEmpty(pageVO.getUserId(), pageVO.getUserType())) {
|
||||
PayWalletDO wallet = payWalletService.getOrCreateWallet(pageVO.getUserId(), pageVO.getUserType());
|
||||
if (wallet != null) {
|
||||
pageVO.setWalletId(wallet.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// 查询分页
|
||||
return payWalletTransactionMapper.selectPage(pageVO.getWalletId(), null, pageVO, null);
|
||||
}
|
||||
|
||||
|
|
|
@ -218,11 +218,11 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
|
|||
public void testCreateOrder_success() {
|
||||
// mock 参数
|
||||
PayOrderCreateReqDTO reqDTO = randomPojo(PayOrderCreateReqDTO.class,
|
||||
o -> o.setAppId(1L).setMerchantOrderId("10")
|
||||
o -> o.setAppKey("demo").setMerchantOrderId("10")
|
||||
.setSubject(randomString()).setBody(randomString()));
|
||||
// mock 方法
|
||||
PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L).setOrderNotifyUrl("http://127.0.0.1"));
|
||||
when(appService.validPayApp(eq(reqDTO.getAppId()))).thenReturn(app);
|
||||
when(appService.validPayApp(eq(reqDTO.getAppKey()))).thenReturn(app);
|
||||
|
||||
// 调用
|
||||
Long orderId = orderService.createOrder(reqDTO);
|
||||
|
@ -239,10 +239,13 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
|
|||
public void testCreateOrder_exists() {
|
||||
// mock 参数
|
||||
PayOrderCreateReqDTO reqDTO = randomPojo(PayOrderCreateReqDTO.class,
|
||||
o -> o.setAppId(1L).setMerchantOrderId("10"));
|
||||
o -> o.setAppKey("demo").setMerchantOrderId("10"));
|
||||
// mock 数据
|
||||
PayOrderDO dbOrder = randomPojo(PayOrderDO.class, o -> o.setAppId(1L).setMerchantOrderId("10"));
|
||||
orderMapper.insert(dbOrder);
|
||||
// mock 方法
|
||||
PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L).setOrderNotifyUrl("http://127.0.0.1"));
|
||||
when(appService.validPayApp(eq(reqDTO.getAppKey()))).thenReturn(app);
|
||||
|
||||
// 调用
|
||||
Long orderId = orderService.createOrder(reqDTO);
|
||||
|
|
|
@ -209,10 +209,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
|
|||
@Test
|
||||
public void testCreateRefund_orderNotFound() {
|
||||
PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
|
||||
o -> o.setAppId(1L));
|
||||
o -> o.setAppKey("demo"));
|
||||
// mock 方法(app)
|
||||
PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
|
||||
when(appService.validPayApp(eq(1L))).thenReturn(app);
|
||||
when(appService.validPayApp(eq("demo"))).thenReturn(app);
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> refundService.createPayRefund(reqDTO),
|
||||
|
@ -232,10 +232,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
|
|||
private void testCreateRefund_orderWaitingOrClosed(Integer status) {
|
||||
// 准备参数
|
||||
PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
|
||||
o -> o.setAppId(1L).setMerchantOrderId("100"));
|
||||
o -> o.setAppKey("demo").setMerchantOrderId("100"));
|
||||
// mock 方法(app)
|
||||
PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
|
||||
when(appService.validPayApp(eq(1L))).thenReturn(app);
|
||||
when(appService.validPayApp(eq("demo"))).thenReturn(app);
|
||||
// mock 数据(order)
|
||||
PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(status));
|
||||
when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order);
|
||||
|
@ -249,10 +249,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
|
|||
public void testCreateRefund_refundPriceExceed() {
|
||||
// 准备参数
|
||||
PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
|
||||
o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10));
|
||||
o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(10));
|
||||
// mock 方法(app)
|
||||
PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
|
||||
when(appService.validPayApp(eq(1L))).thenReturn(app);
|
||||
when(appService.validPayApp(eq("demo"))).thenReturn(app);
|
||||
// mock 数据(order)
|
||||
PayOrderDO order = randomPojo(PayOrderDO.class, o ->
|
||||
o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
|
||||
|
@ -268,10 +268,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
|
|||
public void testCreateRefund_orderHasRefunding() {
|
||||
// 准备参数
|
||||
PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
|
||||
o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10));
|
||||
o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(10));
|
||||
// mock 方法(app)
|
||||
PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
|
||||
when(appService.validPayApp(eq(1L))).thenReturn(app);
|
||||
when(appService.validPayApp(eq("demo"))).thenReturn(app);
|
||||
// mock 数据(order)
|
||||
PayOrderDO order = randomPojo(PayOrderDO.class, o ->
|
||||
o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
|
||||
|
@ -291,10 +291,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
|
|||
public void testCreateRefund_channelNotFound() {
|
||||
// 准备参数
|
||||
PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
|
||||
o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9));
|
||||
o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9));
|
||||
// mock 方法(app)
|
||||
PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
|
||||
when(appService.validPayApp(eq(1L))).thenReturn(app);
|
||||
when(appService.validPayApp(eq("demo"))).thenReturn(app);
|
||||
// mock 数据(order)
|
||||
PayOrderDO order = randomPojo(PayOrderDO.class, o ->
|
||||
o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
|
||||
|
@ -315,11 +315,11 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
|
|||
public void testCreateRefund_refundExists() {
|
||||
// 准备参数
|
||||
PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
|
||||
o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)
|
||||
o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9)
|
||||
.setMerchantRefundId("200").setReason("测试退款"));
|
||||
// mock 方法(app)
|
||||
PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
|
||||
when(appService.validPayApp(eq(1L))).thenReturn(app);
|
||||
when(appService.validPayApp(eq("demo"))).thenReturn(app);
|
||||
// mock 数据(order)
|
||||
PayOrderDO order = randomPojo(PayOrderDO.class, o ->
|
||||
o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
|
||||
|
@ -347,11 +347,11 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
|
|||
public void testCreateRefund_invokeException() {
|
||||
// 准备参数
|
||||
PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
|
||||
o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)
|
||||
o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9)
|
||||
.setMerchantRefundId("200").setReason("测试退款"));
|
||||
// mock 方法(app)
|
||||
PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
|
||||
when(appService.validPayApp(eq(1L))).thenReturn(app);
|
||||
when(appService.validPayApp(eq("demo"))).thenReturn(app);
|
||||
// mock 数据(order)
|
||||
PayOrderDO order = randomPojo(PayOrderDO.class, o ->
|
||||
o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
|
||||
|
@ -391,11 +391,11 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
|
|||
|
||||
// 准备参数
|
||||
PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
|
||||
o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)
|
||||
o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9)
|
||||
.setMerchantRefundId("200").setReason("测试退款"));
|
||||
// mock 方法(app)
|
||||
PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
|
||||
when(appService.validPayApp(eq(1L))).thenReturn(app);
|
||||
when(appService.validPayApp(eq("demo"))).thenReturn(app);
|
||||
// mock 数据(order)
|
||||
PayOrderDO order = randomPojo(PayOrderDO.class, o ->
|
||||
o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
CREATE TABLE IF NOT EXISTS "pay_app" (
|
||||
"id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"app_key" varchar(64) NOT NULL,
|
||||
"name" varchar(64) NOT NULL,
|
||||
"status" tinyint NOT NULL,
|
||||
"remark" varchar(255) DEFAULT NULL,
|
||||
|
|
|
@ -28,6 +28,11 @@ public class AlipayPayClientConfig implements PayClientConfig {
|
|||
*/
|
||||
public static final Integer MODE_CERTIFICATE = 2;
|
||||
|
||||
/**
|
||||
* 接口内容加密方式 - AES 加密
|
||||
*/
|
||||
public static final String ENC_TYPE_AES = "AES";
|
||||
|
||||
/**
|
||||
* 签名算法类型 - RSA
|
||||
*/
|
||||
|
@ -94,6 +99,22 @@ public class AlipayPayClientConfig implements PayClientConfig {
|
|||
@NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class})
|
||||
private String rootCertContent;
|
||||
|
||||
/**
|
||||
* 接口内容加密方式
|
||||
*
|
||||
* 1. 如果为空,将使用无加密方式
|
||||
* 2. 如果要加密,目前支付宝只有 AES 一种加密方式
|
||||
*
|
||||
* @see <a href="https://opendocs.alipay.com/common/02mse3">支付宝开放平台</a>
|
||||
* @see AlipayPayClientConfig#ENC_TYPE_AES
|
||||
*/
|
||||
private String encryptType;
|
||||
|
||||
/**
|
||||
* 接口内容加密的私钥
|
||||
*/
|
||||
private String encryptKey;
|
||||
|
||||
public interface ModePublicKey {
|
||||
}
|
||||
|
||||
|
|
|
@ -102,8 +102,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
|
|||
queryParam.put("TemplateCode", apiTemplateId);
|
||||
JSONObject response = request("QuerySmsTemplate", queryParam);
|
||||
|
||||
System.out.println("getSmsTemplate response is =====" + response.toString());
|
||||
|
||||
// 2.1 请求失败
|
||||
String code = response.getStr("Code");
|
||||
if (ObjectUtil.notEqual(code, RESPONSE_CODE_SUCCESS)) {
|
||||
|
@ -170,7 +168,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
|
|||
// 4. 构建 Authorization 签名
|
||||
String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
|
||||
String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest);
|
||||
|
||||
String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
|
||||
String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
|
||||
headers.put("Authorization", "ACS3-HMAC-SHA256" + " " + "Credential=" + properties.getApiKey()
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
|
||||
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
|
@ -31,13 +30,12 @@ import java.util.*;
|
|||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
|
||||
import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
|
||||
|
||||
|
||||
// todo @scholar:参考阿里云在优化下
|
||||
/**
|
||||
* 华为短信客户端的实现类
|
||||
*
|
||||
|
@ -56,7 +54,6 @@ public class HuaweiSmsClient extends AbstractSmsClient {
|
|||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
|
||||
}
|
||||
|
||||
public HuaweiSmsClient(SmsChannelProperties properties) {
|
||||
|
@ -68,6 +65,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
|
|||
@Override
|
||||
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
|
||||
List<KeyValue<String, Object>> templateParams) throws Throwable {
|
||||
// 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html
|
||||
// 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构
|
||||
// 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。
|
||||
String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号
|
||||
|
|
|
@ -2,38 +2,29 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
|
|||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import lombok.Data;
|
||||
import jakarta.xml.bind.DatatypeConverter;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
|
||||
|
||||
// TODO @scholar 建议参考 AliyunSmsClient 优化下
|
||||
/**
|
||||
* 腾讯云短信功能实现
|
||||
*
|
||||
|
@ -43,6 +34,9 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
|
|||
*/
|
||||
public class TencentSmsClient extends AbstractSmsClient {
|
||||
|
||||
private static final String VERSION = "2021-01-11";
|
||||
private static final String REGION = "ap-guangzhou";
|
||||
|
||||
/**
|
||||
* 调用成功 code
|
||||
*/
|
||||
|
@ -56,7 +50,6 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|||
*/
|
||||
private static final long INTERNATIONAL_CHINA = 0L;
|
||||
|
||||
|
||||
public TencentSmsClient(SmsChannelProperties properties) {
|
||||
super(properties);
|
||||
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
|
||||
|
@ -65,7 +58,6 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,31 +87,96 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|||
@Override
|
||||
public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
|
||||
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
|
||||
// 构建请求
|
||||
// 1. 执行请求
|
||||
// 参考链接 https://cloud.tencent.com/document/product/382/55981
|
||||
TreeMap<String, Object> body = new TreeMap<>();
|
||||
String[] phones = {mobile};
|
||||
body.put("PhoneNumberSet",phones);
|
||||
body.put("SmsSdkAppId",getSdkAppId());
|
||||
body.put("SignName",properties.getSignature());
|
||||
body.put("PhoneNumberSet", new String[]{mobile});
|
||||
body.put("SmsSdkAppId", getSdkAppId());
|
||||
body.put("SignName", properties.getSignature());
|
||||
body.put("TemplateId",apiTemplateId);
|
||||
body.put("TemplateParamSet",ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue())));
|
||||
|
||||
JSONObject JsonResponse = sendSmsRequest(body,"SendSms","2021-01-11","ap-guangzhou");
|
||||
SmsResponse smsResponse = getSmsSendResponse(JsonResponse);
|
||||
|
||||
return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString());
|
||||
body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue())));
|
||||
JSONObject response = request("SendSms", body);
|
||||
|
||||
// 2. 解析请求
|
||||
JSONObject responseResult = response.getJSONObject("Response");
|
||||
JSONObject error = responseResult.getJSONObject("Error");
|
||||
if (error != null) {
|
||||
return new SmsSendRespDTO().setSuccess(false)
|
||||
.setApiRequestId(responseResult.getStr("RequestId"))
|
||||
.setApiCode(error.getStr("Code"))
|
||||
.setApiMsg(error.getStr("Message"));
|
||||
}
|
||||
JSONObject responseData = responseResult.getJSONArray("SendStatusSet").getJSONObject(0);
|
||||
return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, responseData.getStr("Code")))
|
||||
.setApiRequestId(responseResult.getStr("RequestId"))
|
||||
.setSerialNo(responseData.getStr("SerialNo"))
|
||||
.setApiMsg(responseData.getStr("Message"));
|
||||
}
|
||||
|
||||
JSONObject sendSmsRequest(TreeMap<String, Object> body,String action,String version,String region) throws Exception {
|
||||
@Override
|
||||
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
|
||||
JSONArray statuses = JSONUtil.parseArray(text);
|
||||
// 字段参考
|
||||
return convertList(statuses, status -> {
|
||||
JSONObject statusObj = (JSONObject) status;
|
||||
return new SmsReceiveRespDTO()
|
||||
.setSuccess("SUCCESS".equals(statusObj.getStr("report_status"))) // 是否接收成功
|
||||
.setErrorCode(statusObj.getStr("errmsg")) // 状态报告编码
|
||||
.setMobile(statusObj.getStr("mobile")) // 手机号
|
||||
.setReceiveTime(statusObj.getLocalDateTime("user_receive_time", null)) // 状态报告时间
|
||||
.setSerialNo(statusObj.getStr("sid")); // 发送序列号
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
|
||||
// 1. 构建请求
|
||||
// 参考链接 https://cloud.tencent.com/document/product/382/52067
|
||||
TreeMap<String, Object> body = new TreeMap<>();
|
||||
body.put("International", INTERNATIONAL_CHINA);
|
||||
body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)});
|
||||
JSONObject response = request("DescribeSmsTemplateList", body);
|
||||
|
||||
// TODO @scholar:会有请求失败的情况么?类似发送的(那块逻辑我补充了)
|
||||
JSONObject TemplateStatusSet = response.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").getJSONObject(0);
|
||||
String content = TemplateStatusSet.get("TemplateContent").toString();
|
||||
int templateStatus = Integer.parseInt(TemplateStatusSet.get("StatusCode").toString());
|
||||
String auditReason = TemplateStatusSet.get("ReviewReply").toString();
|
||||
|
||||
return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(content)
|
||||
.setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Integer convertSmsTemplateAuditStatus(int templateStatus) {
|
||||
switch (templateStatus) {
|
||||
case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
|
||||
case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
|
||||
case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
|
||||
default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求腾讯云短信
|
||||
*
|
||||
* @see <a href="https://cloud.tencent.com/document/product/382/52072">签名方法 v3</a>
|
||||
*
|
||||
* @param action 请求的 API 名称
|
||||
* @param body 请求参数
|
||||
* @return 请求结果
|
||||
*/
|
||||
private JSONObject request(String action, TreeMap<String, Object> body) throws Exception {
|
||||
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
|
||||
// TODO @scholar:这个 format,看看怎么写的可以简化点
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
// 注意时区,否则容易出错
|
||||
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
|
||||
|
||||
// TODO @scholar:这个步骤,看看怎么参考阿里云 client,归类下;1. 2.1 2.2 这种
|
||||
// ************* 步骤 1:拼接规范请求串 *************
|
||||
// TODO @scholar:这个 hsot 枚举下;
|
||||
String host = "sms.tencentcloudapi.com"; //APP接入地址+接口访问URI
|
||||
String httpMethod = "POST"; // 请求方式
|
||||
String canonicalUri = "/";
|
||||
|
@ -129,6 +186,7 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|||
+ "host:" + host + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
|
||||
String signedHeaders = "content-type;host;x-tc-action";
|
||||
String hashedRequestBody = sha256Hex(JSONUtil.toJsonStr(body));
|
||||
// TODO @scholar:换行下,不然单行太长了
|
||||
String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
|
||||
|
||||
// ************* 步骤 2:拼接待签名字符串 *************
|
||||
|
@ -153,205 +211,19 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|||
headers.put("Host", host);
|
||||
headers.put("X-TC-Action", action);
|
||||
headers.put("X-TC-Timestamp", timestamp);
|
||||
headers.put("X-TC-Version", version);
|
||||
headers.put("X-TC-Region", region);
|
||||
headers.put("X-TC-Version", VERSION);
|
||||
headers.put("X-TC-Region", REGION);
|
||||
|
||||
HttpResponse response = HttpRequest.post("https://"+host)
|
||||
.addHeaders(headers)
|
||||
.body(JSONUtil.toJsonStr(body))
|
||||
.execute();
|
||||
String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(body));
|
||||
|
||||
return JSONUtil.parseObj(response.body());
|
||||
return JSONUtil.parseObj(responseBody);
|
||||
}
|
||||
|
||||
public static byte[] hmac256(byte[] key, String msg) throws Exception {
|
||||
// TODO @scholar:使用 hutool 简化下
|
||||
private static byte[] hmac256(byte[] key, String msg) throws Exception {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
|
||||
mac.init(secretKeySpec);
|
||||
return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private SmsResponse getSmsSendResponse(JSONObject resJson) {
|
||||
SmsResponse smsResponse = new SmsResponse();
|
||||
JSONArray statusJson =resJson.getJSONObject("Response").getJSONArray("SendStatusSet");
|
||||
smsResponse.setSuccess("Ok".equals(statusJson.getJSONObject(0).getStr("Code")));
|
||||
smsResponse.setData(resJson);
|
||||
return smsResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
|
||||
List<SmsReceiveStatus> callback = JsonUtils.parseArray(text, SmsReceiveStatus.class);
|
||||
return convertList(callback, status -> new SmsReceiveRespDTO()
|
||||
.setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus()))
|
||||
.setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription())
|
||||
.setMobile(status.getMobile()).setReceiveTime(status.getReceiveTime())
|
||||
.setSerialNo(status.getSerialNo()).setLogId(status.getSessionContext().getLogId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
|
||||
|
||||
// 构建请求
|
||||
TreeMap<String, Object> body = new TreeMap<>();
|
||||
body.put("International",0);
|
||||
Integer[] templateIds = {Integer.valueOf(apiTemplateId)};
|
||||
body.put("TemplateIdSet",templateIds);
|
||||
|
||||
JSONObject JsonResponse = sendSmsRequest(body,"DescribeSmsTemplateList","2021-01-11","ap-guangzhou");
|
||||
QuerySmsTemplateResponse smsTemplateResponse = getSmsTemplateResponse(JsonResponse);
|
||||
String templateId = Integer.toString(smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getTemplateId());
|
||||
String content = smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getTemplateContent();
|
||||
Integer templateStatus = smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getStatusCode();
|
||||
String auditReason = smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getReviewReply();
|
||||
|
||||
return new SmsTemplateRespDTO().setId(templateId).setContent(content)
|
||||
.setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason);
|
||||
}
|
||||
|
||||
private QuerySmsTemplateResponse getSmsTemplateResponse(JSONObject resJson) {
|
||||
|
||||
QuerySmsTemplateResponse smsTemplateResponse = new QuerySmsTemplateResponse();
|
||||
|
||||
smsTemplateResponse.setRequestId(resJson.getJSONObject("Response").getStr("RequestId"));
|
||||
|
||||
smsTemplateResponse.setDescribeTemplateStatusSet(new ArrayList<>());
|
||||
|
||||
QuerySmsTemplateResponse.TemplateInfo templateInfo = new QuerySmsTemplateResponse.TemplateInfo();
|
||||
|
||||
Object statusObject = resJson.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").get(0);
|
||||
|
||||
JSONObject statusJSON = new JSONObject(statusObject);
|
||||
|
||||
templateInfo.setTemplateContent(statusJSON.get("TemplateContent").toString());
|
||||
|
||||
templateInfo.setStatusCode(Integer.parseInt(statusJSON.get("StatusCode").toString()));
|
||||
|
||||
templateInfo.setReviewReply(statusJSON.get("ReviewReply").toString());
|
||||
|
||||
templateInfo.setTemplateId(Integer.parseInt(statusJSON.get("TemplateId").toString()));
|
||||
|
||||
smsTemplateResponse.getDescribeTemplateStatusSet().add(templateInfo);
|
||||
|
||||
return smsTemplateResponse;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Integer convertSmsTemplateAuditStatus(int templateStatus) {
|
||||
switch (templateStatus) {
|
||||
case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
|
||||
case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
|
||||
case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
|
||||
default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SmsResponse {
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 厂商原返回体
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>类名: QuerySmsTemplateResponse
|
||||
* <p>说明: sms模板查询返回信息
|
||||
*
|
||||
* @author :scholar
|
||||
* 2024/07/17 0:25
|
||||
**/
|
||||
@Data
|
||||
public static class QuerySmsTemplateResponse {
|
||||
private List<TemplateInfo> DescribeTemplateStatusSet;
|
||||
private String RequestId;
|
||||
@Data
|
||||
static class TemplateInfo {
|
||||
private String TemplateName;
|
||||
private Integer TemplateId;
|
||||
private Integer International;
|
||||
private String ReviewReply;
|
||||
private long CreateTime;
|
||||
private String TemplateContent;
|
||||
private Integer StatusCode;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class SmsReceiveStatus {
|
||||
|
||||
/**
|
||||
* 短信接受成功 code
|
||||
*/
|
||||
public static final String SUCCESS_CODE = "SUCCESS";
|
||||
|
||||
/**
|
||||
* 用户实际接收到短信的时间
|
||||
*/
|
||||
@JsonProperty("user_receive_time")
|
||||
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
|
||||
private LocalDateTime receiveTime;
|
||||
|
||||
/**
|
||||
* 国家(或地区)码
|
||||
*/
|
||||
@JsonProperty("nationcode")
|
||||
private String nationCode;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
private String mobile;
|
||||
|
||||
/**
|
||||
* 实际是否收到短信接收状态,SUCCESS(成功)、FAIL(失败)
|
||||
*/
|
||||
@JsonProperty("report_status")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 用户接收短信状态码错误信息
|
||||
*/
|
||||
@JsonProperty("errmsg")
|
||||
private String errCode;
|
||||
|
||||
/**
|
||||
* 用户接收短信状态描述
|
||||
*/
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 本次发送标识 ID(与发送接口返回的SerialNo对应)
|
||||
*/
|
||||
@JsonProperty("sid")
|
||||
private String serialNo;
|
||||
|
||||
/**
|
||||
* 用户的 session 内容(与发送接口的请求参数 SessionContext 一致)
|
||||
*/
|
||||
@JsonProperty("ext")
|
||||
private SessionContext sessionContext;
|
||||
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Data
|
||||
static class SessionContext {
|
||||
|
||||
/**
|
||||
* 发送短信记录id
|
||||
*/
|
||||
private Long logId;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -38,15 +38,6 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
|
|||
@InjectMocks
|
||||
private final AliyunSmsClient smsClient = new AliyunSmsClient(properties);
|
||||
|
||||
@Test
|
||||
public void testDoInit() {
|
||||
// 准备参数
|
||||
// mock 方法
|
||||
|
||||
// 调用
|
||||
smsClient.doInit();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tesSendSms_success() throws Throwable {
|
||||
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
|
@ -18,24 +17,6 @@ import java.util.List;
|
|||
*/
|
||||
public class SmsClientTests {
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testHuaweiSmsClient_sendSms() throws Throwable {
|
||||
SmsChannelProperties properties = new SmsChannelProperties()
|
||||
.setApiKey("123")
|
||||
.setApiSecret("456");
|
||||
HuaweiSmsClient client = new HuaweiSmsClient(properties);
|
||||
// 准备参数
|
||||
Long sendLogId = System.currentTimeMillis();
|
||||
String mobile = "15601691323";
|
||||
String apiTemplateId = "xx test01";
|
||||
List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1024"));
|
||||
// 调用
|
||||
SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
|
||||
// 打印结果
|
||||
System.out.println(smsSendRespDTO);
|
||||
}
|
||||
|
||||
// ========== 阿里云 ==========
|
||||
|
||||
@Test
|
||||
|
@ -59,14 +40,14 @@ public class SmsClientTests {
|
|||
SmsChannelProperties properties = new SmsChannelProperties()
|
||||
.setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")
|
||||
.setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
|
||||
.setSignature("Ballcat");
|
||||
.setSignature("runpu");
|
||||
AliyunSmsClient client = new AliyunSmsClient(properties);
|
||||
// 准备参数
|
||||
Long sendLogId = System.currentTimeMillis();
|
||||
String mobile = "173213154791";
|
||||
String mobile = "15601691323";
|
||||
String apiTemplateId = "SMS_207945135";
|
||||
// 调用
|
||||
SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024")));
|
||||
SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024")));
|
||||
// 打印结果
|
||||
System.out.println(sendRespDTO);
|
||||
}
|
||||
|
@ -100,4 +81,62 @@ public class SmsClientTests {
|
|||
System.out.println(statuses);
|
||||
}
|
||||
|
||||
// ========== 腾讯云 ==========
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testTencentSmsClient_sendSms() throws Throwable {
|
||||
SmsChannelProperties properties = new SmsChannelProperties()
|
||||
.setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523")
|
||||
.setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
|
||||
.setSignature("芋道源码");
|
||||
TencentSmsClient client = new TencentSmsClient(properties);
|
||||
// 准备参数
|
||||
Long sendLogId = System.currentTimeMillis();
|
||||
String mobile = "15601691323";
|
||||
String apiTemplateId = "2136358";
|
||||
// 调用
|
||||
SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024")));
|
||||
// 打印结果
|
||||
System.out.println(sendRespDTO);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testTencentSmsClient_getSmsTemplate() throws Throwable {
|
||||
SmsChannelProperties properties = new SmsChannelProperties()
|
||||
.setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523")
|
||||
.setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
|
||||
.setSignature("芋道源码");
|
||||
TencentSmsClient client = new TencentSmsClient(properties);
|
||||
// 准备参数
|
||||
String apiTemplateId = "2136358";
|
||||
// 调用
|
||||
SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId);
|
||||
// 打印结果
|
||||
System.out.println(template);
|
||||
}
|
||||
|
||||
// ========== 华为云 ==========
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testHuaweiSmsClient_sendSms() throws Throwable {
|
||||
SmsChannelProperties properties = new SmsChannelProperties()
|
||||
.setApiKey("123")
|
||||
.setApiSecret("456")
|
||||
.setSignature("runpu");
|
||||
HuaweiSmsClient client = new HuaweiSmsClient(properties);
|
||||
// 准备参数
|
||||
Long sendLogId = System.currentTimeMillis();
|
||||
String mobile = "15601691323";
|
||||
String apiTemplateId = "xx test01";
|
||||
List<KeyValue<String, Object>> templateParams = List.of(new KeyValue<>("code", "1024"));
|
||||
// 调用
|
||||
SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
|
||||
// 打印结果
|
||||
System.out.println(smsSendRespDTO);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
// TODO @芋艿:补全单测
|
||||
/**
|
||||
* {@link TencentSmsClient} 的单元测试
|
||||
*
|
||||
|
@ -32,115 +38,85 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
|
|||
@InjectMocks
|
||||
private TencentSmsClient smsClient = new TencentSmsClient(properties);
|
||||
|
||||
@Mock
|
||||
private SmsClient client;
|
||||
|
||||
@Test
|
||||
public void testDoInit() {
|
||||
// 准备参数
|
||||
// mock 方法
|
||||
public void testDoSendSms_success() throws Throwable {
|
||||
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
|
||||
// 准备参数
|
||||
Long sendLogId = randomLongId();
|
||||
String mobile = randomString();
|
||||
String apiTemplateId = randomString();
|
||||
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
|
||||
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
|
||||
// mock 方法
|
||||
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
|
||||
.thenReturn("{\n" +
|
||||
" \"Response\": {\n" +
|
||||
" \"SendStatusSet\": [\n" +
|
||||
" {\n" +
|
||||
" \"SerialNo\": \"5000:1045710669157053657849499619\",\n" +
|
||||
" \"PhoneNumber\": \"+8618511122233\",\n" +
|
||||
" \"Fee\": 1,\n" +
|
||||
" \"SessionContext\": \"test\",\n" +
|
||||
" \"Code\": \"Ok\",\n" +
|
||||
" \"Message\": \"send success\",\n" +
|
||||
" \"IsoCode\": \"CN\"\n" +
|
||||
" },\n" +
|
||||
" ],\n" +
|
||||
" \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
// 调用
|
||||
smsClient.doInit();
|
||||
// 断言
|
||||
assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client"));
|
||||
// 调用
|
||||
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
|
||||
apiTemplateId, templateParams);
|
||||
// 断言
|
||||
assertTrue(result.getSuccess());
|
||||
assertEquals("5000:1045710669157053657849499619", result.getSerialNo());
|
||||
assertEquals("a0aabda6-cf91-4f3e-a81f-9198114a2279", result.getApiRequestId());
|
||||
assertEquals("send success", result.getApiMsg());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefresh() {
|
||||
// 准备参数
|
||||
SmsChannelProperties p = new SmsChannelProperties()
|
||||
.setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错
|
||||
.setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
|
||||
.setSignature("芋道源码");
|
||||
// 调用
|
||||
smsClient.refresh(p);
|
||||
// 断言
|
||||
assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client"));
|
||||
public void testDoSendSms_fail() throws Throwable {
|
||||
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
|
||||
// 准备参数
|
||||
Long sendLogId = randomLongId();
|
||||
String mobile = randomString();
|
||||
String apiTemplateId = randomString();
|
||||
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
|
||||
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
|
||||
|
||||
// mock 方法
|
||||
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
|
||||
.thenReturn("{\n" +
|
||||
" \"Response\": {\n" +
|
||||
" \"SendStatusSet\": [\n" +
|
||||
" {\n" +
|
||||
" \"SerialNo\": \"5000:1045710669157053657849499619\",\n" +
|
||||
" \"PhoneNumber\": \"+8618511122233\",\n" +
|
||||
" \"Fee\": 1,\n" +
|
||||
" \"SessionContext\": \"test\",\n" +
|
||||
" \"Code\": \"ERROR\",\n" +
|
||||
" \"Message\": \"send success\",\n" +
|
||||
" \"IsoCode\": \"CN\"\n" +
|
||||
" },\n" +
|
||||
" ],\n" +
|
||||
" \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
// 调用
|
||||
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
|
||||
apiTemplateId, templateParams);
|
||||
// 断言
|
||||
assertFalse(result.getSuccess());
|
||||
assertEquals("5000:1045710669157053657849499619", result.getSerialNo());
|
||||
assertEquals("a0aabda6-cf91-4f3e-a81f-9198114a2279", result.getApiRequestId());
|
||||
assertEquals("send success", result.getApiMsg());
|
||||
}
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void testDoSendSms_success() throws Throwable {
|
||||
// // 准备参数
|
||||
// Long sendLogId = randomLongId();
|
||||
// String mobile = randomString();
|
||||
// String apiTemplateId = randomString();
|
||||
// List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
|
||||
// new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
|
||||
// String requestId = randomString();
|
||||
// String serialNo = randomString();
|
||||
// // mock 方法
|
||||
// SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> {
|
||||
// o.setRequestId(requestId);
|
||||
// SendStatus[] sendStatuses = new SendStatus[1];
|
||||
// o.setSendStatusSet(sendStatuses);
|
||||
// SendStatus sendStatus = new SendStatus();
|
||||
// sendStatuses[0] = sendStatus;
|
||||
// sendStatus.setCode(TencentSmsClient.API_CODE_SUCCESS);
|
||||
// sendStatus.setMessage("send success");
|
||||
// sendStatus.setSerialNo(serialNo);
|
||||
// });
|
||||
// when(client.SendSms(argThat(request -> {
|
||||
// assertEquals(mobile, request.getPhoneNumberSet()[0]);
|
||||
// assertEquals(properties.getSignature(), request.getSignName());
|
||||
// assertEquals(apiTemplateId, request.getTemplateId());
|
||||
// assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)),
|
||||
// toJsonString(request.getTemplateParamSet()));
|
||||
// assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId"));
|
||||
// return true;
|
||||
// }))).thenReturn(response);
|
||||
//
|
||||
// // 调用
|
||||
// SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
|
||||
// // 断言
|
||||
// assertTrue(result.getSuccess());
|
||||
// assertEquals(response.getRequestId(), result.getApiRequestId());
|
||||
// assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
|
||||
// assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
|
||||
// assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo());
|
||||
// }
|
||||
|
||||
// @Test
|
||||
// public void testDoSendSms_fail() throws Throwable {
|
||||
// // 准备参数
|
||||
// Long sendLogId = randomLongId();
|
||||
// String mobile = randomString();
|
||||
// String apiTemplateId = randomString();
|
||||
// List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
|
||||
// new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
|
||||
// String requestId = randomString();
|
||||
// String serialNo = randomString();
|
||||
// // mock 方法
|
||||
// SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> {
|
||||
// o.setRequestId(requestId);
|
||||
// SendStatus[] sendStatuses = new SendStatus[1];
|
||||
// o.setSendStatusSet(sendStatuses);
|
||||
// SendStatus sendStatus = new SendStatus();
|
||||
// sendStatuses[0] = sendStatus;
|
||||
// sendStatus.setCode("ERROR");
|
||||
// sendStatus.setMessage("send success");
|
||||
// sendStatus.setSerialNo(serialNo);
|
||||
// });
|
||||
// when(client.SendSms(argThat(request -> {
|
||||
// assertEquals(mobile, request.getPhoneNumberSet()[0]);
|
||||
// assertEquals(properties.getSignature(), request.getSignName());
|
||||
// assertEquals(apiTemplateId, request.getTemplateId());
|
||||
// assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)),
|
||||
// toJsonString(request.getTemplateParamSet()));
|
||||
// assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId"));
|
||||
// return true;
|
||||
// }))).thenReturn(response);
|
||||
//
|
||||
// // 调用
|
||||
// SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
|
||||
// // 断言
|
||||
// assertFalse(result.getSuccess());
|
||||
// assertEquals(response.getRequestId(), result.getApiRequestId());
|
||||
// assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
|
||||
// assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
|
||||
// assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo());
|
||||
// }
|
||||
|
||||
@Test
|
||||
public void testParseSmsReceiveStatus() {
|
||||
// 准备参数
|
||||
|
@ -156,7 +132,6 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
|
|||
" \"ext\": {\"logId\":\"67890\"}\n" +
|
||||
" }\n" +
|
||||
"]";
|
||||
// mock 方法
|
||||
|
||||
// 调用
|
||||
List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
|
||||
|
@ -164,42 +139,44 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
|
|||
assertEquals(1, statuses.size());
|
||||
assertTrue(statuses.get(0).getSuccess());
|
||||
assertEquals("DELIVRD", statuses.get(0).getErrorCode());
|
||||
assertEquals("用户短信送达成功", statuses.get(0).getErrorMsg());
|
||||
assertEquals("13900000001", statuses.get(0).getMobile());
|
||||
assertEquals(LocalDateTime.of(2015, 10, 17, 8, 3, 4), statuses.get(0).getReceiveTime());
|
||||
assertEquals("12345", statuses.get(0).getSerialNo());
|
||||
assertEquals(67890L, statuses.get(0).getLogId());
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void testGetSmsTemplate() throws Throwable {
|
||||
// // 准备参数
|
||||
// Long apiTemplateId = randomLongId();
|
||||
// String requestId = randomString();
|
||||
//
|
||||
// // mock 方法
|
||||
// DescribeSmsTemplateListResponse response = randomPojo(DescribeSmsTemplateListResponse.class, o -> {
|
||||
// DescribeTemplateListStatus[] describeTemplateListStatuses = new DescribeTemplateListStatus[1];
|
||||
// DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus();
|
||||
// templateStatus.setTemplateId(apiTemplateId);
|
||||
// templateStatus.setStatusCode(0L);// 设置模板通过
|
||||
// describeTemplateListStatuses[0] = templateStatus;
|
||||
// o.setDescribeTemplateStatusSet(describeTemplateListStatuses);
|
||||
// o.setRequestId(requestId);
|
||||
// });
|
||||
// when(client.DescribeSmsTemplateList(argThat(request -> {
|
||||
// assertEquals(apiTemplateId, request.getTemplateIdSet()[0]);
|
||||
// return true;
|
||||
// }))).thenReturn(response);
|
||||
//
|
||||
// // 调用
|
||||
// SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId.toString());
|
||||
// // 断言
|
||||
// assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getId());
|
||||
// assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getContent());
|
||||
// assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
|
||||
// assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getAuditReason());
|
||||
// }
|
||||
@Test
|
||||
public void testGetSmsTemplate() throws Throwable {
|
||||
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
|
||||
// 准备参数
|
||||
String apiTemplateId = "1122";
|
||||
|
||||
// mock 方法
|
||||
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
|
||||
.thenReturn("{ \"Response\": {\n" +
|
||||
" \"DescribeTemplateStatusSet\": [\n" +
|
||||
" {\n" +
|
||||
" \"TemplateName\": \"验证码\",\n" +
|
||||
" \"TemplateId\": 1122,\n" +
|
||||
" \"International\": 0,\n" +
|
||||
" \"ReviewReply\": \"审批备注\",\n" +
|
||||
" \"CreateTime\": 1617379200,\n" +
|
||||
" \"TemplateContent\": \"您的验证码是{1}\",\n" +
|
||||
" \"StatusCode\": 0\n" +
|
||||
" },\n" +
|
||||
" \n" +
|
||||
" ],\n" +
|
||||
" \"RequestId\": \"f36e4f00-605e-49b1-ad0d-bfaba81c7325\"\n" +
|
||||
" }}");
|
||||
|
||||
// 调用
|
||||
SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId);
|
||||
// 断言
|
||||
assertEquals("1122", result.getId());
|
||||
assertEquals("您的验证码是{1}", result.getContent());
|
||||
assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
|
||||
assertEquals("审批备注", result.getAuditReason());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertSmsTemplateAuditStatus() {
|
||||
|
|
|
@ -147,14 +147,22 @@ spring:
|
|||
|
||||
spring:
|
||||
ai:
|
||||
vectorstore: # 向量存储
|
||||
redis:
|
||||
index: default-index
|
||||
prefix: "default:"
|
||||
qianfan: # 文心一言
|
||||
api-key: x0cuLZ7XsaTCU08vuJWO87Lg
|
||||
secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK
|
||||
zhipuai: # 智谱 AI
|
||||
api-key: 32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs
|
||||
openai:
|
||||
openai: # OpenAI 官方
|
||||
api-key: sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z
|
||||
base-url: https://api.gptsapi.net
|
||||
azure: # OpenAI 微软
|
||||
openai:
|
||||
endpoint: https://eastusprejade.openai.azure.com
|
||||
api-key: xxx
|
||||
ollama:
|
||||
base-url: http://127.0.0.1:11434
|
||||
chat:
|
||||
|
@ -301,7 +309,6 @@ yudao:
|
|||
end-code: 9999 # 这里配置 9999 的原因是,测试方便。
|
||||
trade:
|
||||
order:
|
||||
app-id: 1 # 商户编号
|
||||
pay-expire-time: 2h # 支付的过期时间
|
||||
receive-expire-time: 14d # 收货的过期时间
|
||||
comment-expire-time: 7d # 评论的过期时间
|
||||
|
|
Loading…
Reference in New Issue