From 6b69dd74d4f4f253f7d9c67f64f209ab5b45d9e1 Mon Sep 17 00:00:00 2001 From: cherishsince Date: Mon, 15 Jul 2024 15:32:04 +0800 Subject: [PATCH 001/136] =?UTF-8?q?=E3=80=90=E5=A2=9E=E5=8A=A0=E3=80=91ai?= =?UTF-8?q?=20image=20=E5=A2=9E=E5=8A=A0release=20=E5=88=97=E8=A1=A8(?= =?UTF-8?q?=E7=94=BB=E5=BB=8A=E3=80=81=E5=B9=BF=E5=9C=BA=E4=BD=BF=E7=94=A8?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/image/AiImageController.java | 11 +++++++---- .../admin/image/vo/AiImageReleaseListReqVO.java | 14 ++++++++++++++ .../module/ai/dal/mysql/image/AiImageMapper.java | 7 +++++++ .../module/ai/service/image/AiImageService.java | 11 ++++++++--- .../ai/service/image/AiImageServiceImpl.java | 9 ++++++--- 5 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageReleaseListReqVO.java diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java index de12ee1e0..c06842f33 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java @@ -6,10 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageParam; 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; @@ -131,4 +128,10 @@ public class AiImageController { return success(true); } + @GetMapping("/release-list") + @Operation(summary = "发布列表") + public CommonResult> releaseList(AiImageReleaseListReqVO releaseListReqVO) { + PageResult pageResult = imageService.releaseList(releaseListReqVO); + return success(BeanUtils.toBean(pageResult, AiImageRespVO.class)); + } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageReleaseListReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageReleaseListReqVO.java new file mode 100644 index 000000000..17c368fa2 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageReleaseListReqVO.java @@ -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 Image 发布列表 req") +@Data +public class AiImageReleaseListReqVO extends PageParam { + + @Schema(description = "提示词") + private String prompt; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java index fd6e4b398..062196806 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java @@ -5,6 +5,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.AiImageReleaseListReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; import org.apache.ibatis.annotations.Mapper; @@ -43,4 +44,10 @@ public interface AiImageMapper extends BaseMapperX { AiImageDO::getPlatform, platform); } + default PageResult selectPageOfReleaseList(AiImageReleaseListReqVO releaseListReqVO) { + return selectPage(releaseListReqVO, new LambdaQueryWrapperX() + .eqIfPresent(AiImageDO::getPublicStatus, Boolean.TRUE) + .eqIfPresent(AiImageDO::getPrompt, releaseListReqVO.getPrompt()) + .orderByDesc(AiImageDO::getId)); + } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java index 716c7ea8a..abd79840e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java @@ -3,9 +3,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.PageParam; 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; @@ -118,4 +116,11 @@ public interface AiImageService { */ Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO); + /** + * 发布列表 + * @param releaseListReqVO + * @return + */ + PageResult releaseList(AiImageReleaseListReqVO releaseListReqVO); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index 3a8ff8346..81f3b61e5 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -13,9 +13,7 @@ import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.common.pojo.PageParam; 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; @@ -338,6 +336,11 @@ public class AiImageServiceImpl implements AiImageService { return newImage.getId(); } + @Override + public PageResult releaseList(AiImageReleaseListReqVO releaseListReqVO) { + return imageMapper.selectPageOfReleaseList(releaseListReqVO); + } + /** * 获得自身的代理对象,解决 AOP 生效问题 * From ef4fb7ec057ba364371cddff22aef644137e96ac Mon Sep 17 00:00:00 2001 From: cherishsince Date: Mon, 15 Jul 2024 16:15:06 +0800 Subject: [PATCH 002/136] =?UTF-8?q?=E3=80=90=E4=BC=98=E5=8C=96=E3=80=91?= =?UTF-8?q?=E9=87=8D=E5=91=BD=E5=90=8D=20publicList?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/controller/admin/image/AiImageController.java | 6 +++--- ...ageReleaseListReqVO.java => AiImagePublicListReqVO.java} | 2 +- .../yudao/module/ai/dal/mysql/image/AiImageMapper.java | 4 ++-- .../yudao/module/ai/service/image/AiImageService.java | 4 ++-- .../yudao/module/ai/service/image/AiImageServiceImpl.java | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/{AiImageReleaseListReqVO.java => AiImagePublicListReqVO.java} (84%) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java index c06842f33..a5a975acc 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java @@ -128,10 +128,10 @@ public class AiImageController { return success(true); } - @GetMapping("/release-list") + @GetMapping("/public-list") @Operation(summary = "发布列表") - public CommonResult> releaseList(AiImageReleaseListReqVO releaseListReqVO) { - PageResult pageResult = imageService.releaseList(releaseListReqVO); + public CommonResult> publicList(AiImagePublicListReqVO publicListReqVO) { + PageResult pageResult = imageService.publicList(publicListReqVO); return success(BeanUtils.toBean(pageResult, AiImageRespVO.class)); } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageReleaseListReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicListReqVO.java similarity index 84% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageReleaseListReqVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicListReqVO.java index 17c368fa2..816441c49 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageReleaseListReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicListReqVO.java @@ -6,7 +6,7 @@ import lombok.Data; @Schema(description = "Ai Image 发布列表 req") @Data -public class AiImageReleaseListReqVO extends PageParam { +public class AiImagePublicListReqVO extends PageParam { @Schema(description = "提示词") private String prompt; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java index 062196806..0b8acf374 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java @@ -5,7 +5,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.AiImageReleaseListReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePublicListReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; import org.apache.ibatis.annotations.Mapper; @@ -44,7 +44,7 @@ public interface AiImageMapper extends BaseMapperX { AiImageDO::getPlatform, platform); } - default PageResult selectPageOfReleaseList(AiImageReleaseListReqVO releaseListReqVO) { + default PageResult selectPageOfPublicList(AiImagePublicListReqVO releaseListReqVO) { return selectPage(releaseListReqVO, new LambdaQueryWrapperX() .eqIfPresent(AiImageDO::getPublicStatus, Boolean.TRUE) .eqIfPresent(AiImageDO::getPrompt, releaseListReqVO.getPrompt()) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java index abd79840e..86c01d42d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java @@ -118,9 +118,9 @@ public interface AiImageService { /** * 发布列表 - * @param releaseListReqVO + * @param publicListReqVO * @return */ - PageResult releaseList(AiImageReleaseListReqVO releaseListReqVO); + PageResult publicList(AiImagePublicListReqVO publicListReqVO); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index 81f3b61e5..cb1932021 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -337,8 +337,8 @@ public class AiImageServiceImpl implements AiImageService { } @Override - public PageResult releaseList(AiImageReleaseListReqVO releaseListReqVO) { - return imageMapper.selectPageOfReleaseList(releaseListReqVO); + public PageResult publicList(AiImagePublicListReqVO publicListReqVO) { + return imageMapper.selectPageOfPublicList(publicListReqVO); } /** From cb59a61a04698d498c9f9c86bf7a136f7fdff2f9 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 17 Jul 2024 08:50:30 +0800 Subject: [PATCH 003/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=EF=BC=9A=E8=8E=B7=E5=8F=96=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E7=9A=84=E5=88=86=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/image/AiImageController.java | 13 +++++++------ ...ListReqVO.java => AiImagePublicPageReqVO.java} | 4 ++-- .../module/ai/dal/mysql/image/AiImageMapper.java | 15 ++++++++------- .../module/ai/service/image/AiImageService.java | 15 ++++++++------- .../ai/service/image/AiImageServiceImpl.java | 10 +++++----- 5 files changed, 30 insertions(+), 27 deletions(-) rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/{AiImagePublicListReqVO.java => AiImagePublicPageReqVO.java} (66%) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java index a5a975acc..4634c5cd3 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java @@ -43,6 +43,13 @@ public class AiImageController { return success(BeanUtils.toBean(pageResult, AiImageRespVO.class)); } + @GetMapping("/public-page") + @Operation(summary = "获取公开的绘图分页") + public CommonResult> getImagePagePublic(AiImagePublicPageReqVO pageReqVO) { + PageResult pageResult = imageService.getImagePagePublic(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiImageRespVO.class)); + } + @GetMapping("/get-my") @Operation(summary = "获取【我的】绘图记录") @Parameter(name = "id", required = true, description = "绘画编号", example = "1024") @@ -128,10 +135,4 @@ public class AiImageController { return success(true); } - @GetMapping("/public-list") - @Operation(summary = "发布列表") - public CommonResult> publicList(AiImagePublicListReqVO publicListReqVO) { - PageResult pageResult = imageService.publicList(publicListReqVO); - return success(BeanUtils.toBean(pageResult, AiImageRespVO.class)); - } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicListReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java similarity index 66% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicListReqVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java index 816441c49..e7ff80a98 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicListReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java @@ -4,9 +4,9 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -@Schema(description = "Ai Image 发布列表 req") +@Schema(description = "管理后台 - AI 绘画公开的分页 Request VO") @Data -public class AiImagePublicListReqVO extends PageParam { +public class AiImagePublicPageReqVO extends PageParam { @Schema(description = "提示词") private String prompt; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java index 0b8acf374..7ef8b30eb 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java @@ -5,7 +5,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.AiImagePublicListReqVO; +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; @@ -39,15 +39,16 @@ public interface AiImageMapper extends BaseMapperX { .orderByDesc(AiImageDO::getId)); } + default PageResult selectPage(AiImagePublicPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eqIfPresent(AiImageDO::getPublicStatus, Boolean.TRUE) + .likeIfPresent(AiImageDO::getPrompt, pageReqVO.getPrompt()) + .orderByDesc(AiImageDO::getId)); + } + default List selectListByStatusAndPlatform(Integer status, String platform) { return selectList(AiImageDO::getStatus, status, AiImageDO::getPlatform, platform); } - default PageResult selectPageOfPublicList(AiImagePublicListReqVO releaseListReqVO) { - return selectPage(releaseListReqVO, new LambdaQueryWrapperX() - .eqIfPresent(AiImageDO::getPublicStatus, Boolean.TRUE) - .eqIfPresent(AiImageDO::getPrompt, releaseListReqVO.getPrompt()) - .orderByDesc(AiImageDO::getId)); - } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java index 86c01d42d..3858224e4 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java @@ -27,6 +27,14 @@ public interface AiImageService { */ PageResult getImagePageMy(Long userId, PageParam pageReqVO); + /** + * 获取公开的绘图分页 + * + * @param pageReqVO 分页条件 + * @return 绘图分页 + */ + PageResult getImagePagePublic(AiImagePublicPageReqVO pageReqVO); + /** * 获得绘图记录 * @@ -116,11 +124,4 @@ public interface AiImageService { */ Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO); - /** - * 发布列表 - * @param publicListReqVO - * @return - */ - PageResult publicList(AiImagePublicListReqVO publicListReqVO); - } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index cb1932021..1949ba067 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -69,6 +69,11 @@ public class AiImageServiceImpl implements AiImageService { return imageMapper.selectPage(userId, pageReqVO); } + @Override + public PageResult getImagePagePublic(AiImagePublicPageReqVO pageReqVO) { + return imageMapper.selectPage(pageReqVO); + } + @Override public AiImageDO getImage(Long id) { return imageMapper.selectById(id); @@ -336,11 +341,6 @@ public class AiImageServiceImpl implements AiImageService { return newImage.getId(); } - @Override - public PageResult publicList(AiImagePublicListReqVO publicListReqVO) { - return imageMapper.selectPageOfPublicList(publicListReqVO); - } - /** * 获得自身的代理对象,解决 AOP 生效问题 * From 2a984504d98264aa0201a3a03b21462d46ec88ad Mon Sep 17 00:00:00 2001 From: xiaoxin <718949661@qq.com> Date: Mon, 5 Aug 2024 13:53:50 +0800 Subject: [PATCH 004/136] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91AI?= =?UTF-8?q?=20=E7=9F=A5=E8=AF=86=E5=BA=93=EF=BC=9A=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=90=91=E9=87=8F=E5=8C=96=20demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/service/knowledge/DocService.java | 16 + .../ai/service/knowledge/DocServiceImpl.java | 44 ++ .../yudao-spring-boot-starter-ai/pom.xml | 16 + .../RedisVectorStoreAutoConfiguration.java | 59 +++ .../ai/vectorstore/RedisVectorStore.java | 456 ++++++++++++++++++ .../src/main/resources/webapp/test/Fel.pdf | Bin 0 -> 352908 bytes .../src/main/resources/application.yaml | 4 + 7 files changed, 595 insertions(+) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java create mode 100755 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/resources/webapp/test/Fel.pdf diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java new file mode 100644 index 000000000..2e7f792e8 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.ai.service.knowledge; + +/** + * AI 知识库 Service 接口 + * + * @author xiaoxin + */ +public interface DocService { + + + /** + * 向量化文档 + */ + void embeddingDoc(); + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java new file mode 100644 index 000000000..76fa1e530 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java @@ -0,0 +1,44 @@ +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.TextReader; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; +import org.springframework.ai.vectorstore.RedisVectorStore; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * AI 知识库 Service 实现类 + * + * @author xiaoxin + */ +@Service +@Slf4j +public class DocServiceImpl implements DocService { + + @Resource + RedisVectorStore vectorStore; + @Resource + TokenTextSplitter tokenTextSplitter; + + // TODO @xin 临时测试用,后续删 + @Value("classpath:/webapp/test/Fel.pdf") + private org.springframework.core.io.Resource data; + + + @Override + public void embeddingDoc() { + // 读取文件 + org.springframework.core.io.Resource file = data; + TextReader loader = new TextReader(file); + List documents = loader.get(); + // 文档分段 + List segments = tokenTextSplitter.apply(documents); + // 向量化并存储 + vectorStore.add(segments); + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index 4aa6273cf..f015a643b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -39,6 +39,22 @@ spring-ai-stability-ai-spring-boot-starter ${spring-ai.version} + + org.springframework.ai + spring-ai-transformers-spring-boot-starter + ${spring-ai.version} + + + org.springframework.ai + spring-ai-redis-store + ${spring-ai.version} + + + org.springframework.data + spring-data-redis + true + + cn.iocoder.boot diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java new file mode 100644 index 000000000..03dc1c19b --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java @@ -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 自动配置会冲突 + * + * @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()); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java new file mode 100644 index 000000000..de80401ed --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java @@ -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 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 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 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 RESPONSE_OK = Predicate.isEqual("OK"); + + private static final Predicate 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 documents) { + try (Pipeline pipeline = this.jedis.pipelined()) { + for (Document document : documents) { + var embedding = this.embeddingModel.embed(document); + document.setEmbedding(embedding); + + var fields = new HashMap(); + 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 responses = pipeline.syncAndReturnAll(); + Optional 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 delete(List idList) { + try (Pipeline pipeline = this.jedis.pipelined()) { + for (String id : idList) { + pipeline.jsonDel(key(id)); + } + List responses = pipeline.syncAndReturnAll(); + Optional 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 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 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 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 schemaFields() { + Map vectorAttrs = new HashMap<>(); + vectorAttrs.put("DIM", this.embeddingModel.dimensions()); + vectorAttrs.put("DISTANCE_METRIC", DEFAULT_DISTANCE_METRIC); + vectorAttrs.put("TYPE", VECTOR_TYPE_FLOAT32); + List 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 embeddingDouble) { + float[] embeddingFloat = new float[embeddingDouble.size()]; + int i = 0; + for (Double d : embeddingDouble) { + embeddingFloat[i++] = d.floatValue(); + } + return embeddingFloat; + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/resources/webapp/test/Fel.pdf b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/resources/webapp/test/Fel.pdf new file mode 100755 index 0000000000000000000000000000000000000000..405b67fedada1d989b18a7d81df1149fc7ebec01 GIT binary patch literal 352908 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
Fijw^)$D%QILN=)zF{%qagD!ovQOM zZW0D7S3MTb`p^fKgWIBSz&OT$m@Rl7fek6Jdz8_S+Z)n9iaMWsJ-eIfN-B3fppn;4 zt?VDBZp~de*Rj)55N=_Te)lWKog0gd$Vx7131veBRK(fu!8S`LV_k!m^#`~9i@Wu7 zA5k+yCVHXuTw?Kb<}$0(rd-r8M$2f1#B0fP<-cM5Oj`@l((EqzuNBn}`(8U`D)$Hi z`=HBODEILqyB@!TTM*xAl?@1;9SBlDD(?i)EqEdy~DDWYQ zi7s=-h{+QkXC^9#vzmaH6=@#0Y%Nrz6URuCxRgZq zlB56QJ;;neK1T(7j>@d0N9T2hCaK-K+2U!1+VPsJl(0CvuU65}V$ zVow^%oZDsj2_}&rl00WH<5WM-?!kpD(-X#^sQu@*hM?oI)tNl{WPwooL;M0T7*W{C z?!ZXK01V8S<{UUW zrG*TINa@3fnh79H-)ATND&YRnKkx-THS30s^hclGtaeuW*WImhHhbNq(+7JA1*;l( zh6MojGR?2zhQK(>ks9_2Kr|alMTHI{re}JE_)x8ssGm$~QoF5m8m;+EoH6poD5S zya2!iue6x;;1JnM6h>15Q8-VSZY9R#F(8H{y5bEh^Ho<3?fsj0?*50*G6e=|m?Lj! z#&mtGBd2hI{3k#dCFdekL!k5WHCFsM9#++ht;;$P81mqfsbs|j`T;FzRzmSij2)w= z!8wCdL|V>tiA?!M4gEFAEpgdcX`JF{JLBjJ>(|d1-7Dg3KqK`IDZ| zS&-{T5_UQ{`Ej}u6f$2mzmXVX@K+LVr_eBB-H!K=dm4a|)E*k1zeRcvbQT5#+C%5_ zpKuzAWhJ)5N#^VD6%z@ggQ{5L+e<35Ru4Fvlil&@`J-eC^w!w3vV`?hy}#A=D8`c0 zct(a3+eZHjlua;!ii-RbT5p1lt;`sq_I|f);k?w-iZ!V0EU&C;@>kH-gLvC0?#`Fz zCNVPijE~$PPNm}sS^cz4>NMI}AC3#so{#pq_C^96nng*aK?yMyLu+e&ro9S@4XPVd4Sx zerwnWzFxk$rf-YDrKpd`D<$sBf?U+P_-yj02ugsNoOD|&(jZB)&Y4epM(KQ@w&dox zdjfP+fX1$4n@q7U7H{sY!uj+Q5`@YZwQQck7t8mIW%{U8FI8F3EFf(&eUi!V?UO%b zqA@dO2aReUs>7Fz=GUj2FPj;xOgk1QV~vgK#!2$DB#V!|&dH~cDxmx~tiGalt18M) z{xe=yurY!x(t$VT{%nDZ~VHNpsjw`N~s_0blH|$5PsNuUca9a647_X#HSUIKWd2S zr_PS5;I6+;<&dnIt1M@-AV?Xrduk*G>v&hMA^p;gJ){CAH47&4-$~_m3v47t%mn7H zXKJ7bttJ)Nmh9ysYhS}wY>Q6$!U3Id6ALQjbuUs9%Rse2xWlI4dsQ5H-7h~xW!?Rl z%*dB0r$5k^GkcvdTce~qYiVN1b7zKq->8`M^H%u1v`L1L-@x&4Gxd#(L;B6x`mfB1 zv5M4;w{*WNj%8e=gwgY6KEh`HuJuQjJ)5r(Y1aOMGJG`5y(aW-|RL3uTvHwVjZmOjIlFh2;h6a3o$7`z#zMr-+&*RT}`|ANNd3P_@d`ud&k0pf4(KV)}c2PQwVr@hzs zNU)L({95Hr)E`jaF!|kt_sx>g{XcBVc1cKMH92Pf_F68)g^`NfIBW{&f5<-9`nBhh@d z{5WHhF)X4WVw|&0HvVSCg}Ia?KnmY1iHeN{XXRiFqc%V7>ZA zGH4PQlU11X&y-J~8dF;CS17+U8GA@I3H7cQIHBbD%Pn9y$N?Io{DS6UQU$K;8`S}HH*nm<$7$(*S zxty5JB#*99!wq_#mZat`NSeG$`A?dl5*yQ=rtLB!im^7-2EiWve!O^r6)=vUgA=Vo zIvIG%BC#Yg$~VM(i6`Q7nz2yp3}B?!n>-LYC1Iy!mlood?IY>BI>OyPbfo-vbd^LF z?ZUm@+UR2NY6nDx=PsK)s~FMYw*G4Z2WwJCK#sqfT{Qa+xtnR01cb!Gr2^B2`D^64M;nrvP zn;_&2DOw9=fOrx|SY?EwuPf0G_iQZe4RBX*yY@i5w3UD;~^~ z?Ny}w7&Yy{3V<`I9T`qklI!la^P~fT?w)9XL)5{Xle*Z2Pe~xGWw?PzhKDf7?28Ou z>_|@ITGWJ366X}gg1#SE?%0BDfkawb2ak3|NhJZJU8SZXS@$ga@$Pv=9~_2n^>%1D zgBHQqPWPU?)tZqj-^;Kp2s`xI!r6}7ZRRAaeHfY5T!2+7Z!zik+-3MC%PPQrym8%_ z`ylRm-(t|Q)*yp9ra`wd&?@iPCZu1!tp9}`R?my^piEe-&Jls{5O7&wT>hL+VO& zL;A;O{CN`WfZwj>WmHEu`Blk?LgTRKW%0*<-d@XwukD(;v8NS3+$E%6rI{vQGm`Zy z*tje-?=>|?G5nTbND+J?slJILg_QgoL9yQ9VI0JNJ99y3V~S7OzhC0lDF>=1UYtqu zADC}J&>o_|{V&V|uK}1(LSI;Ir1d?LQiukP!2{V;uXKYekm>hhH`nVfCyUni_h9xT*@qC>il>i`+8}C-#>9GT06Y#KX*H5=3b^4BtTT;5hhO;g{|ui}Xbx zgoi~e;&ot||aUb;>|1*}8LY7>F!f|S%h<@i|-|UDsLNl$6wM+1M z_?yV`(-`sGQluM3+;2!}Hoh@o{SIxZQ!N9@|KXMY)bsjl2WY&^f=3jccojbZ+xb zaQj7D%q_3)TG)Riy|%56{uMxN_l|x5FQS~|CU_Y5lq$=vvbaMS`BY*cbWEA(UpmIy z45dAsVfW8GWU)b2{2puiha%}m|M#7EFpilHu$RnSQ!ioCc4Dhk=C5e`6W3~UoV18gNuisJkWm|EbSO%*vkDi%U~kRk_Cwz(Ze7zChff#w?kD5dPf z8E{V=6dsfp24nm6DX2DMIFGTJsUhjT9At!ACrq7}SSm9k zYzy9wF2fI%qJrTAs!xy^SODU>GJ)coZol z|1CT&uZ)=y5_euQg{==(do<`9;|q<&Z-EB*#SDaNNPlr3rl-!7B?y;(}#HDy_W zr?cqpZV^Ib`(ll!*)6eSRJ{VV#BoRb_ke=uVM3j z1*#vT1t0kD7E)`OpLHS1TSzHpFirAUHlZh+M0U^n3p3lUaXtxDcIZ?kfAsSVgh z%$wIs6}J21#_dP4Z%z8~$4O>;6Ej8raW>jVxpEWn<5GHdeg2JS=y|+(U7IU%PP6Fm zN!0asK61qG*+@2~^MUr2$eGc?Q|ix-uBRWln7#-1YXz^!Zl*h`F{7mj{&ZBYJ$MA4 z`s1rcPQF{9y+jT02&{8Mpxb2}s$8pT!uefMan~WKeV1?eC9n-mU zDq9n;H@k%SKh7yMm*IBB-%at8mVBWpE-HS+z6ur};+eA#SU);m&KAiNIh2juI{({o z-u0UM`Jy`xF~`VfCk}0y)MKFX%Fm1E4$#jHV@2Bh-X1I0Yh{KUFbHoPR6X1{GP)al z*Bm2;Lr>oBxpY|R*Es(ABVFOHW70+VanLM4^vq0T&7oLGd#4;B2zYkZcSr4s5Zxvq z0Q9$7F!^!51Fm=SIHjVDBDH;%tGSG^S!{gGA&4P<-|a6QTR?ItxIOg&)Z7RcTj-Ei1K0cwBu16^I6CM z!}o#%{T%hLU-JWLk5mo;mDkY|I7j7fApf*g`j%DV!P;NvulVpx`}|MO(NS-Y>w+(v zYRp2drCZ#KmwxStTd402JG@@HOFCXlHtUkr^H1WG^FN=Mfi)!AE5jW)Dgig*At$uR1-N39*c0CT9P=U6EaxgX_)(mfX=##}W@pa2O}%qI_T_DYZMkgXxuC-P zSA@f(vITH`^v;2SZ#i`Fh3Ct{C6J+BhJI-phLO%r@G}_&eaWJ4g)*)OwR_rcc+Snp zx*FakD3`$v60%ZU{$+0av^n_0A6(h}!|<(NGNPhCeo2{Wbzn*by6=L{n)KD6*$OQz z?{SH5_J;r$*vGRleY?Vh#mA~B@N29&-B>HTQGMHyL2Q2tC!(^!r%P&97N>P6Q!`l z;&XVr>S^xq96b53CWgA`()e^0SPdN>fVmfDOT8^;v0!NUOPt|^?_6f_h@Snen3 zh`HwLv@ja(Mwqav5Mup~b)Ai5+2?k&b7;S*nwo<_@Ip&6bB*NQTMSUrQopBKrYF0s z=wyu$j6PdsU@aAycXlhRloB~ArvQYb$N)_IiRk_~b1SE1?8NiJt$xR3iDUAjzVdnX z-gvGb{b<+l=A*~E{Wl0Fv<9u;cLxf2&J|;|W!4F3?@0M6w1$kmjaSZ**f-T>#f<(BmleL3-8Msp}{y|-?&OOtmgk;zs*ZZpAb zsmnIpiutr4oTvALRHSaJoas;;)#LHx$AxlzcA@Xts83FDZ%)&wlHrq?|IO1uM~5JD z+U~x?C4zfS!?Demdk=gKH??XOpQ>j4Ug26$sj%j5%+p;`k?^UUgcj)qSMKLwFebzMc_(Kz+08!43Q`*%6+nUJsNT{X-{c2xo%1)XZ}Qup6IYvIkydT_Ctcny3>-QM?A=5y>V_NXd&O35 zrKWCruNtsFy*&Ymg}1Pox5O#{zm0ZE?2mCOkQ(aaFTLP|cj@?eqqKK-{=aB@3#ho3 zZhtTd!8N!`NPq+l(6~czmxkaHtZ{dD55e7nH8c`DxVr@!cXxN4&b{~h-dpQ?GxOH^ z&#YeE=bS!LRlBxT?S1M8TQ4Cedmr;52+wnC;OSjfb6lCvWmfM#E%_?nZM*H_9h^s! zVZy8y(mCF9WH5Z$ZI+zY;P1_xS2lxcn%!6d*IXTuR?#k3ok%KEhkl$$!R@xKcMsmR z%!@7S3y%{X#QruV8|mIZj-Cr_ErdAIM!#`=;=Rr~7aZ|vdFI{LUB~cmB@gxy30^EZUrXRP zI%zFou8WNbtG9Vwf1i5OxrXiX3e#JXrIiYo*K{iWMXTPDC57H*UPyJ7TZ0@=n)>{i zA(_CNJ3LY!Kbp4HGux#jOYH0AxHcE+B2 zt!GuY1cefO%jk^OSh%dV0l4+;kt9W9Q9YgdJ?Oup%nL(wrmTkA0q6_9X+au%NuTl9 zi%Eii1n5u%;Z^5nKFBpVbGYt(rcMp;zAj{)J%99GijVVsP0h;rWH2ZBTULy(h-nKi z4H?e*v**m-wOSwfR*i$hBm8@&rdgicu9kC5#M$svxB4Ltrm98P>D^CBnjtZYkE^t9 zd)Y$XU8wI5IPTzRZ6B>WUD`fsUy<3}lXgkp5-9L?buacHud3CZ#!uvF>pUNDkKu3@ z+`1~2x(Y{E9bLTD4FM%D%4423vJFJ=ugw7&XJ{`2nq!k>L=<;b6B7IPnAEUBbOoG6 z!y=}E0_tlAzn6@p(J&|Sq%R7{1QXOT=AkNeD_RrcYX!W-CW!uS<) zAI!aS4bwbJ+*HCQ>bTfZ-z@X5(c6sz_K6L-idwyUk7s&mUbxm&uJ$&l^pMce2t!tr z%1Rzco2m(D6b2l!ygWq06c)4ue{?|?BU%;LsC{g7fNL{WOIVwv+MO;5u3gJ{+u3e* zmp`!8pV)3w$*(79k3QHvek6!iQ9+}YI zfmbH^sGc2f=$dnEFYi&*M@tmEAmg|3fd-G4=3yALY?^k=J1Hh@9^DfI(z!%8o?N%Q zpB+Q@*AXYU6^$}6(YQ{CJXmy8TRtdIND@>Tlfo-KvL5NFdAFsa7<))3egE9fkx^^M z@SxRDA)(uzykQ8eo9=m)B>Y*9K#T2$R$zi-%C4MSVzgbA-qxs{6KpKo*rf=$O7 z_Tjj01I+QdqdM|x4i28fezJW^Ydf#WJc^A$rpf{sEo0o&mzdhn4!{QgEdX!-wtFg%Z(vV;J6NbvYK-8_l zVRxqQ>bqN+6{}Zf_W^Ly815eoCHOq1ihFHSA~tU#C||}VJ0$w*B-?kqBA6dSU?|EU zf~y;jz#p-Jza1;L!ke)hxhj3qR(Ncvx4H3#tZoZVd9ByUE!MgCG}(&V^g9Ex7-uh2 zx0Wb89Snh4n54;H#tIRN1jiP#!-j0vT{yfUT`B4(S!l+RXfFD2AKZ5DF7(!`zy(jg zZY^`C551PwB+d&q#olHJizu=CQ#bW5tMR-gkFD8sGmlbtzFJ|y(e;f$3G@~Ih&G@w zubjE+I{aFmn!YYlQqd$NalW0Cwg-Ek&I3XE%|{;4;_P^ANGWYU;cd~7{_gL!7h*-p z;EO9Br5ktQ?#rzFA~W2&_XSWR)K_{r#QF#8z0C5th=ikhO5(Yo%c~BX zPCr-cNN;%8Iqul(@X-#1e6Y~+UEr{n)VDl-Er|-ovNW}17AKl8Ie8$-4Rv$JvY$M8 zxz+qac#F`(Jw6Biil$`+yJhFFxe9J9v#cNHWxTu2SG6M}%*F4@Dvyf}3% zlV|j!OxS&^>4-(rHKUgSirEpr2M=3BH)_OMNe7|HAW=Z?AufMmSsS4V0xb<|8UH{6=5A5<__4w~e?cGowte(c@Pv{N+z@89XD*6jM6pp9q|Z{NpjAY=G}R z61V!@7Qe70%2MLpD@^ybp;MpC*M*p#lh=nMS-53q?aCJ$MWzf@BKQFOSS+-<1insL zld7>s`vB_?jgoW7dN0W&NL$mDR(D2KcH@+WU&=?!Hz1d-hw|WZm5c;r_RB&>?&D4k z;$NAavUzp*ygg2-msH(5-Q;*wG3~2N(;(DNnl4Oeq|L~En5-;cmKyWhpVL#DzH5<> zpFs=0y~FvSU}u^)%u^0`q=Z3)mi;RGUHn<%s~8IZx&2DbK#{y2<3?i9NHYM{cF}4Z zCrEolDUXl{z%)Pk`W5!ei*Jq~C-k;A=CIQ{e(&}6k!N$0tWt+)6P>WGr7z3WbUPQ+ z^9}sb`78r~)#DU=?30Wtc_Fn(;2b}8>Wv_lUpt7)xWn0Gez&gfp_&7A4V&FHPj8)1w#Q87L4?IEa{|<6p;&hOpd1>L4=E*<-E;3B#7q18jCk`o`)3*7 znGQI8knw@1lIj1ws?mzVU9#?lXTL>dYg_1Kbj;!=H)ajiBumL#=mMvFR$<2bO&x=U zTKNbvtV*3Tu`GrcEOB|j`g*jXWV7Am%<7$7mmvh9sUs-DyYu1+w=I1~dL`ksP{xk) z@P6t|^|y2aTemd}4Kv+IQ-tGuk81C!i3d5)zE@SayaqGsJ8r>VcHAL9J}b%ouD4V_ zXZggZ(#H*Y=uZKz+egC!`J=e@(Ww%!&Cd)W5l7WGl4Cy`Xr%H~aqOcr-$f}a^x-!% z_+5yAiYEqMpOL5`y_-RDmZ#KmJi3N|7?+s*IZ+HO2+M=iv&Sq-lBSx3ZeX>(LO_@- zi8Osdso2ftTh2DjsQi}z(4U}XwbE60%4dW&eKVmO&1YD|n|%9T#>eRKBYR%-xtdw9 zLTpfvWtGZq-N9ur!6~PN=lndph)&62EbkScKAE4zRB&)!dQJ&ra6l5nCHkOVO5RwA_F*KMayHGd|>!tL%wFdI8??Kb_aV+{8c5D=UDV{a@M99RI&KuQ2en z|ALI;_)m|O^MB{D)@Y1IkbcHpDy^E8LKP42`&3^5_jW#T7orZ;5me-)s;GK<%ECjp z6DSr>XUCa*`K>F!6tjWu36Z=xY(*IBok$Z>=32WCHLU@T-s9fkQgJF-p$pN>6KFf_ z*y|X&zgOlTzfy>KBZ2>|)N#d9zClXPDwt|}15uqnfjQAsj=@_mX>_CHY_c)Q4fgcQFEd<9>DFck?2Z%pt68Ct^f zVu#cKmWk3*GlT6~y>~J>8554xafGDyt=}$Rp4sD(w>9De-PrAgA_S1(njM7GX57nX zF4CUU3JC;vdV5gx*8%F0%mIURsYi zj{YVIf&OlSxbGe7YjB%89MIt{k^&?*#t;F=;DRB>dZq7yq<7FI)hOMa^mueQkWs`y z3NKL}b>3|Ot?EL}VWD}76tgiQ%AhmcJdVToh;DgHFMakBua&cM^1y;r^Q_;OMWrpM zgy;kYwKVF0XGJDH+r*77;1Fi{fPWyTRjrRv1;2x??aS6Eb6ubw{(;pcU;e9CqJvKq z4K~`R^@H-!bV(^Veh#Cz=D`;Ejju0^si^O&jO`A*Cx00>Wn4yka~jwk*M5=tybXYX zprpNW_Li=yEy7rEddbszCjxQSEq~;E@?6PJZM5Bf3y&lHIZ#WXwkxap%#&kn)^U8? z6~TXB+han77?+Np^&A%?PiAe)>b#D{=d8JZOVH`1$5MyUwMF1ql=6Cq4yI9Egrc9s zRAi8=$=o3}u0Z;He$=)pn<~cP{6Q|=?ZW7L*Ol-Vd#8cinOug&)(4BxxSO-WqU1)7 z^^DS?&$c2Fp=VUGbk%HLAM>~arv#PB6HJ2~W(FJx_O->degy4~Ohez=-8P~`+mO3( zk%{w$V3W_(KbCVw&*8;Fo!y^T1qCPjSI6t?}Pfn~H$$uoxy@k7U2a{A_9$9VDTN9@u4 zUosO=&m*<_r8ec)|8>evP4&+yJ2jv^vBbu^Jy#dv-VKJ)fgm?=c{^ak1Q(5|8<1&e z!alwToP~xl?kI}wKx`&SWs#g|N)%eHC_Gks!j4YOKIF@j>i35D*Hm3p8a)14?Q?u$ zc%K0AyAm>$zLQz=5aY++r5;bbY+uCO(LvGJUew0Q*2db{+L4kAg;~_b(#BrJ*1*V^S) zi5({PmO57Q9d=l07=@mblTnPo}bq`e~W!DkyHbj-v2%e0Z;6zilv!_`&sXaVe zix1xCv2H0t|J=Fg$cHUO;Hm3H1E?8O zIx5`C3$?kA^R)*6r0<tFY!2mpOeSWm@fZ#iCJwM%#)*9 zolig=2@0e#>JXvju^pH8?M5rB>?0u8Z`kJ_V! z(UyM=v_d~dH_SB2bEmG= zdX1?OB-a-Lhws*=B_#NA)mrBfaeACzbNP%zsuY1NAcNvoI3M1r!`YPA9n<%xqg4GC zUx(BzI|&+K>pnn-7V~U{g4hM1wisHEto)mQuHv(>ZALWG^mtrjrgBs?RNqA6k!L0XL;akMH*+MyRoc@eiv<|ak$VGGR}XmO zXSu>i33`Ft=eIOJe=y4aRI3B-8Fg72P7N30I0A#fzZ#Ub=^`44K^}>*UcOfCnJ+}B zHXdm`ER(+2J^Tn*sRjT0c#o~1Ob0 zr&TC8`A&M+MMVsP3ZkDIHuHFFrnIu;MVofAGbb6Q2*Drc%iKiG|2TM9YD> zZ99$`F=an+{D>;;C@zr zVfz5%iv&twXR;6UO-b?INMz({l_Wy+>w*hgiuggtH#$t+Zes?7ueW5w#kZ@5Ms0-` z)5n|8)d8n>VZ9(l@!C)u6-P-9iZgMd0|$}Vwv=D=!1Fk8@DpjkF93hS$2lZSk=jO2 zqY3dI5Z7xB%pJ7Iar7|gO){E*p{6k~ZJL{bEVTP-_U+U#VVXZRs{%<_oq|VM!7gY_ z7A-qv*~q)Jg6i-zoGQ6(!`*YaKfbs3Z@h*wXuhx zvljzrKjmG!*VOCbT+m&EtimHmlg;FMUhZUM?(u(E@?%RKC7}g)o>YQ+0x2@Pb;ENj zm*>vDMePW7zm*U_is=Sf>XnzM+@@;;I`;*LgNfrKVilf<>Qk3A&>I?vnX-34O^(QD z@LyFCAJH?u%`3xUlFQzc6-~k8(rV*|3-rEv^AHL@B{Y`J(0KfLSOYyjZ&^iO4D~u^ zx@OfPs`f#}i8Ov~!I8x8ck+6FVNPc@n);MGi< zbDm-Rb;9KK1LUHCHGw*jYCT>@tcSUw&!%-wua-F(K&PF zb4EvIZ4-n-)&~sSVTzo}35=_2fAT{lxkIlD5N(AIINh6RTjI-8B1JceJ{ zgA1d^uOfv8XOqVnCDHlC+S)cJ z2ZmbH0V#?a)5Wm!V*ZTlu0_JwEyKX7uhmU)-5>^bzfxB5Kt5Zzq-}-zWpr{nSzchf z;Phf6%hQ*3a|0jG*zLw6FU@3ss+}Ik1BGdL>S@QP*UZ$b4Ah#lBP8n6U+OKW^BRPG zUuh;`sG^I;JBl?Ow!u41(5M5d+n}z-9nCgZ^G;q{4~+v|*7qAD0HPj=t`jo4g)EN? zKfCLy>vd|is4yII1Pt*&iHo8ePDk**W6atOhWV7cX0l)GEwo-dI??5)20Dd=b)!6x z5r6mcp4GYfHzw4{g>)tLxlapbAkF9Se`Ql@5S- zWl|MVuBic4>%{M%UY{!jwW=p~&q}~6+vAB2GhFYaVZ*tmXnOCK^Y01dGXs4$Xf~VI zi!14mN7(A2H%JO(=P26%K|RD-Ef=3{v-&7izz|8*06(C$FNzM`TBI9Ka>3mQ;5s-> z9y{T3&Ld%{Rx&$vtaht{Lf**A^2DoHq!xi|C&qq0 zE<)!O>^wJraCcP{OeYFpmgg`6468{^D3VOYzKvb&b8V6+Ex02#+hbLI^*zPmz>fh56Q?P=S4}ahMdDgZR+8Aa;}+SY2p!13DFveaKZ$! zR(HJXD4oELc@7MEOBk!cw}8hiuOH`GMS^K1uD(P6%e%R2Yq_j)mXvqOd>C9(c!qB? z*6*W5$jEx~I-{=d`;|9GzYVGb#Vw+JgeH2TNPCM=(yaq8=ghlpkpD6PxaDb1q3hfE zykQ3v8nu-DSUDaj>7g&&8>N#(z2buu%bS z$!UQGY>ZHuDh(C~_rPxWe;6;gZ>V*tV-#=n6Ay6OLtXW?Zms6Z= zdZ&oxZ3PT2+V}CSIn{Z1(k8!3YFuq!&keTypIAe{U&t_G20D$DDv3pl zQ)B7WCL+S2KjN|w?YlwZu&;ZAA>hNt#wwi}8+Ja(U8yp)cZ=#xnZ~eWnT8y*dMT`q ze5Cmx@my6o=8wQQd2jMQyb_{*ypr8b*mJ(ch2*IYOU8qDKX-%3(?Z1R4FS}!CpK$W zrsCS4*~Wv3!ju8cMH+HEw{Dc`r6%vxOAC_dfpH@8m&ripzBHEj>- zIEK}_y{T`F-$16!M$8I8$; z2c&YMx*F?QsJnbMDM~JL<|gTmxIfYcabRB=e#qo&txw(W!$xqm10Yw&x~8L62}KqR z@_9cb4|nq?fu$&9PGz~&hPQ99dBiqhVDS`zJvSWWllF>e$B}GS|*J2 z@B}7)%7EA;Rk=HjaMO-W&{`P{td!%mj!3LLb1lr0!(wr}-h7aKJ8Z2od9^Ahm4CDcrkRdt?EK?t>p;{!T)OWgCd|%dIrYM+7-A8nJUm9 z6`E35cqRS*uy%q|vW869vdHE8ONbi<(nhuTp|)LQG#?^Vryl2XO0Ixojb3I~@Fq zfugl;588JtKx*&rH)rcr<(M?WX-ApMdWP5I&YkSon_pT_!>O$c&f{s3`(w%DZ0S{| zGSsu6{s7N5%sy~vH#IToCIVXA1@xMdDSSIBF9d)%3szOUi%sqyqc?AU5=ujV#N@lu zJcvRg$rD5k8En$l9B|7sz_;$4baBH+8oi04+D%M?O;aYE34&qSf7df~yt-UsO7dba zAGOl#=Y?=-&+_4=l~WZshcoSmm@SJxFKSKzUy&A*YCd~6hG5z`jR9~19Df^p`Ff|`S~MaZ!OFD+l% zXvhyA2QZD~`gO0|2d|@Kf{>(XPLC<~#$MA^&y|Q|gyU$2!R?OF zJ35PsaqJWK_d@w?9xFVFO7|ra?pih{v$K{x`x>S2rdR$G9dYqy%xZC%iEI7m#oFkP zq6_nsQ7!;o=s~%rA%cREL~@oX8xo$kf}cRR1-t%X?2>w#A;+^SujDgIQZ=vqrs-8U z^lL}Z7fSJocJ#8eTN4+;To40)lfOEksf9`40nZWD^Ud$cdPa<-lfj=$DgNN%(IwJd zPl1|YON3`OsV|W-fA^f{M>QNbFBzy%|D?AOxupmU=c>x1F<$6{`rv4B^EKVh@ zfT_eZFn4`TC%R{!xo$m@%=2ex{rwQ4XA(+gZ`KR6lQmFJLO0CN{1Fwd1hC(J5wtHj z^+$q{`TB%}+Hg;CYLp5f>E1G2)b8fyiW48GVoq3`>RAaBZ#D+c*73QZ#`Sw=mxs`{ z>U_5^Ey7ur&WI%AVVfMv(YEI1UWUi{J4Dp1d8 z<{Z&(5cYOQm_+|Gas6i!9l*)O`tM2f|6F;A^MAMU5=2eT0!Nr|Ng9O} zX-$Ii<|SOhPhl6fWnK%auwp4C+;JxNqD%&eWUe*y%Tu2jy%kJ4k@BVHdy1xujsA#{ zgOPXhho$H`L^EC-ZmwC^DKVzXLUaUJ#Z*ud6)Gy@Sl@Em?DH3$_s8ujz9Pt`T-= zB@%t!LX_~pHA0eg9zjFeY=nQRIgDwYvSs6lI-EokQi4#jpXUQT*86m6$u@JYW{Jv;xao%m`tRt z<{+4lr2yyyEY}droujWkZM>Wv*`qB~ZLtJYPh}DMT@L$CiD2ZnnW`~L0*;7hG#b5K z2d+wUG)~-xtISUyHyh8L!OyBBQAXatF9NKWh%$xbEyS$wE_IBSmq~{LD5p?ngtKw| z;z`8dn3n-6c=M{Xnesddfvmkoab9WR92pbId(hwPW!cD1A2d>X@Tw)ph~(B$QX}(- z;!j?wfq`L>Y4Nx8LF1wq{-1ZX5!{2t_1(DO=(Lc6 zUwp(xS^ZBU{Z+yrg0Qpwn*{%&8>}AxW0VZQ$@1?g87m9m|Dz}w*B`*Vzew_5QL+rG z_+PTP-RnJ3B(RTo-nbb%_MeR;JMrH~uuaSULhErF8P=HE%j+kM5Vp5J8S;)tGC0ws z-UeccAW{9_{)hq6mr}vyM{q@V*aAu5@-nR4m!zZ~z1DRegMhYYjmw0Vxb@SUrJ7DB z2YN}Q(^o|*_`jDEGT@#qt5Q$7v`VtC%c$=WOIkKWLU|1R{HacDR~ZNCEbHb?x<%7# zZ=-p%I|~!vYlwu(M)y*ScCC&InfD0lUe+)NIlTaS&FY5KWPn?)L_&YPNcxl^FOfIy z%D0u;B;m%IQVJ{?O5`IsQPX=ewEcd=e4tk{TH_@-`;l(KpkEi63Y!3dH)EuiB74!A z`dxw6arVe;Nvn$R^73*DULd{VHVU>#lP^~^5uGLki2FH?ZDR1uXSN}ZV)L{(0 z15=(IS-eD^KPyU7e*MC+1Z0ZyStZ?=Qq2h?xF~ycQ~@|JnNdX=L$sg1Y1^l<&2aD4!(%#o z$U4>jbCE{T=&98ku4QBi1y8J)II3p|((FR^d7P`t{|8Xy!EUDYb+Pn7Q2Wq}WE=!G zIDL7-Av9lZ0~WRyCc*7O6?2ERf~94(?I+5@>#S$-UnGKL+}s9A^E&cnn{NhfxlEF> zHzBD2Y;mMhTeDJ4NMiS{P{nr_%VyDvnfLX&4ke+|ql!N}XWV$U<4fpfr<}SbH@uT`FiuI}#XX~YxtSKJqJG=wKSaU=bz*?J4tjzQj!eF9LV?KBk$?lrtgrC)zh#8?(G z5Ym2a*33rOfuDdJtoYVJO5j^g*J4>3SJvjSjqw9AA^kx>fYESFJZwc{W#Kr-!zLH9 z6wSkgI&6`L3*bgf^^y(fSFfFHGm`JcO4Xt@!4$He>QS?p)Ov#*t9XH)lPj-pE_LS~-m zKTCR<1e&_y*Hg240(*WeSrj!Z(%;+EyIA^7Y{W-CUN85r&=5`^TgA_PboN|)>|f#PcgcOvaWL7e0Y6yNLCZ~_nW1+zf%L`%KT zly%Jb5krqJRX;Rm7-8gmxe}g7)XJsHr8Iki@o* zE3?yJ^?g@Lf(Y`ue-u|7EK=><7!~}|0+FC;QD9Up^w2E5!oTA5z7dwHOArCDKrx;o z*ejdor$DqwXSP5*){MH@d@x|~_(RjyJ}vGHP3j9OY>}7>ed;N@rZz2H)+2;3e%X|k zhCU2PdK%O%2E!-k6IvNikbqwMrxbggj`irD-TI-vJyF*+OBOSZA*cc3Xb5o2#qdiX z$;#XYW@x3LVEpI2@1n)Y%^H>-eF}NjVz1fTC(w0D{UCxI@HM^x5vHmaKWtEUto&A&zgT{ z#J$-_uHFCA-Rl6(U`zy-KBuGSI&QWod3NmB~ zd_nb-5ubhlXDw4E$TC^^!#bqCO;#B4(B(y1&f2U-6ymi12rM_P(e(21apB2W@utsu zXu`u+4J2U{1l0T#e+B)x3DE=w8yPkWB`7isZBxYj_lcOdaFwWysJ zS@+gY->Qzc`u4Q_*0XZ|!l7(z)n}YCQg*ejZ>`v2^ox;S1&4Y#t z6Yf5(b}jS}TZjE!NAg~oop_I{I|f>t1Zv$IOP-GDL<>-YcE6U+o>>g=HOe=>L9Nv_ zHXHa|PMe^?k)N9o?Rotnn%9m|i9{r=t@H5}G3UNf`6Wp;9+qn{O?V&_ipr)0w~}$Q zHQ7_{=LEgKDV5%opf@rUHl`6JZbbM#8o_a$4)~1AEs6Ch?U!eDg}~IL7N2aE#$yD5U+srg5gPQLfzh_J4*#!wm(9EkI^e7 z2~x1cWA0J*#~iMtPkvqpzN0@;(j_5WSLKUGCj;{8So zJ$9}UHH?HUNW*J2t#YirCF1fg-&~9dNVJJfWLz68?p36IuluMX+<(~wt=ZIAETw)g z0kas$RG!``b@l;qG4hTW)}-Brjf=dyvs#3X{C>75B$8->$W|VNpRO?AUt)Ru0b(Q*x+Z3ku%N^l4Q~q2AzR z1Z`HJ>`N|5Bdw|sq78DDs@E>D!L5)%t~ECy4`r<@jL)~gY1cs>!^iuy|P(#1a;koEte6M1GGNP52+=vG1+U-zfXSUx(N z1on!{%q!si+=9(!-d|?@t|f{%+`+ z!bW}7ZgU`(Pcm2Z8=PFawZTpWZSVVDVy3T+i+8bkIgsmi%3Rs$rg4F+(MObz3em3b zTM3)P%7qR7G0Z7WT`2ytC2)#JvFTZNm7=m{HJZkAWkZyt7~4DJ7gP7s>Zs4K#4op_ z%SV7Ne8Su_FvO|k{IkO*{0PB55Qa$0l`5|At3o8SXMEQ&nX+#dQ4^t@?#+|04pl;X&97&aArYmX+Y^6?L)#ymIU45&2BLxRx1 zeYpFOB;e}4-`vEvIPFe>(d+pN*K&}){CshR?~5>2=>XZ3wTml=EJuDO@mnRf$d9n~ zC@VQVc3HR+U#VY{^9X0yYX{Z5G-gZd#LGVo>^ z+_J~nc47F-zYN;lx!*T+BfyszT~Cho*7*fTnSUVG=vE%-*w0m4$*QO;Zp z&d5O2>a$s&+#I}jfv)fGAIYue8jM&|vfn>nUP2F@PqF6<(Gj0TRNSA-{7^)6n$B{) z>SN=A-40Dz(uPxY|MHN>abM)X@c_tUT3cHiD)&@ypf769^!Cyy1B=r0jH+fGc{1Ad zed}KrYnD%=DEbf;Ct&3Q>tF6kBo_lqYP6YfI3r%MM_>alXTt`NCP8}BAHacFHkg-4 z!unIp$06l*A>oAIu8oK=Zw_TZ?0x-}Q(T3)r? zQSb6@-L`@cvv8{L<@p>Wj&Xn~VTrS=x8K(-{NNUKQao(ZSu6uAfzEwOkrmI2eZ+&+ zY7M|515XR1u19m`83yxl?&v93 zI>9$uiUhc*V8S1PJpTNTh&3xJMKoq1UZhS0vZvu0w-48CLaV;*nOv;S^IX}ikKTf} z=8dwDYzubaO=G)5_#a0~p*u?)*+81}wgsDRq^3|yIg*ld3ZCG=H&w)I{6I)x<@;|Z z(W4PKz5E+inkK3cWPUgE@^R}0Cy!&=FZc4Fs!SWnqDjuy_eU$S(_`O)#vKH7v;@g)-yh&6Q)f7cY1?EDOU~yl+-=T(tl9<}sj*AaxLCS$~Gy^sm zukh1polTuqnpXq|qn-N(ZDDF!9ZxerO>c2q`4Z~I1lv(}u>TarIldvA*M(p6`JHtG zM?H_75!9b?h9Z%12lCc=LUBzb{Si-oHFIPRDS-28OX6uL%|~h_d*U&0_fd%~MS`s) zh>SJL4W}8;hxkP+YFU3;0*MkqUG6Urwg>b0>^Hk9I5*a7)d&P3{GT;c>>gHvqn?}w zE%VtVV%Be5n(WhkYjEU~p6J|PnrM+f1HhFowzwUbWeq6j>U&WWSD) z7UNE5xr|eUY|V3McvReUrYiY^(}B)>Le8=6>f^u^u>1}z7#y%y7H&bFJ}-(PlOba3 z`}Oq%o$*Mzz5}HYkWz!xD0q=l{3$?$i{M+v4>x)TaTwwSU;XN-BKr>6<)A-w*R+h{ z;|H5dav_Mk;P!OQ*$2Qwo@Ob|?un_s=%XU7hX)0?Q5xy#PED5Iyl>L^GrcB(t}(1nfQOF(uGWx?g6j{(M<3fa&TQ?}TGdMeIAQ2Io!_|~ zUg7L~OR%mROtZexM_PVux02G2qlP)sS66fsKTKqe<9iSKZ1mpdGEosSq&s`vAkc%d ztXADC@bm&Ui*!?L!ZB_ErO+aW>I6ASG*rh)c^3jX*ceIWp4g?^hmg4t`%pH*6f#bN zI?K?a#o4+Uu_&*OPYtBJZK%$H3sK*3i`3S>61xu={oKZq5N-ES{$3=wYij3R-;`k0 zH62EMUUJyZasm)lL8lF?U9csTquIXCUUVmpU&K+ZXvz6|lIqRj6lc6rJp=q^JLTOS z=+n_^-EQ6Qlqa)X&Eg|4q_J~U%)tKm@dK?5@q1Ncme9AuTmXkxQP3H5)@?JPJtU?i zpXxTzRhwI-ofT`c&Cc(XLg^eYEl9gs`{Qe2%X25RJP!&wKV}CY6;BQS>QnbncRr2HL zcfoPeQg|ZDGcIwZEsyb2(!~(+P2(5=d3e>)NicCB@iOSPicX*R33!)C?@lkRyj6`{ zIGBe(avk@^+|?ta`Jp}%x%&94*WHWRus~aNnl596Umf|Lp@V3SK{DQ{|PU=#NUaq2^r_*wfqdu*SCi8gf)D#Yft=C&WzDKlH@hU&6tmTz+6~25V zuvBF!FBYWTtI6coKCdJ(ZVyO~nUGbC3b+oKm(hcqkQw0=VGLtGO=&{#|_vU=WpZi;Ff zWYbeD*o$bJ*bbWK+tfWz1qcWFkW#H(H()wnR#`Rt(l@S^t%%v|BFbhkqwL7m@#-xF zz@+L7XlC_$LDgR`&J>vP`RbO6@(&(zyLvPg-{e>F_C%XW!c&m_hxc$Lk~<6IaRfu zz&A0?Seu=TB-Ote*A8ulynfI&BO^@?tYoBJO^$RKzjm!dMPyvoZ~`}`?H^IGsD}dJ zEqr8_1`3o!n)4ErZy88M2x+gAjo5Qfyn2W?z4n&G^{RAzf`#uN?B|H~f7SfEgw<7*&xs>;7-A?n_)n%=eyD-PY4kqYsV;Bf>X-@8UOYjC}?CV+8P2c+` z%nHyRFG@el-ShG9M)?Rv97Fu!&Tc1XQaEkSRMLM#_^v9Hpo_2>N1gMHTwpV5&{Fv0 zgPA7G9K90@%Fd5+COBR|YjYN>vcTByK4cQS79I$x9}-fpkH@r2@hh|a5O;q?mrdkOw(0@tzHm;v02 z(kTWkg<2oj(6$`u7ys2VIeQ|nc46Ff{{-0bw1huhM^SAy_;kqay(AWeAtw?6(PGd= zm3F=M7Dep=y{JKt6RpdE9uRPLei|qQEtXpXCzlzo@^1n&{Ngsth5| zysxjhFA8gjx%j~uRwa2M-BLIAENh_HaFgx)ng^c?dEU#Z*R$E;Os`qE-g9wta%FyU z@mQEGfUVGub;fZiT>QAeJU-C%6O9_I^Dq=!F4_tHFV@~FtgdET8w~F5P9V6uJAvR3 z++BjZyE}y7?hA(?!QI{6-QE3Px4nE0PEaU? zog45cTh1^O|5v|HPf4tX$F$hLOnA8@nYc;4zQl5i$T@G_f z=uy{XCvHxv!RZ#6oOMr}A5QMzMP*0+NVC1uirr&LmEX4 z-^l?gbmj<) z47>6k-aa9xp5FOx1a}*d>WrBz=-;zMXAqyO#q`%~Uc79OiVAzUFmp-a%>|X;IG%wC z_4(X%^ILq8PGQbB=g-1Ai?N6n=sygwj~@IS+UfiKEP(ET-r)r4PqW&;e4h2-y!SXc)TtFTFA82CfKAg;nBpa9)(}Ss!OYN7 zrypP#K^d(&>4iMI?*+sm+tkYK-E(PIgygG&O}Oeb8fxLaXS~XCWH?`6-$DtpRUE%7 zuo`@FG_R{R;TmF8nc%T@{Lc23^%{&Alpr3O&VfZ96G=byg`G>|Fw{Gu7joF$tK(5` z+q132;cOf60b12W!qRY~d`jYlxf%jO=xefTjr@t=(vPF5B|Ta@?M6Ql zy4*Cw*G1tTl_?Cz}~f$$I&n?Z|-4>E|y2~JpA4m!>+3zKVGa%twhs}s_ET4Io}3UU)=uZ~}m ze)MEZLX#>&Wz7+A((YAVpc&q0uZAdRwIm>sfV|9Hy5`pV^9n^~OFk?b$hd9sPcS#p zCADf4v?j0(qwHSDb(?OIMYE^Td7Fp?CsqQ!a4m8}N_f`pFFJ~+^pzNWvP`UJMx@^! zO#il*+`$@DZA>t8(om?B^XPXxNS8J`rdp~eB%@vqbf}U;wJ_5Dsnm_7rftW^dDv>Y zuH<@yb%aG28-#eV7g(9WbWNhS)a|u;6OIShRici+g`&rV^DJFdjdfN-rVWPnL$?~Ab2{v>+Uj##b5X8ER_q&l{pC94WXPF#=k<_xp_+!y7v0eDZGxe3fNm>A?@$Q( zI*ai1UtyHf%9}Ld%;w?D@hYW`Yt+-4DH^}$ic}u2l*XI3+4_z3<%sceTlVMsz^gL` z*UxD@z6DYu@!VXnnfQ$dvv&?Ts@1A`=GL;I|0vU`R0(pUsm1*KeXl%QYxxwB1VXNM z_3U+G{`Af)3HhLJ8)nWm>@`Q%(*7v|56`)Jjx(2RC~cImI%u1-c`&;^_qvIl(!GXj zWdA`V;tC`d^tS^x{gk83ApK19c zh}D%BW-Ep#(;qp&10rpXqM7T4s(m>jo{~!qoX7c@-ZYhtj}IjLXLJ8vPfXXmA_kyW zsTWb?Ikb3|rhvMU%qB>TnHF^n(;~WB&?vw}C@wuVBMRe>VCbd`{ia0<*W2TiT*KMz z6TJHbRdSggG3!a3huHmMP$S=XW6Y$P{iO;XM}xsAgg&xOqt}If&1&Y+uV+_c%iq`P zSud~&#m!wm{j8Oi53jF>nN@%ulTE^?PJI^4A|&sTan!7J2rT!_GnlF(lqY^6P>yy?jH>9kys^oxe0d?NI40b7omJSAv-%im4fZE2IowmPkTJmLUh@?X?V#!0fD#KI^X|7aUw%KG zI6w#ml;F)KeiLX|Wx!y1pvdGT;}4$jSI_axnAa^m8q(Qo9LkJnLTpc4mo$Hw*UyQz zrF7r4_#Om-!ZV<`X0m@35AuULET^xRuNCO-vg0ugu41Z7s^m_nMpg(sf(sOGKSxP+7Y#u_=Qkr}NS; zDE`mouB7mQJw$K|R$PUgm*?^l^I6?t&Bv9WP*g-`(Z$a$%-!1h+0AtcYw<1069~I` zO&;G@ceSp25D<4_lJM!T!R;a8lgql?>RF$Jl_G9Iww5g%)bj~2y&>?|3BRJW{l63wwS@RH?JVwN$U^# zZ5U9hz%XO)`u%GzZ?lCcr2gW&z=Y=_w>4_0ztj{bR`DZo*}GKvXtl4yng_{eOD7gl zgk+5)?kUkZ>oWG2=J}r1^RK_)P~LCWk7wBhWrZA7#5HZ7cEo>EEKPqd9}+m!5n1_4 zwn4LWe9JlC)CmQ9fpfo7eg&!Qeb&7{s&OcE2KE!MM8C)*zpDJ*#UrUGSM*8_%B^$+ z5luF^U5)k%iT)Q^2`F8`t^4YsBR~B;(p9P1`unMEMf1qFPVa9XtQ^ciq@*7a3jEE$7a;GCV-*N?n*+zpGC zmZIVs<+gvkRgE;tQ33p%pcN(W;6{b3uiF>9gMKj=;G%h*Nl@z*J6B#}DLAFsvr76z z=oHh~h}{g%shSQ%T>sZ8^S^tvQmb?!R}7{LMHd3qGTV8zXAU2a}4DGsy7w*?9$dg&VjoD zSZY8N6me2jUQOYB6$36zG#0)muq%`G+AcQ#rXls(&`E`c5}L?Bco9u5Gq!a9ER#K#6-uES!Tq`l3kpA zY&~13sL2CK-YdOquxA%HHU=10pqF=EJ{GfYCz$YZ*4&dq^~BsmcDJ{A-lg`a9nny= zFeb-z9pn;F)8(vNzEhrx_|hN%lD+=ab=SKy^#3CKn((?~tW}h0^!wR-E^lI>UMr3%$e=*PU z0p;XRi2f51lbEf=&I<98w5eG7a*Q7iU2p|$>m)1%}6E|COTV>Ji@S+Qf)4)n*h}YpQi@(Od-@0kU#fz^ns8^Y$toLx=xgS6U9e9ZOn!_$2}t{Gsm~5W=_zu~0Xu zCg+P$;|2e~U#vImnxebTV$Szb%MB*P)QG5C;?-|K}Jd#Uc zp2PsrS>!DU+v>tjJ$X)bMOkvG(Zlx0k3nPLU{}Z&;VNcBl)4itQ*!f|4=_Ozcva;* zO(gpQ%k?0!91oj<1u%jxbRMnW4*1wSyC$vc;ly+YbY9i}LBNcl5#jiU2|bDQRB{><}=Jh*)7jNi}G@7%nL+u@Mo7nNjF1qs{!_;FvS?H!r9!0euWlhbuv zbGuEWuz#?_%nt5Oqg!`A^r}p0@(?(WebKeDNKp7~ox^0B4-mhIqJJ9&*smC^;IH(X+PH}2G)A`tOkv8 zW&+G+@Y0GXZW18cG6U=*3VzgUaQXWBdI{yrDpE6bXP(6-l>6|3JDQ8&_ebuOtI&k! z4yGI$r*JCT(`$APZQBXq1Py=JB8=*~GEzFz+*7R|@as99w)Ub1!VASLL6JjYU(Ao!m zvD~ya6ELzSLJXNfEOVo&=R-H)C445*t(ZOtikHAVYm1tz2QGJoUofh?|=c~2~R+j4M04e&bKVxC&i)+vH^ux)Dm~l}gcWf@?Q@YvIwClYn_@^Wy--@)wuCuE!3A5F_6S-Jgh6K& zQeEzZp4>Vz1eC-IzWPFEe1xqLwA^waEjGQdzWM&f>1$&tx%ur8c`{LJinJxxdl;P7(Tcy;0m&-=ZZA{ikIRE4s(2`J$A^fW70gC1# zuIWzd7`RR2%7CqtOXANj0_>r+TZHjOfPejaP4a^;z=1(kVmsybUueZ!JYl3fN-2sl zT9lt)3~Cl57s=NCj6`{7eZGlk>YhZIo5b2FKPcziL2A|~QYsP%2V4GewgWu>Q9ysK z(?{5)tr|Y&w>o#7HEKIt^qqXpVEn7jGBjts6mTLAbvq}=+Z)V?pm$#8EbIETh8!&U z((6{+X{xW~JC8IU`vCUC<}2=I)lpWxq}IGv^v~ZYClAN$1L=L?6p&noAW)0VFs`SC z)I^qn1-WaR#BGJPUtlt@X2WIaP9K;*(9ao=YZcE&7yP;5D-}+Y(%Ggv`W;mBATJ=2 zTWEE7|4hr+n!st1xQ1YEyha_XZCU?RU_vJAC^;5E`N2MZ7Hi=xzq{Sl*Qm344LSuf zYQW&+Q_|J9F0IFeq{|-|N7KgS{|XbodXn8;D#d(461iAJ-TVghAzT+)l(4ob&fW<__ z{5nrb3iiNm9wZTY;LzCteR)-LIznB>y(rH)W~?E-;o zLM~gbVFY{E7>U;PKDuk+a>$689tF%5aqZf2AZWr$U0!Z9CA0R8!nGW`xq3p$l$zFF z`PiKj>ZZgpn5-7Sw^TQwnhhZBZ8U;Rh!?tQw%{$ztC?(Yae)MsV#wqyGO@yCm)7`| zxn|P({(%WNjtak8lwVI#>Tt0t)d^Zyv@b{`pk)39_HS_9PQJ22U^!v1kBJl}ZuUh(y<#t1#3hb)vpEV2LsaRM*#drUAK7z+sarBOcCShZ_k+A<6F6DUEC1Pkj) zxDaVBRNe$5X3a<2_?N5IXe-@l(6dEd=iN%tz=y?P#3r$)wz;2F7_}CN_~G0GV-!mk zn9LAqqE#2xt$0iE5R}Grw!^N|Un)=U{ zn9YsHbDW%kVOtQZol@5-3#K)T&0Z!WR$OG2!R*fo0esF&)wE8nT?1Lw6W!<&cFcBZC7vJOwi7*zC%Yb9!R$>FSP~47|2e4 zVPIe-UK`)X!<58dS1(S(6R;`m-gItGKg)#J_ zt{ib4%Dy(QZsKF1(;~L|=8l@cwG6i?w+ql-Payv@Z~VAsir(lxTw=yDx5-#@3=rL)wA^wy)AyP-}eD_TEL8yn7rvj#sCN4OIZ(1@qMeR0&8FUigi7a3q}yKFmKYS^Yi1Q zM5lXjcZuejIVb)#$k|1LTuT{{EJ7Lleso}Z2*%gRn=T_}gdwiKFAAH}r?2*7sS&#L zT4H%byPJ$%0^f?e+O=+#v4o-RSZvi!P(3|>=Fr<>No757!Ox55_lyM791!oR?(hGv z88ZK0UxW(u`rM~M%>Rd!qK`zLkCY-N7G~!EP8;O>x2mWADx%NH`8VU|Kb6k=_w1%t z6&X2T+8})cWlAhIcqEEc-?ss4+oZ=O28pU#!PgGfiChKq$jNLb2s$j(S1=u}@KOFVzPV1dWqr~MbI`K*n8Ob33KKU! zF!G$XuUkD@ecyLatOn!7+FVY)%tceGY`MOZl8pmkH2TLV?>5-Rgkb$9`@a*dG1H7DjhwS<9c&M4ofBt2Et?U5gdIydt8?`+VvbxP~bw z7oMixo)CLXa~AaGbS9}ri7`|jIAG8sizZW3>@kIYf>WnV5W<}xT$p{7FCQH*t<}z@ z%J$^4bc3@+_SL>e5K6Z7|R|ZAG|qmg3#V+3B;~B(mV}tLZ4{#qb)7@wb{QJk?`n zxPo0i1z31O=XJgaKdf}^<5v1v*vC;hENOh(-j2yL1ErX#iV)WDMKeSH9gDdEj!O|( z`-x)0GAX$BQ~zYoOmL4au9CZ@nP;|0IJqro3=LN}1>^oExlU_i+5^A!su-w@^x+$S zcua!<4-q=XG)rqI&l&4ukL*OY_zZRfU(pXPEBgzne5F(}N;^5ND6r{jY=*bFief~5 z{)fgo)%&*ciuA;WrYUVDEa83Yb8$8p0ehQbm{N@>dcH~L^eeRQ-`4k%33wz>lPVMX z{v3RsTl>bzgO3O)avsbdSihJe19PgV{*|-2AgXkDb9Mo4cefslLzmzMf`NCl^gr

Ql{C5jDPsKZ(&1FAJne`d?X^bb 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 z zSTS)o@7G8M<{$wn;rR2vu<;sH1&MaX+-YUirjAUBpJhQ@RQ&Yosn$--0}*^j3exzH z!Fb@e9OL~gmh^?zT=z#TaVm9sKV%|8zhBpOKcnUR^i}vNe~RqOF4B|s0^IREXg+aj zkf7xN8u#*V{71k2iVl*ttUtF_cKWbzIn6CadqR|4UNpcjsQ)bC zw8oJ+ca`b2V9C!5h{1hbJFBwng5*BGb@p4W`aUjK8kAW?)0nl@(<aH#x=`}MUpdF4E$EQ84tJp*vefArjs6iI}Be~vjt{;O_3*28mN6C+H$~(Uqt^44#LpqpMu>`sKs#rurPH)Mn*Qw zHhS+^!&iKku-QC4zBqF>2>WM+Vp!M%0D6tucO?U*9G-_s zsrl}>J^)hTH=k@h#Aqx{llr2=?<`)4-uMxwoQOPdGBp(+8ju451n+y1)%IgblabkJ zYXX{q#Q1YM1?O1jy^mgA!euuskUZEVb>3k% zW#QdZUD25mV!oF8i@yNOtICQA)eV28V%4Ix6O4cd$O-@}oMFN2se45#c?wFtSIxA>PJT+@7UE*e?&>Ag^c~arUZKj4RuBboCqHXg)iL6n${^8^Z!zyH{X>9fb!8r?Sf8Us^I5x4gG!0Z___n;+qEP;tSF zJzE`pP#lxeuUPN4W0Co>!1E#5PKO^k9~paXWWWYM?E9X!Xu>syYY7`WLj5g6*R$)( zbCZqQc&23P^0Y1g)4O9%ws$FyOgYBiPxYzKAG3C2xO|j#ZwJQucq$_gz1@WhIl#`E zyQu1U{Il_n69k}FQ_fXE@O)#aQ^y}LS{yPZ(3ja6qQm8$AAy{qh>OZgYb8H7x+>yC zVEgV|pYHSl5n=uAL>}M+gx%0Mpt|0QRAb19JU2`-_tVejd#>bGuf8BvZ?UG@H0(d{ zmB@|`h=n%~m`VMC!~*lY_$X(vPy~CzhwlU0D=860qCQX`Rj_8Fr&QAJg ztj;v@ub$4}@O3zLRQkd34oY#wMTIKuC*wQHwS#Qi3o$dToBpRB0J^U1eC@<5No*(z z*%D zsH?y*%u76ZO;E&30~=~faW$j`DBLX``KToB?H51AXjKs$iI{l!?ixFrvcXD_tk#L-I1)r3p~=4OH^m8HvsSj0 z*~e2`8?Q<8_b*!V~?_TwbT&ha{*+F}?x2;*DnNh2x57#Tj zmFKA-XKu3dG|?QunvDkf8RD4qma^iYKoso}U|?_Rk7!(t{SC2yLjj^#?}>Ab5^2+k zXdT`?-E$iXpZPi5%?&t(YJ#!z&Ha~q*9P&kpvCZ9^4~LXlmu8E_#7ab1l5ILdHn^$ zea;w?r*tkWYLU0oS{&x>wDqeiUx8G_VTj=_lH3;TZ(=Pl@Rsu~5$xwhdb96#eKMLw z$r+jHAynG+*hfta?QA`nTi*|_Xg)Hp(Y7PG#1d<2IO;!A+}^AK7AxD(I&M)#%EjJ_ z>U*5roH5doXRkm`S9UMISzve2lp(2HcB*QxvqYT5yFQYitRqi4VBM^Z&k*#suBg&h1JAR=%(IEO?zz|yA_}7%Z+*qA(ZPz-Na_b$&&Fd;7W$(Qn zulq$-^w&v9e^0wxR|=wka3nfv&g~ZQ$Xy5lD00Eq(#Rp#KB8XQuLH-v);k~gBc>b| z1}i_LeaW|JqXp6$?@nY2Z5LGHHU6A8TR?R6!PyKcSHgvkE{ejxqFe-iqoN#F)cE;l zvJZBfEa^Wtofd`^XlGn4z`vMfTXgh;5rvqYL(Qh3m=5DxhD7NdAUDq7^~KTR&4pwnj%<1xTxGh=fTh?3U#tJd&8kO$&-wpU9mNp%q z=EYu65a4H$I#vA{SJWnkj6_oFb$`OTShEB)ks~d>X=F?kPLqBAv?cBD?8~vJ;UWR< z5a9Y>_5#v;xZaQ4ciPPX-}k--xU9BR0FlWHqVH(A1oS-XX2Vy|sJS}5_N=K6EB3Uc zoKwSb1@LKYy}#;kpkr>WvVjh>^+e$__h*s$`FMd6k9|d4^~Y=WoRmB35X$hbh@Lxl zzMuyjh~*cJ8Xa@$4Ru_d;Vqb?{tAwk1E=H-`*4BQKmHii05B8Ln#!HusQRp3wrJ6x zgcD^~z6^16pK+u)<`z=i?Oy(e!}Q{QRi_zqcc z={tR_h@e9E;7B3H_q4IYz-x>;A@D}M<7?}R76?;diwoZf)bBnMj3K_cE5lkkaKh|~ zj9EB$%~7O9DFb%aW7_~myM24k56pnVWVBe z4zsBp$57S&%}EymOOJvyG3S1`wk2a^>1Cqm+42!@opNFi#_K%98?IPqUb#~^x>UW@ zEz37A=RpW9uw0!9@8N~#=huA8Ry)T^L%B;t6l6-Js6P`-$mq7hGYkA>YGTsT1L!Y3 zYfpF`aFjp`ltGhcyA0Tuq+SwHfZzgYQi>?@A<8s2Vf&2ZZCpu88 z=~@hv=P65t6c79&o6krrCjSf&s#rS#ZC*IcqMQELBrEQSf-q7g0C+CT4%MdD*l{lU z3GpO#{RU4jUdAKou5oKIppwi~k_DHE`Xhfc(Wwg%GT#h`=u87AVEb#u#?y`wQ~&S5 zK?K8dXo4u9CXLrGyKwVq$M(*zPuggm2b}fuXU5pW@ja?4aDulBE)F@&`db+{} zPaVnIaY%}BTRE#AZ@Q#vX82lWy!#+yPZ!>KcaPx8n=C`V^mUE#ddK0F5XMQu#vUWb zQy(p|o?~*R0sF*;3l-2`Xzt=nh?wQJKQ^^=J{uDHkK*W#J{)I#WmyiiX-yS(Ao3;w zrb8$M;mF}Pe>H>w6F6?m9*Ko&r3-(RI-%OBB1sqwp$pw7ua+iie837xIre(;*xkNGDF!AJJ>7}v>%Z@fjb zmb}^bCXSBKb6v{btcMvgG<`Ck%{{-nBhRnP2t!z292tp|uE2Q9vq?pCTBC#)QLiJ1 zu|(Cd?{vvqyULZDzw~Dpd|c_HWA(VME1|HInaX2C=4Z-e%sRNLA`8QX#W~3l;3 z-_F_*ZfK~+$Ph@*BsAL^;+Bf9-L_d7a9uce0h}k2I)U()>A)US{#8yfWrXra(6PWaJW9#{AO~Q~u$7R31d}|JG?`{slis~7 zjn%%iVvIyy7B>K=j0=tGh~vpA^G4^bs3X1QtHu}M0vj*e8CtWO<__?HOd>mY z`<4?)?Ob}T6zf4o7vExFav-feJrK>;a-=rYQ2Voh8|ur0;lt~4k&?yBb)1lq4XtW3 zv6raU=mX2+ri=QrM8M&-i8XCOO-m)Y`5J)zloWh5X7AjQ@@>#Go86fea$icRbMIqKNzZC|hq=WJDO zz`l-uvS*okb*rT=@}kVuv-!rC89eytGoyzCnl5Jp5ooUOj17xQ;vgrz$eKg``01eV z9hX>?h(B^609u>rOmRD^MM`TyaB3JiuWUiL*MFdmpz4oG1a3Nr;!IsHx@DkP{{z7F zK#9{sgcJw=Y0d^jmU(fwJc6Y5u~@PYNu&`durJ*f;SzHdO(r8rYehn)CN&&R#Un6BWO<`_Un2vCbS3D-m9 zUY(l$C4bZDM6VS9#BO@*^T4j3k>7{3ey!+#nnnM|GGawh<)lA*3Q_n$^e~X$iaDJg z!0mekrwoGzKANv2KvqiI2rT}{F>E)BJNfGbTSXYhLG;Zg_IYfOEkThefb~*NlY0go>+uB?&T8BBOX!9n|CI}$u52D;b9u$~5-3jD`PA^c zcT;aV8yya^E7Vu#0I6VFywqcN&+wCx6X)PH1R_Wy+!>FvAPirN=ASa;8J@Ni7ro!LKk0@O)3E1 zBm5&OWGG+HP_3x468Mqtq)~u(r$jDFQpuh=%K<0@wi$xm*TCb`FJXA?cA~v^jwhC0 z4n%PfMob{LJm1v|1utpPR@8le!h z;qp3rw|ln6N5I}?QpHJ=>X3W<*DGvWdT_RF*@FshszcQbnc%y=(#-1&CMaB^7SNYh z4}N1>rbu+2NT~$;Xd?lslC*ua{e$POSv{Sg@#u7`T21PqSU5JjUs^K0uhg}dwS0&I zJD&l5N}IqL%WJX^6y7w>oFA3VPOnzKKtp0_U$eOu{ZTi;Ri+1+hog-go|-;}fb)R`?25C7*iQ*t%cqnOhNjN%tWJFoiURAt+m0S5zsbzb#??udLc z<-nm8*=DbYzaP8?AkUBC)E--V{fgn+CFHecS-}EmikP2T86fV0NdpwO%9vCUN8)$% zp-9vbLjEg|6BQoZ^Jw_XF$b<_k2 zw6%#IuZ&iMFjrDyL>kd=kUZjW`GyZOBSv4j}lHOG;NB_kLT8B zm)-ddrK%R+^ZSiOJ(g4+1(8q!^-!S}J~XYy_t2sljPA+GUmye*%tl#)FX7{YKN|TS zbz2D2y<*u6l_=X%QA{)Rhuu4aWyTHF-{^>G{FdOeB&=LNzVr4@9yUFJB~CIaJH(Is zicFAJt=cxMRjOD#&kzf<{s2ibxaVW&-Up%)UuYr20>f?n3iAk-esJoCu{SP#kF!B7 z0a?>4@?D1owr3NYAf}6%hWOq~j;k&?*Ykts|u`Yy`A}{)C^MfU?N? z@*;{_aW}Uz=}ornxaslz$xU-QvsFo)3E7KI5lM+ny57<%k>0O_PUx+=H0af~slR7! zh(Q-LX(i<%ryQ?`Nm-1UCl0B{>K}f7fz^%&H3ma7<`?D!H_FBAoT*{>$&UvdgVb|! z2>`X^GtnqZqSa6Qh34)>Py*ygnp@kWDf=tbdTlq{%KHUHGi(V*C^ zkfs2Th#(GE!&oCM$1uwmzH9j0q&_Phe@X)rdrC zz$CgiQmqG_>aB-cWUXdd?)4@$W4KGABF}aFM!7^rsri^eO#?R+y=^p@OgLImuO7P| zqp(88?+LX#ZmM4#>bdt+Tzn$Nw%GW%B3zuBhh+UxwvCbrGjae-%(9U)tOe9_T|EB9 zT2ahi>aA7#ahX*1$ zrS3O?pxzqHn|%Gm1Jp(g;W-J-$(?j z(T=$=_BWEiNs8&qe0LO=Lj#5RV)xrc{sp3 zdn<1BHTCEB+0cRmr^Xpe*|KNulAd{opPu*(Z9yc;fmd8paW{q@fbrJ2&^Pt;I7AQS zD*{vi++r}3!B@Q}ciLrB`nW%b;%D`IT%LFVqJ@uRG6yj7@KHru+d@`FkhFsK&S{Pi zhHN9X$)S?QFx_E$UEJNpIR-GW%Ae`Q8+5Mq zk`O@Fe#kUCO|r@^jV1S05%AX4f@iuCLe^xcT;7+yOm|b^pBS@v$!IBE@J(aPZO^j$ zl~MH}*^P$U8FFq-Pi17MbO~0OQ3Y!jLWwZu_6g$1m{8Su42({-JdLL>bi~9%I@AlN z1*!F(5MlSmM!gwa?)gDN?D7lFrmL6UcUZPY6O};&1Qd)Q=8+*Tv|+CB$r`#%c_nVc znLaKM^?L7#5t4Rj%z>#zN3}b4u?|-GMhVp7A<`@(Is?qGD+=E%NxnYv^m~Ojja9mz z5FS!4U!^NzGE|Ih?Zb*LK7A@+QI7U}{Ma8)yb1YTk&wt1MjQ8&jcDlJJtA>}R4myA zr5_ii=mm~5A2O+Hl$kDl87<2JnWab>TV46sbzInFDpsER}I}X zx@NRg6YHlj87Gft6L@{1&?pZNN0q@7)ES%z3LNyEOoEpiG-S$S z)8p+>*yaHkH}fa$b}V+N0n=*RT}ue2OZ!q!ejco3rFF%Zqa?FAo?#Nm+{xG*G%`bz z+u4j>^PtQx(E5&gb1N7-9v5|(LM4=5<#ncrQD3zVXN4AVgPqAfBkZv9k?UP!lFhR} z36xyssWZwh=`qU%CEkqbBq$^2N}9e7u#KG4*lAQc*DdsV;;`ut`@bxfUn}5oeXujK z{P$A%OHNvobLQXm z@J}=Ui&6R?cK)T={D0cve;E2dHUNre|9x1Q|8s-Kx$xh1_!kZSX=#?nF;M)!o`LzF zmi`wF{%L8}|G0dZ|7mH~UrX};{7L_`^uK8EPfP!c2LH75zi9AJOaJEvEdR9hzi9AJ zOSApQQDXV0rT;~Pe_HxqH29~b|3!m;TAJhW7yh4NNvQ50pwyt`p_HIVpyZ%TfIsF?hET>(B(gNJ4ff*H zNMHd+2eZcuT^#MqtxX{|)~3LR%H|H1#=jbi0vB6;G>d4_Mc2hP8V0`mb!&4QYhir{ z;1XdjCI&_(Mn*;!MnJTlnTeKxgOY)P61WaHQH>l8je%Rs3yWHs8<4QmGte`WXi*8# z(t_2@t(lp$sHy*U!2Qh#$}YCXU{M=u2c-Y1)A{%j15lAuA7cFY1z18w^sSIGt&+Kw zlB4x&2_s``2XhCP*WyTE5o<#mBj8cMfB#U~M#b73_}rL;k@eSnKmrRJLkxklRo~j- z@rDqI(XtcsZreUY|`Ub{9S!;(}yFNLi5(MyvR!)XM7ue($I|6D;e2^ ziYf)lTW-9I!?h7#bE&qg$QFEb`*;`=!?A`IaV+ z+q3ig`*Yo&i#FhqB`<@{k+O%gTU)pL+si#Ux1N!*rMsVJXAyJpCF_UIr(dxwk-hHj z9U4!m9Qcd9JnB5$Tpvz;-rk=~%@y4kKDa@2!@n;!HSqG?#!nLlTHPG)tO3l5qYZ06 z_KOvcd3+LRon<%+!U>WmL+Q{k32&EWebMLyQ0AhYt-QRUV140Pd@(stxkgbb7*Oe# zp$)L=%)FsbNxnW=)|93@t)Z(JoU8PEXH&==r56>7(9T1}fj9Kchb$>uRiF?ax!mxA zE`J80g>R9Di?ROWK2Cvu7B34TG! zkR))MsTlXqChZISQAKgp$CQM|oMejq`BJt2Cs+M83N!`^joyI9^K~O{vWHJ7dbrY1 zSsTKdj8>-bl>$$m8tmkk<~C#vRcdq17Mg2HF>q)tu-56Pz|(2c6$v=qen`PopKPtMU~WQ%0n_xK(9-YI%c!92&`cyRYCV!l^Eow{Z&v%rl%8HUQrdyzuf zwxS{L&>`TU5%X=CVb)ZEJe%*K+^z#7rH|-2?A;xPxl}}K7 z5pC;EKTYX#!OU0OK!~vO9fpV(PP`g7b{Jgs&m53RxJ*C!m@WK3fx(5xWI+Gk&1P#n zOopDxjxMfRZInXk7JQIVn5m*d?DO4QfZCLH!$n*ml`26ZYE?#XHzdashXhYqMMXk+ zf^4$ez(XOc``y{Dq(n)!@PVn&+Edzn`Wmn^3l-FC+^p!kNj6nMpjjAO(YIIy3~2$; z=hA{GN!@ymIV~!Rt-Vg9bKGK73D4v~Qk2?*^@Y(t9dNPIah21d2}&V#sHkRbqt9Hq zQR(0GI5p_ut1{!ZK^^NQAf)c`)D7d~lzaCwnGP^vpwuDzHZDVFB>BC2&nZD+wIEW) z3#p)_xN>rF4|yBd2pt&0QX2Ag!`VKHmy=$Wj512N23&a%wRlkNT+2=eRi+e!FtE}- zoKE7FKCoSgDDyWNH=3{MLTaQDhZmn~;SW^4wm@w&YtBtC>e7OU)04@V54Mkqm-SB;X?51>wS3V= zgN9x*dH%UC9SWD1HF|^L}1zLVI+TarpVj-xWxFn2ACQ6%0!CM^o|u;I|heiyh9U*JF!qUZ%$NbTcpA)gqU%?%{zVH5or;^sUY2c zsKK=ITxvsIaPxk?H{)%;6EhG1x1GIMbh;tCb*h7DM>L!+-u{v5*tPIbu-I-`?LU9_-IdkkJHdm)TPi=*-S? zL22jO+b8;rdn<6kh}vGqQK#4>E;sx<>nFniIFdB`%j9LP+#1tUrS*MVcc!4VhqmG6FtF;iM0bIkSp6;^1-h>^ zZXfjp3GB_922g)w+xjCss=Vh~#!)+O7Tg(uxWYu4Ki6KKt#8Bsq($-aFr`kcj2LluSU@Q3r;|~OZjWxf%Uu^l?8;bByq8&=r6d02knBrgL*c=TA9}2&-r0$M z-gL;~8b9<`E5sh zXywHr%PXS>TsvD^OJgg*b0c8@{H9+H-=kAzZ*KeO zx-zmp`tiS_CVz!Metq&kf9ZcW0d8TbZwet{27af&qjTp#!odnubQjXM6*o3FHFF?g z<6!yA@pB+~4fw3TdleM4an^jz@%YJO0|_M_2?k~8jk<+F!!5W638SRDYrK_4^1{Vtv!kD(q4#(E+$4s#wNxlIyyP3I6A7O zO)Q*=5KmkPz=UaxYC&t13zEPJqC6MJBYU$w{ldH}!@?mQ1|z=3=Dn$CQld5@EN?OF zQ^qCF8<_*6op;l{>*v~8^YT{X z%ct5s;yYgN?vZ_%cN?Zc0+cySbluMai7u1ny!NM-ud*Z{Mer@AZmAYK>6P*;wD%xe`2L&EWvHLTZq4J{6VZgsHI?FD6k-KyuxrFp;48q zc9X9~^G;)I=56VA-~P83mp3nPhcNqz`&xC`xwy>)w98(tCD9jtuU3Q`s*V&tGqcM} z+x6ebl4QI0QY;OKEnmjWveZ%``+lwxrckWWB0xG-Y1L^Y4M7g-ZwsS3mwJe|7pxch zR<-vuW=K@$kmCmr)@5YKEg8ems8$dzdgVwpmgbfIh7w8VEB640Nhf)+d^y3q8YT8z8Yp{sUoq7W477BN{quG&&5R87_{`YUJ z12RKZ#UzR(y7D?NFSPH=@eQ5R+6h|o+@GBZyeM(Re17oCnW09$iBAL0?!|(tDGM3= z4p72(-!--AxrkU@08q#w%f7)UeEPfRs>5L4PA_(>ruO%X@xt9#SWAZnFv1bor(+ND z_Yzkqq3r>jvR@;O_z7b8i#)r6_)P59WMt1&LqADpwJd(FLZ6gwg-hVkKy zdx1*2_gWuzX~nBQh-fZ+=m)`%(bOTYfuPA-K$?p9BW&WcqfQd)CpA9^H<0zr?VG=* z`G1)a$g;#b{sLc+5qY~?+dg&n^ZVD8WrEPH_xfg`Za9mOlJ)cVYT9Gex*R-NIci|T zO<^5_Bxg1mctzsRQcLnW@-47?6RD+>Xm}J?8qb_b1j+RjPx`O(MH)!AtelS7Lcke* zg=Lk#7prPYX$34l^>&NaPu)e5bdDj zp)*jQ4~6dNrhluQ0`ItFrgEzReu>(vxYIcF4jCJSlBO(|8FQ}CzW*_%mNvxt7OrkI z>}a7xgtjpQ|Ls|HE0tnM)81%99&s5<_UD{ZxcHV_cd4u3F}1iD$2yHB=#B*thE_jG zeS+%gOL3Wxh@n)^J~~XDu4s!lSy8AzKU>Z;CBRbL6Mfb~5niJgv~{qI7n9C~Hsda( z-L&<|v>Qf+6e#9gR?Se992Xf}u3L;G8|-9T)MM$HQiNtuGGU%;N6he{1MESldNUF$ zv}`31-OCkf;>e+pNu_y(5x^Rn4Qo-iPO^TsrJwD!FU&ru7}5QX z_k0V(`uRB8erZGg@JrTj9LB0jDFOzEs7JY9)NULU3KuiY@yies(i;N0l!De=^cVKx zkLOcu{V8dhbh{P~R#sny&iYN0LwX)F^+e{?JKfTUcQeE!mB8Rufm|B0QyVIeJKq zkY^glV4;l-XV*+svea;crUkdqo!3wv9~rNO}bb-%+5Ve{J+Z zI=+<8tFq6s?L1eKbzsX5I?^~*jeUh`E?gU?dRACOp|L@ptg0yRJkSSHoBoD>`;(4aGF{P72gNTM$~FR$Xo}zEn1bQ$}+SK<~s-`}FY6x3IxP@#4Tv z@^Z}=njNQksErK2Vrb{O$y!0?OGHnPRr2*u#v>>6`jo0kxj(U^ zzZ|kkIJ2T`th{x0E_ymb_ zw9t@i9Erc+ywCiAxVieIMP78w;z`YjP%2mPOG5&AB^D$sN_C>?3gHI zRzgwccVf{Z5a!6g$qf%~ZN|T@7~%H!$3~Bz9NxF*f}Gox7JN_B-9blxuO~mWIPcV> zSV=`EcA}zZ-jhv%pZLMIS3)=-u@SnQ}EV;VRLb`c#D#k|ghp|IX4*)>@%N`WiIUY9Jp1R$oc3E79!3p+IKS>_ zQk_p@A())AG(52az65P^@A%GIYgke7b95rl66f zv$tKt>a7tKUtQzA-h?v43goo)o*{MJIz2mVTP>`s`$V;z@#IvgC)PJ} zJ|oOn3I*W+3=*CnB;bRk(XG1etcvqO!lCplHwv(ab;6(|pQSNv4hjSnOk?#uV6c z)CN(rm<0kcN#T-b5F!_7MUgPC$v1~%39J~;8-nmIq-Dn|@vi6!NaZEYes*=!p}ZYL zk;Cm(dKje|!BuA+=yY9bUsPRNk!{`8%bT8G<4+3UMuh4(6}WAQYinwmu12*8z% zv~6hwdoYfWU=EVG6v#*bO*GM2u|rtu;##?{CW`8~IeN;LSmlekJU)|4ta&!S@NK~d zE50IE!@ZtQ-N80yW!+u71w{v}5WZgL_5T1zK)Am=@v)@Fm~ObQl9xLn2(`I;&ch)} zJnXet#1oIA2Rp=c;tpu#h8=8+D$yYNP|*XnE9zw&l?4Kn!O}|bNW;U)RW+L|GWxJt z_^Ry%Wq+#~1)PrT;Ax?jU8kf96h7M?4Fc>9HkvsnWS9PH###(u6vO?j68=e2ua1 zYVwud!QHX9kr^gA#MYHQ{jqXEU17EcA-r6qp6<-euDTdJ5i*!pQn?XOchnvrFT!DJ z#nh!9opBwl!i6dhsbG3nErx!w*}o~pWl!k* z;iyD4p9h*=fzNi9Kd2g`#t$@nTG1$Qf^+QqyQC({Zi2-vL5PFmgSNg@SL4N^>oM|-M>y9s>*k;yi_*YpJQO?Tf^&N< zQ0I;SNGlfT8Ata#E3BxMYM>U5Wby558OsEnSB@(;Wm$@KaCT&MnB;n3Tp92(BE3Fp zU6$w>UKYXuyB--N-B4nWgRj`K4Iz$Q3z*SJ z*;01FYu%jBX*3n%Xc`5EKS+*K0p_XE(d9lMcgJD!ky#MsEVAO|$kO6D&*~LRO=>|X z6ej9}D4kwQWIp1WK69Sw_xtwjhmjNDVp)xu%WQ%kZ8VxYj$zgl!Bh=%)v+y4)%4Kh z6`b<03=Zz7PfD2CL5bOFz20dwI`#S|s{Z{+f(NI#gBw28AN7107p2$uK3L}BSeCg9 z%l+~=_~p2A5+5^(j;B7J?~81e{3R02H4p@8m28rxhD?sQ-GZ9-j76JXS9cI{&zjBy z*FFz#M6Oz_;Rs@nY5c~DqnmBZ4+ab)7S4s|d{Q`BofX*IbXD4h;JK7i)5e>d?`ylH z8(%01Ts!B2r|}H~q$#tL{{;H!D=~12s?r4O%w~1P2oL75<-NP!J(yd+3}598UZzNV z4+m8mnXYcp^=tCm3X=XOO9;1^!(iEe5_EP3i6WBhzSy$#wsmjCS1YG#wlQN916Ql< z$^Z8zE;nL3sF=3#HmSEySQTl83d|+P0bc?s5x`tmDYqyO=bB0OwQGGel)9K3T9(I} z+TU35#+mN@_>=>lWD<{Cnf6X;!U1jlSHt1(7;=4>w(jr}Oa^cct%IM@O{c2yME3#3 zl)1Yp{59$@B!bY~neW8E)u&+%YQHh8Ru?q{*@Skc=iU7o|+&vt|M9G7^af9phN6Xlx>c&qE9FulMxGuuu2`Z#+L32^z?Cj9P>Cy~l z!!%Gk%eKZXC95|6J*>;0f_(xytl}hUX33I#EbE=C*^Cim%7!tXJ= zcJw6I>|lPOxo~w;Yn@-U{45Xh?JR$wXM9cSd1uOm;I|)`G0%u5dR?{oC~wpvYH!Xt zTAX;hk`kW~(WhXB_szh=$vUV*TtMBm-&rY{rNfpUuMlv%)OVLH|Bxh3hcH{l#5HlU zCq1eU68N_AMI}_qQu4d#Aj4S69Z+`yezK7^rI+1(HIML2>`{DokpRc-lbFw_32zMX z%*;u~=9?}l_X$zMBCa-~gb+iIiaV*NZ+yrxPPTCCSY*_;IJV-wfkdv>rT>$VS#qqoA3rWDo#Z0e$D}9Ku;4gLNImOB_p*gK*C@?% z%C?r!>Kr)%61b?!GRjj_;MDT$H4%Ou^SIPoES8<9xMX2uiN*fR5X3huViwl{!pvB% zUUWs6Mfd+>%;zP3?<>nMVHx|HAz!bk2O>j~k3FB*&w*j2BHXOz70=_6laAoErBuiI|Vd#J-P?@(L{;tF_o2SzDt$^L#vgYh<6+$Hm^N12> z^|6(1j+2ZWBt;vgrg{#*%q~Jj6CGwALn&&N0<@=UL*3*BVRqtoKkTf}{BqH|rykr!&t~}SDj3gjAAcNN1{l!#xLz7&Yd36- z$9V81pb+r6Rk$w$P=Ptuk<(^^VG+(qZkE~|4?L5oqX4B5YlT%c_H03yTAQ6mdvQt9 zu~QpX48h*GN`&{h%rtdPWp^}>0meB|yVGVCm08JK`s)>3It0<}jJ$!B522lJR(S{Nr|qmR3jCF zTE`ZPfea^FLC1m`DEQw@O+|)Zw$r}ajfs95nQ4sE0MzU>ib<1uJ6NZ#9x~784|PnZ z2K*nmQ2|7O>{4k#E$N*8j_Ub#>18xZ~> z_(YS%MQ+qAzrGaMv!2j>YBb`-7_aVK$5<% zyM&Xj<6C6&9>R^jB`jlj)b}_$KX18Q+D!@M__xX*g1)Ux?PikX%5h#E^v8`b6I02N zQ4SPeY68z9bMZ;gwRhVNFE$L)0xs5Lixx}DAu6Wk5))RxjT753UL)%(&Ie7K!Y{X1 zd_h3-%uuxgO`mBvJ}Mh&7_{R<4h5sZs=iVOiV|*P;Ror{x5O!@cX%Q7f&AR&6*ze& ztJ;4(t0z$<$dMkIB$cO8C$Hb57uD~{ouDE8x?1erMEOeDF~C{g_6k!_)eUi>;81PN zXs@dCHC$tV?&$TE07r*r^xm8f_Tz8&9x#P*ZG(W9(olTOU@O*=-KLu5zP^!lo%FzS zk0h6xT0|$SW^|^-jHxBSU;(7Yc?izZ2GS%*ygpv#P-2*W-LG)~pYtKdb(0UX^c|Y# z;aSzRed3vMU=%%5SJ7TWGr4~U&=eHeWeXk&6R4ILg)nlpG^(#*jq2NhN$VxucIk3S z6m3T@dm_biD5(AkckIAxT<5v8dLcA_#?a{V?XpQe!yMh>u<8!z(hI~hK zlgJhqj9Z{iciIh=tWTOl##tVaMimX?hwR`er9I}EW-{AX=N9cb-6&!p0)C8>A_;B& z7KCDon$j4Gs9g^KogJis=@{L)KH$#1k;$W&RsBMYYa2EEp4QnCqt|=xb8X9{|F%Fq zh$6^Vz<)WQE8hSd7Q>d+*!-QDz98vD(XUbr1p z`R0K4RO@GgkdsdWy(d|m`zC17mjS12DyQp{z#@oCMZ!1c=3;j7lvs_ zXC2CfF&|Jzhv4ST-0kuiH`Og1JrxMt*bR>z0eY663i^9FcJ6Sl+SezR7DgudbWDAzIYMfV z^fE7GnV|sC?Y44y%)YOPTS?Y}cOXBq-ktUjNqLR-wQ2o;HjQ>bXS_`a)JW`Tf@=oO56 z^Bs%+oRnDNR_)A&67|HQZuqDZ$`lsYDvl}~TnTN#Rh5#ucCz=gdlA~?Y#HNV?LPxh$ zOygxl(092Gc-n}$x`xd6IH}DDd_~prOC~4F0M9@x&4m*l$lKhxiWC<@TEeXAaTL|L ziG2kyn*ba8ZkC%RFy|WJ_SLD*O1^L~Bh)1EbU~)ekx4KUwll8DUeSX%T3TAV^-YXx zA&D7~-#@bX8|BoUxzt1jR4uvaH5E-z;Y&Zc2=n*U&=N~O478`wIO`-_;D`fhe)@FC z$gV5MkJ$8;DRrdHS+^C=d7fJd{@N&;x5NS*r|}w>+7M_FZ)d$bUjvDLHR}nS#vR#J zj->q^C@z8lKChxP)Hih&glYp7VnMFa3k!YJoR9E|>>^cB@%Jk+7xc_(tO-XaxOs2; z&~l)KqYdDZOEJ&bt?U>6iqgz#hWorH>kG_VZztC!T{7ti7)`RQN zpr&VNbJ*+kaHH+xl0{qPNSzzP0NM_W>IUwuykjZhv=*7RadsH$!qSb|+xqK+00;fm zfl4nBqPejVsOFOx>?h|&FDU;FMp@4|#Xh^u`@Tjw{KZ_AdNs{H)88po;U7pcZeeoz)=TlGi44Wjb1s|C%pHKUXET1?|DfUuFx4FZH& z$*Lk>SN;L~Xx=wnQTB2vWUD)hGNss^(s!vJ`!Wi%)}5+s*J_PBBC=_M%UA~cDJ*Rp zFg$w&ZiT|i)8PMm#HD768gW1|nk_6wYOlr+hdDi8EisVHJ9W(tNT8}ex3sjru<)R! z4~{M*CR^bay?LEj42;d6JG!|(LvcF4+?Ls9Et`X^I&Z<~?~+_^v_3M%VlnnxG*`!+ zp~=(&UqVJ%fn*v54>T*ocdbu)Xl0w}`sfu{WhP3RmT&2pT}wR{*lyKvr88~%6d#6H zFmb#hDhjF};Tv7Gy~Jc&-a|Zc&9?+#T2;9sEYx*d6xY=GGzu6*M$YCbrqqU*uxVLX z#Wb?RHc}b$I*x5=8ocG(9CH~&V+&hcCw9Nn0NmTEV)mMH5EW|>TRE`(vM^krCk0n? zK|5rIq*(WT6`p1G_Ya`oUs9@B1!*ez+{{7{tvZHjP2k3(aZj;av1t?;o8cC6cHufm z9Pzd{Y`MY0V640|Z2SaLfp;dpWum7mO5wo*DIDvKK7Htq0mA(zRIOfl>dh@~7FTJ6V_LzT-KVieBH(p=HBM_MaNkrmJX_O=V#SH>bw~yreTi zdM@P{g8Lc;nTz550Dgc^FuQ_$A1w*=sdjs|*L#&_xjwGOLU5N6$6(I^>Vt9aagR9L zPsRnwieArz&P@#?`o=#Yi#cX{0W9hBnbc+e^9Rg*_4e z5}5x5@bfdsd507I8cI^^gwNnKcFYIn9d3~|JGfH-#Jm%q6tU?<=N3mP4%Y^;>c+)t zmOuYymH}H<4k<@69A%Inl-bU9mF!#4WSz-4%|EGr!_T~i{M$#w{Pj$hLva zgcxJSYH?imw5VaLK;~4y&@Icw1p`^8#{ER}T(!eByD3~13{=#ar#3xRGrC#do7~m( zCrTUUn|Cmi{RW1y=bl8TEEte6-Tv|69tpmCqb9?23q&w+rMoz84LZWbcMI5swMsIi~GZ2I?TTE1;_&berFM4fq2qN~@k$@aoIz zdK^RKBC*SV4FcC{HLMic#f38N&czJC#l1eAS&DL-ae@d}ygveBvO_!|4Rfe$@mTIn6EVY(4b_zIo)oNTFNP8EVhy4nm)v86oO@B6us-Z-y21lg7}|7R%jjv{RA!W`8xc;rZWwU?Y`t`%$ZHc;cE z)U*UO>776ZtLWDG`T0<8H*(@)+a{H;)KXhfq!SjIeqi1@$6CF4pOyPQ4^{jwm0JEA z4ej-_vrB_hrZ9E~ZO{udtaf5tn;C7W>h{d+D95mWH;#65na|rDclNAi&TQH~_7Y>w zeR49ze0iS_2;}khn;%nM)er2e-S`I2^mtV39&z-|*1{=tq91!cr^`z=KjtT4;c?%r zG_YTLdMM0f=401gXE;8luI0wpaaS*v$~uhOE>|rLB-l{tb@+x#=+~}twORqYeY5EM z^%D5)0?gyT0CE664Cto0-)S}}c*3NTD=5&*BXgy$mfPEEdET~*cReDI^N6b=_d(*( z)2_Dd;&fSHzuT#H-g*1&r<~Amfij#en*gaZhVuff-Dk&Lg5zrcINRABP7py#8 z+>a~wX%6=LtM}p2nCB-ip6`z{Yq;PTa|2QuwNZEE9lp;F?&qA{4fDQRZk@Z+6o05k z^BfEyU3UAkwid3>-fsuEyRo6qCx&Mmg@u%wLL!b9X#O{pKbPG%;02fWE6q}U4$c~` zpHxwvP1#Q`$h6+kd+gvo&eFqY#J0?uh{FP%kCG3A44eRvR(h^vLMP_VU z_Z;0@z8xdyGQ7Z)Cyq4UMxfz3;<~0r+mC<9JlIJ@Q%r zWI8rAcTsNf@HWByS%fi&RF{1(MynZt02+h zq0^SI?^t_>?SC8I-|wd3yaRLl-3~*3uE31ND|z9_;BWt zzBcadkTvlcnYrIZa-YRGG;~K3^}b@2x-YVYCvn9hNA5RW6CzkD+Y0UhRli57`dy|N znWgQ5FlOdNdpkzmE%yW1^UY$_bPQh9I(T_$NtRAazpwq3)EZhwfB&qWe+@A_2c678+m zI;31TG-P}2a&_Qp{!Wg-zp1RD{Y)EvT%O%ckT zi(Ald_!c8Vgei!IrT7!3sXJhxZ3j4!uGZ_1Q^pOKaUfW_-Nxl|Pc>}rJHYCRBq=$2sTNRrz7Ddfy&+6YXdTCB}x3JR(l`lvkXFUdqJBZ0|wbw>>Z17Y6g%{N@^ytlhD z-|Y_Jqn*s(cR$tb?u1=mwp6saVVT#@IuK*^s2>nDDAa)D8nd?UfZy-fM8jq&!b~`} zW(gDPoQu$nk)~SuEZm;&b`SY89%w&l*t$uwJ>}m7R{S|&@%iqp7iq)}>aNB{R+^Wc za_bPdTg}n%uaNVzQd2(&K;RP%0j|*>{OlqyDwAtr6r$9u>4m^(ez?K_b>n|kKEAbg zRnh&Q9}Ey^F$$l~xYaj+TYW{TP4;Z9PuG45cK1Wl59&=CBGYqa=iRj&CnjbWoanhI za^$X}>O(+$J9cn#MTF1GeWfzNxp_CXvd$ZB>@%*Q{n^Fp#>HJR3iE^luC)zoOi=W6y@ z4j^4))VUqFyV(J}UIX)LK$K++rBC+IDP~zkE$g67sL8#~VXPWgi=!pd+P3bo)N&Ap znwQ*3bzQV=pBoF-lJA;yOPCftYk<14cI|pFV{O%;Dakht3cy@?@`i=a)p0)3bK)xNHr47GgAc z!*4hqCx^Lri_n@p)Qp#5mhncpvqbh+EoPk=a_hi>V-EhM7CLfpoCnGdKdsImLPg%E z9@FgLMm}5h^-5hM#?k;NqAp8v+bYjyP%wq%&&{Egw#r43%6<0D`cb13nzYAlx?=}m z&>FI$wRCpYftcOGtTKaHcnEO&hUH&xYM!e{s(!Q7ARX|;x0SyK%q6?7`(r&C??E}( zqC73Ng&7GN7uye1p#U0^A7BL+QAHi9eJr!P1KXmvTYiJv!2k^^yRzY-6cIH+)uMd? zUP98n;^mi?yVLpzLrS-TLfIDbe z{u^MzKTrK~?BUi3E!B`KBk@^aOGx-4pmOW_E6$xe_x3KjigG&vU6rnca<3Noo3nvQOrbIk;cJ&SwNV8fUA~MkftR zq?yI+8x(4 z)Av}(B*M10&U)(3Mqsu|YSQ45=0sYWqC_Kfz*hlxg0Sli8vIX|)ho8wjcB>#1S&Q- z4nwPGVyhx%TFnCPyv#FoaMsCL$0I@bsGVA5-b$)<*@ZjKHcIw5*%frv!8*xwyxE?S z1F6!9%tqNQ zdGaVD#ZW z5TBPBr4Yv~V)Jf%pR?9>jaJX%Y`BD$dsMb5-(@Vrqlb&8!+x>knY1Xvxn@6-;LVMj zJ-IE~TWGeIT`gXyVAdL7R#yGUZ|6M^U&~J6Xn<{=%<$?ohKvgod3WpT?s+BKW&n@6M2)GYSAe=OC$hDnH{C9x{i(Z z;pOpu<8NjAjU#0(t0wPftS)1(|7>kNFUuHzi^L(x&PLRsClCEp$~2>6c;LEIs=0n; zMO}`n0ksIop-+Tau~?6^ti(uSQZ--h2hz#o=bqaFZd&0MIaxIwKeRdmM@?I+ZDH1V zv&yQ+YvUch-^tGGN`a9t0Ak3q$8v;MZnD3Ej(798(EVp;G?I*Kp1R%(Ratn}*Tc+A!=I2x+@%+G){jhqzo5Mi2CG;P0O)@Fk_A z99PcBJacmb+w&%g)_&rD(&i`~=(KWGRew-Z#Bcll+DRw6E8Xxtb)%v~RO?)&?$i&- zjaR(}s@-)YB;r0NI?TM8lGf{VUZ&pIEc8t-kASRW1P0|Kfs4O?N;^}C5$nlU}~<=Ba_Z-QdnBRjC>MQ~0v0P-VP1mAzN z3P&I>xI})DHc^RIK8mBQcz3B zjx^0;!VQ3(B5e0d;WBsCg-S_AlKfo1&t-jTdE3nNNQ^wu*PZS*l42jc#}2HSCgUgE z_>p3x+;U3rqat0)`8}J@ zwC}+?>v^hYD@);ZN3B?%OFCLjo<+QKJzVx_?t2TAo^r!S0aDd#wj9f=h@+OM%TBQ{ z^s)V6)r>5LHFOp8Wivky)P`;tBHRmTc7!0=#_Z0buiC!E)&;fb3stxtKXATEHOr3f z8%UlN$$j%A@FnAe8(-R~%dW(0!7zd*uBXVftxbpCAe&&Bro=g?z_mrn_L zm+6UuO{qMYB|@dg?k$?7+fp!wmPz%nq|(3vCP191_72qo2VHcHib?_O14fkFa}Nt< z(LC;aC-cu`So7ulJeEny-q_z?+HD!&uE*QqvPFhBl1wTm3udRsJ@ar2`PW;9s_UE; z?ku)(==$W8`%x^y80+X4&CpAJgwtgw=Al|J7evs2@yfRBJFPoZ(~E1n)!4$t#De87 zgQYs-jA$i0eM6oaGTzX906-it)3K+SYz7{WG0MjHaKy}eoM_$i`z_p=jdwffY_nCd ztWA@iGcmWoM4U}<9xge>-;aa`BYq$Eo#4kUk~mSPUc4v3^Lc&;(*gQ-j=DFbi<*61rSb85=-_8WVCHqT0Jb>B8 zF*`3-zl3sh$hm@T+0HSHms_Z{A~qLhH~F=4_oJp20B`I#(eIbxFZ{#UjRJDU`6ZPW zV3pBFc;F@PW^P(@Ze_&rTb)9oXlS%q$1GS0fvPo}XvDpw>OZKGpitW>yS`eEEgokQ zU|H?=3!ppenFJ=zU6R);POp%K-h(^U_C4ETUiEdarJgD5kRr}(vtB@cgH>!C$0lE} z`7Co z{V9ytDufo!iS5RRU_ICfN1p7iJh)zRZo$akVZ1-+p**;`z`=*S&IfE~@EJ;uK!o&4D>LoG z!3kZnER|~juhpbLqMyVuS2=AWvy5HZaOF;eW5)twFVdy!~~%M zn&F-{R6*v3YWdHOj#Y5!*t$xOn6hcuf0Y$QR{A^4c1TI=w%g~4uvp3Na}QtdHa&R^ zMaebzwWg`by^F{=V-)lnpoXR}FtcpCobSZ{Ms}L7?8q!-Ipaz8b4NWmXNikPtcC)4-4`)e`Tg_^x`qDau37DvdEbMAv|O^%9Wb`}ke0{2@~lOhUT4XN$` z-V0HY&(EWUv!FtU<)#(!R$?3Q#LzKq$1MljmFCinlXFBYCl5E_M(8qO{XF4+{7Nd4fm#(tih3@dw#C=?8a$@5i$k3@Oe}B3?kw8=ir0c`x&r z>b}8!lc@t)B%%J}X>vAZM*y4)vh5Q0CI=O2^SXmko|*lc@&X{2E?DnfFFr?d%ugp! zaopAnK_`~<;o!!}ShfmJi#23SaI(Gj2Ttg|4!muVJ8JF#lo4njl8aNO6kGgAye z^|BM^<_yP;z0_oGIa`7k*LTtQR0xv8i7O_1+3`rz#_xO=)#21KD7U?9LI18ZeGy{* zt3X{ePk*OJLmQjc&F4s9Z_3%qCZADVuE;ga0&Q(JG{u~#;#;h9e*vx2ifY6d( zA#Ro0LJc_Z;8W%D!$88iheAEw+}6WxKma{DRoQJ|6q(KnOc-4UY(mM<_t(a!44GNa z^?px#G@O*Fo&84pe5%u=#>LT%IOUkLL~_n5&RwVZ6Dv+vn*Q+gK?kFx50W!%1TxpkRE;g01(^HoaXHq zFV!(Q_U7YGlm#?xz!)E$@Bk`WbO9`2LppCeSVdyv+Qh97UA$Oc+CalW;F^p+#GHGA zAQXEWZvmwuvNM1m5#gs03mqJCbUrMq)bY9Qd5&qgc2Hpk(@PZ*%1R)MOGGm(L0P5D zDT*N4sZ_R1L)fb6m?GRhwiK$Zv)9TPW@ilH%fNH)RW2%zP-+n1q$NSG0ULFcM#-u&A(R1>m(RjT}yVa4#YEs%cs+6sEgt z{tRqtCby_bMeB<4?*WSY@*M}j*$!$CaHEN`Q)6Wp$1gk4p}HIY#+ftc&jr3wjZO{@ zB{*82$Lx&kpgZHnnLp1@<&)i@!|c?}D>fodjAwl0rYh@j`9aHhlT$aju!#;2HBm3g z^Xt#s!SbzvhfMBP++k!-ah?q~jW5RqE^fy8opLO173Vbtcp5tzVl%vOyF zT&?g0%uFlaO&MK!!23fBo|T*O@C&nJ@17Y?gz%px*5XcivjBtRZ$N;wo+Fs zUC%fB%f4#P_x>No-ZVdu}o2`WlN^cnLnC{ zmzh;rg$6Wv(6uJAG9qr=``zukpYH>b(x9T9fK6v%)*?-q~@YXb%y8-c8h(&l-gb#;>5r@pM&v#Im)8~C|#8)^>Jtm`+ zyw*8A6NH{5ULD@7wQ-ep-r9A@1Nz~MLYrTgHHo58F`Jj}9*d_zEut3i&SLfG#ZN?} zT83V^MVR&}R1rq8a=*RT8*C|wNjWG=A8^r-2(adrhdj&E@QZx5=20q8uUFPeKStjC ziXHIR+){ETOi)^H_(SYCRQa0_kWgcv38RQ#MNmTN$6ypoNvgCA)Dpv-t{ukv06@sG9?Q}JAwbfk?HAcKz zskCafanGfRuA6&%DrYJ@n)x<7;v>v)x5}WrwC^BykRvoxMRY2^DkRVbVg}`9EK=N{ z!aXg*xh}FGYn{DRM;fe?$^c@gS3fsmX!=IZf3jd0qK~uB9V=#QALP%+3mhKi#QMJg zic}k_Qza2@_hn?*w6Deb!}>sYi0gA9^cJ$12w}sMT355-vcG-ZWA3VqHbi*dl((Hn zWTO$m>_o_%s#{ZzouR6&`S$3k}{npjghRK7!$>};$YPStXL)sLIXCkzERBwZEXnqBnKTvJ~+FntQ1 zXsw <6?2N^7mTWOw5emaSB}4$pK9$WVQt?M4KdVlz;mC1yWb^Bo*5YJb%yB^NPIv8vzqC~gV>{45dB zhtSjo`FVZUSG8v{3UE+h1w5*nLL=@`qnnosmkQSlkDJ=5Lq^%7 z1Fbt5@4VzGA+8zpEZ^ z@CNro-piCVm37zPaw8>-;HQXsM5Gb_?i_0ZOikO7vMfQB^>Vv?vFX{aV~2TlE)0S! ziq?ysja}PsDVK26){k?k{?afDV?VYnH_1YMB|Q-Oe)#l23sYt@K~nlzFLi3g*J_?^ zMdiHaz;vU1MdSVb1EgXifPo1E%l7+*>aY8e5VLvFF3jV0oazrihwt`_TE zFcnA64KPm>K~$nFR2E!7eOR(!a1Ftd3pZqR$5GhBG4X@=f9FErB>s&VP>+JJ-teqa zwR>TaYA-r;B(V?-mu@yu_dN~Lj4Ada8SR(#hI;m(RLt?j*Ymmr(|eTVH{=bq#zqiE zX@VL!T;-7f@Zo~e4L7e7dl{%@?GpO)L|$0Fnh7rCF0`EGVq+05mIL&tz80^6B3Y5q z{YYTtq%`nUh5YCEo!}++y;Hu!xtq?$m`b>`bccu_m0xHiDrwddtK4edKqStXDZ;o= z;r*iL%odYirVK)>N=9rd|2da#nsZ4`gzpZoc5a2QeenK7I+TbN?-lH0;C&G7K)(fk zY-3}UdfIgrP0FOR>bt@9S>&C5a8wKXcY4uo2)H1WDgjpW;+y}z|_n*H!k z8j;xrU(rf)3Y zbCyURLb=8RzPxIc0d;t zK_yX2I6rMi8Uo+2WZ>KgAZSFY2@QqLO$%ZB8nW)U=rY8OB6X*jKhLe;o}$Z5qcywe zy3S~ZoADQB^BUdWxQ5$FMpS-q<8Cdi$}|AIA$f9qQQxi|1JZOoY&)s65Ms@eW3_;- zohT*aB{<`I90<<>8D_Uw&(m_dq|LT=%m*FVgBhr2((~+3FZ;}0M2x%j`~5kj%7Px2U;4!D2>S4MWea>Ic3H7WMsJD3z#^Ck%awi&W0M~{P+{0 z=TW@cz3Xu~2P<=bG-H4hFTNqdcLOo!m3DAgJEM5oh#K|UpLjug zbz00iFQ;TygTXniGEVBp{yF_9YM<|xZ{9oNXFVr?as}e4NH_!P<+1KzoFLUme!K9< zL@sgVE)A}S^J3NgkXSi0~44q7Azs0X3D@Z7I5jc4tu^pvU~#U)T1o9C5?%OQMp#H(6GKup;C5 z52_mOw-E_KuFZbX7Gcx>kdLxEAraI@P+ShMk9-ZH`T=wMrZU*XOol#9BhQjNVCqK? z*cF%WDr|33Og=)86Tfv12&(jRmOwROsU_TI%<(`UB4d8P0bAgr5qbj$zcvSe}e`)u5kiWhz6&8FNt?=v#PG`yLLf@eHX+N#A(*yfU2LJ$}@)DZ;fg}1RO z;!^-UK*GPS!WQ#p7J{$w7-W#s%BTs2m@83*^$3)H(&9t`XPpN-W+(J|R}uUvivm9` z`KTPH4N#L6Emrj=A{u=c2ZXx4D%Z-+f>2xvY#*rwEl8=MQ1S)}#Sd#WKn{3ehut7+ z`sO@5SyVDHQti3xE)ss{{*0^8N#~6m&G1d#A5!n zpsPh=I8<7?^QPvyRC=#PF^6mNNX6eLb%O{?( zoEywE!x^bdtWt)gxRBUx+y|5_eWPArPAow`1XSwGzp5 zj=+IDv{`v9z0syF7SXFq1@r%n=$0>53~ASc~*_(Xu6iQm&U0Y;63` zFdqRWRZ(v*4;tK6iiI3&Pu)U(Ky2TK*o0U~A|;Fn57-&Y_EYs^R0NrNH%GxOkC=Ha z0AKVR-)5U;|Cmu$hcgrYu;Ar&2=n?bJ@Cd1oVzh3yVou!G>&IGch|>7M59cD0wbe( z_nnG2-f^32U@5{}wbod#UWVN(_TsMRpCp2i*zF2Go6jc}p~f_fq=i1T{B zB}Iy<*Q#IP!5K6A;SAiWNmOZgHsZ?D&(YqQZASs8UhzC4+6jrSMjALUYFSVk>x<0A z>4Xe-sJ9~XI^5yKh(mJQ7pTVft{j8`ODkev1=iIp%6a(#?+mUY*Q@UzAbthI9 zcAqPicB}gFJ(c;SMG)%-CQVEoec9sFQ~}Xivf+Jzq2N*9Vc60r`yMT)h%Q20t1{Oh zyzrc+4Eb)4O!vfKlB21yQHAu_<6j+--J0Ll!I4v| zgVrRfIK+uZ0fteIrF*d;Zu=cEE`!K5J*B+N^A5siVjH7n<~LkXDk+o&#c9w@xuYD7 zaNZ}$DMz}9s6cWXp7ngFMYia02k;UH-xnn1;;cTG^D2HG#GvAZ*YsTr)yN2>#BD`tC$2uY!EnO}`!jSMIY6N*F@#3cuY(;0O!b6TlQ9qqwX z!a%?xv|VabYP+^&SEhUjHFyhEoCM~4^j*-}MeC0UK+9sISzsok-Ot&v^(LdZph#5)ru4+PKj8x0NlM6HjJ>NRE| zp3%65Gqw?y7?#yl$e&D(TvE6 zOp7a#e&YAxz$3Gk7uXV^gr>xGwEJEnX|)-kO59ok%ePZu6W4bVZX?i#VHKtVh6F#s z{3|_u+mO~R494F&)vcVyPju$10Iu!`JN9{SuG7O74Qsssm42H`1>^8nZS5DAX0}SG zw!YGYSfgQm)6Qx&j+ARGk2gYa!)Y4^#ocEt&qQjs5 ztf!lRFcgMNo6pQP^b~B!IIV^=DXPo{e4Q@Vo15o3 z;@al#40?vH)^D^TRBjIjB@eFVym`YKZ-ZgN!u7(!*a~{r@LqJ26?Ff!6ntYObVb!xa z^gLKAQg@#D#Ab0q>)3&U2s zv=d5#w4q&u*1~2fg;Rxmn#o`16~{w+wyUlCHyy+m^F5r%N*rMUnfdx}iF=D8TOdl>gP2&(D)Y;B zm{k-^qs9OXidqc{6kpfR@0otE?-t>GQ#?3-R!09ufBSh)X`ScNB;^Tr!>AOXv})U; zs@-tYUqxN5RkbGpOZp+oVN#jp7S*_-QBE|VG`wj9N$tkR61IP`&#BYi&nKhhrEIys zI;_q!lj*B@73B~p-)%z-Dl%L49_eel_{hKUl3h`sIvt{&`obA-db-NXf^EraiG77c zNyA_AlG8IC+d@2O%?K3E%E!h zx1X-B4MSLP^3{P zh2=VmBO8dgiaip{uWF1AexCVJIdlb-DQxVm6rIX^r3%|x>94k!dzO043zFvE3+XCz z|CyG0QQx|DYCS<=XZGHvR-~?voNXRFz-pt~h;Qiqt{?5R9C0cSbN^=DIS9?Sg#DI^ zUj^1_!`_2^WmQ4hC)hdT-wPi)CD?-{F1h?s6^BKtoNE>R7=KEHZ}$>ZM@0>MYmVmvL@@9@3_wF4Op%|$Hq=cWCqe`HNL?K|B%o22X54*L;vVs?3&cPRHygwJf+c8?n;t|_r87PCq*T_9S*9Oxoel1=ufm&Bq(kASCkG%+h zdek-Ka*sXsiUv>)kxrvs(Y~1FB)cc4TqI`}`V9y1X0_y@^8MB7LNeVngi%|{<<8Y= zbvO<8&$a#e$|%RVJ?`;*V{Z4*R*X9R;Rx`V{=VG5y!GHS-u?Q$1NEQ+^1&be*D8K` zRfP8kXW{^!SgP}|3ujs4IF?_(B-jtvYRiV(dehTb-YG8eYV`>fw?sIw0kSO)Rpw0x zsChB-6C*(=r7Nkv{M2~k-As`8ENrgufqJz86JF!;xko>7iJ0mSuNV9Pw&1KyHSX1 zA5IAox1*?(BoCA?mJKn~gJ#T+g;-_jH)zBhjng!UDe@q%Kr5Q=b$i5E@a54=sndD( z;4b4y?=sT#pt&a}>{vTYE{w+6E*Y}9i~7p`ij1CFqe`FHmL;;?fe)tE6S>p`?ODP- z6bRdao7-~ZBU>83Z=OuHip0#5jjB{S+kwkk8nIi*W#y@>jM0a$n(r!qbI!jJm=m`rr?OUFKw*_`r^Ayjh?6K_&CS|KQ$Tc=f?W zzQ)hA=|jKbrV1}W5Gga7)&<6U1W;S>lqBowxqbJ!5%jAM}W`# z&p!JB@2OnOpVeai?CX|=H6{^pN&A)c6k^38PAPGSeql#j7!`o*9yOxzo$lCTYdU&-4Dk`v>h^}jO(U{w{X-#@FT0MtIsQ6Z|Rb2 zCdnqNr8X^qU7B9; z{viM5zE;}qPx(X|%zb8RF@Vub`+-6}P1azQA(ytnD_08lOuYXnhbgN^-ltx}Mr^?1 zWYzDXCCcgYM+1D!_cW7z`wk(u(r6kUY}23`_#jAsxR9w7#(RjmUA|GPNn5OnvP4zf<(r#u-(CD zrYRM+UpEb}mW{&ly+puP61RsewtuD3uwkSy8nZq`rXzp#2!HLQ-5ko;L~K4!2p^p| zer*2c1`3F0XZjojzRCRkliGu@AKTdhyX{DwvgY-wA&@7G{)|EOyZ@O8AD`#J)l2!z z5%PTA<#s}_uRx)K>f?wk8IOD|v`CDfE|nT~R9w{yZbV2_=cf!a=ndcDc2$HGjr>i` zPZN;}Dilc0E%M(q5+Aa>_jAvEOZ!$2PMmanqzUB>CSYQ^wL$O)+meT)jyAp+;Bm?v z_`pn#G|4)|{5oL%yCiPv)1M7Uxe8TrI%Axwt4m8@xfA-!uvvtDLV;RvHF8WVU(jo; z)jIY1<)SZ~G|0G9RY*2xpD7v3 z{x2ddaNe)}UvpeM7;{CMSJWS#ab{cZ-Q;Z9Hc1LS|vR;OHPR;ReTtm6AqjU(r;eLvMHFuN0%|YT zsU<}d(sRp2X30v_fpssgyw^{J8;bCxudjz~L%NJC5A=N0u4F;AR~xc?52Ld=%TIcQ zh2nTX;LsS1f^a5~ov-8-FtrbM+NbrNzROTgJfz?Dj*K1)QWn!FTvG9qt^L5-F52Km zSxD}ercR$NTkW%m>>`q6+%3y!w_5d3rc}J=TfDyiQ25i*D>lQ?6hBi>SS-wq`1KD% zf@4U$3pdL@`NE z0}sg5D$8L$({z+hXOhVutcP&!q;LYMy~+<&14~ld0XX~6bI|874JD%xyCFQ`3u|uS zU5PVyZ?6s)cOh<;xjW!NN%(22a4-q_t6HB9&tf!(bmXr04!rfZT+}avrML}C0Z_rb z^u>KQcrXvXRlj$YqADM~ca0sUQzd9R_sH6271h=s>rlu@0O$!GTnkzsdA8y#UTRn2 z(hkI(jJeaMt3hRz(VV>hKI&35(=JZDan{8f+nUB#^f=Q=J3hCn*1k#q`=s_fULgiG zuANo!Gt)bvbu!VnwR;wvXqis^XI6jBOt_db5PGY!SvE7~&P!KTRlF{>rf4plSGd2r zR>I=X+0RIR3LXbl+_%^ z_|&(I7Tq>YbXqhBCr(;}m54E9e$r)67O5eEgGnn%K|*l9?@7;LxAjW>1Xaaor3UlH ziqfCfzHYbUm*bu{&_Xwx#gG-#X}G|hS&ek1E0|^Rk3w8KElY7P2Jc9n`E%V2&h9~6 z>T|~|ov0+uBbk4auiV7HqMSAS#xZ`uar?#a>gV4b4ab>{dCP_OKaFDLyIU`b@ZBD+ zN3z~E!PM-b-GSJE<^m!_R(*~GC=2aZ58mnbe^|Di?cHuE7u<4Ft4Zn=X&g5B=*|xFo@U<1D4uW-@0tvr zPq2&~q_=Xys~k$agAAs^$Kxs(8DY%4!8tBC>g4Y}rR~8rD(3`fL6CylUoYS}U`-Ud4EFz{?o)MD= z4+W#_L3Fz7cGg?2UyLMnD_ccWTI&%T|-%C$Tb|NKx-G`wd|l_p2ZgA$>RGF=0QAUz#ra?N@26`RN=Y83lrG$jb}X` z3%s&1Zk2T>+Gvl0jmfCau=wpTmT}nJwE|=r)@hpCEy{Q>NF@^jyhmRN4aJv%Y)McY zrau~{sVgt=Y6G||!pp2(78+Pu-UZNXxkh=Og`jx(^5tUdZJioi+JbmaQg=EJzokw7 zT=jqR1LpcXVA#$=*TiUKM4jN}a>op1b5@wcg(pH^V|d8a{3b!Tsa;PR;xlPl&9c$F zUn5uL(Yyc&jUFkND86cN(NhDn88X&=}CgGGId8@ z6cO?Rln@)AWj+b$T;e*digL=4g_owl^z=`J&9qpHyG1#OP-4V7$oWsc1(evDj2_(elHWGQ2^1;% zp%vzykLo={seRwZe-k$~sYDq$kV`kOw(h)73XpfMa6Sje|EkY^)#u zGOy{)XB+9Cm|qRuu;Fca3_JEKD*iE1iecE%_oJlnvLm0%+xld*fS8QtAq_#fsNj3B zY}+rw_qgvxm!pt#KU0$WziDAt#eP{JZ!P`eD#UvxjyNx)O&O(;#kGS|BH}$uOBd9p*BzBc^Hs7-DnS(cBcUNcb2Bz5=U3B|3) zOzm;giFmgz2G{|%i*3PHlTXbh44>h_6WST`e2ac-E#dfnucw8r5nks(D@*kI4~MbN z|C;BegvO53NAq~QAnyoj4XJD+eLYN(g=uv*>f*cxcJ>EV{DrKJN>%ty4fcJ~dX4zh zl7VGg0N`Fmz+-~_p|k{6u{MPDomH;yjNYkeuIADzkWIxaOH*h#!{zIv_{D0T4;gwP z2kLyB@yNB-IH9mHE+iYd;=>#NV~H6`zP3jrvBYT|?bob9pI z>3w}^p1e|xlt_OEi*hMs0(VtBou)|$m0V(4DfgfFaOO&4L9$(}kqDNO3y92|VPUx$ z=hHJxH^gsizr!#Xm@dAY!yt+Jj($_g0Y76Nv06zF>5UKa1rwcU5=KM2XGD7#0n0!B zpM>>OFb7x`KtF~+%^Gb>pdaz6Vu)(3biM-0(~;IsPx~*HkrkPUXcaBLJT*8NZar@EF0QYy;V`Mb>YRhqj;BF z76ks=Lluc~uq9Mf3Ul_PKP&v5zNd5A&mBg8j%ure(bsUKjK@Xk=FAqft=FLBV9{nK z&1lg-pzThddHZYJf15|5&O3yHLF`LxZ*FTRVFsJR>dU4?0`^^?*UD8u*x-7b5x-b* z1yMgVBROp|e_h)ZtJZS^9|=!iPvS^BJ((i1jtGCg*pYa^hD%F>6qmyt8Od@m=Fs>X ztvBsLEB7l;9;s@0vxuxd$KCUYtOqiMVO598#AK)Bj?heIF}#NDriagH@YHtuW^P9CrPG(=Dk!>An&{H zeU-!0sGT`HjUSwrOrKdeW9oBf5c&DyN@faiw}lh=H#RpfT$?zFbI!4okU3ud-Q0d{ zjk*CVU?S~!q)su{ofAiNY&nx+_8;@KDNwymUVL0#T2+cwA}w|IwW)gdgD)2kms8$5 zT+Yd0I&`XuTV5^UIYgEbw6GwNzCtTJn4{k2=CxZB3%Uj;7vzqnj(xR23!(Pxt`;6D zJZ>fhr{j+GDD98w!_#4niZfX^(K;T`KUgbSo5cWMsZ+0!659yN^q}f z>7^1w)6(mBMI{M=X&`N@Kc_)%iD#yH8hW6J@uKu^D_-L9Ov?tDkyWiidQsgvKojY_#qU2S$64!<#%X zGUn=ZHzw_!VG19L$;R=k`>W`NYo@4>I^ox-z{vE*Adh)7&`LqAsy*I^u-FEMotnY` z3uhkPPui~SJk0$M1u^tu)G9f-C~+XQES)GL?OP=!n-l)v!ou2uzSAbXqP@gb=KHCu zsO13tr{oiQd%VgtE^h|7h0#x?xzp#^OZ>-0y_qGQ_OP(=*sEmqw(ld13Bca&=~^r150k_{M{DL$vU z8Zf519{2W4XoPDery)u+);o<$7DzR|&M#Jtxw2f;21nF$pnnhY0-6R}QU+x|=hyqwoI9qek=At$1iA z%91U(mDdu0AFYwVYE65&u$roL3KcRQWy z`An88gX?Bulyfq(8FpMkn0wKP#Xf!ei~1_$nnPcOXEN@oINEl3n&z5=|NVc-GjnzA z9hx4({lfXe{khgHOwcNgBJRh!M32=#jrHmSE=$Y)URot&gN|2UIGs7y{C1g% z>CnW^Oe84!hu_3`G1!}V1FwOEwDrFd_cwrO3t(nYg=I9WWvOcL@Ta+ds+JEm1V!I# z02p#Jgr|(EpbP*f%+kKsbor6Qg1P?1thm$A#>gw?9>PB_3WDQ|V;u2-U?&Y7!4bLt z-sa*<$;}woG-?iNl)E=WVk#&BxhXXYh*D8bVRMD#Oue+92A{6gst?sWKF={KRCkz$ z2bI8>^HP}4>@YQvUnyjT+IT|Z8^iXwDbJ&W2chjTarLn5|I5I8NiX_yPwEffYQFj` z@$jd$nk<*Ty~k^(Lv%I7E6boc>!S=4+!EdoRH#c4(F>`Y%5mqhN@f*h>daB?0F`d8 zFEEQXH#!U#MHU$}9p@w5mY zU&+hBUW_fr^8+#iW9(Ve`oKJA21cPrUf$Ta5ie%=Ohj-d!YQiU1>aGe^*tuJKwjik zV1womCeIp-FYLh}(!+PUk&Nn1#(^F;9tPYacQ~tb$PD2Q zSyA4nFR1SV0}0>5{e2?V@_A8L=U{UWtd$|mAm~Lb{=vizZ4ZY6C6VjWkW9n603pzF zp$BS=hpIPQLgpL%kCbx&E*(usiGXZcy&{|)Z$6@^3rOWQmOU@=&(pvKuJ zT1{n9SutAOu(kb(R3Cn&$pjIVIA>SXk||zLOc+-jzu!h;pPH60d{?de?l~}YU_th z07luI1F^P6>S8;W`3@{uLVJe^{skG8n;d6pbqci0q08q`EP>qfB}w)!QFSl#1NQN) zn7TXEo6)*>HFR(8Ou_E%;}LUb_(rK_VohGP=~;2Bw)yNls$V_Ta)pWz>r-zftIZ9$ zH%vU@xAo0l$zx=Pi2dj^GlO7Fz5T}OL~dkba{e&i<9?4Zuh#~90gIEoMIQ_Hy}$#t z0o*KBOsR_UPGOD2djThY^vhiBFn?QFZ4uby6e8Pb$s8!Lx1A6g6GjY}J^>?N&%F6RjHYDHg!>20{) z_9B<1j%CXJvFLi>3%kM_aOU-UV4v~CP*XJZ5?6wEheQkSlb-v{A9;%68@nm0<_y&W zlvB}y5`UMeOmj0Jw z>r76jkkcxhl9nIhGmVoC6XIKF1P-6!!N7!H28hhi=ce>y-b{Hd0DV?}o8ucZ(bd## z{r};dJC#y-0%Ukgg%8hp!HtVv^jk)1dS-CyP8uLT`**@_%xkeFL9M?TQwojh@ac$7|v_U_EO?mR?Ps_8PKYi}ZncKw=RPjtm-8m-R7pufk z?1%RB?}Mo=e7f7wU`HXcFtV5px08@ul*M}UUBtnuZ=5^-cYMQ{yXD8H)_{05nAVP zMBUu#>Z$;8)i^7Zf`s}_s`?4`*O?;F6&h|A6B}*^k(tbpG0&CXuUC2g{NDRJIQH?v zqw}N{aJx;_rm#Cqx$m!YjSXzO^-?v0jpa*07EOhDHPT<_Qi$>%l=6_dN5Ag_zOSJe zj;pEqqaoAITZdy!=je|%KLgZ<&5R-DnOxaQTeQxWD4P)|BI!K$Z?7`%#yRf)oEbQ0 zh67(Sl!3d@mb>T9ojVn41np|QfQX^)Irm*Gq-9@#8V{<>Zx~bCK&D9F_LbIrC-naR zeqOCTT*p!7fC(8gCvxSTuWI>u2M(#3?f%Ypp554ZsPi3jObgBT)Htq0|Le-i*7EX@ zuk;&vj%4hwFCO*MbCk2PJ{kQQ?pbe`FCR5$yGxTC<~F??&Tuk>q+|afxyFb)|s-{%Dz&&^SW==Xrg~kgIR=t}F@TrB00C zINoHb&*^*iZfFVH#ik(L+>`IlbItxz6cDAaHi(*NN*YzZx4HR1`NvbLcjycH^@3kt zUfx_>%%(~dFKQd)NNrBvUUYXi=i2bZNkm4qCT6PI;k4O_s{OWelvVKvJ;C~sPoog) z@e6AHeVc8sOaB4KeOd;ea$Ng%?y}y|IG`VcYEK%Wy2o*=W&yT!Qh&N?DVR|@?V*q+ z3o>~9j(PkO!9>V}2r;n;cdsi;QYX6Ul_`okCh0A;g4YgN-xCiNf7j^TtE(#w_(iOmLJ@IadIGJiPXD8LLaHKNe%`EJ%Moll~?&aev>Z z?F5$U&}1)8L{cFce*T-wPQBy=-|15(tXE&%WR{6sZOVx9Qk*7buhC~H+|c)Bv*N+@ zMBYzmx}glqb8|ESZb%a`QJir6lr~0Ue&{4ll)C1j-Vyv?tj_|fpcgrgp@J6P0wq?- z1S>`Tx`krmt}=7gAHa&RK%bdwSR0$5>)QC1%cFa_e@(FW-FM%sor{5oirX~rrR#QDmy1?hs*L3W%ks(FAB`ND`HYG_E7;U;s_0#y44YN|s$lC0Z4A)#i@hqKqLloKiqWQuUrfxTY6SRG z$XC`<6%7_D6)&iEl7noznQ67F@4)TCVmymKi6R+=gs~$?te_M za?`#?fA?h(ynv;Xfq-^up!?)>NJQ#?UnRBWOgFNsJw(EY7-B8vsHoIX8F4Zmcx4-D zU!~|T5T{aVhN#*>Yc@iw<`~x~vz5E-!K$>1h4nw(SDo0PYpAw-9Q!D){Aaj22mYGWye$(uEc4y$W>@+4F;K{ zLV@o%r60HBeT5f2=FV&QGHBWIvUIvz;=cLwQzcYfOHESE4BH|;L~}eyvB=8eSud|8 z`qR8h()h?z+GgC!vn7)?`YnxDhiRw$m^YI3{-Mhzfr)aCkN)@qTN4ZXU3obOVG!B+ zh%Yc7`(jEvN0{4Of`(m-a9V>wvRw}#b3OI4UU^uBs^sQO0ZE$9d4g4lc1-+Kh;M2% z15`c{ajU~Z<-hw~DRW;T)WOuY9P26WtQ1kU!WuP6ORbbVTy<)5S#kgPwtPriuuHjjc;(77&oOVO%Dl}!_rJ;L zp8R+-!A^*3xPk>KE9OndKIg`|1yvX&mNa!dFQ#*eiP+6c?f#!zUA@ix8ACs*QO3ME z=0_p2kmm;WGV?>?$2IX>;P$T1*HL{t9!`JYc|G)3HM$%WPU;hX+u;dC}LB~wmlys>RAyINCVfeD&X^g zD~@I%kkHo41((l@zj z&kr8cf&vnh5(gtpUI-Mvf*E;6UoS^k^$VuI-0>(>gTXmWXPLLI92+BBuP+o(6rGm{ z*<#*)U?D97*w+%NLTkHTUwI*R=O6WGgG+7+k9-aS=)7u(2A$VjZ4eyX+}46A@d@rw zdb3u03UTJUoI=4|C#FdD0%_?V5#l;T5Kkh%P4kTFP_C@Pe4d>)`}NF-e{dqrG_)AU zK&Cbzy<<)GIZ~4c(@Z8eZ2Z$`;x4}vz@-_W)SL{i0S_#@b0tu%Iy1(ZfgsR04yboJ zp8KH}%;Ob(M9wVtR$`kVgnb%FNU|3LEne$ftrdwD(99Js;_;5B>In`UTGPOXy0s|D zEJR}u5$R*Qe%dv?)rQVG42yC6D$|F>1Ey@fH7cTrwAvhShnXfT#)zvNePkMws2r33 z2kkYOKEa*BeTLmzTP6v6yU=jR<)$baR%j}4q}=03T;;IO^=@)>w;%o<`#In+Y!e4s zH-Y1TqH~i5j0zvfrwrh|Lk+dq!*8eJgAYIa)GHyO^Hgo<{{th-QX+veel8$%oq4-L zuh4NU{eL4No=QD}9G6H;U5Pb7w|InHafzka7Q$-X*1r%=M6k^&RGh9<&erQ+61Jos z)~~|@%L|a|_`(* zj&GzJFbMz=OQ6KUltqk{mSph!sSG)_buCT7!Zhd(q-kDz4}=fM;5mP}7uwA^O!?{( z=&Z6crgag5;=k9xN{C8r#w8RF>WPnVQG|{!6|bjt*AY``izBZQB67pAWt7z{3kWeA z_L~@yW?M^@;=`QVaHfNqU{uyP%w#g|J>+m4QMo!3O!-C|ndD-6yo+iA*+S=F0+JdM z%f0`O4DaZhZH3g9@B&Y2Dtf_G14XF(<$y5EHp~W8VMFCM0RpcBr$n*5Mb zg4dHN6f2mPM?T!w_1evB0E0irL>tTf@tKZ3IqkL4*xbC(Zap8`F|M~W)RqQ~w{oHX ztqoYLm-IzyQ_2>mry7*XfjE(#yjoIfr5K4hej}tIt6uP5Q52SofRKfS6w+|+Y=D=W zFd1C8_A}Sh=0k2ly!n1rQy;e@o(+kdFMJ6(0Y6uKFeM8ru=I%!a!hqL8~Se-8HW*F#`evEoo zw9bqIRv(g-va4Hz{8kN2wV`+4df;w&=4ueqy+)V+Ck8*fSDFS1I$hUP<6;GFr*7J zBH96UjK#+y9ApFT25ze?7t1xpS(UMjGFBZ_T0Jl~us@D^jXEI8$g$YaP2272U|kf z!+!eznKRp$M^)?#d6obAc*fAigk>E4bo|}1-t!|RD^tAyh&iJFXrxlDuxdzeO)#L2 zZ`Zc?MhxX9euoK0?Og-%6!x_B@;_m6B|DC#4S^e=QUejohi80FI#o-%ZT7+)C|%nw zy8m`#V`p=7cW39GflrvgU(AWJ@@mGvEc|)l_wu#uj^a`O&`L~7VP#Bwu#t~%IMm>G zso)&6GvE!dmfI)@D5Z5L#7Q(mu`ko*Vyg@qUFYKSu8*N5SxLhK$67*33ud9IQo`B> zUd$cT%a`|UTQ3#IP&?-ij=;%KA6X6g+MQ{>%^}$0f%}`3yY&T*OxYG7`|ZUHC9^&B zJH(AN?`-atUAXX*=lCjTSeAG~2`%H8#L<)%Zc>@2vOfTqe`Fwp+tN^ZRB!gh zWZQ&)ok2pJ$h)z%#Vn*qfMf^SPOw#D_Ea5&p0K;`3L)WEHD~Vot5GD|u@ES$IO_d|c(M4Y@xf4g_~XyRq9ff^R(DON>;>uH zP&A2dG70^%t4q%VM$R=ru;sHtM~<8-D6;#DHpI>@FdCk?I=Z8~ct z#8FoAF%C%kD~PM0@-UhT3_}k`8XD^@#-4UIF>*0O#icewvech~wNnE4u@`FW5r*2f zT5d=$1J}|U9K*JcY}ZFi6=B!w5HRgvOy_lLU(-I=R}UiovA#9u3s=Wp*ko0dXE^zHL>g(Wkhh6&fqV#L z&5veorhgj=ARZvxwV50JoVNqEnA5|K@7A;R6~-JtZD8%7Q@v?F*=StJDLV!qph{%y zYxOe^t9n!g$<>WMXw9q|-sjb~TD6N>h0!1LKnW}A>JMDKI z*9UVAZZ%Cc2+pe+zcSh@Gsk?Kt39}RD+hHThu%aw%$lr?`~=56Vj$MWd6RU)jBNI0 zUrhZY^V)4qBDG|BQM3dNY4WSNlss zqfKnvQR|CI@;*kfMOZ)y(_Cc8~fFfBG!HqpH{7o~t>|Oc+w3hUr*Fp_5&@3zP9jd0NI}>z$ zYDh9_2F`ZNlu&66GAOGF6-s%2Q%v{5?eXjhJuKuT0!`gAiqr-JKZQ1a|@%?;bI z<^D>ZANm%+PuJIdkWNp%Q|gVzzZkj7LN>XB$Ysfydx}rRA)04GM!h9$0iJBO9!*hQ zY5f&x{q;Aac$%3;bv2(vb)c?tcVeiN@5*{MPJj(9-SDy*WpAe4mE4SUc5U>Wls{KGe01P32n^4-SYh=2j^*u~LF_dfiVMcP+l_J$twJ5ZJMVxdx zM*631-y_YE>N5Z=prx)jAAeP$Hi14t)K`>Y=(yqZe~MpHh7RzVFfYGm?UNtYXI-t% zp4al(_97oB95QX{c!&ge=DOaqhT-SuMGt_Y@BLF?O6 zRh1N*nG@Q^I-SF{!v*B2K*p|#o7)?A9x>E5&X$l!m)iNoE3 z)=qR(pOof$Y9r4}-dng+czfYJhKV-a1--T7QWQA{SoR-AOk=!s*BP8^X6jdNBuC>- z*GF4U_4-9-D%1iPDP9smy;Le~g2;`SoD))#Q-}lOF_1XcsX=CkZEayX=&@51$pM!GKiPL$coD#*<2Kz9jVz!xQDsXa+~(0FuQNZ zXf|?VJy#QDUUNAVjWAK0P&_9v8lJTn=I2ej5qWkqZ#4H!_leO#QXKS0M}sU=_t&{a zoJv#JUru@#I)k>+f3O`|aVyNFY;ddd;Qq9_dG S&vVgA3mzVJ7i;d^|firQX1! zNkDdA35?T&lY^F4lJzol7#a$Io}n! z!f&{Sx?b7ze(}y>Dm8+{GbNXZn?Do&Z>8s_mMOxlla{vWx6{Qn!|{^kc6W3|lAzU29!!GJC`t*(!?QI%ht+E8a`qnE zG2I-cZYrL+$@Xm5n8N-C-iSneJ4kl2I2ewK<4$HS?hL)96XVc}no{D3#OT`IuAP1E zjnAyFuU_)wrqfHyyzs*;ZZrbByEb}z+gyyYhVuhS;ANTV=jwRwmFfg1uR2@tpG{71 zI-LkdeMcQm9cqU6W@*l|L8DG(iZl$8&N_);u(<%h>b+gWQk z_rEiX-FEqsx|hMeUs|H6^&PD&uv$qglG3a1WT|8|iDDO(S88&D`*2}ZV&yAnnVy{v zfPPcG(zgO|tgb+@CtJs6&Mr}B(c@8JGZ#7hux#dLTHNQ3yMfj2*-dk_BUXE+T2S$v z(efkrN0T_UrP1PhBF&7h=q66Hy=knqo6Ts+=qGOaK%^EDKPN;&SHO z%Tcj)PU20px3hJaHj2GXH8tud*&tV^a>hoIuIzWMz%!f1(rP<1Tf^9LB5A+`ap-hR zHUd<$a<1+E-O%-aJh3-*(1tE&JsoV{cto`cgb4o`PgQ%opRb*J?|JDhcz_Y#4!mVL#8R^;CbZ?>==FPGAR)zTto!qkhKr zs81?lav-MKMsn7xnu+RDeKOr=SMxASgD5V#8=*DmuACF=!_*5!xoDQTl!Ggxv^Txf z4x>hJuJ6X*<3v(7N}V7mrNNh~nd{OhN1oadaM6p7OFwjjzju6mWvx}NTbu2!Wg1@4 z8-$H?vA4B(v17JQU+Q=(sq3sLk8x)7j@|euYZqoK4I4oaBvCtmxNUcyZ^dSD&{Hd( zB%8?xkzSece^n(-y?WF>I@=&i2MWN5Yo@p{oi`ZtUEfP@ zMxyMCDBTn7#x-^8eU#tdkFpokO#LAA%iQRA&h51Ppx^4csgoDMMmudT?v_z}WSpo= zPL{UBmCb81{covzZ?nex-0nCmt+d%WjKZ~*(6fhbsqjKc zrVTR));7XmD=|A^+O!s>&e>fz-D)4PqQ^fx^ADVpH2Fl0Z-8Jivze`eiM~x@dho;0 zm>!x!i{C!D`TRKC49CCnUnRw3lZ7karA|^^-uC=EcGwz6{`JynTCb<&4L{jf6pI(K z(@Wy1mDQ0Q?>WBvnEQ{0!_~FwZ!0c`>Nd*A_8lu9r_Rv)n5WJIYp(|lTXwybyHT$r ztqiLXcAbVJ2`G`hn^?eqbhbYCoV3GkF8pT;-@5Smh3{gz-OP&Y4F(NcI}1x73K=yi zKrbbOt6q-J?$bGiWj<%_0=$5@g601x!oM3z>ughTxZiN2hc=zyo3`G)70Ca-Gj)d} zNiI_NNIIf3auUb+n z8ukO~3i$^gyyfO`^mDOmSxqbID2v8*$%&x7)u&lmo(-k znN&<{qgnC#P|~NBWR@94p41!Fl^T*Nmy5v2*5#}1CF(lW(1_zQiZ=bw539Xfe_riG zh@}0pfA<>0>Ta2h>t>rBU^3ZM`(+Worf%SUmmfdZ^@LsSY#O7Dmb(-k*iEBp*-hWM zok=eH58jo!pZ%medPWjroQL-CczYB#!r|7nj|TBdr;rVAU5(SwuKXjD+Y;9oju&oK zXU-xEC}z*R25G<#*gtFf#3NC4;Fz}EUv4>S<9FdF=Z$6GiwmRIH@hvNmhGHZX3@Ey z7115FQ2Mr;+~2Z=Q%YiJt>OR?WKK6~y@WExriwfV~JYMg+4R^44`6I`jIJEA#qcG|;!)jJ!LKEsVt?|M^HH)r! zqCO4pblkGml|2Dd#A-VI?!6ewE;QOJbOOQjyn*rMEJ>~Kf{{wRc`o=>wMEl2n%`77 zrCUTNQb$ewFRK0ep7OAN!)n`Y^v;hqw!(2Lg-3AcHAGV?(c$j|MLxCRqD_So4j>__jv8d|%nnasDi_gK8)2cEs=$Gs$PL<`k^RW&#K zRmlskEqq|%Bhq&G#KN~Md`_MFq_2%Qn;+3fElCn5fQNK%cx)MUTkxINOb2 zOc+2lodnweCP&i!YhgN}=!!UQh)vO2G+VNp?z2vGRa9$a`%z#>j-I=AS0Y5_C%c0n zy(GT9aIJ|FA%!iML$al@6$mGy!o8uP@2J_2uPfh-zCUbowJD!BHV#85EN*iQb z?Id_UJQAT3KKtuY@teNW{bW>p)^okjg!$)uH+io*);)PW5%KGP9Ohv^-iivdC&N%` zfQjpc)_q5bf?6KPJjA3N#axsaH>(`S#;qF5=P^h5iJE%P{uEca=BhSL92 z-GyUSJFaMj2v)pN9T|>Tzg{_Cx73whp|iUstKKlrHKnbWIR0n4?oO0Hb{J>h?Yqu> z|HKTV*k8IT>wRf#?t5{=d)a)C?>rEfAB_r$`G4zR0ruVvrHnp3I|+M+_F(zB_B^$< zD{5*8{(WiU9ZS{W$(6KaW^JS6IQE^WxUwE+e`;i@5jfG7nU}T{2)6HTSgHHuLtW?6 zzUkh-*fP7<;;

nRS0ueB5-y*zf5+@rWbTq95Ww|HjE2iQ<`@NE9Ww^yEdp69Z4DKRNCTKtNvb6^XJrC z2Kw@bm|P;%nx440F|&7AJ5ko;K$wAjDO_rXjYxIQN!*S~kxJ}Ov|EXk!X`}xw@tT`7t(U63*w3zx&L3rF;x%k>y0?9`-55N0 zTkUA`hF||D+f3u&O-);)$@5`RW_OH4@BeuR`+Hu?*{Pd!k^?YeOk=d)?IQM2qKRb9H%rPwWm`Vbjf{UJ-7Hp=5j4+p?dB zQumTJ?UlVKeN23tnZ%8txxVJa?vfd0&A2H%$N#d_3zwqIkjAT94m>mIxBI5qDvH+s zusk%~71M2do)ZVzN|>+Z|56Th;`EOs(!R7}w0c3@lwF^kaQknoTddY9+vQfR9#1Ow z`m~dk$ekU+(=%HApG5SqTE(|mZAI+%#O6ge^tX0`^r4X#zWd>yHM)*O!1$aI^&Q_7 zYFXEg7kM|XCRUbMz2lB8*~p<`G@I&9W&1eD_Px-&e*N=i9EWEA7YAn8_oTpmrMgSD zTFLX5)lPIpS=EmF$&I*oW7iDt!{wmdO>tf5?u`w<6ro!VO~16FrlPWQPV9WLC8g#d zE*>04+4XZt_J}&IRDQc%%76CP`&O)E;0(NB+zWXX-2-g}jyV7(`5 zcEh;bjEbGO+-=$CmX&+R-IUa}WXti2f?>6DM4b%syLykN+IRe@UIBbKv&XFJ-`T&? z2Ip5-=_P4Mh!u3cLJYWL$~cKq9B z>dXE#EZdIUv9v>%%p|ohpPqhOkS$AeQ`>FaZNphMSGTuc*lk&fWWJY6Nn+n+_RK4xl-~qn+?PMRmUp5WT5tJ4otU*pMGj{ zE#EBEgx*SIh2iCm4aI+Qea@&l8|$U&JRnOQb)nA;I|BRv><7I76?&`#k|=&X*(P%4 zTRNSb>MZTAL$wn8XB^9GB$+W-37X>Y&ZAombLXPCIyRe~OX6y>6NxA{+PjXl4I^>$#b)G>gUxQz==V2U*Q2m$oVU!_Y~HxvG<$1hGq%;jw6=XAY_!k& zjm^Mpuh~{FHy0((>kV92;zkk~o|g(Qm7=GNnpV@yVlTJSxI45=!;jZK(D(hOHA>T-3m&fZ8mQl?(Z>=$2t&sLt;+t;>& zM!tEH1rIdrFin@Mar{@+9T-uyru4qr(*`CMuHKgRR(sr6?353WQEmeIMR~ueC^*X{MFgGO-=zgkQTk_`SHER56=M@+j_$A zNNrv_om%U(7I^mhR(7O_wg|YE`bJG-G zXt+@-GS|E;VsS3LAr8_FJ7`))xIR=zHb|scm0?X?w-jw$mWf-VROV?eh0dkpeQAvk z+s%fvIO;dNqRiu;FXdm~b+gRz7Q?9NJEmnvQEV0QC~2fUCor?nDNQ$yea8>eKpl~1 zWv=aKZnR)X3r0z2wdU!3t^PZij#2BnR;E|eN0X}q%g5jU38^H#Ub%Dk4wL-Z3)c!U zytz~6LnG^%?WWxr^mcyh#*HVF8`2sKoTFAVybu*v9XWQP)f;TL%;I`a8jiup`G`Ha zE$3fX+;mUPT+oT-$K&Zjj_EZF=%ugPdkTj(YA?)Wq@}{EhY_@llhd1Gqu~!bU8#}# zwp3kLnvJ4qHF`mEF;ka*hJLcSBX-4`p1l-iH~ph1d&WO>vasWK8qIXnmTYBk;Dimg zVX8AXJmtXKvZcW^jI7MEww>7QOVLrbJu^y!>`LDD>~4^4N@aJa{1^5x%(udP#m&r7 z;zw!TiA>8obR$tj)m0CJ*2Eg7op|eN=jACL$f71^3_N-9jQ>n`bKC_cdlsrH^{@4} zjEkck$$jp4Qbu_FUeoHl`1UY8+6=Q#$mnaa<3H|7(QY-zl3$g5McjV8z9BXh9-BdY zuIEZM!m(uH&yPxT?XuL860;w=QIsxkhWQb^x3>AY7=@bkiIkRfM2^; z51xlhI}g$eSJmM>Vn5EGQXAuPCobZF+b|5fy&8_zjm=KfctdT+uiUq96sh5Q*^fuN z%eL=i5V`jG5Tnm%?%Owdt<@Z$>6^q^GR~@_X)81y^vzOB_-(FoBhxXFqAlVW9 zUThEAtGPK?9bc;0du}*>?6$IB5_aDT$EGVaTeIN>*=iiSb`)e?%k;aEHS%4%(z*X_ zrIYSf`t~iUoxMW0L`=v2hPVePZWuEcz)a6yQd{-Fq&%LUztsNthV0mS(@WNRPUxme z`5Jqoa2nRoi!Ww92}{?Fme$ur=|#@LyMy!<$M2?Y(UxZL;V=l?z>A`&yBRi@%jMJK zLFzUA)bX3em1v`IHae233<=Z7J}})i-?u!epIyoe&u+Q#ruTiOmn8jxx;s5K+ld)1 z+Hsz=VsCG|HF8bK0*7P2k*zH5ij8i(+zBUpg#NOkuHA*J(q?{6YH;tW*4_Y8lxJ`| z&~V#laI4W(Cp(DLK8s`8TB45nnl2PNJ-#6kLfZ8!n~H>1Y|q`24fYDfhhe%4P`{UG`1RrU8;Pa;9rOE&#Rd?B$myP=)?N(>*F z?y9fO)AQ3yqimchS3x(E_;R?s{L_n)1DDN)YeaZ8iAbNxb_-{CAvQN8Xrh5sFk&;2X21<0a;p@DEef@u@kgW-ZF2h>E_L>j8&Cgx?^~4I*0<>3pk!QEvcuNZA3z@amQ`c-ackRR&bWFEX_@Q&JqP7Qz#z}QgY{Q!IQ~Yg1v~3{{g6s*yD%S(4 zIHviDal=xFQEy0dN!tB^vvl*gmzjH4)>~HYg=wMAhFbSy?|Tk2-|cq7JkPvAUMw#A zdtGz=j_KZ1XDYi(Kk5~RFdB_O7;(=m(=d#ml&BJ0)oj`SQPGXHFp_fV)|n^p?tR{q zbselH<(|1R+m+XhG8KV;RkWQ}m>s*GHRuQTOKEc6$Py8FrN4Ods8j4-S?S2}kFwIK zdJ#FFGow5TR=0Pgf+OY3hy0+SHkdi@tLDPu*jxIN7d6bPAEw`GMNt?(RXO7)-lP8@ z`|%aok5!4|cc$lRTsNlc?p7?eN_Vy|D|_Z_U*7Xvl)Y?A2xmn@sq~+T^OrJbl)WHP zwQQT7Wgd;CEG!};v?QFbop#IJD{C#U-HeiMH9B_c{sSp6KW(&Dq%IJb;da)DQzLBW zPHHzD>z52Kiv7V4c$w;$~knZI=Pq?3!C%d1_xZAFRF zqudQYa>L!*+e35nPFNk=*swab^-TIMGi+ymxAR`ZNn&sGLqTH54mCp4@0fi*4ALjm zP!Yx{P1TZAv!zaUg4u~`aasflz~r))mA4$JFBmPa;aS%|9cSBa+G{l}zw0h-8|RBo z*I3Px=6#NT-N=*=tZ25nnk0f8*G%pzIhFaFSY2>k*67fe89oxpj zL}A*Z;rrVc<)7*H`++?k^n7O=WEX?%a%2}*&UekhIn(`s^7*;LFS)5{m<`J_yuL`v zAWEK|kei}5#rf13Y3e*b@&nSj7#4VE@AlzJ);JkLgK-YM9`<8TURdhyT@FKkknKn5 z6HAt~Qllr;T>8?v$af!k#$9^;xY)YB+VNVAIO(Xllnt3^Y@|u(K6h~NPIZ8N=oY52 z8x`x){N66CBxsfgiL)+y=az;tTa2(PhdzuRsV0G*mDCwUOBGlDcNJGZkyiKfbQ215 z5m={(t+PW4*H5QIcs8}Ad&wES#^aAlWvaO>hp-=}m$t&}qKFcyk2XxVnU8YOF)tcB zW=pB@V>gmE$pzWvlXCC!YR8o*o3%qr@s>ntlu2L&%`XQH&l{(nqi$~x^Xn2;X(!7J z`J3H$yJhyTn(jG?{H?7Y@iWtsd`3tX)abZj;yqDy|2g5yA@5BzslU=VJXuMke4#Jb z++3+|!9E+^^@({l>FB!Mns|g;dWJh1 z@DHecF7bWI7tAaapxIA?YZ5$nc{uTKXX`H=2^G3oB4l%yHm7J(-bHQ26)NT0?1%Ri_3W zEnHFeCy?C2ZN+yfy@%6XtCM3=I36%s_>d7x^~o018TZHLSa#_7)GF*WSP!!&!oBKp zl}8jad{K%s5lZ`Xbo=4NIDdV)Yj?~rt@J6$=e^%IZ7G|2H>4p@?bW(p;{Py@%p?i( zj@q6+=va|&jjxOxsVc^az4-UM!nPWw1VK0uT`Tg!cWC{}s&=T;n!6pz6Vxu1g|@z` zJ+5X8^pTP4YT@ZdH7_*LMQCNn)X-YF>#7+#J?1AxwChUQlO;*iY!|8H&}1o^ufXFLE@Tm>PYMIbS(*+VM{q)Ki$31RS0pI+U@dZBn7A?y4AXX$tv9T!ea|h zRaWKM{w39CJp(f7F;V_>@Nve7ctv%VFznM&lK~2I#_FG+l7`TZC)KCrMjSO;i=mgd zc3zhTkR!*-%9=^hk*1`VpK~JDh)*LCwnPz&APX)?I9yfdzhAP|4VCAe)84^n&P%Jb zd1Jg$T9MVX;?3Ue-~FwLg&74=ELwd(Rg9o{t8|Le9^7`^hVM7rW)fA`lUi#<`i}n6 z`s!-M3I9z638%UhpkJLQDEm1+zHZpC%1rZsdtJw<&TROTIQ{5-2RBc?G_)f(xnWA{ z&waRb`?=Y>AW|`Y$hf(pCVoC3&7#Mn{I%5^k#n*4C%Nz&rtPigWwO+Aj3mpKdy%!C znyaVJw(RRW*T;=@Y4e0h;J8uoxZe=r@DaL2QO&8B)yb5vF1#^kPW|heQs-xiXtig( z+O&6e;n4Kx?{9pEWXpLv*v!mfcd#keTd6d@f~@p5ggRaMnmYDuPal4@8Kn1%SJfex zLu1WX^U?e5R)5=z{BGP#?IddX$$pZ$!F40?;>K^gQFEA;qG5Pm*$&OTY>iB#pLNQw zc9*N==2_x3^B`Lcvi0zdJFE8j8>@@v{-!z}SSskDlL~9U7lk5=?I6pnu-$vc@RPJ# ztwi`gtD6+fg_{ev7ak-%ukN0u^}BVx+PReJ21s>K=E1j0(%n}V!n|_S-hU#FgZ|R3 zIDc)wIx+L$B+Q<9Bq`J}tsg#AhaB(AzwqFtet);K({|d@CYPF?DS4nU)iSZR6k1zd zJLre`P@OP5lv=KJr}76Z^?GV&uvh8le{`nXsQU!3E<94Lv0(bm>AiY!HkDm>n68At zB*X3F@bKd@gd#dTk=pZPar#eWH$HeI|9R$EtxNcr6@SN@Z@wuLys23Fhpgnwe;B1- zb^Lc%(*cG2>qsHxNG8=$XGTaC-m;|h${ojlXPUONtXvB$&$W_$&vt#~o7Jaisyz_u zc7n$%PtRQ%EG{*WO6keztSd|}R699ydf5`6_2hxXd{{!J6<)^lCYAh>=k-L}NrQB+ z6-qND5!JzWeLHk*U%AH|qm+8^Mv#18A#IHyF9O$h2XgR({Ts2kHqOfC;<7PrhE1uj z`tG=}ZKD+zmeY`=a_qVrK`aGiyzyW32A$YbI|es`Y$=yIrs-}tQ8SSwFo=VuqPBLL zrGwUQgtoM--Ozeeo%7T1arn82OD4eU#b(K zt?Gt~m4$$ZYeky4@t$Pl z@u^a8PVB__naKJHd;1gLZW!)QS2OfK)oW(rV^J6Kq&_Jq$lcXxWXxx$CLS3_+)t0=NM^Xf?cy|*n5#>i9`2zT7iEIH~7 z&NsbCa#yEaF5MU@nR#pEB>ww1Hg>nRJ{_m!DF4kwHt`qY!1_|tw11&m%=NRO+Hj?O zX1!{ivRbdJZV8<32A&xH)l##GJUyK(Je#iHHO`ju1B&+4jcs1!+O0GdN!Cb2KWwzh z&0lq!o*Y(@wVKXxpLLyP)HMHL zs``JWJ6v=l>$d%;-){S>OA@OF`W_&4=9W4wR9S%wr~0O&!|58|*;63t@D+QiBvpIi z)KUQ1ucO~yNy8usAA8YHUU2-uXM^-XCpG$mK$7vUyErn+ZsXmOFRscy4Wx?im1dC^ zYu)(e+1^py86z0b${&X z!i~yTao0v+`wa2e)dZ8xm3nKQb{s-Kg<11Eg=$M8lNv*Y$ znCa=@Pa49xuTna_WG%=Z@cm|FOA)Z?oAG+XOo}jR_uh8+Jr%KD?6k|&3hjm+FZZtg zp7L3AD(mE*sT1!P_A8nGUZvydoev5k+Smkfb^>F1w4rZ;DfQ6KXm}^bZF1|=R{SG8 z(BV0=)rcEkOhj*EP-adX<%aK;R_tWst^Xz(O8n^9c{6Bbk?5zRR?>Xt;D+!bqa7-5 z-mX*F&Q6d#EXCS+E3u72nmq>xzi4DhJp5s0jv0O#C%*4LY`TtbJ&;)9chr4nm)%ak z)9~&4?cT4PT>I9hyEg9iK9#>3dPYwQz>e!Rq~a&R?)ug{GfQ2iXb%sbQku}@9O%ic z&{&^j@eDg{TkmUP3u8*X+MkTxyU&Im^8*L(T)HFjzR_BB3pf57LHgWzKY4i9PaeD& zq(5>Y4@1x2yL9PNQ?#5Xg7iK4g{9c+ZDp1uo$`21tRA@6l_}6}?`{x~rS-zTmc*q7sGs7CYjrhVs@{m`R!c_VZ#TBbD^dawQh*;KYx7GBdNmyvU1UqMwg727X{`-!X5-zWcjL_Sx7AocGv?E#e#R^Xk^t z*7=>Ccee}W19Qw&Cb3_&CpFmqhQ zWGa2Kr=vZYOV9tyq!^2&z3lk+qE}(=?0(L zXD1>|nt6NExLz26KU7|#`@&@Ff|R!HMrDV7G)iw;!M@sq6FqV2r0c83`JQbgTWY;T zaWQQ4!)9T4A61Ib=X>`Loq=DS2(jpe*0nTqKKS~>&dIrfXO^qS8_%|q#ZEhwR#u>I z%v1An4^}sCzP#|t!s`nkT=?w5m+m_Ajh%rp(U&HE#rEml!vI}AyC#y3uOp#piQ3um+pUk{hTwmbNHtiAiQ*r9Ua6E6S2f8bTdc z|AMqj8s6B6FUu6Iu^1HLQqo8lKYZR0pL zpIy%|y}%)!976U}g)`WW8|&#{%jm4EzAZ>39ff9YyRK1~y+&eq7fU5FF3B0%W z{@d$L?0U8oxKinN#XtM}y34 z8^d-YRyU00K`>YjBXey>8QM?1DUtd)sbOF1`Gzdb5{t)9(72TO*3}oy?=-wd>PXn6 zdd;xXlE>9a&+7ahz2=|%o|CIDVOhnTBeD7YqB80Ykp#kXUi(Ur{^QX9VmJ5+g&;pJ zH7Y62hwl?2dAd4;?YZY<^i}okvKu~e^{b}k241U_{rh=SIFG+#53dZB>h!oA$R#O=ZEXg-Wow` z{_5O+k^(fvgaQ!1uj*ZR4_1%%^ zc)cLKkZp$cvS)}tapSZbbtPke^~TWH{Md!Jmrnc1b8TZ+%APgHvj=`!+Ci)LO7#5p zz}&uQx{DI!dqH-qG=;Zu^+UPx3Z=5I|GwJU;4UN!?S(}-mg=;sC$&a2apjR`v)V-4 zhI=^Q!c%lMn3Fu;dHqv)lvvR_3rE^!#rq_=2hHWePEGH9c{_27j)<=xg!xmqlGblY zNW5{Z?j*UQB+3`O_{Xp0S(sY>Ke~0x3vy{}HzKp^8{@yRn%RDui@X&rm8ReBEOoxP zSK96<%=^j}CMEJnGPVnj)DLdm`r)uFRG$zbi4iR@N)dEyO;`rl9NNIh^Z%&uBX5)xJ> z^*FJ&Vl1zy(@1Z2{m@PB-`#cGWJyi-Y;{AS&SMQ75f0j3d|~g=pb?7ZvTsW3c<{Dv zuMvb{qfE`vuon}F^wO@ByxjlxOOmf%5A)NTLBsGnH!t3KTWYqh-!gCcaoJBl8$@;bzy9G{pHsj8{cqg9`L+M_wSOsN|4hCHQb(I4 zQ2+kf*Zyo_RV;k%KUJ@j__M!fuboAF?kw!fkjdc6kjh}{5Xo>VgRj5&I?mDQR9qm# zq70!7v5t#%e4_KHa6yJc9Y^_99!~~ChK7!B%3#Z2>0cG!(dAk?jiCd^Il3Oo)6i+C zES2^^hFsS@)O{~?K1;Vd)@_V*Q2F~Z6uOSChK-~9XzPABx~v51696|==ZOqgbsIc6 z4jmcZF2hqYEX%Md!+T`d1Gps+G96TXflRlm;YGn`X$dX?e9z#WhiaT;hi!Z=;u}ad`rKpzFd^yM8B(Y&dIPP!@7>UPbi@`Df0;#UefS* zP=;q@_@E3A%Wy}Ahh%s}x9bU=M&-RXIrON z{aw@VmvnnibUl}J|F(7fwhn;~DsG_DsW!BA{nWNcbl+58-l5x|zCEDB9o?5}y1kF< z_(yg6$Mv^U-H+Qk&$@2&bzSCT8b&8NjT%cwhF4|yWf{gYd_sm58G15gdOQ_emSk9z zA=P86%24=F$gm-UuY-!qHN09fC|oXOc(?w2mkbv)JT^3(2fFMf9XHm`mvnec!)H&2 z59wdZw+PPzogd#%ZKU&`*LAFG*c4()TZM+(k}k8W(=F=!T^;&5 zzNf=woksO*S@&a0mvKoyKcM?^MZ@G&m!-zxM7MoShdo_~8jBs>W$Io3o_(Ee zTi3m=VSlLGtDZ{@%T--ptmAjY6j#Q&-=|{AOC$YV{ayuls=Nv(-Y3J$x?Kc6m3~u9 z@ouR5dsFv$SJ#v3cC~cafS~GrME7}5x9wPuH~rqPzbjevu&#^b!rOE_jn}?TOSq}V z>YN_qZQbU9ZpV(!cLd|FWcfonzJPDnblI8i^PcW+NB6I*%RbW21n-WnPvv{RhMUUw zb@5oz5p{cbSkb>rVoF~sEbDJ-9N(q;eM`f8MYrvX8urIJ{{ZqSKB0K|Gy3^q4QJx( zcl4M&E~b12=|TPa8t!V02!|=`=x;A+cqv@FD5f|?=`QNu>-zVcn9{o{V^_ETd&PwA z#=6al$Nh?K-~GBTiXN4$eNOjB!Q(XzBQ@TyXc*ko-yhQHUxq+&AaQ;nVC{K=^uek9ncgzyCr^aCxBnh~a`BXNB9}CMG=iV`8Ff9B4SHF;P0H zqSa7L_?UY3^_bXVB0oYMm+16M`dzg_>0ck$&kL-b_tLQ_|F(W6J(*+&!L6s`)bls# zzA8QABQkuWnBbWDReVC}t_nXD-n^vKk)5D$_$mEM?}{#-&_TiMw$6V^Om)UX-5%mE ziU(fTaQzkyL(=OMd=&lsq?qu&RXq-ho)pb2Xd9SpTU*5-3WRFwd z8~T^Nc>tRQXao5eLJf~4U5|=i5K}!a*0@OTp&qMLk6obgJ%;r3JSuC^Aa7Zx)twJG~dzXE8HE6sT~tSI|)x8&~@kf*Vko) zI?ji-EsLprO7LkxKd4NfrBQK;|JR&Vz7` zbsr-Q3+e-XqjVvZ6G49+4ew=LP6qis7%v*D0P3TDd%AoK2or*1EM~`r;t-~VnBoxf zR~O3hSRFouK=+Aoj@r-@Q~V@&xcc1@Q+)Oz-h=$a?8M=HM{V)gFSeg(WJ65x z1oQhfWqD9H<;Q1|BUnD>BU%e;0`I>sX8Wmvt?P)zEIctUZxg;dEFITX12GFvEQ9xd zUfVI=WpRin`0l_nrlG#O5OBJXSxi2m6F|2IB7aaGVWU znFQ-t%=QV(#PN^BY`a4EhJnWs>+qh_8>byUc03=}TbCcl+-LP;*;GHLr#gMW6|?e$=d0^4=UoW%uqKWN z&NKZvY3uqvFWzO(d|c}ISsmZv@R~>0KC7#)&4kCuyBsKsw~51)w*~uwuyf|*hH)my=i3rwyO0i^i>dzD5>wgN5wp58**j?WkROut zXVRw={S9?euK!}YIQ}bpU{k-N-IYQ59Mb1v)|T?2f0U2f;ESn_P4HDV75Vj*&A$fE z)Q1s_?UH^+-;!#>K-Zr^J5n*V5j=nc!iw~IvR^#NlfihA?G!?J1WT$jV88V2iK(w9 zg72u)Ct_BI;A2K@jG-?S7eT;!slC((!tGehjuE$^F(A&6Jf{99@J-oEmxIITY8s~S!&#AAmt{3@949_&))E~~rsN4{K36?3V z8)XjXTU-}c<+NEE8V~Z{b#;7COnC>5Rj9vHUW%)eZ|XOPdburvau8upHlPphjvnWj zl~25a@YaE{3EqS&Sgs4-2(P%!hj_sGF6UiHD=5z*F+2VdD>H?1@1^e&-Vr`y`!PS_ z6ZVgI0R6^dw!iAPt;dUKlJLIHHlVyE%b&4w5$Cy#L;bJLWP#J^8iyZ?5wxcoz_G zF0g9?~NTA1`=Hm(PXgxt9ruqQsW*wbZ;SuSwL<6Mzw#9V* z+1Gg$US?v7f5cBzTBZM$I;b=|IzP1`)$Jg?kYLr)Z7Ow|7L0eM+fB4gIs&!72V>vX zX$baI9@X2{X$z<$(BIqocUw&T^GFY<`xC#QK1Di@5B(?JM$d(Y8O62rZw%#9J20;b zeKExa;%_OuCy9@rIAUPyeE{Nn^S3N+V1V@SM|;Fan34r^;0 z;;8KzD;r@*b@4uV5T8I=8WWN!G&VGMkU<}pb-F}M^)XtLkwV$jUeXV9RtMn~!JvR| z)P^?XA-H0HsQowwgbxG@oEJjdgvO6-z8Lb8ev0-I(F4iD4CMsQ2vo$xmoQyQgjR3GsudZzwRJE@I{nDR0Y(zhWE8q!iMT4SYqCmnA(UWUs{lt@Q!GS@)13e-H$MjS)lKnXCUoS9|;!; zmx(WutR&i{@2@@_Np{NJgpOo|xq?B+F<#TVjg)$X{?AkuM_Ma(vDq zO(|w!kL{)Ta2yw$eLM|?ke?9#lU;{$aRBwBETsG-570&6J#nD>o0l<;)jPwn9 zM;{5r&ToosqOsxW(3U29o6l#|&2MlRqMglsquj@U{i*Y_Vm{smq+POMxz7#j zq~DmuA?$Fhv5(vzO8M(>4fF8w`561+?tXHAA(rXa)JeJv;ULK(^zCGjp5r~zBlkli zejxnkwGYeW_`qoaaTEPCoL_J{Mjs`o30^<{Hjf?==Q%$jJgb8}wg<;F67%6jFroD1 zdnFx}V1e*N-#^DYPDhAioL3RwLw**rIP{U#@gAHzL7be&qv##i1yX)oGe!Y@-@Lp! zI*V8u?hEDo3daJ+8`ll;e%IkPmPhuCUDJOL($IL59@b{x(MLsekU)CE5h{aVO8J#6 z&f%SS0QzuUsE6YA;Jmd_k2!_(#d_NoTH#VaoVlpcbs?gbZ$*L zsxM}F_xY4; zhcW6A90r`v)r}V)&pN#Vc@FFfG|We~j+|0AA>eW}xr>c+gTjq_*+aetnUQ#U5m z9zHK$SALx=r}h^RhjccNPf^IlZ2RldAs!QqdHI;m5wkp}u1{1KwtHTm>Vh*T4)YNH zaenEuxH=j|U9zrz8fTByk9-*4iO*sFfR%&m^7wdjS;E(wBA$~kjPpK@KY^I-E4G)* zelAn6EnH@)IK@ZQo{dd0)tzu`3FncQArHi{p*1_C*F~Zp4wMe{eze6y$b+<>)YMP@ zFXD;RPxSktAAsi2DL?w?X>4fz2yF$NTSZ?mu1`U~DEipRZ|y<3TQ zAUlbCl{mjeupz%X;RET<X?~$*S=$hJ%a|5)F zgZf5uW8_mMA1>i5>F&7Z4c8waUnbfh+QM;0y2JHv&1(HFEKW@(o5NyHJ{vqU(I z>ygn$K-#X8^ECHM{pEDdeThhu1bg%oA%7vA1ZfoYojRGs^>$tk)m0}8QP)}*Qy>4b z9tX6Q2w$lGh&wnxxFn|M&)|Hl8lwTEr~Q>7lt*PJP$sU2UKdlDtMbv=6xa@ILtaxh z%~wz!oGZv#{Uqa4D39V|sFVEoYHne%rd%pJ5VQTqa<`!UA;jZ;Ok7uie(AB8<-OE4 zf*r0`AfA;$`7JREE0ik*q`@@~*gjfAq~2Fqy*a!`tX_f}A17L`Mtq6-OXq@kViqqb zA8!|p9iQv=;Ty#-iP^T&vj=4$36SMS0bCNWl#c2RxNaeMmeD!@Kru0qoX9S}$l!>&1 zv_LWe>2(C}e7zsfkMuyai0~n}(L5ma8}TiGdQj&fJt2bfh;I0L7@9{$xzZC;+UScZ zUUB~G>2cfC?9o4lEZ{k zq`To7Ak+&ZD3jX&5(K$Qli>7i3moW~X z(T2daKBO;@Z9*~v^^iK)k?um@P$vtaTuO&NP}FlmNLT0kr!pgW<~AG7+n~ORx)Pt? z;5Ih)F@m@N;xHgwTvi6IJ*l%xu`kGTe2A}W6R(%*4j^A5X4{VQmjo;H;S-La-<;3$ zdF)$I^Gx{(fAKr@U-355sYrJq-i+&Ysc#9qqYg~6D26%-_fdBsnM`G2`6$EbJJA&4 zu>*CWK2CNGy`$gVh4-GA(o_%1AYMUs9NG@letI84TQQ#3$?@A`<#xnuTRq5+_=oLx zYw8Zf-F@cv5&ohdvo4O?nq*&M8thjHc{ndYdo6^3wh_udDl3F%gb$`CoJG1LTnZo# z^+1wO0;<#s7uf1jL{!6Po`5|>O%y1 zus&`dkbI$f2){^|Lp-&ie$tT$=etlRpA#flA`KxP;Q4FB+bIrp2J(?{J%MZ%f&urX zV?2!)l})rvWm{sZFQR{+@D0x^AzGrgBHk0ca8D?`=Q_)$GR}wMEU?sTl-8L=Y=B z34Xr6=l6S__jBL>yx-3~_niCObMCqKyk3`a!;<*g_#-IJNuhX1OA|WiEcM4G9%=Bb zrM;@Eiv<;77&#olSQrmiyK>NYIvws~NvO#MAh5}wBAY3W)5#&wX=|gG5U(xFww9ELUyb2<>R(dmO!>y|3<-(7F_1k{$I_4J0MXck*b2 zYD6LVxi7pyYbUmQ4)=09EX{>>7$i_pb)RwRA^L+boW4ESJ~V_L;(Ib~_)Dr*F%o`a zU8l=X!bj|WrVxX5oYkTci#EZ;IM92seuVK>*Vg7xd0`{)ol=_q-}l~h@PCo-=L6U| z=>BYRe14`SDG`lR5*leQOvC^FT=G-A2OST}x_%(O!T!f9dkcABolorw&$|sKEaR#p z+HANd!|)Sp@L~hK=J<2STxKe|tfXCGu#=F*1oO3&3Ys%x=ks5u+coBXW5yiQ1j>rT z4ZRvT(fHf{%=Ai|73PF{Qbp;%5ZXBQnb3#oYKFi#Fwfb8(mMoespvefhF(z2LOZhF z^%8bdOUc|Ej5X%eH7eJZ;2uOjp%tr}9RdIome(7l1uIq8^UIDrG zurvL0rJQBc*m{FR3EyaY2GKUbg?c5-T`%6tjkM+@fL%l z!#JT99xH852I^4R6Oe-HczYoOiYJBLmg-Lt1H+y}@Fh=9y-FM$IO@z(TD+Zxvcf8w z{~YcL_@j1N*)J| zU3qm!6Qaaz`X4pn>2J$fWZZ$DwIN)ixtn!NkzMy~fDel$%10&EHg-qjE9V@lny`VJyalIhYn({*Wt(z<}5966*c1o#dz%Ph*-7n zOIgHPw#OIt@|xT~0uA0tE6F=5ku2Gco79Wot5gB&4H{Po+-nrI(!>=}VIF)Qsq>G~ zwe1`08&n(aJRw4bd(`h4%EJnLW(80fp6KbrJHw+s*@9o~#gNQ3?hj@JIsVz7y*1%k zPaoFmpO!b13B$VcN;3J>N3W~RK7e+payMj$>_obUEuG9f`*Udj<}F44MNxxJ*ksPX z8Q}w{#oLv2M^&Nnn}1uqdpn7rS^GXMmk0~3wiufsBUJ7lJWr|-#@&!p(A9ETk>-Oy zvJk=Nbo)RLo*3fS;~`s;aVlyToDR?h7Vo9&Eo}amC^ajg#u4^p-ponJK3mlvs8&V{>oV(p!&$bAClQ6(L>xmrXC@OB)rgO08K0>Z?2PFoaQ+@YS+ zZalG|An^)DWUG$+a-hXB@CLh%Ps_HJj6&QV^aC`I8cUpPA0OdC8X)Fz77c$1oy7H? zK>nO1_Z64_Y80j1IlDmRohYqYdjGhA%(n5xs|0ileB6843aZUqut-t(yzQmXPDbI` z`}8emRVDU5a}?peCZj;th#n>ZUeL7dqq0s*fMBN3o*aLmV(Ht#%N$jM8BR`oi7KVr zmStCt6=v`0&i#V>neUPeI%xe<7l@M_-tv6_;7q&A!XPA}{v@t*l&$7cvmzB-AlUZy z5;QTj{X__zzP|rBDcDQt2Wp+LH)t%FI>v&uWhxqi1j=_U`k$bE7jPfsb-PYBOb_7e zI9vW~G-*i*Xk+ewGA-0Fxk@C7PbTX^MpDBlXd67F)OlZg@(&e`+Ck>jf;hQ<-*p$% z)xuOlOW1W3XYLLYqo0frV+#e{PU7%jGHu6OL}Vf3*E!fdwW9g?{*OWo_I?l%XXfTkPl09rMLVE=c=oNpiJ z9Kt5rzg|Ki1_U1}$H^|Soh~a_yi?kLw{ZG_eKg#v|?BVz(MVp9t_R zxpV*wFvP3tFOB8ug7@rn2CRnmROUCTcH<4rLGts~jarP?*t-ONfm{XL6%tk1+ht+} zkS6egm**SLv+@VP_i}l^?p}e271`+6EEC^tr=)e8W?m4W=>s3QbwlY7FV|JM2S~SS zQT4A}yC3$aL0MdQar8u0TS{7i_|k8MI;mP_jF!6^q6bg2ewNAL*uq4kWuz??&BeB# zm^~tCPgH4urF{CNezr`zD0j2q`HvpAy?Mg4C2vX1@OgU`9zyKL`9Bfw<*HO_{ow(b zR1tXuW}OZ#XD3Ii2o=8~ z)a){8yciQsh44&CxYF_SEqXn;SW>!*rh6yY+*DVZGv|jIgZV^S{5A|a@ z?BY`#QfSiokMBsT=#=YE@MO9NR&j-&($(5r>z2Bnr@B7wEf#(H@+c|cHUocl&kcgG zb8+g138H@53O{XqID6|G-{R|Qw*UM-Uz*M2nafYb+(%N;7zKbt40C@_$MZ}dQm%7S z;;Q}8bOoKQwI4tiU*n+wII%WF-&fK&yDUC=sY<^Xa>65JUSc66dDAc{by!o~*s3{! z=3UQV#@KV7Yqo&8Xv(h_dD*VC$&pX~`c+CMv6JFHh1$Gz;rCUFH|kzQK-l}AXSQQm zvY8kZwzS-e{q#ysDZL5~Tff zm#x-6@yB+rQM^S0(syH;eXRA0mlAznOgx^TL`Ad3+#4}DVTvFglO?dGV{W$$>BwqswK0narxkngGg(*Fz-vuBYc ze-Cam;L_X#@-`{%RBS7pZ&LA+u8K2^?D1_|UwG;G5xkBtYQ{bukA2jUH2~ehS zELSe>WB(`vn)6! zO!{B>->=6J-VggIN}PrJ5qqH2%pSs zUu@&s65Km4=pa)KfXUf_=PmJA<|xKW}MOUOFjpdG4Pt>5N(!G_IkBkx96s&=G!CTxUPNf zn!D=-gdip=fVy}h4@9NWsZxkL#{AbS9H3-^~xT9h;8-6(Yk^39t;*FDA@5)D&9#p>~`qK9ADnWS1mw)F8v=t6QT2jX3 zMy}5Qyp(7NFU{)i^MFosDIjf#mlCIHo(sxrQt4bt7kf!cAZd3-vx;2W(>8;7@~d{R zS-~rDasbJ(vR4MFDHzUKc&JUd%Z}6T-Y#)}xvAC|**c@_#;s@uv2eEk+yA@MB&PH9 zj{UJu$H9&4NUZQjpk(2ZpK$i8rn@9yH`k;Wc@;`C!;Bq)_*pkGW(}*}=jDhNVay}z zj2|J=RE>g1f{Qk920P;WH>#_(+WvZLTrrr*c?KxGdv!#c!At*sx(;6dLPL+Qd$Ez_ zIyCRJ`1UvWt*G-}Sjbatuy;O}o=Urr{gaG$xl!Ej*kZoL^nb;sBk7OKjx+8Ty9r^pbpFrGKNK9%QS9`brbfEOgghGNXl%*q2bPR^8{248edj3 z2l02`)Hc72?-T&nHl|tk$kAus-5WE4U0IM*jv@g#ULBK4SXAhC%tD8em581hO_w3D zv(BCu@#8x5v$6f((OvuOY_f$rT%m(^Z@2MHEnQ{Vo&zs3)ARBE^JcN(ECY$9tA{Wm zoV$(kqlKk`p?5%CDg6@6)UAcytKqpvR4Q&V>4W@mCTv!;L5~kk*rjo78^&%`)$UQg zJ}z$jglGOZf@`^j)f-;Ql3~$3WFDd&KiN{8pXlR?lhFcBNhaC!!YajPGx~`g@zqT$ zi}aK4&=nAy4?^&jR#ajebnfBzO5iUrU7`98VY7pi6rw@W)!^Brrc6|n@! zx_esZZwbaC+6}J5)JeJciV9kkZwl{P7llDvC9hJ{4kefmek(0Zi{i=mT&6oPh8PQu zuw|E|swp+;N{5{8=xB`AzdB+to^Pt5Cbaa#^$ zsm7G_X+m!Jbo^*iJ5}oJ61% z0P-;ubWF1)I*fm(X0@TUSKi3SBpg$cmGXe&bP4LuS*HugoCxR{mJ^M zoYWqB(9tgYL-6Q$vU~;K4cd>)U#mY%GyAeXEDy<*(#&YmIa-iTzh#_ZN}x|afcizv15NNjK<*;6cq5j_WI|O(Mhjt!#0!)iV;R)r$^NOj3jpB5VZof z(`I}t;+VY;jJ`o~C>cFAR5}yiSvvCFyo~7Pt8AeBS|Ll%5x)Mxao}ESvK`72XA?*@ z5gBl0Cwa$5r<*FzN0Q!ABbmzaZ+@E zuJm3yr{WERp81=z_CO}0=VZ;!&7VWlh2l09HVN!w$z6dL)R+>=8JL%=-C&E$nXBsj z)A{{O0%FwLzU|v`^Z1zu;!brAepYSx(W5lpL)1i} z^XyTW*HepqjwAj&irQ)Pf%7VWXwxeIRPO`xtf+p*-wu29OH8NJkJO#C(*2$WXdpsR z%5@T<6=Te_lUUMvHp~w%TID`zt?ilkbvxbi(dhmfXP@S|?T;TVzB;{UlJ9%o&L*Bk zxJYi4EH#?4`4EOSI=6^6&Z(g@QH~NXA<>2kH`WFB~%0 z#^^FgEF@pIByMFU-5H6W$!Khv<%a#jlN9h&fVOW9J3CqEIdxA3(b~=C=^6ua?j(kr zw&0wWehA@O*DP5?t`w50um@Th(2!d^qJ)k-D`SH}xp&um&%!Y;%dI{?U^u?n9CBtn z==2+=AnDi^+HZrQZ;On`pdIx%Gl|Q0VdfI_c~O7V_taAQK=-wEj_^^|lPK8nnaGRb#~ zl46B`%*u5W(PsI0Db@EhM4?HWw?4d*leu?J+%v@v19@U5RA|6tSG|u@m9s$7nOUzu zef_fhQKIc8+DDvt?R+Vn=C@@^a^gV}^Dpz10hBT8gEqB%`YiTeW+ElIa`@wyDAj3f zWsuv${;#F40wfP!fK?fnv9PBW8`KaKL|FAS3hHXsvz-@WE8mrKIoWI^1|Fv>#*r}| z$)f@)E(IM??_wT^;YVaLMA+OoQ!8<3yeiV_2V?(>@B zeHkncVnvd*WbTsk&+a4r{)v*`QU_i_K-g>{zn@16GXl)&9zE1Hqk$7fvoATtrRR&7 zPdn6=!U>f>l|PU5sw#R=>^~O0*$UgW1HeYL0>TvU);HHy2&&pzQNHi*C%0~>JS|#q zpOn#OqdL5(-dbw%IFk#hbxDuYJAb)gG*jlmFk?UPx))*}F$v)GWe_B^$D1n(g$?69 zGui)Qzo#}e-N;X9N%V?Gn5+pb`76#Pq89rgZTrDtP~3(ab&&6cDr)Ysvg0Q!%5#tR z%^G!5VD~@HxLT()AascM=(|sturDfazq9)9tatGAU=xN|j?>l;4~`;P9W6BX^4b7LdR3%HvxE z5I?hR_%Z$25aK94&iV)P0b<;$T5|E^{wvx6eQc8!uQ1&Se5o6uRNybn7igMdlwvyR zZyb%QEftuq;QJ!5qP(4Z)qG0x5#B>|b&QFdLABlU&BKThY}uHK={FlW4U;1SMre#OtHh#r}cOt3A7YEuKD?VXqWil~^?S(_LZR$q$o8 z>zsW@4=hTLo?7JBu;5=eKTAQQ83J6`zHHxttF_o+G-k9i`gDtDJ^If68H-_z*1cCG z$K>xqI*{>^(EP#v!|&d=>P2^x$)YKJo|b&ui86gCJ-(ZLj>7PNt%!5(rppD1RP!?Z z2F^>4x6PJgqxmXODll}zrLOaFe-2Vwf*IAYgCIUVJb$?i_mL`i57?$cew15gcQl#* z(co*u;mo@Q>@A65KVo~u)9=O8d!IibEz&=pbtO8+bU*ZHg34uhTV?1a$3G5f-B(#c)G@ItLnRtV^!|fiU(_$nneRM&Sy#+qfq8 z#%edWNxqSbDS2t8Be_Jp?b$C~L-Oe*2Z+^7frRiH=cfy<(FKzmg;d*ixBE8i4!$x* z;jbvw_EBTuYs8S>z04^7D#m9sklFLnylL1aS}mj!7=a9ba7TBm8wWrk_$GN;Xpnx| z!=};+@~eZm@{Jm|Z)k!qAj+dL07CzusG-!}7I^EA=A(YA=BwzIEuqbc_}ici(8s#; zeDjCx0M$Z~&jzm`i{8uorKZMM`j__z;^ zOjwTjHSD5I@>vR*%6CAzH{jv8WdS$LIVk1mpVE!Ik>^8uj6+}pr1dsq26E1xBg2>C zqNc(Z()!MqO&Sb3fn+^B^f&(thRQH4_}s&#bEZ(WBt{@hKP&|}-0n0}2lzxPvMiOo zQ%uKE1Np8iJ=snlQ%w>1_w9rM*g1&JetxfqZmM*Ox(oH-El$EMF}uaR!n5Ub*049- z-Zii5SY4z=I7vciUwN?67%>G{3WNbVPWhmii01Q88zK(7O9a_}h?m@TMoe*jl@+qf zD4T!*C+`JtvD?lMC8A`YT&gW6X?btL8Aht<%_Vf$@{DSC*ft@g$s2l;e7ThT@+5B; zOO;wtqI7TgEG^S01PZK0e|4eO5!z&Hl)KifKHQ!^`1yS{H&+7OEQjG_VF#)(a2a9A z#J-v>^A}tiIEl-I<5IoaYT4uK?w(Y(-bda)Pj*4thM#_))x19@-DaYbW}(56MI7!3RI|veJbDl%hKE%1;~KQW%!p`FOgc_dUoYf3f`7J6CtxoA0X~skKat zVQp^=?UA=!iUt9^_Q49(DhxKJkpy9r`{p?+`x7At#(Wj(IHsWHluc#F?Sf$cQsw@s z0UCJpTUwNqZ(!e{o`V}FhIRvOm(!eCR27JSVnMVwM@i>IJl*q6AVeThiaYoIQts(N z3zIQ!Vq2YPenN%5H{EN#FG1x5y`%x0jaK3Ez07D8x_Y&TKiImD`Bh0~O##T4 zJHf`imu4Bj&51Hd(9MZ?(#o$oUqn9Lr#a%;+OD3pJryGMDd(f60vS$j>%pG47_&Rd zXa!Jxq)8$?i7yiK5&nBv|KhF>)`@j=EYt15uJuQSIXW9`PKF})ym-gql?`y6(RBT= z94+z9EDkZAUts4*c8%nG6D~R{ba@MylNj5{5`G}IYSL14O{M!d z%zY9~AJ=8x$YFrlnGN+4@LUXC0%57$)DSgKx3=EdThbB_^=EMq#v`Ufr=Kx`e8<7< zmM4QxW?Fwq=Lde`0P3_D7c@|`Z!gO*qr;Aq{h~K7F=Mmz)3udU7`yAFMnA>$w8-RF z6to%iyu1e=Q<19%esi?p`y-~l75N=^3&RkUzJLe0jV?{ukh?*7rb%OnCNHe{AWj z(;z-=tVW0U@2bL8%0rjju5+h)t!Q=i`E?{dv&bx(-Me;L|I1X(MPh9(=IDFs5PrO+ttb*pBqF48SxEW`Ipe|Ay8ZqeM+^nT4F98% zzelH@tb6bKSqTCd9}{e{%cHfMeP3?qPu69?xDC|K9(y8KRmYGpy^EV`GkO;(wud9F zCRyr>-wU&1h>cnUyMMvG2oEwvWMGw^RE{#LG6x*|0XYoPmHqDXeA%LPm$vAre zX%Qwu@rZb(YFz|*KROAA013bM;gA?eJh(A3d=j<~wd8r^_Bow9mEitc zO8`@RV|KelXy7L|`>g~YKEBiaX`R3OeNsPB5;wXT`{=fTjDK>qt6{@ahZYW|OpN^WX1xn^s z%1E><9*~}WM9^FeH#BkQDe?F~o|52toF)W=q-~M@i{j2#sX!!9qMz!lu^F&Zjy9W) zb@S>x;{_``C}%WCN6wwmANhBMWksCT|4axU%ko_0rp5ke0YA&G_3GzCgCE;uZMoce zhGb%j^Y<+}O-s>;NG#vP%ahnP!KsLdj-%y;d zy2v4nZR}YTP@d7YZAZDNZ;(5)HOGUoxTl502Z#Q1q!{+y?i#%*`(A7oEw&PV_n?RO z8I+DkrM+4GU$)~0mQU#?r3&W3LT=?PsS1Lt&i5IQ{(YKXw_Tse#Tx5Hx*W0(L;)x0Y-mKd;3um^E@aeSo+frGpOVTa2f(jx~ zPhrk)9kC(E_(zjeDng&l`8VBN^AMK~5cz;F`zR??Bkp)E=(}6M;p2JJ*M2Nr~fd2cjW$JL%hkttM4>*Nu z>VN{+d2onjRBQW`)BYsBEmK+jTmw|^MmqJs>yy*>+;2ab`P;?r-1Xc)ij8g$pGvMA z;(Wf!pc+wSv>ngjNgo;|M9AUXV~|<;zRqf5a4(!F$bYok=UW}56(Byg(@cH3pE&+1 zmprS%--;pR%{}(z&PGN*4ehzpmxv$&&c*kJsx|=FlXF8yqPjy_tSzL*%QiB#EBf@^ zr1HbD0O;*Ly&UI)_uiJK5~7wV(M@l#?}}$_Rc^(m`pOkJUa(S4WgpM;cF?fczeXPq z3C~v&pbIn5`&W2kTPp4C@A<6V!bdmtwtf`pZhfDxx?Fiza#a3~?f%5ec~d4w+xcC< z?%%+i++Josvn_Mt0Psb|wVrN{?l&pg|tT+!BAUX{&RPLB1ynANZ29K zmgI|FZb#8S*WB5=ISVr}@;Fx5Lg<#a|JR#(@T83~)+@6%=QE$%Rt3^r=@{~2i{Qa` zZA`GOBJ$W{Tb96&HerrJAGy4!rOMf}7=E!VfO%DCvWJ<{Osn7Z|1=^<8P*vTeYn82 zpvb?C$DJUceBy1AP5DnL7VNz*o*x<9>Qo6nqmNf^@yR)AC;7YMWk-9DNFDZL|JnwH z=z{EUGXBpu2r8P~v1xgGK8MfpW(8n!w&~{ZdVmFN@~aw765$4W>YVxtvINm^h#>%^ z*^cn1e`e+XlIic~InyzUv`sXK9#JP;NMi(Qz5BfIDz+^X4HzJKMfI+5jw8Nq#kbdT zluPOIaJ+!y+7`_D-61|~*Os%uH4(Bm;}B6a@U=mI#WssP^QNEXf*~c})X;E}S>BVU zKXvb~F@50cW@ToGG~Y7$?g_V7kIZUrz%D;9Iv*@E{T;N-dw<>^E^Rw)(&E6LAFQ%m z^IpGDfv--$IQ&U@pHz;QL2*{f+U?(|RM|pcgvS=wudr=c|Lwlad|4)|Tt!RH2pB0V zC+Nlq^LMIRrfgd0eF`f}N~D4qHPU7qnSQN*P(+m+@=R!SLGfxzWYW@-qU3StG~qf{ zaeFR2lhgzVatc1e)}c)eprRVVm=xpn?MiTvT&a(twoGn@0jIWBkmH$I4g_OW-O^ z(7)|8*lhNyC`Frbu*QzGL=KOds^B8E+>(3JkHT#!YkxKkIoiqS3`|CysTb~O%j8>e zsFPDcMi!9mr>k@KEp}$;;RzIm|6VMIAbMP`&ZJAMvIctJ^6EKw9427?^5ji3E#*^= z!la%|g5*-o)@%g3mo}%rX$0n;r_O4o*S6L&{oKdyIi=TGmr6M+%3UnQU7u7W>7>5e zu)V1y3UA&v*do$-(3=f^;3snnR^UI{IdI#~2_A8;ESZ)2kYoqGu=*U>I%L9hQfz|m z`jg>~%ZK=FlknqifTs!kd;fR2J8iVL1GG*%r@|NAL~F8fr}DOP{;$h_KFQ7tu{(Zu z!iAe%5u^4{5yrVH?}FV6dU0V@JfbAP{lRHM)kXRB&-YGQaP2W2>6Q*)9myP3yq^F) z(IbvF;E`<_`N=>&cj~G0@|`<;IB%v1frG09R|bSW2L~|f!wJ7);?Fq0`>)1^(>hfJ zxdGauQ*)?jb^zj29*`^dxV}#f-0{4Lv2?cFl^25F<%uotIhPr4&Lz%F1(LlxN5&}@ zto~Wp-iw5W=HYRQwI%0rS?*rU0H4Q`Jb&NwBZrMkPJu!tCdnCTvCe-74(hx`jTiF% z$K~z^jo*)N1yGq@mHws*T-{S){OjMxpxk6=j^T}*cj?e=!g zIpvl7npl1N{zYDKOxz^D(69XSRSu zo@vgm1HCrou9;~42cR?F=<)){{o&x9ficoJIy*Y>;pzG5dfjT=s_F=%hFhcsU#p-W zct3CC=mnPj?3t&LCwRYSb(j;&GsknL7jW2ma3PO=pYo1Pdp)yfgwz!J^v8s!d~VID zN(CzFwEn*eq-?*^LwSD3fb1W&uXun`i$fp4_0mQr0YE85yo5?@_`wHjYewz(oOU<~kDDi_D6h?WQzmN_!x7ix=N(vtiqTo1#?oAGFIf3X}3vj>rF` zCzpjsMJ6WY4+jnPx*Ax30c8W(6O9!=_CA0oey)iYMt*z&_ee=q1uhhX7<aqDyV_uqZ0;FcjLo`;MT9L~zDg=P9jKp)OLcb7{5&-A*{^adF+ z-}h-f8E2cfnb}pN(xBaR&gn3yePL}C?G40GHzDtixt+!AA{c_RMWTvM-CMMP8$LuWM zk9*0#+2dLg!{Oh~3FWr#w3nxKO1i{i<}J*ihnx=@@yMVEq>q+AYaNe~`RxSHpV&#-Z<@m?xM!G@sV^TlhU`qz8D=RjXHXRqFV6hrN^G{ zmzrW!V4PV}a!uY}{N|^sKoGKi?_g^BH1~TJnM?X{Zs-4F6JCd+XFoNaSSVO_7BMzc?<4O6NkKF*^d?@b3aNw_-|uXB0=z_qjQgmkRTuP9+B3jC)9N4sn@CGDJV<%Gj2w`% z>qxJ9=HjC1QaU>n=pi%OA=opQKg9mga_NuDz68Z7CnAw8TatU2#d)9-)k(vc?I6>aPpRllwhZP)Ernp2&$8ld6|w zZC6@u2C`6Z8^{CfHdWm(Z*)I>>Y*T$^g*H`crCFr!d^ID9fNr@W+@(~zH9q)lXHvV zt;1f>zeCEnqqVa4a!*t>cx9Ht*JxqN!PWQCxUyD5;MTXO7!5|WG3 z8^~g5Xouri2dL6BC4`=@aCPCg+Xf&U8M==Tt6 zG^|N-eczYqT{*gD%cGjVI4!no@qB-1)N1)Sb>YqsO!(P=nF->Rkr5{yUh*MM;Gns3_hm` zg6N)zSN4m+ZvN)^nZ2aGX}{OwCYS7ACm-q#Y!Ht&g_HhRJR=^xrN2HIUN(3kD{URy0ni zC>G_Bw}MYU^>MX}nGB-Ej{SAS3Ld-;m`dj=eXkscchxa!gtYOQ%HTxio&>%lJ~Hz` zwB-%wHGOH}B(Ai4DMS9=WAVdQ9JK0aGZ@$e&p_|mw3bh9D)<};h8uFBPc@1p%2acd z`8pcn=Jb?&>Yh*%WfEOitaW2;Th`U=G&om!JhBw{Ho746f*R%EB7T~(^6-G`qMxVg zK3c8#Fr#@ZO>-}Z9OD3tIqcAtN+-hb21Zr(b?@U>~Hl}lQ6tW^PI*1~_N=g06^ zsshSxUF?cevLBank;hd3Fs8cnHaX$$Z5r-EKvyfGc<-B1_S!NP4~*B${J5sZSWO^qTh+&1JR$v+3;i*opCE z1g5@BBPKDmTq%|6i4%2_IG-vtd3qk7o(o;xpA-HDL-`d7l_5c29DXRZ*-Xr*D>zki zR1_A*J|1Is53vw+> z-f@!tvzv&N-AD?Ce43mIl93hIOXAFOBylaaqReB4h1U(^SKL=Q zB;7@J-FIp3t< zYzkLKP2q&5M%-qMgSDr#WxF+UAKf85l(8or1fw+lHCkgYl-7rG6qG_~;{9O~vFdlZ7P2@^{ zxmWC5zqW19wNkaR^dHj)dA}P?YiD^IYa(;2pQNmGEh|V-YD$a?xRf8uM`?e2Y*&`r zT@E9ZuNW9uW@y?C`)dDr!bxTGeE#*@^)FfJr^dP-RvOl|T6WONFeh_gv0L)?X^OF* z4L1IuFOpA`NQsYLC)fuC$CVSPPb;-`&(w!K7PS(X`0jCcoiZ{w8Y+XlJ zT%jiW&wbRx^My>t?` z+3D{SEN}i%wfUl-D@iI|_TfDl`B4COOm-RFs3pk(Cz&^zid^29#ud1%+W{tpZ2W_< z!7G}OUVGsM`e%W4gD?yu>1r0liejjk^4iu{=Pd}+R zNZE`p{fN;p#*hls>QB9tcQ_WIpU^sOXorzo)qf;sS`3HGn4HQ})C+P;J!|SZB{oZw zZ@Jl;o;nBZ$$Y65rJyUHH`%`oRn^$Z&CA(GeFuGFz}fa!%z2`Rr^DZ5+2+t}d9^aX zdf!l2gV&y0|715DkRjw^dFUZ8C7NLVx<=>T{#UGRxZ>^C`}$w}Eo*W%BNQ39xXUZx zjd_Z|jjbu;sa*fA<4(D^y;fkvN+1_^g*v=WIYM{RN{Cpw@+=!h-MroeEBKq@Ke*hm z*stKcY9b#v+PQKuyFs(R2R8}Lx)CTCUq5q4peVUN5)(AuYk_wj7#up?obKI_?bmLp z!sIH*0N@EH+H+goRU}1JW~(fi&EwtJM)}IHmsz55F6`)BX}ZH z9c+72<_aD|J-72+w$#V+HX=UxShBhf-OCHf3hAH`esC;pW#5k3@VRrsI@YzCR+S<0 z5&O}HZTqMf^T(?V!)v%+nf$eM%+FyY_}JFp>6sFy?)YmyyT|vsyBQ4$SCK!HOP((q zeRH0=HSJ9O-Cc5^a)XBDn-b&4ABzEhdB2VKb-(U%ibiPOe!E?(e4X=R=XGcnZRFZ! zUN5lk$rDd$%a%VjZ@Bc@%C$=9&3O!z_>`g;(-lN{bSl!wu(xrr2Iqt;JeY&c2?#EIZA!m3cs1vI?f*Sz%`H6{EVsH}Ml(+OChg1)-^rn0K(gKn zQ>$`nK;gdU(^izdBObiTu58%H%K~_Na`S23YK#@@Zf4)GapzgI!60{RWdThx4+7pRB5!Wqw*f=oH7@>e?^l?n81 zZ&odDfYAAV@o(6biB=%nut~P`EWPYwAc;h~ovuD8_tU&5iLlvrKZuRuSxk_y*i8{` zTG5LYO2rj0z8LvqX}H0TIO#<3p<5|m7Y)cTtUJ1~eV}~a2D;nP?M7+D`$AmEEtEI! z_A>$nqY(JezXs9ALyX2|L1>A^zL!v_1QIU?7ge_OVcWn=*!O_HyN>kNV|M490KCzqfe=geL29|i zaod#9^TlRFxk5w(iCr}i3G-&GXtbjB{k!CoJw?N7*5%~(?nxk8mWuM9wan~(*bu3i zPxWG_iNNwd1DivSGEPBz4L$ZKK1w#GvjM+r{+GSU_v|d{tb@z-;5ggyimv!lEXp+w zOQwMd4T}e2bE(6NSZ{T^E>2d6u&^`ELpS9>sBYMHtDDpD*^x!2?|>}fMh=FD5{zVN zj>I51d-z&h2)<4uA#jnp=xzVG_O4BPpSW#c?$ierFVJEfEh{Y*aEBwznhAKEWL@u0 z5D8mS>8i1!{IenO`j}T;hx)mZU|Z9Q^M`lV7q}#A-a?q4 z>-3jr5S*8v69oM0&hcIDK$6|&OeuX2z9Ns<`FxAimV~hXHKdW+Q(bs~USu{BeKe!0 zHEE1-^EMFd#vZhe*u?4PJb)f|{LN`>j`oksQl?C+W4fp@oiI1UOvz)gP+fS=oNwUq z-kxfQkvL#C6lv*8S-|DNHzm)PWG+pRtG59>9~B8x1HHpdM{^xnR4BOZly+MNIx`*3 zK@)J%1flbO)hKWn83?sntU$ON?SPH8sSzO1LWL@Ck0sF4YN_1`Wl5p$F2~_2lz}99 z%iJkOmHom^oLlP~5|p&3EBrrOlPDy*?e|Xrq+ffCLH^}>W>d%Aw5(CB?bt>?pPe24 z|B1q_K)Hm-oAVJlyqW_|{GV+6fO^HiwMp@Xn*YfA)E8#Bhp22jriEYj*BurebbRY2-}wkn)aC+T&_H`T5Q0BnOsy~!W{GM= z0$WK73sub$W|SK8Awy1}wbSw7qg!FtC{l83Coo6@^pA9m`@ibphEk$Yo8srTEPw6g zkj(HfQUw4Z3*BOh4mz8lW4~CbwU#8EXysm7+j{nbLUO};krs`3r-CTWNYUc@yWwhnIDg{~pe`O4jY}LH{@qg<1|F0?D^RdW)p8vzyTSvw9EDNK! zySr;}cXxLPZowS}w*bKj?(T#DAq01q;O;O4cXye2VRdsdk z>Z+PGz4oq7LD~{SzlT@*@z=#cW3;i43+N`okF^!T`38mq!By|1ZV&Jk5c!{xlbAt^ zJ6U7UK4u4xVfI6K5pjQV37`wfE~(M(_q0Uev>sq*4Clz*@pr# z{@d({x?fGO!tEqsawR^Gn6bOV=&ZAbFG*)m*Ftz2r(C1$_jn6yxG@`phw&T3k7w_O z7}leu}$(Y|RBWbmJdRcAmG)xFSUz&T)A91`)_-5z*;@rEaEmbv=l&>Tchk?dr9m-t;n3;as}eg&Bz*!5ZUU*>~{wGk~`Mq}+(`EU8} zo)EMR%XfzQqos~qFUaVBQvTcM|K}3?-}mnEH2??1+d{<|vJ<9(xSce9=7HDyE^lRY zTWj$HDLy2|#Cczb-Ei}2g5}Peqs9&+o|^$nI)R<${lR}C>TAD}#|OXB2XP95rW=8t zb1dkcFOMhW@$j`l4sRFZRb=t#JT{Tp^|&7Q$A%~M``5|$*U9_Wv+uy)E2sU1?1a_2 z(fcJ}U+0Q%<-NuZc+XRj70Jr^lLx@v%G~KMQ)M`Dw4wsiSq=WN6FKatulI?i_Qcql{R+<{FUutNWsC&{tv|%NmTSd`u&Uh@8i5{sBGnI1F(JPeYuoLXbe|Q zc~lBXJl~h(i?MMFD%#m3C--=(Ts$59yNz5>ZiF@Y!S3?i84g>&r@e)HRK*Wd!kh+U zu9U~M+!MDnJmB$_e_;GkIVsvznZ2c$_s!X!cb(d*j}e{r9>^ zbpHi*>L{V|tJYFMemd^d5lAq;;HpEI=J$0j`bW^89p0gV?U4Pe3n^kY%Z-7`b2Ycd z$#k`@NyWRT5BF!m{eS9?T^&L5*w$m5LiKd;3_Vy-;s~`6v>lMb=FkXVShmvCAvO_c z&7_tgo4$C9L$VVV(h&5-n?YW{c*AF4CT?Y#{cXZM{{{Z-QXDhb|EE#D!+%>5^?cnl z=JCfv1W#tb7ePEhNz&Lr?DL%Pc=cgPxo?*5bhmdp|2v(eNwHN2%~_ETp0q69Y`{a> z?97fObDw<9l|dLioT8UYB5^B=Vp(H~6qf{PN{TY-CJuYRcL;G?cn8KU-z|oJieDzz zCU;xihYU%^&a9G(l3gMRBePH*L0O#!hK>OX3L6J81Tp4I4~&eYS?K>DU%s;}vljDm zk*pwnriL|-^nSQX-UM#H;NCn$C}f{-v*1IYQ?4UDycC}7bXVDv41Ln~1pnxQIOn8E z`|DZ%D`fuhr@ubN!}Z?*^N%zB+wuNiLPJVH`mfvmrEvcLh!7;U_W@eI9snyR1!rrQ zcW2hLvax&D!2R}Q-U((<9 z7th1XO~J|YFWlcdCm;X6@PBFlYQ{*M-2ce<@4kTHU%r2M-^c%z@!pJ)xOffuI5{bJ zdHMb}VUMjKsrX_|Eee{|>*;^_QQQ-;kU8 zUGBRM|MUJ|dcWiUONW2K#z_3{a{i_B-*NAKo`1~nch3L$Tz}{P58q$e-0wEw{5uc- z`xp*Bfqy1`C%ktAc>i{ck^YZV^pCm!w@3WH00193KuJ?sE zca^uYv$1{e^1hS)mG%G>ESwy?|5{Q>Nf#eO79L)H3KlMQzP~E5Q*f}ezZ>$O6<0NP zde6jvQ!X_p`+L@Xzy6bWk=PUf=8krj63#Y`Ruu1PTI=1pbl+|=c0k1q6@+ohtuaa{<355U5}3_y^leQldg#iW3a=NzT1G@XGAmc9+E z;r21J&jVAjLC@uV?tUx&+tntHs&?BK`7^GEzCDlo8X$mcrj4Zd>v6}*oDEA%oInWC zIfUW?CDrrTBkpB4TOr6 zifa(ED-T=t#yYsfW-My-g8tWh;$aTf-QdI8W?RLKCg|&oHO-aB5>PWA=XQlY*7?az zVYpG3Q}$S`_ZExBD6G9cg3j=0m{zRArpzqh{d>S)Q6VopX>%99#dE7u)lx1~O`be7 zqpIHYe~tW>T9%Z1Q)_gj{3^2wLxFy^G^Uq?|HLNiBYb$*A*K*T z4jBNu3$X)<&;DK;P8N#kbKS@qlo9cUvd0%*A>O3k`J5f*MM{7LP?Z8C!ZE%RGEaUf#h;3hv&D4-C zK7>tZwo*!flfEGr~ln4F_H)GXhja2#i`@MnejL;iwNgssg z$BS7^bQ*|2$eDAzEp~Pyc}!n*7vXE6=>#BRK4u5wqts=@BzTKH&t_1Vb!ZuZ47oZh zF959u)*^)Dy&fXPuOvNFHVZPDff?JQxol{#&FD!` z_p9IrMMKmd4_$~BNt~;io(~jjhn9w4Z}i*Esq}mKDn7&hlC_GF4cw#FrW`&xnT&mm z{w8FauLiBIY9VKv$DMSp>8(|FE4Yv8bj5f00a8QE)&rmXZnw4II$2ff)er5%;%5Qr zJDvsL`;YyP=NI7I(x>2Ho6nl9)2Nc7?-e%zDq{Hpdj?Y2FJ{J4r9pm1HVyX- zvO7&<^?cuO(X@+5H1xw60^{Hy<>=9+Hb$Sl)9k;?5YC=p`-;ot z9(I2S6ANc1h+yCUK$9`JUL}@DIHC?xE%ULi{iPJFTgy7!teH&!-+8Keh&yj}snIb0 ze$?sC!k7_^zXUn@NRP86=KMsMblx$4o(+p%78MwM$o816a9yd)7FgBH6E_<7#%P?i z3vPGTlb*9ZQq!hDrKW#X5CqnS#E*t{k-)tn$Ol6U_U0^#e_UD+ybG>9c}Z&%O;EqK z`{k?_#hpfCkGF?7LRQM7@dLnitPj!k3<-*S4cZWWB#*&zq7IUu7RB^}T3Q-oVt63g zi{YVrE<7QgDOl@qk^9XEDE`A5XHJl1ZjB_Qwfr?)oV%|Nus)BP6Xy0Ca&-Nb#oTk2 zPBbLm^7kq2^%TBW;VmzZ;)5CJKG`rE>g|miumf>;iAhjpsm<1j68vGi8FI|*g@rQK zu}FUwenc;-!8+9+(Q`c<5aOV|PdXiGTX-v+$3_|wcp35&bA*KrHV;mF&a5eN|K`H3 z%K#})xCiXt85BYb0v@&?``v_@tx(){MY#SDWs7w3jt3nFo^7hl413(F%~KvPj8P^q zr@-#qqHUp0Pu2xcpTT)uHYa)Gu-H7jpqm>--uC=FH*ft?1982JJ-xpk=-Sr^Flo9Yo?cGtv=Ymk@oo{bK zQ;!#t)y72yV8gq&_MX=tFy%*qk3ze3lTtXpH!r)P1*1D5J3DcH;fV9&|H+*8iP%TH z;yb<)*+=h?v>5jvdO&%|T{1^`mmji~kT0DzsUCBtgYeFOXfG6GI() zcta}hk!@AEMHJ4wn)_)WM+ha18mNM0^>hF8k4uda9r4|Tv790{$T~0B$NuR-aXml8 zPomRKMoNP%ip{XIQa9My8TmJ1AiL|a9-QTRHZ90QpXpJIvo34@Yrs@XBC~*AKAA(w z*1_>>F!oQce0UIy@D2E+i6BG|lBsa*8}v_jwvY({1(F3ohde+Kj~mVplIeo`)_&OG zatb2gQY^HkrqDrx(q7e@Y7bxfrDE=XYN1Vk~>2tmbdK3VA^)dFR z++-d4TkUMY+WU?Ycf%CIOu;E72wQLqSZs5~BY6XM;BLs^hhG?`Tn=tD3=VQjz%Je* z*YfU3zemW?T%PGm$GxxrN#6c#xfylNPWfj(Mk4|#gQJak_XnA0-;!nMr?xYA2T8w@ zc1NoUTVgZVQwc$G->zYk1FI1--71b}$BU3AVN8gSsQ6 z{6t~qC_dn@+rB%T$k}YcGGsRvh)3u_4E3Qw2(;sh-cEya9$irSu-lm^lJZ5w6+vWi zBN(Z!Iq{*O?oBH2ur>butPJ)zj_*fkBO%I%)y!M0Bgo5O5DtXD5g0Y@sm|yBT!xua#bSg)QWvX*&ZCf9=ACRpb zwDJnw6^MCP5{p$;cft$L8^7nCeNDT@?TR~=gEcOhX$>Z*xcCljyz-|7NC6r@_=z=_ zV)(&jMK_lHB=g;PeuW5kG4(EPa9x?K>6(S)=k)kulJ1u%S&}xtw*wRpnHnW85m%aq$$al z;hy&C15-NIqc4D-11CokO>hM{b}zi{`IO14xNNd3dxeVX9ie8#QaxgCGSANig07I8)Riwyl+$lGY@R?DFbS|19=HDd0ucdp}4bW(tO4!hD-5~%?xs=2Wv$Fib+zo*|nzw z!o!1x{hqgYaKY?wq8-$>5$YReBI^SX=9!Pf%h)@l-ZuVrSKm7nb5=}#@dBnBnjI`I z@Lo2oVlLFdAv4G;>R_P&lNWa%K>O^kXdlsm5SNgzm#4OHoYGT8kg|{nM%SpGjkH+M z(5A2L+C&F;Bic;fr6Iu(*n<01Z5_6?+m|Kmc*MpJ-c~&f=or3{9H-Dl#WE@i(W#9fHT1 z_)oB=w-BS_2$IWth@mdKzY{fVEM3(Ks|%k*LVP4TMKI&kju8it4HmGYY6a!Qf>?o5 zcRL5$bN*0QN8s?FSEJ7SeDRJ0{AazjcaAso=PQq$+(qmtwU_XqxO2pTg!2{jk&1-R z!*y(LLJaq_pd2W-pt|O8ucKFtpi{P2kI+9my#WqW;;)av(8|cEH+)p+_Z=WMh*OzY z%c;T}M698#UyGMe0-k^bRa&) z0p^ygw*9G|-)FbRnhxlE8}Lh7sRa-kF{gO_uy#-PxAS zaTWC~kkHXjmF`eQQ&hI2WerI*#g8`aH~hEYC?YU|Km4<-O0na}5z$;fw{jlMIpP@O!6`(j*4uBDhf?T!j!IDh=9g6jkpxw~;|o!rFsCnvEB8a}M1{bw zXR|<-=to45IoSYr9GLRy#$vIQXodyW0Fp zRpM!}7*qMUY{n-!7RSx{r_E84&SqVViQ|AMlO#G^XBEv86AzZCo-22sUT58|B(GMQH zSL(Kn>cj?Py`h2-6}OD}ssth3Uq}#EAH+i3i8yiCQwvb9;(OU7!iR9!+a^&1O$+VCQx&NCEh_i4BR`S=BkFuD{Po@xW2l(h4v`d+g zSC|RJRev@Zr*661H^7$2M_cxua~;`> zM}lBK3w-|u;NiK8%;}H5{jT7z!wV1sDeY#B1gSgWwi&*H%GT1%R;;vevp84&+Zsfv zRS(ZHJP;`W9n3laM&1!Z3p)K;#shJxu?e*f4_2Q}5cj4)ceFJ5{If?V$|KYS$p|eI z+RPNL=OAP;q?qMMYg*Ba8w0bn1bT1TVEU}5*sP1O^zy4X?yIUg`C*tiE@QFiy(~L8 z)!BDaC`B%0f{ZsZ!b}B~;}tGI|43{=w9MTXNyc1tBIHf#U@Rms)>$rEM3(d?>Ysz) zYZLLqd|`RqG@?C+pG#o)`@rqae9})EaN@N=5@Bg1?y(-o^IFJpI2ae$CpK!>zcB<~ z&?IUq_~+&li+=tZcp;`7^@iUpu)&6QYcu`m(hiN6_0fisSnn(SkNXhPtco>N-M3E8 ztIazgU_(&;Fu=Hbak`-pRWCd!`mN+6GTUd5QJtYC_k1BDm0<6Xx0+y>JQliOShLEoM@*h+H0r8X;!yX&-%r+*arwvcX3%TrEL}JuHjFUf^x-$ zngY3f7=YNPvP9#03V8Dy_>-!Xf*C8{gl@OXVDjfRIJ>4G)eqZeZE@H(T~Edb4aPPN zMcoS1OLGg$XGurRC6ewf2mo=yB1e2HHsw3J8<3@+- z*GW>v*4`ctulI%uqeY^HQ25ZS6k0$vAi?M0x@El+ zze)-QdYR50bu`eHNCa+Mxly&m6(4)IHIoFm8>azL(udKO{_u9mvCnxApIp$NoJ81#tor0?t21ayB&205M6?R&?0`dc z3TDbzvVjBQZCq|?CPF^*_jK8FtD_luIx-PnK*b{VI-j)H+C})i+q0;YU&MzrN_j2` zQ)g^%L=a@J2e;+rxI(VyMoD+VmA~Q=&=L-H6&ba#Aj}~2z_Gk1&-j4vQV#Bwlnw5| zb-~!b=nY1we*fgo8A^T z3@#m=;RIOgVyEDyVO|jW+ObYz>ZNNf+r~HXei)$uDdX>Z8bb@o07qp&>H?NsJOVlw zA?kN3rCbfwuQu{2RkWRzGO7NGgS($@FiCw7yzKFePdi3{%tE; zVUR4mz{A3;)_5opyIYebT4go9{G4syzP6E=GuH<&L}%yoDF` zej-D_6?a2#Biq5qv%^{qcgb&jjxjEW=lN$mJHIen>d{EADnl$*kt83wsL>MmrP`x> z`;5gUn*0l_-n)~f76Y%inv48nJZCBBW7|KQlqx5Yg?}74zBKL_+s4*9e{(UfnkiHg zj`S(-nVYzukIF1ZHe#VuOS9R0M$TsEP&hHPU3d3(&`kE44$e)sH#(u8_g`nh^-}A< zJ>CthYY1Au=FQpvm6w;CQ$Iz7ORyN2lwL^Rx4yyE&ZP2iv=UzHWwg}7IYT|=`co>_ zp&VJH@?k6M8PUc|%s{N3{MVm~kRt6d=JDYtmhxVelNzh2=9p`in5AR48gK+VI6uWS zmy}C{pv12Rn0SfAdSt;}8u`j1YO3VbWQNgiHqP+OFOQzFyU*-!z>;rWg$&a#XUX0) zM?8AX@#x%~xrus4G$CGSwXpmwzAVFaT2s3BLS*2RA}@fY7@fnQsT;1@7Q^S;GmL&x z%Q@6YFxMW$viO*p?Q6xt48l*gfuD64_O~Bat@K`~c0r1FI{Q*D8FCwBY$rjEOk>t# z_=!!{z6HLT&sniEs;73Fbh}cZ+m0Ze=y@57tHtEkO~mJfHQLS(mlyLtvSkWICFg1V zb@ISAjl^+gW+D--5e*-6B4F1e*qdK~%=719#njo~Sr)xE_KY`qq-3tTkJaZg)fv;~ z0(ncSnZJSpm-X$WD5@80$+z85^??mRe(k{vC<$Z7`oZeoGme+|mbk;qyElSu!#b@- z8c}_se5+BaCo1g=B5O$7)hr!`)hfkoY29n`)PbXfn402?zOhUUYs*}j&l0?nj@?TNY3~nLb zZTO0tenf3$cR`zlIf~_FT+`Peq|Ym)TjpCULq8G`r(?3HzSM!3xO{;e43_rmt|>29 zcYwd!J8IzJs=6$cw^dPOeGR)61xgX_N)PzgTzQG?Z7hEOURcp!+B27)XZUDhm3hk= zi0FD`d?Yf8l4?IT&h+|yHVz>|s~(7^A{{cst65~$;rWMVc} zWXV4MgLHUsGFg$WyCQt)TW{){Q<6rwb}PYZ-VA2T@aLT#y=_tm1BwkfrVy1L6Y(Lk zCsA!z@;zuzD%n+*Gg&e3;tp76(Vq~@Fe3zg#!&@q?{x!?pcG|R9LjPS&;Vh864eVx z6{t?+E3|1LOgw-;dw|e4`EC}1V5YvSLc_3Iyn!`|W(wxRiQY?2yGj#hg~anjyk^1N zMMd~oV{u4|UT^FGKJ!Co$*3*PqYp=ton?WA;f46H3=t>Sy3KM0C1Yz?j^RS=r5Too zY{MhQ@k#BupJ-|j_Vh7FQkZJQ^KnP&p?r1Cw2?{&ZC&b?v-=vMSjGEV*oInSAMsk` zK3n>|F1YeR3rWkj!R%y)o7AMR+D-sMfA2TlEg>F&RmG23mE0i2jQQIX= zrUjHWXqr@xz%DR9yHzF*^>6?LWgFt|skJ4k(rR5*RSU+izax%ScP23EFLi=Na zMg=Eby~>;g((iWlZ7V%FeL4Ml;Yy3HdOi{@c%pPhZbZxRqYJYrMPyqBA=YMG6qa$s zm>0>D&_o=>UkPGv2FxLiBu9`)!od9BLK9)1m`7|Rs$uK!2C$5hIN@xevcoFv^%TFw zjeyhSAqRjnGE}@+mi{E?dVTP5;v;d1gp^$Fq&pQeQcOnp>~M*TR&V2|^{Z)P&qgeA>N9msH1xlf^+m_WJKu8q5pxlHk0J{rcOY}( zpQfG&dv3Okx8=EqSM(Vs>DOsZKJ5D6aNT&1swT{xj04V7`M$0TU0)^1$&G%c#*Z@+ z&^(T2x>nS1bJw@&ehdYPl)fJLc`ua)dQ$x~E-oiCG)9?S`J-dFQ%^&e?bZeXnN- zHYgVB9SuaVpxsyRICx2^n~MG<OD)WiBMUjw?2JJd>TcxXHE~|T zuZyf7v&+pjcJ#SvZl^fJtg)A3VYlRt{AX})NC4KuPVLE#!iR=*Q_5@L*Xw4=0Q@o{ z8IX`&oQbm#@sDA!=*RQ;#au$$yZgo5J=;6p(HX1W+R5?o5?DUBafFkgq4vZ)U%PiRCZI_KtT(AqMl2o%FfDG;%z2RUo_43GV@r zZ16@!@)DZ2!?XD&EzF|DAWzO?HS6K?jn$O-!JOThqh{5OylOipV!}a#M-#9WJ#0O+ zBbU_md?M>4V7jE@{FVY&tFu#h$+z7m=0=yTuV#|ToeX!QF!pjFC2>8GYee8zK(j|w za~`EWlboeNX8m~m$O`$)+v>_?)5fHz-B~dc>^I8?NE==Q<%dJ#tmOv`HcDv2?|TI4 z1l9y}ghNDd1TiSBa2b??iH=b`vcxq*@|IsX#%;O3%dU)k{S@c)K)w{PeWGKb^Jb{4 zt>5zNyg5e96_w;sQkIi?c-1aC$=>(u@xvmPu~H-UcZ26=`!!UM8_QcjBg+s+cO`Im z*%K9^@v`wJ4gGwWgv`8s`*JWD&Cs=;O9OJ7#obT@obHHqO(V`-K$m0)=}Y=nXrH+RgY3p2IVCFFG}A3iK9=%t+#-;RoPr zmhm0=Cu^rUU>1*7E;+Zay*0E73vv2{lA}IP|K2L!N#0hkq~Baz)(uRaX!{=N)ah}$ ze!l%u*AdrLlCTzy+Jp8*Nfr(1gDgBjf#+Y^&Blg^q|H=SxP=4{raG53rqm3NcGQC582fM|ZhXcx=; zBm+pThzUsj=n>?(bg?}cn}NV=d>q#P$*!2J876j7q^rp)YI!QXId!SQR%HQIS$TMN zYii4H;@RhE#=WaB!0M^|`fSgCEUiq57=!f9=XgeYGqa7t)9Sc<{;bkpODBoF5%gKN zOXO}$+m5j%nx$7wrEzm#Q~fn&E4eIo5WOg5P`pq8s0rM99@Wl9bjaxCoGK&efyRN{ zfeZ&BJm_q>^oT)b{Rs7^cIBIp(@lQo$6c^dZEm8Ci)Bw0w2nNj}?QeQr+&zt`v37L{MK51^u>LUL zvzUweWD=ay#s-RFVW7klitCaKBZBiaU!Dz$9ffO?xSp)P5xQOO6*$1|n z9Wk7h$s0_X^w|2DnXjY!{`s;_E{UjIWO&4qtr&YKB^Yn)!O}Tatf7X=W7)h61T(H1 z7@UYj8JTluk`+sxO7iAk{~;gpV8=39-4&BWOg#$z4W^Uqu0Yq%4kkwpyP%E<_3TRi zG0gi6<+mA3B%)G62p~7?6%BV@a+s1MJ;^06|4$(!C}$cDM>Ut%{&e~CYEkBa)jN7!}s7@$-;(FBq8DjdA2TlaO z68`=i^k~b^Gb`JMU2+AN_n8r6QqkCgf0z4xBVZB2UmY*_=6NH@OK!?eU=o;Nc)KD=iVfmb`p%;);)u*M+c* zItEliyR?et!?-?u`T%&+620m73XkW&>F$rbq8B&7t2EGW(90Y7aYnuBv7DtH5=)^K z0C6K+KQKAJIQS=dE22Qcs2Wqo2h%5)p5<~a+-H->^&O1|xN%LU#+@6ue9oBGOPsf5 zTi*BytDI%L#mP+z7q_(bq7Pyz2Br9=-n*JIR~bd+K6NSZ8Or6pnIS;)2A7_jO*OSS z59au?dL!W(Vg@h^c~8|GpRP4cdf{&<16WPJPN}>S1cwDv(JFD( zflb1vc#l5c;*~Zqzobb9L@*MkzRcsJWJQ=q5Dww16`YQo+aCHfrwFD92G6yY-9%3& zyrsO3%>=XUX_EQ1!F6`v3b$Yv7;g-I+U%@a(@ z*3~^K57=<#giZl?!zTJ|XblH{eA26{T9Ngt2d%`|to2=x-s#Uew)SJKiFR0DW!7Z@E z?H?ocJHU0%Alu#13qJ%MgHmeAtYXtXJ~h9JRL4qJ>g)H@Sqx~4O0=oBsXo_pMh4Qh zF0VOu0=l??!Oh%Xz)A)^bXUXr-*>e~O9cQdPb^qplG5aU>Ctxduu?Cg7S zy8(Zc+EKS_ol*Lq0I;OhtGm}1F&1N5<2^8R8spls|0pjAsdaZpIAXK^yGg%`L{Y}m zQvmeB*x+aaKy>I0a<7@-LLz`=EZrofc_uPB5{%tm#ga(GR)^_MB$n`C(@yColo_V_ zD|~+3neHp8HMoD(92>~CF)VwHFs_TtSKr0nZkoJ)%EK5(Bu%a_m|$u7s25Ot`^``J zLvPao!CMxq6&6z=uJO)-0=^~e`T!TjoIQn#iB)*2e6#pw!Eu1LyyKpV74+k&Dr4l3 z2BgXkn>yj*P~_wM9FMkqWKWnw#vO!rKs4HnYaHBoa zCyNDyAe5bwI$rwKJx4npp62<5(<;fm#t8cNJj_!&WPv_6YLU<(oM-p-hlM&As+EOS zR3i{t9(>*nUqgZP-XRdDO>0u3);Mt-Sz|kX(!BJK2Wn>zzj3@MdY>RUHG0_Y#&$P^ znvW=8UjA_4;4lEp!cE}8HyNB349)X=ajQ0{>e&Gf8Ub2?bU&2wC_)#MKJHqKEFUau zP0Iya5mPir&a5TMnWW1X{JFt+7LylHKH2LQ_tJf=z3WzWBS_GWn?|vn!3JQyk?!y+ z3rfo5l{|@pF)9g(jzx%0x~DEanR8DCQ_@_NG|Ux>-&a#b{=)8Pm&or&o(3|<*gQq$ zeRsUb?zXal_D~Y$f@qDF@5JZ}hEHNdZ7E=2HtfiTd(9eIM-f)T$wFF}{^T1m(iY3` ztXaJL46$v-M3XC*%3nl`VdFrnC@A~MR5~?aiI86-_}G?esvgqEg?g004KC|YZ4on* zu1wmOIvQ!Zq7NO4At>@2Ko{a|>IG;{@oeM|n|D-j&6gbF&b z;}1ky*EnZjBS~kTv&#VNHk3~%>?S7^z9NDV@8dWNs2Gy=N%z}FWa+?|6WXh_ncOwM zmKnM#7#IJrqA5M=?XGLi{!)s$D3O~6df7cvTZesGwCMZ8HZv}5{!1s84Q*~Wc%>Sc z|9+g%hqhpl!gqds^S5tmkcQvDDy<2qmDXg8ouw@e-`GW27by_+?n+|_c-cBlNOKEJ z8BKHb^t0f9wrYAQXbz4fHAZ*M!KSrR2{vm6tlZfD?kAU-nn;!Q7M^wXU%WT|f-`dt z*2@_-te#sBwOBj{m!cY%dx7f>kc6A0`4!%_0`PlLFcC3@nSFW4#+A<;C37ScV_=-V zrXeKU{#+rUlZ&y5RZ~=qLTZ->S@y}`3>NFIrMS48y4n>_J)AYfyFe!p1}w&3BXR7C zHn|#Hj#1-rFqW*%wY7^0E-CK#b_<(=GeLP=&qME(*5aOeb7!)(58g z`(*+KE1yLPm!gCBsAs}K`m;5omb+ynAIx*^X&;eHNZMEk4gIRPt|Kzo?CIf=r@UVW;`Zl#24B4iTq6&2zGpuCp<9tj+oS zzF5xilJxALnYR}?T7-;f@lb?Qt-%KnDNPQzidPHZ=GP8pnm|dg2-l(_neApOQtRE? zGw?Dg7_Y{W`{*N4@G5FF;eqr1neLwzu5W6J=P2&{H}3U)oET}CWD<++4y|h5nNt~o zutBzexOpe=wQ$G_Fw%ZZ`2drPTOMIxM2?OKHLgM@P~GyeU_PdP9~vAQPQ7PgVPRCA zYGJ1j7@N3QN*Dg6M7+edcaCxsfm*@W=`JsH@q^HDsQTy*ebM^9s`^EQxbr+hUevMw zH3{E%k&MgpJC+^6{L}M2F}2WM`ipKgxZZQ&!%7MxZJi#~emq&%^{ur>u}zU~B7oaiWYv4QK-O=!**UeJ- z@#k>I81U+QTe#v&CCct8&1aOJ z3bP~}G~Rggc%&5-+(fnnB{h9V)Vj4J4KK5N^IDa}`2G#U(oQ70k{Q1DON_uN+fTYF z@7B9YQP}9rBI~kW%Z7>PclJkbvmPCLCJt>vB9x*Wn|N%Mn435*8U?dB-<63|`<`F6 zh!bH=m9=vp@qBEMl#n9XR$xpOg3OVs1SL8pMQe9F{n?gCYZ#7zMa zw~2`dwyO3v=kW+i8P6*XJGaTu`94wBqmUwExibUK0Au5gwj$X4^^W<_j)qXEW=?RV z-RQ<}ZSY~;Z~sZLVBl_0x5zJZA5%HN)wf7;Vx4)nt5gw>2hx0Z;Oh-}wzj6=Zi16k z>@%86U_%oEiTX*f=@VwhbR^uY>$tJt0$J_eqCvTA_UWG=kMiwfx|L`XCT6CIt%bG{ z3uRt9te+aUk%1OFv9Js7R3)+X4aISxtG$F=C@Q~I^i{&^;d${{2_Ku7sQON0HQ;e| zt!d>leVe{F)h8Jo7w#@ggeB7o(6cpD0DEGR>la9Xg_(*el~cOghP|d|v&)e$s8{mk zUR|2V&1fP-%V@JY2|G;lLa1X zK~{zWWftRSc8hy5(u3w&ND!NSe^K10XCFg2Os^2icgq<AUWQ|9SK!_=o?L+LK^haQ!?*19 z#OK)s4>*w5({mZFQrS&sM9d`p(iE|&(0bQZb_6nO{<8bgb+8eK%U#cgR)k}&e{cK{ zI@#OJeg}6umZ;g-y!inO+ev*#|NUiW#=^Q#Ajmj;+n;$zl8yE#z3<}$8D8rsM>Dq( z!yH19@>+iZYB1MpimQO+;f$O%BNnzLmZCOtY!tgOO%117i`+*B`XX96!$AnGL53p6 z6)NV^%yUIILpw<~=W_^SOly< z0*;KKK1vv(MVlC>XWZz#P963nHma)7ipGyA3hP?6kS?^?}jUV)u|@7I^#gm`$)*_-$zO?qMTBbhijSPOE(;vTz>5FqI0}MDj%Jm zRE~N_hErA}ggMV`XUQWwa&A{KEKRM%KZz;n&wjp5?kbat86vdQs;*qTULIZkzT7Kt zpPL`O5ff?2j^ty;h}jiFyTG z1X|2*akynfU$#;F23HZI^_VGO0NwfTr+CPtv+!SG>WAJ9)OU|XR|d}J`qJ71bC17k zdpj>Il*Gh1WGw9qlJvt{q3*ib!FttFv$9DNCUULkq~;nVrdhcVaThk+|4Lmqiu4~C zbazy_Lt!)cFy2|es`W7=|Kv8kOnPvQdZ&t$L|~==^!{gxa7vNAjj8hwfvv?nQ)yX7~vIys>;aBlrE|51Ee}Q zd+{D}S6?RO@9zk_zH?bnzZ~%2C#a6KsMh=B#|nAZ*W5DGPW!Nwgz(V_OM!=eJq^&d>NG&&uJCs!~a~p<}f7I(7qCusqsnAo>u*+=KOT z0NGMe(KG2t0aeBIGbL+&`}8OFFa9N#FR2UZx)U4otwQvkbD0G7jEma3^EM;fL#;2; z7en5dMRwadWq<#ZpMm!6(X+#m!?ht8Abjtx&~a$6a?;-bv?dK#KVh z4w(`(P=CjG=>>$>=qh&!&Y$1t2vC`4dQ!WT`1pYJbxygoV{0HNj)SAfnsv}U-dRd7 zLJCT#@neQI1&_k}$2~_arHYX&VXv^Qc`VBh`K;k;(U2^)45lS8HB1HUmXC##EgYvF zdu`-&jO_zt4qw37N0?S*`ix)E8ddq)R7t7=%GZ-r!82D*D$KUQ8 zx%;I?#p8IrDy~Wknfa}DL}u>9@dRr&@5tq$#7HfBQtM~av+#Va%2kM~f^+#LGI;(| z$zHi_kT^fzR_UkP0*l-PMMXAu*r045J~^Z*k;jCY7j>?!!#c{Z@ZKZNrC*+(Y)mn= zNLX;3Bm=aWmt>?&9YTL3E0uhiT+ok7Xz}`;S}vr!-gUn~sAl3eqs8-vpU&#Gi(7g( z^`VNDP`2cD3Ft(NAJk}FuTZGRiD-@vu9gC?hXx*05m#>%p?7d(rWTAp>OsP`` zmSlbA^?@u4PFdGzw7QGLz!p4GLc5t_MI>e>mK+u#=3j&9u2B_r*uA}XX$gJS^35MV zjFt?9zQ2~2D?HJ4^%R=SJ!FW38>^z2+x`iI4HKFoAI!wtN2(>-n8J4#i_Ybf(xG!* zaQ^BxQ5=MJes`TJnsM#zJnAKo8;b668P|?`RZz8Wr$>a<|3Xhght8}zzqS0u(rsYa zhHwvFrguK|#zzxl3)pEN*Z;AG;~3E!yq_o@rj zQVUSdm8FR1Qb_0U()9q$D2`&}E1xV9a^kc3RScK6gfO*TpWz}6q4mkS)|+M`ZX-sn zd35)#E2s*v;OSz^&4-{O>5|*OFqjb%y%*nuUl>9?m$VhI^WHhyaTN4$YWG>Z2h^S& zo(X{~%mzAv=oVb8xxzHjjCo>y`nYS)J-837#y*!>OT1Hg{!5_A+GqWjs0%KV1RYVt z8~qVkvQl>N)nXKg&U`yV?zWbD;R`Batx8%#_D5aaZswYn%17I)tL=+E6z%a3Doi37 z`)7(mnO+PRtsM`-7rx-^2mRxU&8R;^)m~mshSue2t54BQmax@@#veA>7#Dr;fRjxZ zYBK>(gN$d6t~m?2X}*~gB_BDjy6yg0vcm_|i^6CAZc0bMqm0Mu8{Yjfx|ieOy?bpm zaN)vTJy73#gWkwqlX=DR_@bbh{WX1?2&6!qH#Z}yy9JV{R9meH=d?Zd8Ux_zEJV%^Ob z)J?f*Tb~1$wOnz3bw)~#QK?YFw_R9Q+b<=v(h+vv)pc`M-YZt?49xCwWhA+oLmPaL z%%U3{=Q~y^E7g@L*UB4H3?;n8JyDwIn(UsRyi8f1yoRq&TAzHkbiZkb=eWm09%ivd zMaRU&Cm3U)XQXzPtVOxpP%yH~on%jz42iOW*zIjDmpkSV)=i=mqeYm_>;vK-xZSWp z4zWRGXG8a|GVBnu#=gO;rjztzQfJjq7u#I(yTkI+-ge|CqSR-Q+Q$; zgqn4l*n8;{d7^7SrcWPaoLEz9jCno<%8W!Hu&gxmBDRPxa$U);tS_;wu&M@6 zt=Gcm%L}4sIhvK`6lGciObUbBAnVqn+RdT}PL;45nY4U#=e!1Hx_$NJtH&;0e(8cd zPf|vH?Wm>uwr#lRQ6?)RcOA&swzhr#fmIoO#}_91tnT7{D=&M!EZ4v-LZ8-SF7{!i zB~m7x*0PtHmW5psd8O$s@26f>mDuIdrShfms}tmMW2UM|9$ThOm0XQ5+^|v(xUj)Z zx4;zH*p^6&P{nN)GsEC&Vve-v@FdFCvbmPsl)WSSc($Bv3yeFGD8?G&iYbWEVm8O@ zh%v<2vd$>v(=e}21U0h0Fd)7#s@MsOZSI+|d^lXSt6U#Ktd^W)uPHhu*^$iED6cu) zYw}<{SnV^&9SL7rSo#c>9POGxsS$9f>i{J?xIkE3q+|$5F`<#8ilfs?i`y*z!gGS6$SyX#NuW*%?Z zb?vUHZ7k&g8`fFhQN3n-)B9EV_iWw$gA1!*r2mAp6RRMco;XOd|9D%BeUPGm#WXxN zBU7YoQx$3ECi4#S^XxhPCVP_~H)BXyI3u%WmbfCre!5Xhl6b5naal4e+Rzf^1Ezwg zA26_aJG=G3j&K%ki%<^nPe|e)YY`+{WlbI(MT9t#03w^K%;|P0GoH@a0F=V{V|I%6B%x>=7u&`i4QHnA$ z{fEcor|o%7;ey{G0!dCY})~YN+UxA*v6v8!9XqE9JeEm z$34ot;Lbl|C$0DKl&;T$yH+3TW}?P)H&@-k)B2qJ$|^ynmVn)wSJ@F$74TJw{#=J0 zLA$gM{qfLD&F=O-5jD5;8Ou|tJi=^`iL;s_4kSj1e#N#+qcx{PON8xgF*#u{m)fmm z7Pr&A$}PE{wcFB!`K%uzLgnh#YzKOoZ#|)_Rs8A~4&6H}{mg!WB?UWwUeAq#v`-84 zUHjpOpKc)-Wzb>l*KQ(}{`p8>i6u~?%7arEE|bx~G?TfQDU_e@(|cual_5T1u7|5i ztSWI;;eoJ;SS(30G4+syagwT9B^R$Wv1)>4!ruZXy+tmPS+$m@DXcolqQ8YA)0GSv%(=D6b?2V58LM-T0B|QV=8Y{H>rGqx>DupRD-_RJ2ef4 z%E{By#Lq(gh$4U5!jnn1j*g^65bMd4kgAD38Dh1|16BkZPK#l248Iw2@#z`TwjW>Q zPqIHx?!373f+_3`G1p%9S9z-bUrh|OVxMZjgC0=o zFI7xi=)(^7+iCjsm)M?(k5}&d<8uPi;&G?O>HF=&a)X#ZCTn4dl%R5-)L-hFLXwRJ zMNFZ>{(~tbF@>r=g>rBTm98n|m_nb%SOr_4e*>z_6q#FC9pfUriEU@{0>&&XA5o1+ z4b5Ms3Qc|lT`-2{Wm9+3CY4r#IM}&Pd5AH+T;eLx9- z0SJ>m&=QP1*&$hs$?+-VY{pZAZH*Rgr2Gn@XI?t?QhvTrGw9pjzhL=+AM537o_fj( z^eM1wc4MT)Vz!1kOwKWGHO>-aO^Qmg+mjPhRJZtdGTstF+6wB6b?VF0X=_#>+La!N zPI3gI6ZGiTIGt#>SYwLKmT*+w-!j57#5&wj=Weh}woZ(#cU)kZYn|sl-4q6Uc|B!Uh@q*=f>$54(JN{~U)B3sP6YD3A?=AnbexLHa zBgbT^wR0yVJ_dsCHh^R_9=j zcK(GH>Cz&{M1B=1Mc(I{zh37ITN0#YVv;bU^y@Qx;GPmkapmGLTUbwPrbU7+)?g3f zOC;+_WsVKGYtrvC_9X4e_%!LWjF0mp`q8Bs z%ZqL+ytQa&+ODEElitjDGc!ysYv&)gS>~3O39d*^Ef!S!TU<{UV{;T2YMx9u z_LSo4v}*6Vq_@}`X>S*O=rzb`%xf;RN^z<^Db^967M~eckXKlfHllbktG7+b*utZ% zWG$P>rld8MHJ7a_+fimrDo83ELz2~yl;+5^<;yCU90`uPqP1zarM*>TaFuCgW6Eao znNpL|q&69v3YMvhlNQ^X9ZS*{XIz?jwR(;H8po!hRb|iTzn%Yi+7D^A2BXDkH@Q=- zPJ6uDQLFHWMeW5a}*@=2EHMYL#P*8vlW?Rnt5S@pO}^y@rvo`M~nyD)Gj?bWY-0@kb{Drng1 zn|P^pNLi+Dw89hiSy)q=N8MYj=L>)M<1{}5*==Ip!e8xTo(7+_>I!~_}@!glJ-c4 zB~yeHDf5(JQiG)A8?8}kc9^!xer{*%5VcFS2VA!)naL7s=kM*0^0io*wOy**zsbDa z%+2jmzLp{+{k4*|!=64nyJ+Q9xwfEXPzuvs?{@DHZ|8VH^ z3v9ySVev8c0x|MXmGZ-oPDQL#tI$g3CSQ_#d%**VdkPL094|3WvNfyChLy&Zrd8@H z!zSYv!Ik5~Vl?eB8r3}&;*^98RzsaZLR}weWAwU~)_n3Cp_9VJo;S@mUX)t9kVpC@aoH!VVxP17UOl0;o-0Yq`?!)X)MYu z=A$UeqaTmB#c5@cep=hNND3^oGwV#uEPQMa%U+UEta`nXk`hsYy14wd6}%eTRI{_}-FU^?fRT z8up|7W0RUqanWsiqoFpPE|3fA|^|*tC6}S zcR20oZjUb~Gt3wP^Bz-;;ZMjVPda5UeGI*(5BU2qI`b?(iktzC2rfkcb9$bb(9P1N#O1GxP z>lr8Xvt;Yvr*%zU;CfC>p)WXl9U+P-6j$HadPRvT!zAW$k0&bdIz&-iHz^sKKcus} zznfTR_Ig=HP4#!?Fjr1NpN>NX6VemSVNNiG^bfNqsb=;Cn6%Gp7j%}?jqr9(n(MYj zCwjeoT$f502K=3GG&W?4`6`03`djgzNazV|fm>fuZcRL=Tgxo5xu-|XDm6Oo+?|@} zv`4#BZB9FLdrVGyl-m;>g+Vta+PD}un^6prO%_FMsitP*D&ujZ`UO|rtbXpq?wUMqZw&5mo%rwb9+vEW^9k`WR&|fG2}GC#FTKu#&!u1 zAdP!K2y}^CN-5CAwA)g8EheXdQcRmtdZeUX%0gQf+J8!+;BA&o`)5n+{NL|sCP#Ps z`TU(o@9F6n>FIZTf5&@*!#L8D+|=n04#2s%GN*?KGhe0+W2^PeC{v2|M~9-!bo5x1 zzU()+zi}6nF2{-wcec^*q_eYx8DO3GpIznw3)a)zJXBs_~E0yRaZEKS%?!jy+ zabDj*{nY13oye%uzh#`bhho$_de0IWZEj?e z=I6LKxYM}T=tejFQFoJW@{<1ZC*HvN`1Gc$uD z^cpH?$(ay_^0jM4HS}1OA43-JLvWF74#UrM<1sj)c4yDIG!#)z37+r ze`~kPHpDn+3vF({huYo#2=y)WsN?7DoQ;C;q*;hq!}eBeUl*-)>l*8)sTV4*QSVno zt4+0)P?aw03-$gbkD-62zp5Xi5Al-=k5a#(j-i+7SD1IHchL#-Xa0onzFqN_y_HIP zsj?0|NPW)!`S$xO)`smHEs8DS-tdye9?Daq9VLoMDPA?8SQIXk45WHeG>G-tACMJb zuMzShinF}|y=Jkq0SlI7O0=aI38bSfy$j|Add((Nz$}8?i$x-VR5aF8=??T@`@h9u zcRL*R1r!A)&$?2@T`E;5WUnkRFDX$ADyD8bqTvJWEbAC{Jm;VtN!G-&Ua$HsrMIUi zolec`?#^VA-%2SypT%NM(PpmqK9i$VDw@X3XxNO*Q*@Uu==Od)J!VIfc4VKT|69)$ zMIs$VA_zqy9mSSNm`KMkX$%vd4nMMZ$#+mM0j+8iv^K>g7LcP5>>)0Qo6`j28UpCI()?Id0=W>K)3bWrmM>H)g6mt>9y&Yxd z2U^=B=>6RCt5Q<{KUrqI25*|D`_J$*@- z#2&MS2 zP~!=y@T8R|q{2TEy=?HTA~mGd$&(}`22He)a!{Plq1w}8Mr<{eSa}|rR~%5bp{tb}iaV4C&_l(aD8EwPM(-+i zyMke~rL?rfbSNFAWeVdhrIcieu_)$}&&On`4BqOYx_vdJs#Z(&_BHKSQ@2t#DLd2~ zOLtRylsiffQV*2APCZt7w0*Mur@kL4)9t_Uy`~&%pYi=s`J?(~`(LQP`u=Yzu^g@N zEiGP*27DWe*ZOWzUr>HfdPRAq^n2y^C5K_(YLS3H+EO4=gpLHbXw0y9qeO!E6r}~+ zZi-4FRZ&2E%r6z)CB;`ND%j$|_wMtns*kpE97UB%sWeyGjIn`QEVO7^^wH>K6m;>M zQA_j@y&bh98ZKd%L``&pt=dMEG=7(mP~Qj07Ajcl6zA{@Z7{dtMuAAlnE(`VtU;ym z=|^y{VGTcmIhX{V0!7I!)RD20YKr2lDUw{HIHl&BI(_V*ujVVcYewLgECUEDuqffn z^SLsKi3Xu_5WzXS5wmpmWFXOBnoE~9#Jz6EsTVgy zdl9xV*Hrh?NDk2`Mdw{zu3KDRpx>ZR)2yqdMaJ)pkG9}<7KpYm@LpqXc&{;8b|RW? zkz3rd+=4mu%et8Q7UHc|L z_$lgDJ%cp({PCL^I^%M~B|On+sRd29pccZhEfSpB0=eKkoZ6zMFZ*(nYYmhEuX|)P zOJE1CzXby->c+dCnc3ZlGhzr_Bd?f+L?CJq#B9Rn#DZiK=oQ zs*L?Dl@aqQfQFfQKr!f5pr!b!PXv-{Fkt~9^~~!D^_W+mVi`x1R6g8eT72r+GlcWR zlAOy*(Zp|;CAd6?Zh49-;Ma9ucePm9uG|>7F?f4=xNv`v-Ju+gKb`({;Mc)l#Vu+| zDx{OOL@m{mE)_0LU6UFvj1_DzP{<$51Xl(BP5pJi{CFDuIR2XNSMk?UucqIPTY`El zl;#{DE?Q6|z(!+On0TTwDx~FFL+N^KO$?hPtfw{Y^?GQINrO@dcGcI+^h$%P1bitCZSWMu|YI+;nt& z{NvP$br}AJF^k1|Of6xphl*+u3X$OnRF-%_Mn=&nkX-hEks`!jbtZ+nd_mpYTvDo z9dj(>`7e!adSI0czxm~J+sr%6*HIoSNzLk+C1?|SKVmEnv=P^?L^q*3(Kz*a?uX*< zDXU4;sfCEyz%dV)rs!jOk@Kb{h6;a+<3LA4u){=M#Bp{e+tM3x6*I;a2X0bk4osXb2rQ#{qMQEOn>~uKhI6h z9p1NR&zDi}e?2y~18kJB)4w&D@q6f^H|nZ*(t6GXc{UWsADSza?NX+szKDb-LShqwPlfjgFhd;qY)|xHw$e!`*4yZQt$ql(;+l zxask-B-_jOioF^thbo~eI3DvREv!W{nS6OZnorkFCAAbTMM}|mm3h_W_T{bXY#Zzw zr45-4*-!*U=s-DA4Rowi)~W0K1MQcTFR5Hoy`M$j(q_D)$EJji3HYSz^eEYv{uX-fp;Po8i^QSGB^xdq3IPRwLv6}%rr>1BVJ-% z>7*@ZEmIj0goX>R*b?64^Fo=j3At=a!XiX;WLgt479(8IQbr_?>6{@=hWm7o8XW|i z<8s@T_N%1JvtV;zb2>|n7%4P@O%ZFRl2|L_1ysxcA`YL%0GKe{NX0hN)|}-q-Aty5 zC!5Jo;3i_enUDv+b;;FtW#_;1-FsI3={r4@$oKtfh)pE?n+{&T^D~{@skyJ*xBAU* zU%#Wv=a2H{>*lh%AH8(fh4ag+c3yeI=PrEc4Xe2xE~1}*_A^7Dy14zyT=@GpetO+! z|Fx<{iV(Y8ae9W?!#qK?Q}dV=jo77DCtjht4szWSU<(P(W~0|9h-^<#LKzT(O_u5P zWiK3pxBvYBi2E}5HasBm%jDQ{jU^?UC(1*!;!_G`Ds0N+N)7 zyxOuvThg++Td(g4alC`osFoFI6~Dr^qPnVcQTK{@8*NwF?y!D}|CCK!=l!IYj?}l* z=^?I6ReB4VeB~Jwpae=dJ$=Yp6VkStKqPN>RTBCITF0v)fze2DlVB2h6@V)lTWyW9 zMcJk>MP-*lD<6wU2%fmqtM}6Qv4{3-kf zEbGh9psT1jm4IKwR&FAa7)y*NOnTx(f*wntgaijAo}m{}EakzrZ=~j#LRabGK(W@w z>W-Sm_OoLwBeCcNi~3o_E}FmS9}Gqu86C~`VQ$J|>kagE?`*bd$72J4szVmMG&544 z0lPoztidtaY|-$X>|+Fs>;`5U0~iQLF01wgV`f)pS4S6Zv2r{|Td-x*LR+e~8kYVc zSCEojV#FRqEwLVREl7276%AD@HaRE-k)s8#x-Gpyin!N-IStj4ZEc0X6dFa?uE0!* zIdap!x{M%|ElZ7J_B_yrpR@oT0ErZb9JNjjKRX~v0g5+uo2{m3wwezwL7;ZOt;XY5 z*_not$0d(TD=w{P5M=WY!^eR{GgGd!qodOZa#=h+_nG_}@FRGL{~kR09)d5N7Pc{{ ziC*@P@s4?0ZVP9A^4`X^^+b{`CKJWUiCZt|3Cg@rlmt)j@Re=d=z-jt#T&X-f9eLO z`iW~7wJpA7LwwJbEiJk3LVG2@VLTIAn7w1}N1yC*v-aMu&o90Y4fd+Jq1y5-nDbA6 zc=|Z=l=)uDOU2PI49_{f}r>) z6K!=(F~8HDZrzFxb7KUMu-c*nG)rW5{q?LNv<$C$ro181qezUB;qr&YiL^aE0BA6`w$v0$xgrqBD2W z7BP<-sYFkh8S}e>m!g!oAV~Ip`>=i7ZW_a@NjpQ?0a}%Cb=rQ+&e}2eAMUN%hZ5gg z)rjbUPuuW)%?^$l+HAD$^wmf{I$@2psxdY;sWD*<`FSNssftYqa(F9ZifTx;1%gz_ z63`kMghV+Jz&ipoK8QvK2GH3oLQGo*Y)vJ~PN(-w8X;>zJ$F3xkH7fxJ>TsA>IP9$ zf~^kZ%9n4bZT_c!+FGrq>AyVnpFcl&|5$f7bMTAH{ZedrHa+{B_VSC*O+FjI=!{y5 zX=o+Z9#Qn?eVhq3^#|>@5cqE)@ZUmofW?~-tt^afrXj!NAkb4ZgeCp~mz!RXD?dI2 z8YR@mU=feScXqITWQGvRQOI?9QjUQb{b*~xLdD?i`Rp6bbkMcVwAQ@VvX0#p*c4>1 zGT&q#qsF2K0zc4>X>U@$H(NW=GPFTiAKVffQig&zDWk!=<$GP@&T-{2^cDJv*nadp z`XT#6^&Reb@Q>O_q*&;c@<#cd$UWLv>_m)pYUn$s-=s9WM6kf1LKFz#5~hox=vb7d zqEb{N;fvwu_&J`F6H$Bg%Fr7aM*h&7u(BaAKixGb^e(xEAJP{6X+%J4guMbS6eZ$g z8^RcVoSLK{pUDahH1&;-`akKX`~7IbkNi_e(B%^rL|G(@F_6w|S=6%VDf%-8RDuZc z;ONNg$l&o2LRQ&qeP(8ai0J7h6JDWP^aD zB&8O>a_^9L z|4Uo0C+xJvV)M7oyYNeUK5{{41zmFR`^fUftH`mpFIh}_Zi<9ge&kDE`EXHT2Y&j+ zr;lS)elNC*^Yp3)Z6%8&Zk4epgs3=!zzDHXTF6TTme;0%SaJeAX#xl}hPB|5UTuwbsdgoQy>_cMK}~4iV4q@NXtgCdS4vnQhh2+3p_F$)FdSMO!Kc|w zIS=WzNDk$45ylpwY*9f2fFXN^ykp)cyiCM9?xnr&X8JAQ(McC7P##`ZwJa(u+STwi z_07zVLW&~%froe$KQb1N5|NLTbXH9IlUWm&N+h|AMrBQSm1YwfYBlGyX0`*QRd)e} zgOQ0wAt!49YiC0d%UBUt&q_u^9sA6&suQ4kgNErJE*e|;`8PlK{*EpJfm=>&g^r(F)-^I#iKZ_Wv zg|%{m*|Wm3hyJu>w{W*~N9dpFZz=~}FVnAyzmiVU|HZiEA$Eux#!t7$`aJuhc!I^6 zjg@tUycv zQC(2HsxOyo(8req5WErgr<^w(sEJ;*&AZFH*UNZMy4_j61SIogJUF4=kSwo zliZt}h5LrXW1{u|T`@UbE;%48lA#=uLvt`E9LNE8V#R&wSQK8=KrYw>=$jn@*fIj4 zy%~)2NgDhpkW_ZmiC+=M+S@!BYeSwf$)X-5!&0fPE{LZs+H}A|A(|c;AjSc4xQ-G( zFN=T87OM$*zGlY@Os}&qU1N;`WW>IJu@x}(HMV$Ti#N8cWJ`BgYaU6hshYE9*NBT3 zWzR$D0|Nsti|_2@12mBZUQZ&LG;H!;p{-kYUwlVC;`#9xzWVNe9(w47+1==Iv!q_$ zvG$X6&rff>@$y^Tdwz@1YwsfVC*SDa6z|eMj`7eMiehdxf11kDT!Xt4d7`iKI;fpI zv8w}FBsnaIb7T-lyrr@OqWv;3s6!^KZ@5h@pq{K)C-Gc75%y7(m=UK?V4rM(9O0R1 zX}W%NMw&5{(lo&7Bhm}-?+D3}2s9^o5y?3eK2;B8EOC4#E`taoA`5UJBDk8|#;dxG za3g8Q+rJ|C+TqCOnkwjZSmB>Pdej*H5YXq}qdn+(Fv%=t77NSOJD58J^Ft<7%MA^oVmZZ`Y-!XYjz4rx;>pivgnjPD~c!%#JYcIs6xu_ZlkQ% zjWXUwO5GcbR>mC&NPLT9*fH%m=CC-_+|!JOVH?gSgDCrEFzFJ@wig#~4IUpQj_BUr z+0owmEVgWm4KK8uNV&bqgeRHsrh`<<9S@?0)&W%m8Fm;Xo#R)A{H$2D3T8FINuenMsIMr+OB$K;d00Vo;SCY`8CGmJ=6+nfO$awgzW8qAo*YiL*=E- z^i8cdt);0}OTlu{J({WBxn}dW&h5$J&3jFI&7bsrO4(bzd;TYv>|J%|n)`kCD-W)j zGCgHJ;5(rFsPdy#)0>ZNeslAQ%>lpWDN9v%M`W}4Yuw6?dVumW9nqBmN?jyFlC~Yv z609zl+sch4kemSjkc{Sh_uH=g<>Q~29T78!O1!cj^)kpQ>UA%==FGLsSa$?_inG)7_VZ5$UeN0J@NF}tb zw6`=-VoD0=*OCA%QL5Go%-A}#4xY`9xfNG_d_Z!O%D)p|%sL~Gh8byHB9cbL0q^zs zD|^#uO?o&zoj#T}r5$i&x*4^@mH*IX5E$w0+UC+`ee`)u!_W3Dam*lEK$> z;z_yT4{rK-Nvajt>6TcmVMAGr(SAN-{tmV=X@@aQ2N_n@(wHX6m_*5P?iSvzuDkf? zqn{l5Ue>`_%#4`5x$DSR7ca|2qNU*Q&*lwoyY`D8K7Yq5o3qMpsbp)&vvTX=O8@GQ zEGf@@P%L$CefGdN%aw` zdT&EFhPrfMT#0w`L=o~z9JUn_E>ytF13aOCKaN;DjrW>homK>}K(BQLZ|N3urCQEVU{Imb5KRFQ!tt7MIT zF;&4Ag-be9JW;XG3cfrQN6>1<-|`!tjof0fFEpFLvpzRlaKo<7P1-CdazQyG)Z^(_P29n5+f$ zcMWw7LzCWxG)~Ecol}gcJ6rOZaB5`>p9xDVW6?}FImI~iLadr9EU1L5i;u+bHh_c9K(>0 z3w{h|hmrBv$T2c>H3uKer}9Bs8LO^J^yzdO)iw>O*Y_R2xRZ5p^kd~2fHqPwsfZrO%;7^c}F zNS0XbZ~;q;#f8z}D_E!>9h6=lY(oM6`o4@Aitu5)13szznO z0;8g8TI=H$Xwb1aR6s1L5SQk9rU-TfHX|={1}muJaWoVkj*rJ5jh~2{wRnG=)?pO~h1uR-A!S#$ zQRYiV8B35-FR1-$UA4y*JzG;q+^D0wGOnP01q~^~%DD2VazZgH`(pe4*_e_DrpW;wz^Zq~ zfT>vX!We=xlbaYS3X{6w7Cv{V@)^4B=r}Xs38w2zYfn#WYj4lT)V2k4ixw3ERyOPp zrX9#_z87}%wzl@nMQ61QHB89<-u39R`*WHq#)m2T^yPC)(O&ajOtl$wq#@^RX%{hq zToHJGCl7#tCY1(xzS$(tS9O;`p9XF7&}_%n`y4q1SKcG1;L2~vDG@lu3a3OUOC|;M zDx`IA=A|>^?01^lX22 zJo~ug@zA4Li-tGGvW$dVj%67?mriL5QsMMs6&}g5-sQKpssSw{u-+-;(CrdM2`v6) zal(aM;P~ooHD0s6tjZL!KA#`IUxR9i>kpn3uj7%(xQ0Xx!7M8$G)B{icW~9T?q~&Pf0HL>;(E%76W()_b$=Sxl zH%4xFFzgURiJ%w>qOcS)y-w!22K{9H zME$E|tr6##)5tR-Yuo}9;Dt5D25%D@fr_3rKte~V5J3%iK*l6QEd($Wi2g5lfw&4y zoKR@ugaSxXxIh7hERfW=f`ptAQC)=8rQ#^RUF3>%tE45gWUoikL+K4uaZ%Kca z%qyJBBj2-rkN-cs*=O}ikx(SEgkC6GY);W-_X{C0?1)&_GV4ui%>&Xp=Q@`~6+@wL zWG!uKi2Y)RLTDOE44j}XS0cv? zg<98HTti?D;F+UX0i6l>VRfUgmqbK6WtU4*BmS^jz)&UC!qe6;4@e~y>qr$BRKp#M zsUl^=awo1u+!~^_2*!pbM7t59A!$0|LZ%cg@{*+RofPGpLhtFT72&5g8*jmsuc``f zD+yx)eL_ITgf|6xSeS;Gj?XutAjKc4p&EuZRJ>TE3Q}Q`B+Z%o3uvq`UZ4v@U9~B6 z%l_zN|3FxKWE8S0F}l1!8ilM{@SqKj^qw7)43oR`s_@8Q6k)=VdV5KxyrY?4>o8_| zsVNN&U?j{N!;@j(5tb!+^P?jW@)<>BiUVw~P)5F`gxSW8?UhIxTV8kx>6q}+BBV@C z@3YlxP&jL%QCN)v5*(g%lEmC>lVk@V-tGjO1=3ooFyAN3x`<6vZjO3J&H1+4JI|sm z5F~5fSt)SQB)a#a8y39#?nhcmadrOOqGTXF_XoAmH&<91^Vmd(=J&KZkz~I2)X2+= zWkGO3z>xz%L2|ANSb*VVToAM zc+h8>_{xXj1;JNt!GhR^e~!ouL}UgM0A@F0;eR9+hR}pi0st)%3ZFXt?*~bA-25yS z4jhd6r(9S-*j#7g;w%7VPRZ&R5&4+ zg?-+0V2c6Y>%C`z3O1RE|BT?2^RY>jGHz2V%jW9!g5MEQ{Ank0n(zH^!G^97!6A(P z@G=8E6D?vXF;7q%nSX6)5#Io@HV23g*5@Rz*13LlsVVPE@IvABL5nWJJ1%9(QQ5Z6 zrOl(3Hji3L#&N+>OBXC#K#p5L2xS2wlm)BZ@N27^=dEt)m(|S+;L2Zh6^>oa!(~=y z$>~{gdbX3~SV2Rl1gCXEZj=qq>kPt$JBbkthj!9rKN&XG=_J>1l5032p~bj{R%*mQ zzh_)SYbCMKsnfsGZE&bYH};>xGz#%juUc$hvK#=Fwrt&c9gZokM{Cw^Tfb{PvwnkR zS(}o`+1TEk*~siDf<_x0#K3BH8vdFJ?F`~Nui7AW@TVM+vZQ>0SP5sG)_O0#JifY( zHM8s1Z(xGazN{feq!nquq+Vhb|irMRNckF8JjEm#*`znIH(_UW}u znxojnvJD&I#Uye9Z(}VcGrMeQTS@Wp0~mcu&Umz>pGtr&@V?R>|) zxn5UK)W@3wsSVZZR@1bnd+A);>YB|Q&8_LEuFb1$tLA#@?S6uDQzCL_>G!sZ$=0n~ zZdtWzefRBiH*L_o*ueKmF=s!zd$^!im)o*)tBBQ)wbDhntxXT*I_EqWcLd_`K+k$~ z=>xeY9u_Ey`M+3_mFY8*tV$$ViQwTjBhj7{y)h681=xy(;u(&Jq{bLrA`dt(aj$ua zd(BJoBD_uU=Y3+jux5 zLj~!02?U9?jaX)F?RJ<5N5V^s;cwHeaWUS``g2B>Sg}Z)YZA#7tDO&x&ygPz2oT9| zvYizhmlnOmJ5T)IZKO_apv}0x80U!glomn!Fq|dA-#N6(UE5NJEJ1 z43P&V85&J-kpW=>M`SY9%Kxvk4P(>ps@_`Vs=)uHYJYX8I$RyEn)4>4llmCmnygwT ztH-MJWEBnJhUqF3;=Gx#XgJ<7nQ(k%3zrEyR>ne^aLjPLwWV4Yl)`O`gH)`&OrAI% zi;1Fx_j%*&IEN-VByz*t1oskW;=u71$dp6z)<~v5GX!Jh$1>xY$qYkfqzp}l4Oub6 zXND?<^DX=T?|hRLm9dx-D&q?xvqdrcn`~@M7&15l*+#_o_IEvRFfqtEjb}kX8Lj%# zXIEXXc^$U4g>yYFz08{y^xb@u%>j()UfL!`ntV9(y;U1}Z=c(-F`^Qmo45wuymRCe zbD=?R2y@!9t!Uj>m-~t50t@Zq%u|@pL@GoJ4NePU7-%Fn5;$krbP^;(3w{&u4(x=A z?t(@WIn3lsa5gDH8Jf{ZVKclAXOk$0Y4363Cs>$@)Rg?V}Ui_pGUh!nawBn+RH$4lP(3A-;k<-(l0vB!i4&`Us zDHG*p+TW4h37oTuwZp*K*^fI=NKQDCbRwAK6PBb?bZb-y`8DLV;+n$Zn#(R}D8S&A z$L7;0)r?mSo4NT%GJ!FzVX8xmHtEjo^mfaw{H=~#qQ>(*-T3YZ z-MWa3bZLeTAl|G0&;talY51tL#gU?r>cBT+gS&wnAUV2{AsNw`?4M(e^q(wH^~d-g zpw^nSLfS0tahmSTp`Kj5XH{-{X)d9|R1?6Iz30Rv%!a^h zd_IND-i`dpWqa4$eaXmO!{1ohk#6_ZR?TT@XUgS~VqqnLDpto0Yq!q7@DhDfsTgN! zqp$3^?D|i=JoE4_kC>l(=hAW*29?{|wlW_XC@GFzbKlq&>)v$1l~4U*Rl%-|VKAJDg9kS(^;?V?WW z;wMb3-io%gq6p9;LG^dWQYmdgGMrjW@wQf{Ta%DUfyugSl7NH(hM`!D+qYPdZb6np zq!qPN&Uhpe(a@MSuF;exVH`iL9n;L(Q0B2S<1`E;K6-p)v@w8kbY{?LAnqDL4!Fl4aCUAS&?-KQ5Y3 zwV3|#NA{?wA_%DnCET0@bwLUpD9m6KqTFev1^93Y}Rw+2xi-L4!V322Tgn|90xd;Bg@fh%9Gu zWcQOb;lw+4Hji8KADrg#^f_-M^{b0|ME6f;Gv-(Ev2@Gs%WGYSx5%3dGa9Zkb=`c- zk2^k#RpV|&tih%!ejbNGh8wZTJ}(>aRzDaBEL(T6%03dwSXy3_WiZ4XP#Avo`GJ&iT=Ahc!xYeQhq#>D+Y)<+&ax3t z%{JgB6yGLgW%XHS0?kqk_jHD{P~Q$M2Mu+r6tD0TQcSEC0kMck@;@YtNP{MZS$!)5 zN~m~&0Z<|~@GBy-_tQa>F!P6eQywLA&1$aSe?2_S;2K8zMAi(v1E5wT3CW9?^keg1 za3E>jwv_taWun_nW!R$WL@;buUVk4VPUY}L9m2V7l_VnOvvMQSPKhwH;pB^=i%=wR zUtGB5F=GXSUnf*qEHW=kY-}5xRWXi8)2pA z^wWbNjh;P>-bV3L=DzM*VD`CXB$6zA2_2r$MJoNalCZCOqkBoGFWpZIS#E&}JtQXO zk3AS@-55-Sxq8luKYHWdwht+2CgY=z zgP2I(o(TLcx%~d9^~kaJZWYDL@I1$m{c23nlRt6ij}8%W`Ar&?eV0Ydnq7Cq``{-) z7t(Md>#UcZ8jMEKihQTf<|^)1{`J~A`A>Wq87)U^43YIo-8Rc237F%XxMk+{Sq01W zAJM<2cTOeASstXDN3EXaM$M9xM$j4OF{edRJpS0upT}zHJe8^>>_!w@zS~+wUnU!R z7TbQv*ao{WcZq4`S6vzO`NVrozbL$ienF!*C?PxqTE5l2n11XATsp#3hCYdo8s1xc zum&6_8_@V|f8=`~O(|$9!H+=V{)uB*HA~!%Qex0CODs!N8Z?qPcK)rrIR8`Jx-~1% z&K@DdZc)G%*w@-Turi_c6rl{?@iQ$3cmlZGd8BGrD^}63nr0Xva8&}QmmTR~jcVPJ z=Lg)Er|176r@d^_e)iuaYIbZHti`M!={J-*_|0scPv<*dEK5?6-E<|jvkQXs3-e3k zyydL~CpMC9Mkm_O-ti4-bt_R@zX;tv#q~aO@1oMEh*mX3J+`S9anLMPUa+uckjpzn zlBPGW7&t=oMo$bC(;$ZnR7|!Lhg~RZV0uz zMSp2LGc+W{BRKK4BT5jCS@cbJ=(Qq(u(t1Lc=p#~mTUvy&aO2*Mq*`)aZf9N#Yw?q z76rh2+bX6UnRGzc&`G;j6VW8ryNAHYe@_(ET0o`6T@$T9#7We>_S-myqlFv?k(a<3 zNtYREG+mWTg$JcJ43qfQvs3eKzHr(2$?gR|+ap58=)5&`uLdeyT6h6Y8sadj@pnUd zDO>AP4F0InDjJpHYPN($hQqxs15q^Q4dNvUNZqFf3D#?GD5?~+{x={pgKewbJOBY6|Ser;;M*#SKFzujj1aD2&iF}BCB5XoNQH}wlGGRalR_<9 zcu^TPLNxZ+NPCDAFSf*Hh58zfa8*@-Ftxa9xc{ehkR&qttJ+;@L^ipBwdvf{U}S11 zL?}CR02$~n9`{=~AWn>&8u`Fp_%KrEx0FH*`7s%^6ifxLiui@a_yK;1N{2+zjOGjE zcz&ddpaAuj1a4hL(ZU{Jal@kPlCz%16d08rSo+AD8kyj@{#; zjpqrIF2#wP=nn@;r~X7jK*Ltw`NEA7_`E-fZc8P9b~}%%=svuVkiCMFQb)o~7qaCa z^_ob+kserB8*LOtB6((xP`*tnSV~20$H-H|cXklNe-i99Vbs5&8_nCb^&1A_ClYEBy;Jb1^ z&WPz#RQ1xtkM+|^EVny1PS(MlC!m{i9)o*aMntKmlsfu`7{(S(Z?IyiI+gQc`j<-0 zip7y37NLAOJS+ENMoMP8myQk9BISLXKt=VRy{hb5W$L5zzd9CmoWBDw`jo4`t&}~4 zDz&68VXk5n7<(C06;r?kYjc12QY6qk?rv;oiL2G8$(7RKRa30ZTZ&v2D;L#mF{D6& z4lv5MXiHH4RF_$5hrNJS!P31tGlgsTH&x1K(x} zOll^H$KeDs2*zXBfB@KJIhEDk8q4sl3E`UnGbU?X6{;S}O6oZzx10rN_pj9oLcWdioFr@|i$^D+t4Xa_q}1WlHd8e4)wgwx5p}rZXz4KYsv>?$+G8w z@4DDoW;!@0$>=h{H3LGR4%=_C^2#nENfc_hf6fb61m@W&>R6W_uev<1kAA9{u#<@| zr5B0VQu0FW7h{!)3nvD=XyW zf;6XZl}VKjGt{x0_lGxf^7N7d-K>TvC)_m6%jfBwXbzjr*53=+8HV0Qtg%kw$^%Sq z<`>GJt>$RoQedhk*L%;m+)6oL?GV;}ul8LkbqV?Oy!e0IE(1!9lQwmXuoFz`TNl&` zQ3|WRbWAmJZu$J4Rqg7U8*RS^Z?#Sad$pz&D4Mrpzt%-cp^?DVtpLAPF_~ipuBtMt z!-r$!OjUo;{(81Qg9n2BYJqs`w}azLDI3)}FskY17*|C&3j8jz^88D}%By)#ky&GE zhPzSsnTPB#ZmKKxfq)2~Tfocl+o0o={xnGF`MQEcesa8fFsq;yZ`1dqJ>sRpQG%2h zx6Tha0*%g=n{pqYZdqOosFsB~&B_1leHUFIpKLXfzaXYv`^V|gna{y+>SR*6A^D(h zYzO`TDN;%8CW#rbM5K(|>HMfVt--o+nORdKKjx*(uVuGtpmm{6BU6w`_%asVD3A>M zmXdYR_~bsf`cyoPmAV*q_LooZwaaW`c-9q3yIp%{WkJpg*@IWA|Hz?zcuG10>LwEb z+A_3SG0N^O%lC68`10|iroocoaq^xzO)<_0^T-yt!U5C?m3u8ZvXPTBEJ(|xhNAcH z((rtS_ErP;uJzAgL``Y<`Pcwi+Yn}Ejuo=+J=^tt)u}HN!@70wm==*?{iyjK*EpU6 z`?<1r-uy$ky^!98otrT-uImEUfV21f>Iq|w7ZL&wH#7(YCUe`%0S zbjQ%s9sk1Kt9T^-> z_BG|(;f!g}yMJ2+KkFmp2NuE5x%jyW`!hlhsq}uNwdfA7Fm&iHiF1sq$F$;( z$E@BP$_L5`tuyPHxIg5%oE?wHOq3%7&#ey4jpM?6?Yx=yLsFytda8Xrn>ukR=Vjm& z(BhyYc zi@sQI?RZ(cY4<&h3>H{eyanacP95t<6k{@LFk4M5D(Tb2meE90g-TzOe~EX7KWr$v zPZkv@vE>ze#*U!gslpr?_hDiJs$oH_#~RIl_{^X3qL5FQbwV!z@mR^;aB#u@Z{oiv1CdbYnBPLipObCHR$G|G=hw?OPCqwUjy zvYp5Qw}@mNf>j?k-ixa=boSKzIO!7Cvi4?oDU5}oNdu|6wS32N(Oe<++V~wn0fR2HWHCbRs9%E!m3>S1rnC}C&o49BSAV&weK(?298I7SgeC)0lzjG7`6 zVsgr~!j2Y()^e)!(k7<1&KAxd^ipt);f1Vlpl%x#D zC}!$p{Hdy;t@A%4I_Z3JIRC}bgA)+=Z?*o_`kzUB>LqJx3vf37Y{&KQ5hN_EolPAX zC9DmdO~p+A8U3H0|FMF5rryjC+aRWI&|CZ^S9Mxq&_i*mB2uu0sF4DyD-hqjr06k> zaR5E%R%U-0B1{Su9-dMF?&};YZR(k|bYepWm%DR5T%>up(D|AsA&h9i3^uXTg1 z&hdKu8`~Z0+1A}O|1$`7?4kZ5HH3uFJi|5(BC3+0xNh&V))mRuqdO4M?;f|Kb}>}5 zc6)uI)ed`?oUW3ljhMol4k@)8zTWGl(9pH>%jPOAnJI49AK46$BKgnzu5)VzwG?5W zdtd%6(`#p%Neq#yCDUK{BwwM7B$r6Zw*7};W5WXN#m-jai!Vwh4E2% z*G4QPjm(AfP(|V4k^ueTSHDWH?;inGKANF-yrjjp8$$~_WVO{+lF`9khV3J@nvGR^ zFpr)owf~s=f1~FgBmWCMO#cHr|E$n|R^$JI5>aWff7bFJ4$J>1Sl}2xo2hs>Ih)!@ z+nU*ZuB);sz~WQ72bJ(Aa80Qh6&y`W9X}C7^2gkxh%)&{`&dveH%Av!-!Knww#;n7^ z%>4PwuEWm6qzA{&`ENQWv(9I;f12vSadQ2`BW7V?(S!Sc_hPQueh3D{MPY_2)wxQ1ZXm3!!+jLJ&jaKiE^rQGD+G{r-7v|Ur|IG z1Qn^(X)G(v8yV(y^b@rU;mD?5`5!$feue#gSo}Er@Yqx^yXicvnp&UaKV0+5AtMVL z7G*C~vLD?4@jb(F1C(h#XDoKmzYKqZrYmmJlumt+bhj#hA*h$LH!xFoR9@ znB_HcM~fA50%HH_4Izf9&tqlSgIC~&n)Dt`we1(SYGz_f)wNd%D29^has{-93qF1{ z8e4Z=_E%Xc&U&!1NeyLlxw^zW6TTOD0zmMrR=*3zWt>iloNiTAh!nVxmw9)|6J+!_*#~UQtr@i((Mpp#O>t zZL?K3EC}mPLtsOTl%Xt@co z52lseC!?hpMO%@+GKLDmpEC(V2oX+83rLW&1ZRVv2fXLlJ>t0s9607=y#9LNwRvGU z<4A6XJOLrbG5)AP7Urj4S->6B*|x4swG=vpSnlR(jb)gbA`iH3TYZXSgXT&YI_BS! ze39irHVkl;XF4&52o0Eli>#D*BU~vZ$OD&ZU(t>A9q)I4{1T&#{LFb`hB?i0waCQx z!Ly~$cZ)WF%xdxd#pm<91R>t9>b{N`Nj*yUfm{&ePqKp|=$6#g zpck*J)Z3285jpZ_v!Np14t)FwyHY zh{E$8Cl+uta~NkB0muuO0|)^4z7XY{T&ac%2rk=jLlp$R!KB}foo^EXI2TweKbX~L z&Bz2e@OvB>vuz3!3MMSV-_otrvA(Z7gL5Tyy?=9 zbv?nRDmWN_$u3J3+>x`%v_~_{AlGPa)!%-8NG=OhgaVRWp@psL|3)-LK%j7LV*JBkV_eX z0;~hSCHgFR44;?KNJjM$KqgjR{romnVVY%^>wthovC*xd9-2AZ{L#A;*BBVEkWZAh zB2yvR524~AJE!~2v_}7DR_S`UgPEQ)EB)d-f>*#8rpoWXSfe29A>qe5h3Ht`r=7Cfq#rR}W(LOk|0|joS8} ze>7~PH`cg$nQ2laxefE6++$irbNh?i85WXpdu-OfZY?CAzmf6H4PwDgp<;cWipX`3 zBky89?6A(%daNe8`*Z-K$fRCk8N6_%+ZFC)QJ_d}g}VXHn{Ua7a&TMX1UlKQfEnw^ zR5ZBSrDh={0)9nx&qBB@WY?6rGXxhkjBX*1_wMxQyAR9#f~Vm*g%al*vS#__U&aP7 zx!K3RjCY>eV}|@`JQmD%T}VyjLtP@q-1@+fD?K)GM)7C0J2W3$`h}*zM}5j_0X(N@ z$3gn71SF~tjOK)Q*$d@qbY8x!u46o1YZJKUIgqAyWx7U!*)x5-#H;WAZ` zY8$GK)of_cLA9Ow98+)cJhcbDBVYX@2svL!c}G|a0|>&mML$9zaQ}K?7GLY^*6Y69 zwV?00&tw3SM_HF|Ctf8?v-@)yu`_8O4skt-`n=r{X(1w9?QkPF;S{wnl%Q9V?l>t` zsVxl;dV2omF*kX;m9=>x*_y2%;F8dJU=|?CwfOk5VIfQ^OR#=L)O;=ob>N1hSTy(u zFVpA|hB|<^z?+*efQLTE0D=7FtD7hnw?Ou_hX{y(1~?B2&-X9C&nq2?hNZF03@I?Qvn=znkrWXqf}(tW*dvBW#Odjjy8Up`3+yfz`o^>w zK%flG>nM1ZKs4)^FERWZjo&|%IFup~7|vyWSv=`a^iI}40scllb6vgNg?{~Nqp3Vq z937*V7T$6$uho3p0qgp_+eOOb3hCa<;O7m`g#6wGF?~3+3FU)%$FTQ-ux|_IGbwME z8FIDX&HB|Z=4wC)3Un%@BHaFEMCj}OBI^?2^Q6FCm%KvP$CC!Nbux}QAa$xkD5Ysb z6g0*rkORoAe8jze7DTggFq@88+4bk2g{gTpqJI<+Y#0hx83(BRI7+CHSV|OVDp*c7 zebriyMN)g^G#$#<&rjqsT}?aJXv$jlg<3D1L_nC&mhPC+ICs30__0hAGunrE)XA7BFq` zaemcRPWl0nwX)jNp#B7W;)3Sl%9zo{`e^4$xOk6sd44=PO7?v5!;3k6=Hd$~~ z#Ko>W|9iOv4)~fgGWq7#S9u=e$GbMl5FVUcD556VQ^Ru#Z=6o2aI|Km1!_-CRz z7iKC{;I8IEs54DKTVl*-u6`H|lnI%#c((DoPZp~Ad()`XMUQ*8(Y-^T*dIxq(>-yj_Ho8_Jn z{Pj#}ed>Cd$sXtp|9m-s`v$uJ=dRc#y1u=>1?Ev1s@fWP?#1f#MKQ#6LZX%4+YKFN z-5qs7*ax*zN%7Ef;f-Zr1>Ca?)Y)c?z~vT@_M)9$I?1N60c&=YHP`Aw>GR$H8Kz34 zCFrDGbY4?jiNW88;4yycpbd>=q7u=0QYx#Xs>J*kEu61hF3KRsNiMzrTOE8I1B{a! z(gC~BSR*o(29XeJ16lgWH`0offy3OIIh-5T{w^p_yk76eR|>svT^jXMCC4qj`b%*B zPAOxl193WH*-i?Y$4{%vzWqtluE9^NP1BjA*}ns$Dq!JL}l{!A-IUyta})>r>QJHw|Wq z%|_;OKX~C@dsguJGjyA@Gcc^)HRv@5ssLs=+P!yobGxiCQ^_Mhekkh+^$4wtxM9HxJ-omeYt4z`Nz_D3%9vl&`-K|P$hb2Q!ax+uOM{6 z(?Z=`;}_`RD9i6hC+Lfq87G28PD$fF`{;U&iX(kpf6d4*>4t&ICpIsJp`hO(HS66h zYpF#yni7=d}TD+8S}CZ z3>#boCqjU58MCGRE!h$Sk4*>Ud(7T7@c1XdNVAr<@4oQjcd=AM^{B4vl^m_qz6x5ezmfAg~A-pGNn*wPc8149SJ zP&dmxp>6hx*VT|y{Wp2#8Lt+J9vGBBKWVwqocBNypX~bQmSH~Payr57aa@peQtD%X zl}*17BtLI)@uV}$D|9&6S97~Dg!`G~m((Apxj;u{-B)m5JA-C5?l=hP4dWJf)w9y# z^iczN>61)UV|?y{SyBRgL@mMMYN&bc%w(NTz8uLcOy2*6_Ay`2dpJ%{E3a_-9^gaa z_B(xZS+KFtQeMjGOtWcoO6d^j&56(ExQL6ZhOLuL+UT=HWhhhZEvkpJw^qMnWoClsQH$Q8|{o}zQ_OSy&4N0%eIWOL(^Lvt)ZMTiu ztn*^5)c3IR{*Nl8_U)e$Wh`Adh+a4Q!gKIi$hC4+z;Bd8ibcxBL*j;{} zT?H@jkR+#HUOGrTuZYJN?U*xvP2THc@(?c5plIOMUH*U>Y3A#tHLf8AM0q8&am_rj zK#{T!9J!(Vn~=fy{n+GtzEoB3$2}x=!ji}+A|=iOl!qSPtsn?h zOZ*}OY}2_Be0cr`Y-UD8NOwEVj7>`QOVrC|{g@n|AwzY=KZnh1s7_L#cZtkVNpN3s zzu$i)PxL%{7FG+I#EUm_nikXRob>w4xfia#{!F~xc(i`?Mc|-X$hPe&S&{aOTUmC+ zzsyT#(#EU~g^fd;094C9XK@_R4`GTaoiab;URqVYduxyk-Mni5Ig35_DH}MP7le2k zzsbgYR5-=?@Y}Q-QGZn2bTe=+c4M8qs~U9c{W!>yId$Wlyki)`>8rfZopd&U);GoT zfV>-(cZzHz+Ai^5B{m@Xd1Rt>;yu^2sBQ(Tx%#69hChRc3y|+EpAP?=R9>wHc%eNz zRAMoi{q}0T!Ltp@4&qVRujg+1(TH&G^-XK?lKtv-%3;!O{!)%DXZ9gan$AkAm?o>| z($zQz+JiO0G}1dhVjorqI{Xc;zfLOa2a+&eDAgZOT_mYWNSq{Q0i)Z}^p}N584B_h zmLH^i)!F+vch&UOD;8Q!px@Q2EKu)lIc*4$+#BsrqJ z)xJan8-kxLz zwVs?Wq>TnR!Ol&>@+;5Rx3268sid#A1`QinS3XEnHr@($IT`Go8-eomd3S8FyF)c& z9n+r&-gt<2=X|D&PQzy9H*!n@73?fGUI!i>uREbT$g)$&?N-4V%3h&-n=^vVr zndW%O82<*H=$R?@pNj&Onl==9_L{rPyNUc6{W;H^{tSMWYYKJOO@9=9yncm{llsSK)h``y-nMy1Z;ZerEnT}CfM1sRc{zW1?yZu|F(xtO?H z_wPF`B2*KG4B5zCAx#kRjICDE+T2rgXMh>d@%wG7#|ubN#mW--E~rb}3gTjnjS_M2 zC8r5S>3o}o3sK0y!Q-tmda60==oq!tGd4Pk0>K+63X)+`DQlUR{-H2Kif&b9kB@BO zbXNI{qSmuoSb#b1J^8*l4oaIXQSWa!PSTZBMmZtC{L%IDb8v*1kPuh9w$9YDsmaWJ zP#guRN~@j)b(O0s-_(+@Jt@f@dUP}pW!_36_oNZK{{|Lij>4{@A}kzF$@G_3Z)e>r zC#@gXkm^)qKi{+H&0$rVMs(STHd?{8#{RsSskoaa43(9;$wYqys6$iB)p!5SkXuK^ zeB!CR$y9>C-+%Y|S;y0iy@WbvbP{-TJwZy_BK`~rA~A}`L1`%b zPUM5zJy`+XU^WP5(pRFKt7=VQwwIp@FuzaV(!!tGH})_?bJ!Y z_SD!eEU-*K2J8E`avTlAP*P_@S9)e&5+jDM*yksgk`?&KO-hi><(<51_wq2bo1DE_ zbX3ZIj+RZNaVu^|9z#7bW2j73xV82q(MX6%^asutSU3O8NO@$${L_r&L@h3l>6DJc zTYfZm)veN7!Zy_`AsTcqq9x!kn$EtV^7r$=a4>)u5?=w0_U5~&F&ivxJ(%GALKS$_ z=$R(EC`Tfz|Hl~>rMt|a^^ie0vJ;@Wij??hp#mqK2&cB#bK=gk{Gb;k#pE}E^qlE! zJJGv6?=V=_jQg3VDOyt}yf&S7vNu1D&BO|*$!=?`E~qeZ(;P~vGG&@kx?7N>oWDRWMMy@vS8ARiC-cz-theVDZ(d zmFPPk9*i3JJ0B>-na$D%r}ING7Xtmo1nnzq zs?+TuWs93$i(72;h*`3teOSF1j+{`xa+z%Up*f(Xuz$PCs>0ixDlWYOvzWAkDcFTs zKf?q5id71hJ^rk2*h@k09sGdz7?zI@Z6c?*2ije6-#~bN&d&S>!u#X(&3Qj+WRE^4xua<@hfLM-gR{Dn0Q%oUy{l;Yg0U*-ALyRb#pie`)zvidp2L9khW9dMP4 z#B(b^h^YJUY{R15u-1r~vw z&aY}!-21ejmZ&>kc>cLQIeuOp8 zDF5J#k_wrnqnKP(X?R3>$p9FFcy!o87Ajr1Sy*~!{(*y1z2+QxMEVz{S>ZV`u@Cdk z@pArl&G_vIYrOGDq&vwVVxKQ8$fW5o*;7HXOR$d^-O71XEaEGxX{=6ZO`8iv<4A6T z<&S|q1$(ztPkhDO9-c(g%)(Pa(b+?y*+URt?8p2qx*0xE@*egP3kC^d)PwlP}Zt|sJ*axMA!g8C(6d)kXr16be=W3Bp zRLr9SBM^#IYhm1PNqfk{f2K0OT*(}v!Yrjf!?pbuu50P_w3->Tt>AP*-jTD%N8rn< zTphV`<=fBK#t-tGJh_VGKUoj+D6Y}>XA@IhAmE1RjlERUsut}HS-EPIZ3?X(!NJMS zDL^$_hQ*?jpu+tc%b1Bu7<`C0*2;vh7X`3IPTAM}#?a_=zt6JT$3OGy`|2uG^m%3h z&BT-%a^_r@{&=*jOQoAy%V5h(t7m&#+l{WF2CqgF?Erxz%zC(O&g!XayIk`{vaMsu zoDNLBI4FfdS@s@=(fyQ0O5;7(L?f%uP#nIw(1|L!B5CXfR#eQxdJ%!9iwTvNhonp2 z-LMk-SyYTpRem2Wv>zHhX|cWHy7paspLubKNW&UqCt0R{aF53PI2_}?cc&Du-ov0K z=$BO6^?`M9mTgCC-EWq^42)w`@Q5-rILv2k@{PNgSs;_l9Xl)?1A)BnO+|X96`Wob zu1Wi~yzaX-qTb|4$V5$|pe$+A0VJEaC(9@Im_=yB@(Q|YtOPZIh_EhFBCdl=njE}{ zv|FYAEUUxSpNF(U9($+Vke`1Y(kMQ|@ew8|U}f5!?jZGr+MG`d?@Px{8zli8JPS>; zJE4YE6mxG1=co0P0Cw7lhvA6IT8@r!g*Of zCfWGBu^&Qwu^(VJ=x^mYTA~pV{Y?Q$ti?CQ32I%}T&bK;LZ%a`_sE zqjk=E$y{SwYn6bp!fj10s763{arnJN`eJH?w6*G8SHrK$%EYq0sYsqOC&PpmxLqvI=T>nZ2yW zghYq)?v69)+GT0D?>y9gUq9bm*4L(q%%FV3hbbfPZ;g8_g!ZfS+8`gILc}2okEI=Q zvHe;Gv{7Tz8nfm$-LT37hJne5A#yT=P#Zf*SIIps{4$FP9PX;mZ5KkyUAMyzmQbX9 zHAT{#4T}6$BUt*XIit_2qU2&dVgUGAFrA3`z8x87`&ug0$lWRXbUjru*VL9N2fEhr zYi{G({ybM{bS-?M-v`LZ>Poj^^gR$G=DD)OWA3Re_aXuY^-tNSXvK0^S~>PSD$h8O zdYusY6L&2wVT2`FcbQyK$_hV86(y~|AQpI;Q@=&2i&V-9k6Yr#RtG%ggv*x4curGL zHOVy~drXyBl?D723%UOcPsVE$%%6+iD|ui`9sK*HO)Jw%PeSF`$A|mA#Oau_L2*tK zV}X9*d;#K)_X8Cknvb~C0FgZ zH{F;(6#nK2u0gnDra|HCx48QY-HWD4Ska0i-@bbA&vRHd1#*Y6jWCK%Rx8~m8C6%R&HY@slZf49VWns=;PX)(d^k*WE3am2b z^v5*(-JfBssZpqqsUsV)=<>=%>R`_qvW5r4M359nML+VA#U@su7gr=4J7v|q;kfw@ zE1ujljM=E}j1FtqtMSK3G_Kb+ZvsQF`(|)S7YxS=gU=1dxyDPui$lHMEI2I+tFlyA zuEF2>-q>u3ej|rYcTtcUQqt@m;~vb))ScwrqVDX>Df|&?TN+`JUql=>@nASnWUEn` zCbcb^Tcpp9+I2WS9p&ir%@3QZCaKqG0mx0*7bBn6+TNBCP+?iVh1)Dq`n$z_llnB* zRiBIbTb47UXH$@VUh|btj=;ANsc+#=5@Ok@WWEuXjXP!zT}yO?C@!V@YS#fz=rqSx z4!R<XxE5cqjP1adg1-NOrwkqxOStmPUvOAioqS- z8^(Gs*CWRQ-Xl&%v}E*QRA2Fw3%@hy2c_@)QqgrMg{Ag<1F)>AZ1M%Z$OtKqzh;|8 z{Iq137x}i65dRVKP3eirRVzOdn{0AmSQhQJ|GNXZ#0_+rsnO)l}66}VJ(|`P!{-XV7K^(74&7!`6d_`@=lfJmKu(RAJ zmtbCg%j#I4zoK~&{{Fj9p@3YMY?sD|=EqX1C1~Sm`Yt^sm3G|o_Oiwjx^=K^u=S5_ zb6ZopzMDo6Z%zs2F}3A23QWUk!}dyob0pmYYi+Ox4u~(>%(8%-OU)Lue2+a}-j%6} z4Q4c8hmWTM%c$aU_;pW-TM>_bbO17a$WJd^&@2K7RjBHhjdVPfYsa`dqLD7(Pzm~b zr&L*UN$FR9KNs1sIlH>@+FN9oFL0dqm?acFHHhz8kT?yE zzBXb|?`CfZ#MD}xQIiOMPfrz1PnCsw0hekn(zfGAcE%|jRUK|P*IY-8g9vJz2^#@B z%!CJ~Lc-R9Z?+RtI@?c+YwjM3Ms&)$9EfQtI7Nwpmx&gB04@0BY6N_Jm#d1m8-aCvE4z)HVfcn~hXd@A z_!)Q2Ryng-2&-yV+4vCD&MVxzA&1rblRr$~Hvm_{qP?``n6GsnH;-P-Z?nWH&XQe0i(!Bl7V2{h$}`b-nLpgDzmdJMsAIDt<}^O|3;phMNZ`+>MtJypm-@w% zoAMBC%ti55rb9Ry{{P=Tb>S^rFX@`iT_9q2& zw$hKvI%ID6e4M((jyh*t`drgH$YZ>CpwgzZhJ@0Ux!ODl4ewkzz+rP|F2v{c!D0C^ ze9yrc%j5X`h^cz&5RWB1@l;M$%HN<>zj+h6%dxRkux!fPC#^$m<9vZ=HaJvP)RIv2 zsc`AE$kR$=E+b{LgcA1MO6YzepzWukeBfcdxiT@BLCh2k=gg%>ZD>JSfIoM5pa4<3 zuizS84$3{u3pS`i%r}|jyTn|agqcVX6$YoW#EeHzkXmHKH1*+Q4hj{)x~7b&=`s2D zvO*07C2=+JUitAc1r|AtP2aZ{tg&5}pbxI=L!vv){14~pzgPbcZEqD^N024jS{7Nd z*kWd8X67SimPHmbvn*z2W@ct4i}{H8h?!Z}-Tmgio{4!c=6>AYU0G385t+LxvvRFn zBXN-xtd@*&Odly+J+UlAf11FJ59Zo+FN^28r5c0zP>P8UF^Ib|oLxo!`CM2^L3K1O zDaTIn9H)?ySjej{%GXEe)kY;wpN%AY2-nt@nVyjlny5qY$ljvIk9RL+>)}C)D-+L* zA_tpv=jKh&@me}UbRme<(eZaBDXmMDUH#~LP+>Xa2@L5UH$Lcmm^@=HDvqGNrLJ-M-l_Kn4?DD5Q-LM%CN7K=%k-%@te+#?k&3*6HVTNpIB9@5P8kQAnU#r|nY6BQ z$>fDRG!nOmP^Td9wPDh*LXWs>(nr76KWl+^?(U>$Bum=*WwRlN8D_B(8y%eQF+Vpc zHAHynY6`wV4*8Wb9RFeFlBJIwFQmsYhK7yMZrLov{2V1>ls3QmhDef>#GagtXHm(W zifX9$yMejRQ*oEAZ{wSvquCdJf4p)O)f8@D%zQ$&npy$QQvIH8Z`j+ljOyt37qmw? zfGn8lj)ZI05ySje-~LuPYPvSZfIcjmkm;E4RUL!< zOix@~L44^F7gd%?*}kPXq5tgxIZgEmUta?Gsn@B0CuXX29|`sR#bm5a5mC!LH|;~g zD_rE73gH^H#4bi8qC^Z`)R;6NMY3=hZTckK)S1oHnU3CA?nslelu?6ncAiAuzO3JQ zgZX60Z*@W-9E!>B8+fvx7xHdTP;<3qSL?=3biPQq0HJWIKs2iAij~7R8HZTlP+qPf zdWJfoW-lHRy`dsmM9Th$x%p7nUy;|n?ZfbM0M_6TJ*b5tQwO8l@uQyNqNn(2JyD^5 z_XX{iUMoW><#DB^W}V0PVd50X=6y5VkgSny|2VdM58xW`uHGI#pj^ut5#MQ)7 zvN6y1$kB3U@$y3Q@ncpqRzs)&%wdfodsT@4RO5~bpP;7YTnza^v27z4aB}uIH?2W<}QNXIgboZRa0T z3;KbK+_qf#w~wy$?0oa~1B&C@Q~GoBXV$hn5W9F+;pwQbXO~A;=6j-nTfuq1cc=Xj z*tCT6#NCyb;>hhu6~=pkk9woG4}rSuU+kjYZ+|FuTmD#tnUX{)K__***U^qAB~G4v zDWb`u`xD9}?`{}bZ>gv+#$VbRA|?@^PL_H{ik?byrAsn|_Z!65RGWKK{ZxidMRgFFXpA}` zP?2FO*tjpLo-Z|O=e*h7+;($#`A)fb#@|Ps?-!XTS{Fgc!|!zg3`0OtcT z6RX9U_@c&(OY8yjrzRoRVhohs%Ir+H+JGe4`GS&fP_vk2$BSlnMd&w~gTs zBO9*EIp~Ye<3;T|n6O)q=nJl+2-jaK9PhVHY$4TXAj2}Z{guh-EVmX5fXRE8E8jJQ zxD&UNcKS8rv$+AQeT)9lQ=H8PIH>P$eKpkfkwg7T^LS{7$ogm2G&DPg2QN9JUYYqq zHK=+)+8^zsGEV4!2_+KM2x$odE+@S!%1d;AOoV1gK)kfo0OEK0He0=Nsr2KqOAOfi zoNB(d=3@o-o?e9W7vIXI7{2a@sbYos>lXn5dj1~_>jRz*FdA%W8f?dj@_bFmJe1G? zqA&_ZK$wAgaDz+c@&om{NZZD)i{9a{6wY7>s;Me?PZ7cH6hZJ zD%HA~SS4NY`&^Sdv}hN&DQD%a^`)Mrf&GyQhTT&VD{UK^b%tBS6wa6UW8-X5a&1Ju z^xCNLi~Vuuo!|Jxz><+W*}<=)pgdI!2}Slqm-e-IQ+?#SxyWv}9IeXh&$kx+wI&>*As#+AP_jksh zyGeQUvjgUWxU_07kFr0q=BMO%I<82zO8DUAk%mQ_qqo?-0$RCBughW z>p-&gZS9>NuJ@uszCU>nNCZt>akS)dl8EFR#6h&m%sdiai5WuttVTwzvxbFxhNK+y z;traTltk$rZY2_K*!#HD4$NpnENE=w>G?yy%^>Q@2`Q1_Mg_ZC>C#ttL|{_k$bK>> z`u)+soexxFI22XpDIG+ht_zSptL0?tMvf^IS2j6m*JBuT&QLNtyIHkBb{ejgh9}lm ziLO-is5+SwYb?EK6pwpH4hr&q}0zt~=dT zwWLRHy6l8MJKE~-c}|r#OzCm#v`@z9_naBfb=>OSvUk6K46g`Wt~)=Lo6@2f0;{IO z!yIij_^pS>aO5-!fKl$H2QPFPd5F)xwm3<(wYrIvYjv}?vR*tM`BO}--C=*XNW@I- zW&6ynfL5E;TpRqEkC5` z0X&Nt5|*BA)STc>b4X`M`PdX`k%xoeK@Z;^;HK=S?^U4H%}@OqustVq`v&?TS`?|s z$LspqVCuF7ygsi!NoEUPoqDoM$=vvYtBoQK*>&j&-fZR%QAF4G{J5vXTD_h<+s}B*MNCGfM{G2&nmr`RW6gj)iM5 zh|?LDTE_3Y<+Trg@FPHjSF7tdq*V-)>AFH}y0M5{>yZ29z*!kKFb9MnD2i*EFXhz- z6!nQbX_`ARQ2VPk)q4i#hoc$n*T7wDX0>`gN)HtOoVwBI54_R1Xr)4P_7HUJz^)u% zJn1&@sX|SuCwq9$@lvF{Y2u~hlBYQ{YiHGbgVwyhD2VD=zDs_3-5^;#M8$z05f3#@ z5B@`C_#RDT(;as1&Y0#y@-!~;DB)|2-DdMxMZe<^SA1ybG`b!5RKUK;0rh7&={(7EVLm-r(g6Ed`Me`Ie#{1LQ=nBCn<_>EM0ev*Hmiqa=3!Pa>@EoQi+5Sa&?N+Kl!m%S(XHn!z256 zrZQt9YczO!#|pI;ao){tlIJLRc`l`;mFn{44v|+r-U;CeiJB^$S1`bSEYs=JI~`C1 zP@qHJYGS$5>1fxosYcFt=`!mPKPC?P#u(yqc_`1UepZ#vxv2Uct|VeIi@%t6aBgcQ zi4}1`y}2ET$}lQwdc|MiD04n-#doPXM}YW5nLfoo`&+k6$2R%)DkfiujElRLCh2r} zpGYKDn<*~^E@lmB?&$iWd%NQ`K0Kli%VbPzEZXJBAe4SVistI!>i$mkRjotabLrXT z8Q~R$xVw(uO83Y5-0UL$4uFsYUsJ6Zok-e~FFSNi(!K*e^miz$QXwmE>B)R(%6hh@ z4HKY!akyLgk48(WeT^dtoX~d4tQ^^VVXNvUMbUn0VJOp5EMJs`QC8hJrxm7nmrfI1 zo`<8jBKdb1e(GPwSDF|?g2wp%sXuqNN1@Q+n;|5BLzEVMDSX?q(Ye z9ar_E{$8XRuk#c~LGzF4WQ5krG{xjmKCfg*x?Y(pKl@jnTI3~))mIC&rXM^7fG(v+jHlP9kmsmwDsRWBlpkGt%H;bKaV$}q z*;rJITpj&2IIjs?lW&aA1Bndt0@H7`8^3GOh=BMJl z-$4$ozM^9*T!)?%uZK?ptO9a8j)Dtz|>Gx)6mq z!etE=kC|pCX)GzShhuxRX5>c78U_GlDicw&(Hs$lC}7W|w{m0rL7)Bd9<+GsX=NG7 z>#_nuwD~wr%1~SHczCM0__3O9A7HXuhs=C8w`uy=`95c}nUkWObJX@i__y-uI*u%& zvK+OFRgxkoEs+tBVM}{8A#GPu4|WE(*mvai&@`^NwlTwibdCaAru3n6vB*EgJNdEQX?yk;!(fs>-V`FA-`?k21a)5_RG*ArQ z;d{;}B~0a1?xG505RxnT;StR7^}PRG>xBPd#yKz1v7!KJRs8bDlVY|2ec<~WrThrJ zx1rC@G%QJDRcwF;&!vAh?%yWT50u)aJLtHnI4@hGk3}+_Gb=jB0D{`>Mkv6 zkG=nO`;k)Ig;i2nD+gIo>rG-alDmb7wFL&N)4SnFiL8EjqU%G1sQvB_+w&-vCH8K* zk!L6wy{v4h=&HZU zS%h+rbiYxr0?cF3P5Z7L4HnHE>$!#sAm2V2qtnwzMp!IXD>U(%SMlZ73%ESR9Op>S zVQb*h;`TfFOE0dP6-M~+vt4Fl^N4R$MDL&mQ`>Ehe)&#IZcs9IxYKm1o29sKEqz$D z<1>qOYaUVCrKE!MntU*QNO#Ru+NpX}&(<(sJe)D<&u=jzrsuSOnERZ{!Mk?_7az~D z>f*=XfW@roZZjJ=q^DP7U~ZxgTzT+)tBs$9pC6)F-VdU}3hmcZZMOB~2nm+xC7Uav z$JNz5?X`okK>C47LAGpK4rxrIUPfFLo(L&iNunrjRExVZcr?@>aYeGcusOO?D?H8PpcEb13<<-t-B2K=T7Za0WeABjh@n)(O4SwEgqkB3 z@T(L+tv;iBLcf@Jsw40+&PROKCX|p3=c`1}UqEy2cv=daZ_*7L+4wtmEq>@)bs;0H z0g9upq#tI=iRcKK#oa%kOzs#{|xJX!>h70arOmW(bzbkuq`PpSrOE(YNYt-I|imK~1_ z{xKb@F%ODjF?l*ECJ1;;F#@}TzO*!Cg#&9+o$H)}ItkqMG8D>-BsC`pp~3hQWnWP~ z%OC#R1N3SIDxZsJGXDVifafe^;gREq%znYL z$#KVIhk&ECtYNl*nVe^ovY(^{Rv~{+>MWA2wcaxAP3kgh@WBC&?2&{JJJnJ4Wb#~R z#xtbj@x57E(F*kIcLlA-kmh|d8i$d*XxEr)Hot?_@Sh6%UrhjmfpSX(lS-YJ4L&&e zz8mh%l9^Aij`vu!{{~0@C!PEcVws7Jm6h#(>jD1BBmX1J%FN8f{J+Ehp*Z-T$qZCQ zlmMEdG|J}Ie*xf_R^(!r8Mu zCcF~+4PR}W_p<6qlg!|vON7Bir@$>6zD;OaHVR$Hw;l zo^!z*4>)oZuRjE9i%#rWwu)sjRqR-j=k*du4!TX$;SbE65)Cu9kA8nJ^L{xdCVC4ZCtz2^4o zCUdEXHmoD`SJ`cte4%YTeGWHNA&uwr{m8hVT$M{AoRBy*_O_oImjkujDN{LOEExpZ41R+m|s!~ou1F=Lz z`8yYmd~yzX5TTC-LYB!SVwRfFF6w7tP%;Z8axc|U2686yQ{Ycp;h!^>1%~8DGqTza z-@!H-*VY|`zAba$^GI4m<`w=S)TjfS8qtXPD%{Vg^&^42)-AK}inBjA-K3CQCxnpp z_jE3lwFnzBYoc{iZ#~%n*jh9c+!$P+;T9>7kpL%<=o?fYlYc4_Z!)3k@9q`cI$Nay z@2`^af_Z{Fq}vdZJlRsKrbu?ov4SpNdw;X|Pk#%ZQtQog-ZZcew~vWc)Pf04jvXZ? z)gmVOPWh!!F!#GmUy`4)0#r4gpdkd~-4aXi;};tT@zGQju5~KJfn~uTKetp+70M)J z{q}RaHZ9~U9@m+4guRG9_>p8k>u76%cabwnd)zU~?~ij6Ep5Lk-&}mZ5Ty_`{C8vT zKWqW)|K(QlCv(DM(aPSm}-XzhneLph>z>9!=IbJpFNL<7a#Md zYSUF0OUvmLftZ0%#>7OwktDUi`j`=z63`6yn*1;a#~d{J3}BEn!}}oMh&ajp(8$J( zshFe$k)YNv!SyuR1yq|Jt91{%feznEVzut4w=T2u7j4~vZ$B8`$#ge+7J&lfG8+d8 zJxuQ7yAs`n9@{aZzihr;E6aDk#Y|jIcOUcF_S{}xyeZpDcnRwt7!niyetr$($w4|K zwm;lDzU6V-de7)yT6aHm6|+TpY~{E>!MOi}H0TC+?wmt>>8R@bx|@D6yE5%GVlCMkUK2{)O%c0cG+wixX?#Yttqy`Y)s=+*!9 z2LAz%ycBfeh&O+z5!~zY`mD5x^^xwxQ5TgItevVKW`5iy3(vL`lr8p@vD@`}<((j3 z_G*x~6NHVwR9mwnIk3jI9(7r$=Ly${O>dOeCe`!6TV%Q{utGIm_I^6;rNDieV(AtY z%wg_U5L~lh2fjlE|7g}KKR_4Q!RAGmE#POJ=XiU@)aiqI>lPh+`g8{M!H;q4gK~WX z>1KT)uocU;blUdxs^sUi$^=f0J zvopKD9>4E3FeK#qSqvWIYSUT9_Ttj8LoXcE3AOg2;DP zS`RrV^uoj?*yB^)#9{5kzU;x>0Wisvd}n9%iVN0{^urej?LjoxFgL4Xz=5}2stxlt zxj=N=TJI>|Iju#6o1!;rkI>`CK>CCN27hdYoHMn0^$r|z1wz*{Ozpm7``T9irt-u& zHJ=ketl@!Izj}VXdIn4OjdXTr>Su!xF>*D?a6yXZT^{C*uC-oEJ;2RYb}#N(WC^^R z#?p&=w|#kfUhc*)*vi-{L4uZ;{sM;p>A!R7$`^22b3gTd=+iEL_{?2d@@WMjB5^sg zU1ga!)|ZqrU7|e8+$|wgA1q5iYMk7%Y*Z7 zQ`fCF%7o#%Gu#jTqQB8I^-;C6CE}j32hlJ?1}CTYR(Fu1D9@*uJ+1Cs1Ub@kS_D^T z;1=h}b8X-mT;tJlxmYC7v)0B_!*x#fp%#zh`o&_;GWd=9T)=qL8X2k&fO_J%g^b)Q}_4?tTzk#;cG z&fjYo40Ua`pLI2+A9tI|w%)M%QTwjy;#kCsE0H_~57~F^!$5C=!HAYmKJWr%I|bPM z`8yrgc;J_e?M)waL(;h66?HwIT$atNGyPIIor-SYm0eVarMks|-RW?%19GizUa0l(>q^rFV#Z zeAjzdF{%sDKGngU^7!|0X`nVPE-`%7*UaoHuQZN~)c4%;#xI8?XvIIZubp=Xb}5D= zhFsUWXO*OT6vKcKz9uy^S;M$R%?EG~lQqYD2_hz*P4#@KEBGwa((i+JHYJD&0aWrU z-R~B`wk%PN3AviGKwn>(g3kG@0yvO z4s4vRZqqqdc4*~sy~k*(f*DRq=W=TRT~bv}o`6Ue&P&!rGnAIWs?kx^65F4_2xq;v z?zLT;Q<0r72h@y^1R|W*FXNebE|yboqfpY9dIj#o^iVxC43-oxR(b13IVkB( zmC;bl&bR@~iZYzeM>|m+?*6_IiZz%^(%hZ>Zdk0qx00IQ7L?LHn;qKF+)~DsAL!ba z@O$*23|)KpXVX!I>XQUtAos;7Cx$RfaPcS>@$B5+>T*V|h0krqZRO6aPGMQ&3 z^A}|3BhrHMWlg51&s9dwGT@OV3DZg7ppBi)yVKOFofX?HOqRJD)Wx%7?XyR;h1}kF zIY5`5+J%FG95;NyAC~^7KMM=H373GBIfbT}_v88fv2z`U&tuD*yJ`7No-Wt2XcjQk zB)3M9rjiG&W6MJ0C={-idg&7;p;crsQk5_r9nEJbFWB=j-B*;?C8aGa1+C_nji5!& zpN-VkSV`yBLBOsgvkVVo%DXM=ch_<09r(5HG~TA_F>gF-AHZhvw5hEHO#>gUXwtT% z>#*cc!6B|O(lA!QLYik+J})&fY+^Kx+Y5tTsqi{=VahtF@7?f&J=D_C#wkz_ z=%d`CxPn|iOQ-ZQ#j%o1$-^xzl`YD=_G1- zS3Ioh#b||_x_)GZ-q{v59Zgt>NGs0>E6?{^_+D93TINyxxzaA7s%UsQl-@G0LSI?w z30RDFVRcPwJc#CE=~U$^H@^bfIR>MaoXv}uSjT!QSCs+Ae?vDTvR58doL!1&38V@h@}aq2g;`i8Yu%W9N;q|lfop-)4H+8`|s_QS5enbu^u|PuxV5a6TlB9 zaV^!f+!b^ADbKmg&dY3+t(6Pq_BwuU;nC7-dn4{^nx5Aplo^klEJU4mOzn0y`F^)d zF;UZUVV5AmO6Htnd+A!5yQ@AQAAZP`vOH;OG{K}ksY36pMXjx!#-Jpl8NY4v=GVa5 zSo1MI?N#?iCK!-r1JrCvEn7u1!akcwN7`g-z!-0TG8meteF<4Jq{(F3P;CaB7s*MO08WmY*^7; zMQiVL%}|)SRLCQJJ4L_hc@TXkWPBQ-)^kCr`Ca|Ui(Cw16z&>Iwh;k zq#3k5W`w^?`YVX)v}3)!TtL-$fOq$3mycoWan*=+jRBLeDD z|7m_F>VB9gM}+4<$5QhiTi#cJ%k?|&?n3bVnAi0g%}vY}H97v6ze&@eiVBZ+@+xXw zx*5ei-or9dJzin~D*kmZzHg+#V5AM6uB4^%!lIfz3)&3)QkXlz0>sDhn1a6Dh4!I) z+;SB8E(Pc`{G8k7l{?6;caYM8tDXGzLM<606MI268^g3D<>gHx(v@sQ8 zQ?%$WXnV}d7viWaXSrvO#d^4WyZ}40vGAZm_NLf@VqD~ho@02N%i|X$-t9k{R2jZvAW!TgqFX9a)Wh%0sl+22ozZgNkSx(M6QrAW3(o&aK-KRv2p(v0+IW7Zzu z9fG?{uaJ7|YI<6Y_o)$5OXauxMwC{`q1Q=2TS>pGzMVvVtOkGR!aexll}F z6^@&uTFr`?#mw*scUq*-E+Ut-2=0J;$b3c-^!_zle2XTY#07Sc;Yo8LG+~f&qGQyxV;hamyevPX-c}&h{LCB^-)mECw;zX377t4PUjHO{fYhBUj(1Rj zn-iBMmh8y3OLL$UrUH|hN|KV$q~x}6pbXX&T-Isl)w@}SYi@d`j$4w~7QcS${ws94 zj+M|V-ozpOGwwvnE#D=!OLRf5+bF0$GUn^GJW8_1s4dQ8ntsSc1xsQ!(fyC%(y26> z97d&Zr^JlLRdhgof_0E9jnHP-ue-O#mN$L{gm7eCq{XKuyhrTEZ=R0dNN8yQKmyhu z$wUY^6J>TPaU?Cj5Ju+i)xxp2J$OpVA$Qv_`F37njrVX9Db_+Clz`~RH>I8HH;Ue^ zcN0n)M58HUZO=Cv+*F+W(vRBaIKH*3^|LjOg^O_7(Clmz2u+k(j(tvW_;lC=f+XPF z&}~{8+NJ|1t6W-=3pBYYR27r1ONM4u7$kjIIl~VClAi}M`D&(xlTgwIsq>0UJ~}^h zzE1Bi@sC0*i@|FVb2V4hv!@ALt7r;*f6{CHMP<=UYHtutJ)}q{a{LvAQ{t3ZA|q@Q zFgv&Xj2>G^<{)ItB-)@5TQZ9hg=n!uSuR$2!%5eaEp|w&_p}M{@@D<#q5to zeu-lR&f}qv__~~U2_a7H&^fIIv}AB@*MLZ!AN+;P@27i%08?bo6zeKHN;mm0=tWi`q= z8Tb|jm!xPM$Fm){&tDgr!@+ZkNam<0*OKYqV`Nx^jrE=jgN*$<>#(HjA*B(n-oH#~ zE&PzQjelr!xZzrS*XekJ6(S+KlQwn7<%~WET&X|Hgp_hGmV{kI0`6>;5&nI8^)U z7`Xf~Y)`Wfd&7bjbStA;J$1$5sMeYG?8jdoSOYHwoNg;%O=j%{8QUtH)R7X?gC_+;pjiV?DvkXP*1{&;t`=(>#>)$z4B?}4lc~a)4NB#aEY#JOPpuoP?QR05q_z5S zQ(g&?dGWE1%dE+4U@@L?r5 zZ!%H+^Eo6V#>LsTS3#TzlncIo!FddlbE+B`9wZI**>qZ?MNUQgob+*7GB2WG=75?F z<2=EtGL__6BP+j;uk~$C??O!h71)-4SE&cQmbOxFp4qaCU$hsb$8#MRf_jrsgm8 I)3{_X|vOpW}69n+Y{# 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

4RUT#uGc`f|wIHtUm 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 Date: Mon, 5 Aug 2024 14:00:35 +0800 Subject: [PATCH 005/136] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91AI?= =?UTF-8?q?=20=E7=9F=A5=E8=AF=86=E5=BA=93=EF=BC=9A=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=20PDF=E3=80=81DOC/DOCX=E3=80=81PPT/PPTX=20?= =?UTF-8?q?=E5=92=8C=20HTML=E7=AD=89=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/service/knowledge/DocServiceImpl.java | 4 +-- .../yudao-spring-boot-starter-ai/pom.xml | 6 ++++ .../ai/config/YudaoAiAutoConfiguration.java | 36 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java index 76fa1e530..eeffebf44 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java @@ -3,7 +3,7 @@ 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.TextReader; +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; @@ -34,7 +34,7 @@ public class DocServiceImpl implements DocService { public void embeddingDoc() { // 读取文件 org.springframework.core.io.Resource file = data; - TextReader loader = new TextReader(file); + TikaDocumentReader loader = new TikaDocumentReader(file); List documents = loader.get(); // 文档分段 List segments = tokenTextSplitter.apply(documents); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index f015a643b..95895e9b0 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -39,11 +39,17 @@ spring-ai-stability-ai-spring-boot-starter ${spring-ai.version} + org.springframework.ai spring-ai-transformers-spring-boot-starter ${spring-ai.version} + + org.springframework.ai + spring-ai-tika-document-reader + ${spring-ai.version} + org.springframework.ai spring-ai-redis-store diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 05a317294..58340d45d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -10,11 +10,18 @@ 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.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 redis.clients.jedis.JedisPooled; /** * 芋道 AI 自动配置 @@ -73,4 +80,33 @@ public class YudaoAiAutoConfiguration { return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl()); } + // ========== rag 相关 ========== + @Bean + public TransformersEmbeddingModel transformersEmbeddingClient() { + return new TransformersEmbeddingModel(MetadataMode.EMBED); + } + + /** + * 我们启动有加载很多 Embedding 模型,不晓得取哪个好,先 new 个 TransformersEmbeddingModel 跑 + */ + @Bean + 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 + public TokenTextSplitter tokenTextSplitter() { + return new TokenTextSplitter(500, 100, 5, 10000, true); + } + } \ No newline at end of file From b453856864d44879befd7558b68b0239b4be7870 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 9 Aug 2024 00:09:19 +0800 Subject: [PATCH 006/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=EF=BC=9A=E5=90=91=E9=87=8F=E5=8C=96?= =?UTF-8?q?=E7=9A=84=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/module/ai/service/knowledge/DocService.java | 1 - .../module/ai/service/knowledge/DocServiceImpl.java | 9 ++++----- yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml | 4 +++- .../redis/RedisVectorStoreAutoConfiguration.java | 4 ++-- yudao-server/src/main/resources/application.yaml | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java index 2e7f792e8..47905d4b1 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java @@ -7,7 +7,6 @@ package cn.iocoder.yudao.module.ai.service.knowledge; */ public interface DocService { - /** * 向量化文档 */ diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java index eeffebf44..b0f4afaf8 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java @@ -21,24 +21,23 @@ import java.util.List; public class DocServiceImpl implements DocService { @Resource - RedisVectorStore vectorStore; + private RedisVectorStore vectorStore; @Resource - TokenTextSplitter tokenTextSplitter; + private TokenTextSplitter tokenTextSplitter; // TODO @xin 临时测试用,后续删 @Value("classpath:/webapp/test/Fel.pdf") private org.springframework.core.io.Resource data; - @Override public void embeddingDoc() { // 读取文件 - org.springframework.core.io.Resource file = data; - TikaDocumentReader loader = new TikaDocumentReader(file); + TikaDocumentReader loader = new TikaDocumentReader(data); List documents = loader.get(); // 文档分段 List segments = tokenTextSplitter.apply(documents); // 向量化并存储 vectorStore.add(segments); } + } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index 95895e9b0..ae1f37948 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -40,6 +40,7 @@ ${spring-ai.version} + org.springframework.ai spring-ai-transformers-spring-boot-starter @@ -55,13 +56,14 @@ spring-ai-redis-store ${spring-ai.version} + + org.springframework.data spring-data-redis true - cn.iocoder.boot yudao-common diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java index 03dc1c19b..61c38dd1d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java @@ -30,6 +30,8 @@ import redis.clients.jedis.JedisPooled; /** * TODO @xin 先拿 spring-ai 最新代码覆盖,1.0.0-M1 跟 redis 自动配置会冲突 * + * TODO 这个官方,有说啥时候 fix 哇? + * * @author Christian Tzolov * @author Eddú Meléndez */ @@ -39,8 +41,6 @@ import redis.clients.jedis.JedisPooled; @EnableConfigurationProperties(RedisVectorStoreProperties.class) public class RedisVectorStoreAutoConfiguration { - - @Bean @ConditionalOnMissingBean public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties, diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 8677a5b71..804ceb71f 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -153,7 +153,7 @@ spring: spring: ai: - vectorstore: + vectorstore: # 向量存储 redis: index: default-index prefix: "default:" From 387ef6f396d4d182a5755f74ca2a7cbfd8b86814 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 10 Aug 2024 12:15:57 +0800 Subject: [PATCH 007/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A1=EF=BC=89?= =?UTF-8?q?=E6=8B=BC=E5=9B=A2=E6=B4=BB=E5=8A=A8=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=B8=BB=E5=8A=A8=E5=8F=96=E6=B6=88=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=B8=B8=E7=94=A8=EF=BC=9B2=EF=BC=89review=20?= =?UTF-8?q?=E6=8B=BC=E5=9B=A2=E7=9B=B8=E5=85=B3=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppCombinationRecordController.java | 28 ---------- .../combination/CombinationRecordService.java | 18 ------ .../CombinationRecordServiceImpl.java | 56 ------------------- .../order/TradeOrderUpdateServiceImpl.java | 4 +- 4 files changed, 3 insertions(+), 103 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java index d363a9109..8a3ea838e 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java @@ -10,9 +10,7 @@ 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; @@ -20,7 +18,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; -import org.springframework.context.annotation.Lazy; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -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 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); - } - } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java index ada81d224..41400d3d8 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java @@ -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); - /** * 处理过期拼团 * diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java index 1f7c9a073..cb70b8ea9 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java @@ -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 updateRecords = new ArrayList<>(); - // 如果它是团长,则顺序(下单时间)继承 - if (Objects.equals(headId, CombinationRecordDO.HEAD_ID_GROUP)) { // 情况一:团长 - // 团员 - List 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 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 expireCombinationRecord() { // 1. 获取所有正在进行中的过期的父拼团 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index 7acba7dde..945e36fc5 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -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:需要退款 } /** From fa585578a050cd94d2eef5d6387da064a61ea055 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 10 Aug 2024 12:24:49 +0800 Subject: [PATCH 008/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=B7=B2=E7=BB=8F=E6=90=9E=E5=AE=9A=E7=9A=84=20TODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/module/product/api/spu/dto/ProductSpuRespDTO.java | 1 - .../product/controller/app/spu/AppProductSpuController.java | 1 - .../module/product/dal/dataobject/brand/ProductBrandDO.java | 2 -- .../module/product/dal/dataobject/sku/ProductSkuDO.java | 6 ------ .../seckill/seckillactivity/SeckillActivityMapper.java | 1 - .../yudao/module/promotion/job/coupon/CouponExpireJob.java | 1 - .../module/statistics/job/product/ProductStatisticsJob.java | 2 -- .../module/statistics/job/trade/TradeStatisticsJob.java | 1 - .../iocoder/yudao/module/trade/dal/mysql/package-info.java | 4 ---- .../trade/framework/order/config/TradeOrderConfig.java | 1 - .../trade/service/brokerage/BrokerageUserServiceImpl.java | 1 - 11 files changed, 21 deletions(-) delete mode 100644 yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/package-info.java diff --git a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java index 5479b9adc..707ccc338 100644 --- a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java +++ b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java @@ -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 * diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java index bdd8db09d..e4e497dba 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java @@ -148,5 +148,4 @@ public class AppProductSpuController { return price - newPrice; } - // TODO 芋艿:商品的浏览记录; } diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/brand/ProductBrandDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/brand/ProductBrandDO.java index 9775f36a5..e2178d5c4 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/brand/ProductBrandDO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/brand/ProductBrandDO.java @@ -48,6 +48,4 @@ public class ProductBrandDO extends BaseDO { */ private Integer status; - // TODO 芋艿:firstLetter 首字母 - } diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/sku/ProductSkuDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/sku/ProductSkuDO.java index ea9528d15..267756a50 100755 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/sku/ProductSkuDO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/sku/ProductSkuDO.java @@ -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 - } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java index ca40e7602..0b68609c9 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java @@ -72,7 +72,6 @@ public interface SeckillActivityMapper extends BaseMapperX { default PageResult selectPage(AppSeckillActivityPageReqVO pageReqVO, Integer status) { return selectPage(pageReqVO, new LambdaQueryWrapperX() .eqIfPresent(SeckillActivityDO::getStatus, status) - // TODO 芋艿:对 find in set 的想法; .apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), "FIND_IN_SET(" + pageReqVO.getConfigId() + ",config_ids) > 0")); } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/job/coupon/CouponExpireJob.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/job/coupon/CouponExpireJob.java index c6e26af31..37e4f6526 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/job/coupon/CouponExpireJob.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/job/coupon/CouponExpireJob.java @@ -8,7 +8,6 @@ import org.springframework.stereotype.Component; import jakarta.annotation.Resource; -// TODO 芋艿:配置一个 Job /** * 优惠券过期 Job * diff --git a/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java b/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java index 94fa71f7e..463e950f6 100644 --- a/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java +++ b/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java @@ -10,8 +10,6 @@ import cn.iocoder.yudao.module.statistics.service.product.ProductStatisticsServi import jakarta.annotation.Resource; import org.springframework.stereotype.Component; -// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置 - /** * 商品统计 Job * diff --git a/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/trade/TradeStatisticsJob.java b/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/trade/TradeStatisticsJob.java index 74b65a133..271f32ff6 100644 --- a/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/trade/TradeStatisticsJob.java +++ b/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/trade/TradeStatisticsJob.java @@ -11,7 +11,6 @@ import org.springframework.stereotype.Component; import jakarta.annotation.Resource; -// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置 /** * 交易统计 Job * diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/package-info.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/package-info.java deleted file mode 100644 index 37e0ba7d6..000000000 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * TODO 占位 - */ -package cn.iocoder.yudao.module.trade.dal.mysql; diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderConfig.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderConfig.java index 715169275..8d6ebea15 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderConfig.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderConfig.java @@ -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 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java index bc9e7accc..c874f06ca 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java @@ -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(), From 83bd96d6725b0769808ea5c83b43d54e04859c6e Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 10 Aug 2024 14:34:57 +0800 Subject: [PATCH 009/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E9=9B=86=E6=88=90=20Azure=20?= =?UTF-8?q?=E7=9A=84=20OpenAI=20=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/service/knowledge/DocServiceImpl.java | 3 +-- .../yudao-spring-boot-starter-ai/pom.xml | 6 ++++- .../ai/config/YudaoAiAutoConfiguration.java | 7 +++++- .../ai/core/enums/AiPlatformEnum.java | 3 ++- .../ai/core/factory/AiModelFactoryImpl.java | 24 +++++++++++++++++++ .../yudao/framework/ai/core/util/AiUtils.java | 4 ++++ .../src/main/resources/application.yaml | 6 ++++- 7 files changed, 47 insertions(+), 6 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java index b0f4afaf8..7ba5018bd 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java @@ -7,7 +7,6 @@ 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 org.springframework.stereotype.Service; import java.util.List; @@ -16,7 +15,7 @@ import java.util.List; * * @author xiaoxin */ -@Service +//@Service // TODO 芋艿:临时注释,避免无法启动 @Slf4j public class DocServiceImpl implements DocService { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index ae1f37948..d8caea0df 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -23,12 +23,16 @@ spring-ai-zhipuai-spring-boot-starter ${spring-ai.version} - org.springframework.ai spring-ai-openai-spring-boot-starter ${spring-ai.version} + + org.springframework.ai + spring-ai-azure-openai-spring-boot-starter + ${spring-ai.version} + org.springframework.ai spring-ai-ollama-spring-boot-starter diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 58340d45d..543444fdd 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -12,6 +12,7 @@ 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; @@ -21,6 +22,7 @@ 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; /** @@ -82,7 +84,8 @@ public class YudaoAiAutoConfiguration { // ========== rag 相关 ========== @Bean - public TransformersEmbeddingModel transformersEmbeddingClient() { + @Lazy // TODO 芋艿:临时注释,避免无法启动 + public EmbeddingModel transformersEmbeddingClient() { return new TransformersEmbeddingModel(MetadataMode.EMBED); } @@ -90,6 +93,7 @@ public class YudaoAiAutoConfiguration { * 我们启动有加载很多 Embedding 模型,不晓得取哪个好,先 new 个 TransformersEmbeddingModel 跑 */ @Bean + @Lazy // TODO 芋艿:临时注释,避免无法启动 public RedisVectorStore vectorStore(TransformersEmbeddingModel transformersEmbeddingModel, RedisVectorStoreProperties properties, RedisProperties redisProperties) { var config = RedisVectorStore.RedisVectorStoreConfig.builder() @@ -105,6 +109,7 @@ public class YudaoAiAutoConfiguration { } @Bean + @Lazy // TODO 芋艿:临时注释,避免无法启动 public TokenTextSplitter tokenTextSplitter() { return new TokenTextSplitter(500, 100, 5, 10000, true); } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java index 596118168..1922e9a2c 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java @@ -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 diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index a5df28246..c9b04dc1e 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -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} */ diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index b25658c67..e18f10015 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -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: diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 804ceb71f..23c622ea9 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -162,9 +162,13 @@ spring: 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: From 5bdc2db0c38d1641379d6038378655b3a7f04d14 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 10 Aug 2024 15:26:51 +0800 Subject: [PATCH 010/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E9=9B=86=E6=88=90=20Azure=20?= =?UTF-8?q?=E7=9A=84=20OpenAI=20=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/chat/AzureOpenAIChatModelTests.java | 70 +++++++++++++++++++ .../ai/chat/OpenAIChatModelTests.java | 3 +- 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java new file mode 100644 index 000000000..c85958779 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java @@ -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 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 messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java index 0d956e5b3..676832546 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java @@ -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 芋道源码 */ From 7c9714d014e7e41916920e594ec79a5a0917165c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 10 Aug 2024 18:21:27 +0800 Subject: [PATCH 011/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=20=E5=A4=A7=E6=A8=A1=E5=9E=8B=EF=BC=9A?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=80=9D=E7=BB=B4=E5=AF=BC=E5=9B=BE=E7=9A=84?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/enums/ErrorCodeConstants.java | 4 ++- .../admin/mindmap/AiMindMapController.java | 32 ++++++++++++++--- .../admin/mindmap/vo/AiMindMapPageReqVO.java | 30 ++++++++++++++++ .../admin/mindmap/vo/AiMindMapRespVO.java | 36 +++++++++++++++++++ .../ai/dal/mysql/mindmap/AiMindMapMapper.java | 12 +++++++ .../ai/service/mindmap/AiMindMapService.java | 18 ++++++++++ .../service/mindmap/AiMindMapServiceImpl.java | 23 ++++++++++++ 7 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapRespVO.java diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index ddfb489f3..50934e7a0 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -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, "思维导图不存在!"); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java index 015180265..a1d8de0df 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java @@ -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 思维导图") @@ -32,4 +37,23 @@ public class AiMindMapController { return mindMapService.generateMindMap(generateReqVO, getLoginUserId()); } + // ================ 导图管理 ================ + + @DeleteMapping("/delete") + @Operation(summary = "删除思维导图") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('ai:mind-map:delete')") + public CommonResult deleteMindMap(@RequestParam("id") Long id) { + mindMapService.deleteMindMap(id); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得思维导图分页") + @PreAuthorize("@ss.hasPermission('ai:mind-map:query')") + public CommonResult> getMindMapPage(@Valid AiMindMapPageReqVO pageReqVO) { + PageResult pageResult = mindMapService.getMindMapPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiMindMapRespVO.class)); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java new file mode 100644 index 000000000..c123ab70e --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapRespVO.java new file mode 100644 index 000000000..f65e809e9 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapRespVO.java @@ -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; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java index ff25e89ff..0292ef473 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java @@ -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 { + + default PageResult selectPage(AiMindMapPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(AiMindMapDO::getUserId, reqVO.getUserId()) + .eqIfPresent(AiMindMapDO::getPrompt, reqVO.getPrompt()) + .betweenIfPresent(AiMindMapDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(AiMindMapDO::getId)); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java index 2eb1f1b1a..65a5aaf3a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java @@ -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> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId); + /** + * 删除思维导图 + * + * @param id 编号 + */ + void deleteMindMap(Long id); + + /** + * 获得思维导图分页 + * + * @param pageReqVO 分页查询 + * @return 思维导图分页 + */ + PageResult getMindMapPage(AiMindMapPageReqVO pageReqVO); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java index 72be20c54..3197c0d8c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java @@ -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 实现类 @@ -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 getMindMapPage(AiMindMapPageReqVO pageReqVO) { + return mindMapMapper.selectPage(pageReqVO); + } + } From 83d86bcf9bba3e39337650fa6bae68c17effaf59 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 10 Aug 2024 18:39:43 +0800 Subject: [PATCH 012/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=20=E5=A4=A7=E6=A8=A1=E5=9E=8B=EF=BC=9A?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=80=9D=E7=BB=B4=E5=AF=BC=E5=9B=BE=E7=9A=84?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-module-ai/pom.xml | 2 +- .../java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java | 2 +- yudao-module-ai/yudao-module-ai-biz/pom.xml | 2 +- .../ai/controller/admin/mindmap/AiMindMapController.java | 2 +- .../yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml index 7135100d7..69a5e987f 100644 --- a/yudao-module-ai/pom.xml +++ b/yudao-module-ai/pom.xml @@ -18,7 +18,7 @@ ${project.artifactId} - ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。 + ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维导图等功能。 目前已接入各种模型,不限于: 国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java index 19cbc8f8f..811189efe 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java @@ -22,7 +22,7 @@ public enum AiChatRoleEnum implements IntArrayValuable { 除此之外不需要除了正文内容外的其他回复,如标题、开头、任何解释性语句或道歉。 """), - AI_MIND_MAP_ROLE(2, "脑图助手", """ + AI_MIND_MAP_ROLE(2, "导图助手", """ 你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子: # Geek-AI 助手 ## 完整的开源系统 diff --git a/yudao-module-ai/yudao-module-ai-biz/pom.xml b/yudao-module-ai/yudao-module-ai-biz/pom.xml index 7c529f118..ec6f8c762 100644 --- a/yudao-module-ai/yudao-module-ai-biz/pom.xml +++ b/yudao-module-ai/yudao-module-ai-biz/pom.xml @@ -12,7 +12,7 @@ ${project.artifactId} - ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。 + ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维导图等功能。 目前已接入各种模型,不限于: 国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java index a1d8de0df..f1c59b964 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java @@ -31,7 +31,7 @@ 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> generateMindMap(@RequestBody @Valid AiMindMapGenerateReqVO generateReqVO) { return mindMapService.generateMindMap(generateReqVO, getLoginUserId()); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java index 3197c0d8c..f0231081f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java @@ -61,10 +61,10 @@ public class AiMindMapServiceImpl implements AiMindMapService { @Override public Flux> 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()) From 8242735d84407c672410291a9151e6c1f5efc2f1 Mon Sep 17 00:00:00 2001 From: scholar <1145227973@qq.com> Date: Tue, 13 Aug 2024 19:26:45 +0800 Subject: [PATCH 013/136] =?UTF-8?q?1=EF=BC=8C=E8=85=BE=E8=AE=AF=E4=BA=91?= =?UTF-8?q?=E7=9F=AD=E4=BF=A1=E5=AE=9E=E7=8E=B0=E4=BC=98=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E5=8E=BB=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81VO;=202=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=85=BE=E8=AE=AF=E4=BA=91=E7=9F=AD=E4=BF=A1?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8D=95=E6=B5=8B=EF=BC=8C=E5=92=8C=E9=9B=86?= =?UTF-8?q?=E6=88=90=E5=8D=95=E6=B5=8B=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/client/impl/TencentSmsClient.java | 212 +++------------ .../sms/core/client/impl/SmsClientTests.java | 139 ++++++++++ .../client/impl/TencentSmsClientTest.java | 243 ++++++++---------- 3 files changed, 284 insertions(+), 310 deletions(-) create mode 100644 yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java index f18598b07..91f4c3f6b 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java @@ -2,36 +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; + /** * 腾讯云短信功能实现 @@ -103,14 +96,15 @@ public class TencentSmsClient extends AbstractSmsClient { 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()); + JSONObject JsonResponse = request(body,"SendSms","2021-01-11","ap-guangzhou"); + return new SmsSendRespDTO().setSuccess(API_CODE_SUCCESS.equals(JsonResponse.getJSONObject("Response").getJSONArray("SendStatusSet").getJSONObject(0).getStr("Code"))) + .setApiRequestId(JsonResponse.getJSONObject("Response").getStr("RequestId")) + .setSerialNo(JsonResponse.getJSONObject("Response").getJSONArray("SendStatusSet").getJSONObject(0).getStr("SerialNo")) + .setApiMsg(JsonResponse.getJSONObject("Response").getJSONArray("SendStatusSet").getJSONObject(0).getStr("Message")); } - JSONObject sendSmsRequest(TreeMap body,String action,String version,String region) throws Exception { + JSONObject request(TreeMap body,String action,String version,String region) throws Exception { String timestamp = String.valueOf(System.currentTimeMillis() / 1000); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); @@ -155,12 +149,9 @@ public class TencentSmsClient extends AbstractSmsClient { 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 { @@ -170,22 +161,20 @@ public class TencentSmsClient extends AbstractSmsClient { 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 parseSmsReceiveStatus(String text) { - List 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())); + + 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 @@ -193,48 +182,22 @@ public class TencentSmsClient extends AbstractSmsClient { // 构建请求 TreeMap body = new TreeMap<>(); - body.put("International",0); + body.put("International",INTERNATIONAL_CHINA); 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(); + JSONObject JsonResponse = request(body,"DescribeSmsTemplateList","2021-01-11","ap-guangzhou"); + System.out.println("JsonResponse======"+JsonResponse); - return new SmsTemplateRespDTO().setId(templateId).setContent(content) + JSONObject TemplateStatusSet = JsonResponse.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); } - 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").getFirst(); - - 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) { @@ -244,113 +207,4 @@ public class TencentSmsClient extends AbstractSmsClient { default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus)); } } - - @Data - public static class SmsResponse { - - /** - * 是否成功 - */ - private boolean success; - - /** - * 厂商原返回体 - */ - private Object data; - - } - - - /** - *

类名: QuerySmsTemplateResponse - *

说明: sms模板查询返回信息 - * - * @author :scholar - * 2024/07/17 0:25 - **/ - @Data - public static class QuerySmsTemplateResponse { - private List 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; - - } - -} +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java new file mode 100644 index 000000000..f1db141e8 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java @@ -0,0 +1,139 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +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; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.List; + +/** + * 各种 {@link SmsClientTests 集成测试 + * + * @author 芋道源码 + */ +public class SmsClientTests { + + @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> templateParams = List.of(new KeyValue<>("code", "1024")); + // 调用 + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + // 打印结果 + System.out.println(smsSendRespDTO); + } + + // ========== 阿里云 ========== + + @Test + @Disabled + public void testAliyunSmsClient_getSmsTemplate() throws Throwable { + SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") + .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz"); + AliyunSmsClient client = new AliyunSmsClient(properties); + // 准备参数 + String apiTemplateId = "SMS_207945135"; + // 调用 + SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId); + // 打印结果 + System.out.println(template); + } + + @Test + @Disabled + public void testAliyunSmsClient_sendSms() throws Throwable { + SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") + .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") + .setSignature("runpu"); + AliyunSmsClient client = new AliyunSmsClient(properties); + // 准备参数 + Long sendLogId = System.currentTimeMillis(); + String mobile = "15601691323"; + String apiTemplateId = "SMS_207945135"; + // 调用 + SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024"))); + // 打印结果 + System.out.println(sendRespDTO); + } + + @Test + @Disabled + public void testAliyunSmsClient_parseSmsReceiveStatus() { + SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") + .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz"); + AliyunSmsClient client = new AliyunSmsClient(properties); + // 准备参数 + String text = "[\n" + + " {\n" + + " \"phone_number\" : \"13900000001\",\n" + + " \"send_time\" : \"2017-01-01 11:12:13\",\n" + + " \"report_time\" : \"2017-02-02 22:23:24\",\n" + + " \"success\" : true,\n" + + " \"err_code\" : \"DELIVERED\",\n" + + " \"err_msg\" : \"用户接收成功\",\n" + + " \"sms_size\" : \"1\",\n" + + " \"biz_id\" : \"12345\",\n" + + " \"out_id\" : \"67890\"\n" + + " }\n" + + "]"; + // mock 方法 + + // 调用 + List statuses = client.parseSmsReceiveStatus(text); + // 打印结果 + 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); + } +} + diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java index e93435f4d..66cb8250e 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java @@ -1,10 +1,7 @@ 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.collection.ArrayUtils; -import cn.iocoder.yudao.framework.common.util.collection.MapUtils; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +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.dto.SmsReceiveRespDTO; import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; @@ -12,24 +9,19 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR 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 com.tencentcloudapi.sms.v20210111.SmsClient; -import com.tencentcloudapi.sms.v20210111.models.DescribeSmsTemplateListResponse; -import com.tencentcloudapi.sms.v20210111.models.DescribeTemplateListStatus; -import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse; -import com.tencentcloudapi.sms.v20210111.models.SendStatus; + 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.ArrayList; import java.util.List; -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mockStatic; /** * {@link TencentSmsClient} 的单元测试 @@ -46,9 +38,6 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { @InjectMocks private TencentSmsClient smsClient = new TencentSmsClient(properties); - @Mock - private SmsClient client; - @Test public void testDoInit() { // 准备参数 @@ -56,103 +45,92 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { // 调用 smsClient.doInit(); - // 断言 - assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); - } - - @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")); } @Test public void testDoSendSms_success() throws Throwable { - // 准备参数 - Long sendLogId = randomLongId(); - String mobile = randomString(); - String apiTemplateId = randomString(); - List> 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()); + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> 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" + + "}" + ); + + // 调用 + 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 testDoSendSms_fail() throws Throwable { - // 准备参数 - Long sendLogId = randomLongId(); - String mobile = randomString(); - String apiTemplateId = randomString(); - List> 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); + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); - // 调用 - 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()); + // 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 @@ -170,7 +148,6 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { " \"ext\": {\"logId\":\"67890\"}\n" + " }\n" + "]"; - // mock 方法 // 调用 List statuses = smsClient.parseSmsReceiveStatus(text); @@ -178,41 +155,45 @@ 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); + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { - // 调用 - 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()); + // 准备参数 + 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 From 664abe70626acf5a0e23439f6e900c1803427784 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 14 Aug 2024 00:44:14 +0800 Subject: [PATCH 014/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E6=B7=BB=E5=8A=A0=E5=95=86=E5=93=81=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E6=97=B6=E5=85=81=E8=AE=B8=E9=80=89=E6=8B=A9=E5=B7=B2?= =?UTF-8?q?=E6=9C=89=E7=9A=84=E5=B1=9E=E6=80=A7=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/property/ProductPropertyController.java | 11 +++++++++++ .../property/ProductPropertyValueController.java | 13 +++++++++++++ .../dal/dataobject/property/ProductPropertyDO.java | 4 ---- .../service/property/ProductPropertyService.java | 7 +++++++ .../property/ProductPropertyServiceImpl.java | 5 +++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/ProductPropertyController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/ProductPropertyController.java index fed5d8d69..1ff2d9c4b 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/ProductPropertyController.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/ProductPropertyController.java @@ -17,7 +17,10 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "管理后台 - 商品属性项") @RestController @@ -69,4 +72,12 @@ public class ProductPropertyController { return success(BeanUtils.toBean(pageResult, ProductPropertyRespVO.class)); } + @GetMapping("/simple-list") + @Operation(summary = "获得属性项精简列表") + public CommonResult> getPropertySimpleList() { + List list = productPropertyService.getPropertyList(); + return success(convertList(list, property -> new ProductPropertyRespVO() // 只返回 id、name 属性 + .setId(property.getId()).setName(property.getName()))); + } + } diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/ProductPropertyValueController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/ProductPropertyValueController.java index 4a613fb1f..647df87a4 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/ProductPropertyValueController.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/ProductPropertyValueController.java @@ -17,7 +17,11 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.singleton; @Tag(name = "管理后台 - 商品属性值") @RestController @@ -69,4 +73,13 @@ public class ProductPropertyValueController { return success(BeanUtils.toBean(pageResult, ProductPropertyValueRespVO.class)); } + @GetMapping("/simple-list") + @Operation(summary = "获得属性值精简列表") + @Parameter(name = "propertyId", description = "属性项编号", required = true, example = "1024") + public CommonResult> getPropertyValueSimpleList(@RequestParam("propertyId") Long propertyId) { + List list = productPropertyValueService.getPropertyValueListByPropertyId(singleton(propertyId)); + return success(convertList(list, value -> new ProductPropertyValueRespVO() // 只返回 id、name 属性 + .setId(value.getId()).setName(value.getName()))); + } + } diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/property/ProductPropertyDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/property/ProductPropertyDO.java index 8cc646bd5..d23a828ea 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/property/ProductPropertyDO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/property/ProductPropertyDO.java @@ -39,10 +39,6 @@ public class ProductPropertyDO extends BaseDO { * 名称 */ private String name; - /** - * 状态 - */ - private Integer status; /** * 备注 */ diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyService.java index fe14cd7a7..087213618 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyService.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyService.java @@ -62,4 +62,11 @@ public interface ProductPropertyService { */ List getPropertyList(Collection ids); + /** + * 获得指定状态的属性项列表 + * + * @return 属性项列表 + */ + List getPropertyList(); + } diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyServiceImpl.java index 4747b1703..6c1d32815 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyServiceImpl.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyServiceImpl.java @@ -109,4 +109,9 @@ public class ProductPropertyServiceImpl implements ProductPropertyService { return productPropertyMapper.selectBatchIds(ids); } + @Override + public List getPropertyList() { + return productPropertyMapper.selectList(); + } + } From 5d5d5c33f56479e45fc5cff5b87afacab53b5951 Mon Sep 17 00:00:00 2001 From: Cyrix66 <120878696@qq.com> Date: Tue, 13 Aug 2024 17:00:19 +0000 Subject: [PATCH 015/136] =?UTF-8?q?update=20sql/mysql/ruoyi-vue-pro.sql.?= =?UTF-8?q?=20=E6=AD=A4PR=E5=90=8C=E6=88=91=E4=B9=8B=E5=89=8D=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E7=9A=84#1029=E5=8F=B7PR=EF=BC=8C=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E3=80=90system=5Fmenu=E3=80=91=E8=8F=9C=E5=8D=95=E7=9A=84=20AI?= =?UTF-8?q?=20=E9=9F=B3=E4=B9=90=E7=9A=84=E7=BB=84=E4=BB=B6=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E5=91=BD=E5=90=8D=E9=94=99=E8=AF=AF=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1029 Signed-off-by: Cyrix66 <120878696@qq.com> --- sql/mysql/ruoyi-vue-pro.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index ccc8dfcde..274e17a5e 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -1961,7 +1961,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2793, '写作管理', '', 2, 13, 2760, 'write', 'fa:bookmark-o', 'ai/write/manager/index.vue', 'AiWriteManager', 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '1', '2024-07-10 21:31:59', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2794, 'AI 写作查询', 'ai:write:query', 3, 1, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2795, 'AI 写作删除', 'ai:write:delete', 3, 4, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiWrite', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-17 09:36:12', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiMusic', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-17 09:36:12', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2797, '客服中心', '', 2, 100, 2362, 'kefu', 'fa-solid:user-alt', 'mall/promotion/kefu/index', 'KeFu', 0, b'1', b'1', b'1', '1', '2024-07-17 23:49:05', '1', '2024-07-17 23:49:16', b'0'); COMMIT; From 9af286693f995fb80704ab9156f0bdc620491bd6 Mon Sep 17 00:00:00 2001 From: Ordinary Date: Sun, 11 Aug 2024 00:11:49 +0800 Subject: [PATCH 016/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E5=95=86=E5=93=81=E9=A1=B9=E5=9B=BE=E7=89=87=E4=B8=BA=E7=A9=BA?= =?UTF-8?q?=E4=B8=B2=E6=97=B6=EF=BC=8C=E8=AE=A2=E5=8D=95=E9=A1=B9=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E4=BD=BF=E7=94=A8SPU=E5=9B=BE=E7=89=87=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/price/calculator/TradePriceCalculatorHelper.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java index 2862012af..891f1e0dc 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java @@ -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 一一对应 */ From c2a50c4d9c00b760aeef35063d0a20bc8302d5f1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 14 Aug 2024 23:52:19 +0800 Subject: [PATCH 017/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91SYSTEM=EF=BC=9A=E8=85=BE=E8=AE=AF=E4=BA=91?= =?UTF-8?q?=E7=9F=AD=E4=BF=A1=E5=AE=A2=E6=88=B7=E7=AB=AF=E7=9A=84=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sms/core/client/impl/AliyunSmsClient.java | 3 - .../sms/core/client/impl/HuaweiSmsClient.java | 6 +- .../core/client/impl/TencentSmsClient.java | 153 ++++++++++-------- .../core/client/impl/AliyunSmsClientTest.java | 9 -- .../sms/core/client/impl/SmsClientTests.java | 41 ++--- .../client/impl/TencentSmsClientTest.java | 26 +-- 6 files changed, 114 insertions(+), 124 deletions(-) diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java index ed6dd7a8d..f8158cdf2 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java @@ -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() diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java index 4df820861..fdf2faa1a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java @@ -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> templateParams) throws Throwable { + // 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构 // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。 String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号 diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java index 91f4c3f6b..23a01db24 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java @@ -25,7 +25,6 @@ import java.util.*; import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; - /** * 腾讯云短信功能实现 * @@ -35,6 +34,9 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. */ public class TencentSmsClient extends AbstractSmsClient { + private static final String VERSION = "2021-01-11"; + private static final String REGION = "ap-guangzhou"; + /** * 调用成功 code */ @@ -48,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 不能为空"); @@ -57,7 +58,6 @@ public class TencentSmsClient extends AbstractSmsClient { @Override protected void doInit() { - } /** @@ -87,32 +87,96 @@ public class TencentSmsClient extends AbstractSmsClient { @Override public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { - // 构建请求 + // 1. 执行请求 + // 参考链接 https://cloud.tencent.com/document/product/382/55981 TreeMap 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()))); + body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue()))); + JSONObject response = request("SendSms", body); - JSONObject JsonResponse = request(body,"SendSms","2021-01-11","ap-guangzhou"); - - return new SmsSendRespDTO().setSuccess(API_CODE_SUCCESS.equals(JsonResponse.getJSONObject("Response").getJSONArray("SendStatusSet").getJSONObject(0).getStr("Code"))) - .setApiRequestId(JsonResponse.getJSONObject("Response").getStr("RequestId")) - .setSerialNo(JsonResponse.getJSONObject("Response").getJSONArray("SendStatusSet").getJSONObject(0).getStr("SerialNo")) - .setApiMsg(JsonResponse.getJSONObject("Response").getJSONArray("SendStatusSet").getJSONObject(0).getStr("Message")); + // 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 request(TreeMap body,String action,String version,String region) throws Exception { + @Override + public List 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 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 签名方法 v3 + * + * @param action 请求的 API 名称 + * @param body 请求参数 + * @return 请求结果 + */ + private JSONObject request(String action, TreeMap 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 = "/"; @@ -122,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:拼接待签名字符串 ************* @@ -146,65 +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); - String responseBody = HttpUtils.post("https://"+host, headers, JSONUtil.toJsonStr(body)); + String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(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)); } - - @Override - public List 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 { - - // 构建请求 - TreeMap body = new TreeMap<>(); - body.put("International",INTERNATIONAL_CHINA); - Integer[] templateIds = {Integer.valueOf(apiTemplateId)}; - body.put("TemplateIdSet",templateIds); - - JSONObject JsonResponse = request(body,"DescribeSmsTemplateList","2021-01-11","ap-guangzhou"); - System.out.println("JsonResponse======"+JsonResponse); - - JSONObject TemplateStatusSet = JsonResponse.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)); - } - } } \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java index c6e015d81..093060e84 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java @@ -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 httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java index b22f0f3f0..6eb22af1b 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java @@ -17,25 +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") - .setSignature("runpu"); - HuaweiSmsClient client = new HuaweiSmsClient(properties); - // 准备参数 - Long sendLogId = System.currentTimeMillis(); - String mobile = "15601691323"; - String apiTemplateId = "xx test01"; - List> templateParams = List.of(new KeyValue<>("code", "1024")); - // 调用 - SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); - // 打印结果 - System.out.println(smsSendRespDTO); - } - // ========== 阿里云 ========== @Test @@ -135,5 +116,27 @@ public class SmsClientTests { // 打印结果 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> templateParams = List.of(new KeyValue<>("code", "1024")); + // 调用 + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + // 打印结果 + System.out.println(smsSendRespDTO); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java index 66cb8250e..b25540b44 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java @@ -38,18 +38,8 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { @InjectMocks private TencentSmsClient smsClient = new TencentSmsClient(properties); - @Test - public void testDoInit() { - // 准备参数 - // mock 方法 - - // 调用 - smsClient.doInit(); - } - @Test public void testDoSendSms_success() throws Throwable { - try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { // 准备参数 Long sendLogId = randomLongId(); @@ -57,11 +47,9 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { String apiTemplateId = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); - // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) - .thenReturn( - "{\n" + + .thenReturn("{\n" + " \"Response\": {\n" + " \"SendStatusSet\": [\n" + " {\n" + @@ -76,8 +64,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { " ],\n" + " \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" + " }\n" + - "}" - ); + "}"); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, @@ -87,7 +74,6 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { assertEquals("5000:1045710669157053657849499619", result.getSerialNo()); assertEquals("a0aabda6-cf91-4f3e-a81f-9198114a2279", result.getApiRequestId()); assertEquals("send success", result.getApiMsg()); - } } @@ -103,8 +89,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) - .thenReturn( - "{\n" + + .thenReturn("{\n" + " \"Response\": {\n" + " \"SendStatusSet\": [\n" + " {\n" + @@ -119,8 +104,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { " ],\n" + " \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" + " }\n" + - "}" - ); + "}"); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, @@ -162,9 +146,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { @Test public void testGetSmsTemplate() throws Throwable { - try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { - // 准备参数 String apiTemplateId = "1122"; From b4af042c640cc0e710f7c4121b6375c50210d54e Mon Sep 17 00:00:00 2001 From: xiaoxin <718949661@qq.com> Date: Thu, 15 Aug 2024 11:28:02 +0800 Subject: [PATCH 018/136] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91AI?= =?UTF-8?q?=20=E7=9F=A5=E8=AF=86=E5=BA=93=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/enums/ErrorCodeConstants.java | 4 ++ .../knowledge/AiKnowledgeController.java | 39 ++++++++++ .../vo/AiKnowledgeCreateMyReqVO.java | 31 ++++++++ .../vo/AiKnowledgeUpdateMyReqVO.java | 36 ++++++++++ .../knowledge/AiKnowledgeBaseDO.java | 56 +++++++++++++++ .../knowledge/AiKnowledgeDocumentDO.java | 55 ++++++++++++++ .../knowledge/AiKnowledgeSegmentDO.java | 48 +++++++++++++ .../knowledge/AiKnowledgeBaseMapper.java | 12 ++++ .../knowledge/AiKnowledgeDocumentMapper.java | 12 ++++ .../knowledge/AiKnowledgeSegmentMapper.java | 12 ++++ .../service/knowledge/AiEmbeddingService.java | 26 +++++++ ...eImpl.java => AiEmbeddingServiceImpl.java} | 12 ++-- .../knowledge/AiKnowledgeBaseService.java | 30 ++++++++ .../knowledge/AiKnowledgeBaseServiceImpl.java | 72 +++++++++++++++++++ .../knowledge/AiKnowledgeDocumentService.java | 10 +++ .../AiKnowledgeDocumentServiceImpl.java | 16 +++++ .../knowledge/AiKnowledgeSegmentService.java | 11 +++ .../AiKnowledgeSegmentServiceImpl.java | 16 +++++ .../ai/service/knowledge/DocService.java | 15 ---- .../yudao-spring-boot-starter-ai/pom.xml | 6 +- .../RedisVectorStoreAutoConfiguration.java | 1 + 21 files changed, 497 insertions(+), 23 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeUpdateMyReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/{DocServiceImpl.java => AiEmbeddingServiceImpl.java} (76%) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java delete mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index ddfb489f3..b68591796 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -50,4 +50,8 @@ public interface ErrorCodeConstants { ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!"); ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "写作生成异常!"); + + // ========== API 知识库 1-022-008-000 ========== + ErrorCode KNOWLEDGE_NOT_EXISTS = new ErrorCode(1_022_008_000, "知识库不存在!"); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java new file mode 100644 index 000000000..9d9c99a9a --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeCreateMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdateMyReqVO; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeBaseService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - AI 知识库") +@RestController +@RequestMapping("/ai/knowledge") +public class AiKnowledgeController { + + @Resource + private AiKnowledgeBaseService knowledgeBaseService; + + @PostMapping("/create-my") + @Operation(summary = "创建【我的】知识库") + public CommonResult createKnowledgeMy(@RequestBody @Valid AiKnowledgeCreateMyReqVO createReqVO) { + return success(knowledgeBaseService.createKnowledgeMy(createReqVO, getLoginUserId())); + } + + + @PutMapping("/update-my") + @Operation(summary = "更新【我的】知识库") + public CommonResult updateKnowledgeMy(@RequestBody @Valid AiKnowledgeUpdateMyReqVO updateReqVO) { + knowledgeBaseService.updateKnowledgeMy(updateReqVO, getLoginUserId()); + return success(true); + } + + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java new file mode 100644 index 000000000..817fcaabb --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * @author xiaoxin + */ +@Schema(description = "管理后台 - AI 知识库创建【我的】 Request VO") +@Data +public class AiKnowledgeCreateMyReqVO { + + @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "") + @NotBlank(message = "知识库名称不能为空") + private String name; + + @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "") + private String description; + + @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]") + private List visibilityPermissions; + + @Schema(description = "嵌入模型 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "嵌入模型不能为空") + private Long modelId; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeUpdateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeUpdateMyReqVO.java new file mode 100644 index 000000000..2bc39d5db --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeUpdateMyReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * @author xiaoxin + */ +@Schema(description = "管理后台 - AI 知识库创建【我的】 Request VO") +@Data +public class AiKnowledgeUpdateMyReqVO { + + + @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204") + @NotNull(message = "知识库编号不能为空") + private Long id; + + @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "") + @NotBlank(message = "知识库名称不能为空") + private String name; + + @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "") + private String description; + + @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]") + private List visibilityPermissions; + + @Schema(description = "嵌入模型 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "嵌入模型不能为空") + private Long modelId; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java new file mode 100644 index 000000000..3bf45d348 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; + + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.List; + +/** + * AI 知识库 DO + * + * @author xiaoxin + */ +@TableName(value = "ai_knowledge_base") +@Data +public class AiKnowledgeBaseDO extends BaseDO { + + /** + * 编号 + */ + @TableId(type = IdType.AUTO) + private Long id; + /** + * 用户编号 + *

+ * 关联 AdminUserDO 的 userId 字段 + */ + private Long userId; + /** + * 知识库名称 + */ + private String name; + /** + * 知识库描述 + */ + private String description; + /** + * 可见权限,只能选择哪些人可见 + */ + private List visibilityPermissions; + /** + * 嵌入模型编号,高质量模式时维护 + */ + private Long modelId; + /** + * 模型标识 + */ + private String model; + /** + * 是否启用 + */ + private Boolean status; +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java new file mode 100644 index 000000000..8de3697a7 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * AI 知识库-文档 DO + * + * @author xiaoxin + */ +@TableName(value = "ai_knowledge_document") +@Data +public class AiKnowledgeDocumentDO extends BaseDO { + + /** + * 编号 + */ + @TableId(type = IdType.AUTO) + private Long id; + /** + * 知识库编号 + */ + private Long knowledgeId; + /** + * 文件名称 + */ + private String name; + /** + * 内容 + */ + private String content; + /** + * 文件 URL + */ + private String url; + /** + * token数量 + */ + private Integer tokens; + /** + * 字符数 + */ + private Integer wordCount; + /** + * 切片状态 + */ + private Integer sliceStatus; + /** + * 是否启用 + */ + private Boolean status; +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java new file mode 100644 index 000000000..4ce3bb4ee --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * AI 知识库-文档分段 DO + * + * @author xiaoxin + */ +@TableName(value = "ai_knowledge_segment") +@Data +public class AiKnowledgeSegmentDO extends BaseDO { + + /** + * 编号 + */ + @TableId(type = IdType.AUTO) + private Long id; + /** + * 向量库的id + */ + private String vectorId; + /** + * 文档编号 + */ + private Long documentId; + /** + * 切片内容 + */ + private String content; + /** + * 字符数 + */ + private Integer wordCount; + /** + * token数量 + */ + private Integer tokens; + /** + * 是否启用 + */ + private Boolean status; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java new file mode 100644 index 000000000..3a23aa55d --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java @@ -0,0 +1,12 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeBaseDO; + +/** + * AI 知识库基础信息 Mapper + * + * @author xiaoxin + */ +public interface AiKnowledgeBaseMapper extends BaseMapperX { +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java new file mode 100644 index 000000000..35532956f --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java @@ -0,0 +1,12 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; + +/** + * AI 知识库-文档 Mapper + * + * @author xiaoxin + */ +public interface AiKnowledgeDocumentMapper extends BaseMapperX { +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java new file mode 100644 index 000000000..c3cbca2c1 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java @@ -0,0 +1,12 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; + +/** + * AI 知识库-分片 Mapper + * + * @author xiaoxin + */ +public interface AiKnowledgeSegmentMapper extends BaseMapperX { +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java new file mode 100644 index 000000000..38ff0d022 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.ai.service.knowledge; + +import org.springframework.ai.document.Document; + +import java.util.List; + +/** + * AI 嵌入 Service 接口 + * + * @author xiaoxin + */ +public interface AiEmbeddingService { + + /** + * 向量化文档 + */ + void embeddingDoc(); + + + /** + * 相似查询 + * + * @param content 查询内容 + */ + List similaritySearch(String content); +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java similarity index 76% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java index b0f4afaf8..37c414428 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java @@ -1,24 +1,23 @@ 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.ai.vectorstore.SearchRequest; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.List; /** - * AI 知识库 Service 实现类 + * AI 嵌入 Service 实现类 * * @author xiaoxin */ @Service -@Slf4j -public class DocServiceImpl implements DocService { +public class AiEmbeddingServiceImpl implements AiEmbeddingService { @Resource private RedisVectorStore vectorStore; @@ -40,4 +39,9 @@ public class DocServiceImpl implements DocService { vectorStore.add(segments); } + @Override + public List similaritySearch(String content) { + SearchRequest request = SearchRequest.query(content); + return vectorStore.similaritySearch(request); + } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseService.java new file mode 100644 index 000000000..7657ab748 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseService.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.ai.service.knowledge; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeCreateMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdateMyReqVO; + +/** + * AI 知识库-基础信息 Service 接口 + * + * @author xiaoxin + */ +public interface AiKnowledgeBaseService { + + + /** + * 创建【我的】知识库 + * + * @param createReqVO 创建信息 + * @param userId 用户编号 + * @return 编号 + */ + Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId); + + + /** + * 创建【我的】知识库 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId); +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java new file mode 100644 index 000000000..4f28726bd --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.ai.service.knowledge; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeCreateMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdateMyReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeBaseDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeBaseMapper; +import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_NOT_EXISTS; + +/** + * AI 知识库-基础信息 Service 实现类 + * + * @author xiaoxin + */ +@Service +@Slf4j +public class AiKnowledgeBaseServiceImpl implements AiKnowledgeBaseService { + + @Resource + private AiKnowledgeBaseMapper knowledgeBaseMapper; + @Resource + private AiChatModelService chatModalService; + + @Override + public Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId) { + AiChatModelDO model = validateChatModel(createReqVO.getModelId()); + + AiKnowledgeBaseDO knowledgeBaseDO = BeanUtils.toBean(createReqVO, AiKnowledgeBaseDO.class); + knowledgeBaseDO.setModel(model.getModel()).setUserId(userId); + + knowledgeBaseMapper.insert(knowledgeBaseDO); + return knowledgeBaseDO.getId(); + } + + @Override + public void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId) { + + AiKnowledgeBaseDO knowledgeBaseDO = validateKnowledgeExists(updateReqVO.getId()); + if (ObjUtil.notEqual(knowledgeBaseDO.getUserId(), userId)) { + throw exception(KNOWLEDGE_NOT_EXISTS); + } + AiChatModelDO model = validateChatModel(updateReqVO.getModelId()); + AiKnowledgeBaseDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeBaseDO.class); + updateDO.setModel(model.getModel()); + + knowledgeBaseMapper.updateById(updateDO); + } + + + private AiChatModelDO validateChatModel(Long id) { + AiChatModelDO model = chatModalService.validateChatModel(id); + Assert.notNull(model, "未找到对应嵌入模型"); + return model; + } + + public AiKnowledgeBaseDO validateKnowledgeExists(Long id) { + AiKnowledgeBaseDO knowledgeBase = knowledgeBaseMapper.selectById(id); + if (knowledgeBase == null) { + throw exception(KNOWLEDGE_NOT_EXISTS); + } + return knowledgeBase; + } +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java new file mode 100644 index 000000000..5af45e5e8 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -0,0 +1,10 @@ +package cn.iocoder.yudao.module.ai.service.knowledge; + +/** + * AI 知识库-文档 Service 接口 + * + * @author xiaoxin + */ +public interface AiKnowledgeDocumentService { + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java new file mode 100644 index 000000000..84ebb617e --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.ai.service.knowledge; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * AI 知识库-文档 Service 实现类 + * + * @author xiaoxin + */ +@Service +@Slf4j +public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentService { + + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java new file mode 100644 index 000000000..003ce5c96 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -0,0 +1,11 @@ +package cn.iocoder.yudao.module.ai.service.knowledge; + +/** + * AI 知识库-分片 Service 接口 + * + * @author xiaoxin + */ +public interface AiKnowledgeSegmentService { + + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java new file mode 100644 index 000000000..aa5facc36 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.ai.service.knowledge; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * AI 知识库-基础信息 Service 实现类 + * + * @author xiaoxin + */ +@Service +@Slf4j +public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService { + + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java deleted file mode 100644 index 47905d4b1..000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java +++ /dev/null @@ -1,15 +0,0 @@ -package cn.iocoder.yudao.module.ai.service.knowledge; - -/** - * AI 知识库 Service 接口 - * - * @author xiaoxin - */ -public interface DocService { - - /** - * 向量化文档 - */ - void embeddingDoc(); - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index ae1f37948..4585311c7 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -57,11 +57,9 @@ ${spring-ai.version} - - org.springframework.data - spring-data-redis - true + cn.iocoder.boot + yudao-spring-boot-starter-redis diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java index 61c38dd1d..615b05f78 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java @@ -31,6 +31,7 @@ import redis.clients.jedis.JedisPooled; * TODO @xin 先拿 spring-ai 最新代码覆盖,1.0.0-M1 跟 redis 自动配置会冲突 * * TODO 这个官方,有说啥时候 fix 哇? + * TODO 看着是列在1.0.0-M2版本 * * @author Christian Tzolov * @author Eddú Meléndez From 8e54eef8af762e77084c4d7742307afa2cdf212a Mon Sep 17 00:00:00 2001 From: xiaoxin <718949661@qq.com> Date: Thu, 15 Aug 2024 15:57:03 +0800 Subject: [PATCH 019/136] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91AI?= =?UTF-8?q?=20=E7=9F=A5=E8=AF=86=E5=BA=93=EF=BC=9A=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=88=87=E7=89=87=E5=90=91=E9=87=8F=E5=8C=96=E5=85=A5=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AiKnowledgeDocumentStatusEnum.java | 39 +++++++++++ .../vo/AiKnowledgeCreateMyReqVO.java | 4 +- .../vo/AiKnowledgeDocumentCreateReqVO.java | 27 ++++++++ .../knowledge/AiKnowledgeBaseDO.java | 12 +++- .../knowledge/AiKnowledgeDocumentDO.java | 11 ++- .../knowledge/AiKnowledgeSegmentDO.java | 7 +- .../knowledge/AiKnowledgeBaseMapper.java | 2 + .../knowledge/AiKnowledgeDocumentMapper.java | 2 + .../knowledge/AiKnowledgeSegmentMapper.java | 2 + .../service/knowledge/AiEmbeddingService.java | 7 +- .../knowledge/AiEmbeddingServiceImpl.java | 23 ++----- .../knowledge/AiKnowledgeBaseServiceImpl.java | 9 ++- .../knowledge/AiKnowledgeDocumentService.java | 11 +++ .../AiKnowledgeDocumentServiceImpl.java | 68 +++++++++++++++++++ 14 files changed, 190 insertions(+), 34 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeDocumentCreateReqVO.java diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java new file mode 100644 index 000000000..a37fa8643 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.ai.enums.knowledge; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * AI 知识库-文档状态的枚举 + * + * @author xiaoxin + */ +@AllArgsConstructor +@Getter +public enum AiKnowledgeDocumentStatusEnum implements IntArrayValuable { + + IN_PROGRESS(10, "索引中"), + SUCCESS(20, "可用"), + FAIL(30, "失败"); + + /** + * 状态 + */ + private final Integer status; + + /** + * 状态名 + */ + private final String name; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiKnowledgeDocumentStatusEnum::getStatus).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java index 817fcaabb..fa9161eba 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java @@ -14,11 +14,11 @@ import java.util.List; @Data public class AiKnowledgeCreateMyReqVO { - @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "") + @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南") @NotBlank(message = "知识库名称不能为空") private String name; - @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "") + @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "存储 ruoyi-vue-pro 操作文档") private String description; @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeDocumentCreateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeDocumentCreateReqVO.java new file mode 100644 index 000000000..fa24eef72 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeDocumentCreateReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * @author xiaoxin + */ +@Schema(description = "管理后台 - AI 知识库【创建文档】 Request VO") +@Data +public class AiKnowledgeDocumentCreateReqVO { + + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204") + @NotNull(message = "知识库编号不能为空") + private Long knowledgeId; + + @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "三方登陆") + @NotBlank(message = "文档名称不能为空") + private String name; + + @Schema(description = "文档 url", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn") + private String url; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java index 3bf45d348..81cbf3ac9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java @@ -1,10 +1,13 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.Data; import java.util.List; @@ -40,7 +43,8 @@ public class AiKnowledgeBaseDO extends BaseDO { /** * 可见权限,只能选择哪些人可见 */ - private List visibilityPermissions; + @TableField(typeHandler = JacksonTypeHandler.class) + private List visibilityPermissions; /** * 嵌入模型编号,高质量模式时维护 */ @@ -50,7 +54,9 @@ public class AiKnowledgeBaseDO extends BaseDO { */ private String model; /** - * 是否启用 + * 状态 + *

+ * 枚举 {@link CommonStatusEnum} */ - private Boolean status; + private Integer status; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java index 8de3697a7..75d927cf0 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -46,10 +48,15 @@ public class AiKnowledgeDocumentDO extends BaseDO { private Integer wordCount; /** * 切片状态 + *

+ * 枚举 {@link AiKnowledgeDocumentStatusEnum} */ private Integer sliceStatus; + /** - * 是否启用 + * 状态 + *

+ * 枚举 {@link CommonStatusEnum} */ - private Boolean status; + private Integer status; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java index 4ce3bb4ee..657a3739b 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; @@ -41,8 +42,10 @@ public class AiKnowledgeSegmentDO extends BaseDO { */ private Integer tokens; /** - * 是否启用 + * 状态 + *

+ * 枚举 {@link CommonStatusEnum} */ - private Boolean status; + private Integer status; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java index 3a23aa55d..cad90fcfe 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java @@ -2,11 +2,13 @@ package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeBaseDO; +import org.apache.ibatis.annotations.Mapper; /** * AI 知识库基础信息 Mapper * * @author xiaoxin */ +@Mapper public interface AiKnowledgeBaseMapper extends BaseMapperX { } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java index 35532956f..af55f545a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java @@ -2,11 +2,13 @@ package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import org.apache.ibatis.annotations.Mapper; /** * AI 知识库-文档 Mapper * * @author xiaoxin */ +@Mapper public interface AiKnowledgeDocumentMapper extends BaseMapperX { } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java index c3cbca2c1..5043ee0ca 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java @@ -2,11 +2,13 @@ package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import org.apache.ibatis.annotations.Mapper; /** * AI 知识库-分片 Mapper * * @author xiaoxin */ +@Mapper public interface AiKnowledgeSegmentMapper extends BaseMapperX { } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java index 38ff0d022..9055cdc18 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import org.springframework.ai.document.Document; +import org.springframework.ai.vectorstore.SearchRequest; import java.util.List; @@ -12,9 +13,9 @@ import java.util.List; public interface AiEmbeddingService { /** - * 向量化文档 + * 向量化文档并存储 */ - void embeddingDoc(); + void add(List documents); /** @@ -22,5 +23,5 @@ public interface AiEmbeddingService { * * @param content 查询内容 */ - List similaritySearch(String content); + List similaritySearch(SearchRequest request); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java index 37c414428..a2c3e819d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java @@ -2,11 +2,9 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import jakarta.annotation.Resource; 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.ai.vectorstore.SearchRequest; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.List; @@ -21,27 +19,14 @@ public class AiEmbeddingServiceImpl implements AiEmbeddingService { @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 documents = loader.get(); - // 文档分段 - List segments = tokenTextSplitter.apply(documents); - // 向量化并存储 - vectorStore.add(segments); + public void add(List documents) { + vectorStore.add(documents); } @Override - public List similaritySearch(String content) { - SearchRequest request = SearchRequest.query(content); + public List similaritySearch(SearchRequest request) { return vectorStore.similaritySearch(request); } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java index 4f28726bd..63f4f53db 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeCreateMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdateMyReqVO; @@ -25,17 +26,19 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_NOT_ @Slf4j public class AiKnowledgeBaseServiceImpl implements AiKnowledgeBaseService { - @Resource - private AiKnowledgeBaseMapper knowledgeBaseMapper; @Resource private AiChatModelService chatModalService; + @Resource + private AiKnowledgeBaseMapper knowledgeBaseMapper; + + @Override public Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId) { AiChatModelDO model = validateChatModel(createReqVO.getModelId()); AiKnowledgeBaseDO knowledgeBaseDO = BeanUtils.toBean(createReqVO, AiKnowledgeBaseDO.class); - knowledgeBaseDO.setModel(model.getModel()).setUserId(userId); + knowledgeBaseDO.setModel(model.getModel()).setUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); knowledgeBaseMapper.insert(knowledgeBaseDO); return knowledgeBaseDO.getId(); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 5af45e5e8..52c62abf7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.ai.service.knowledge; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeDocumentCreateReqVO; + /** * AI 知识库-文档 Service 接口 * @@ -7,4 +9,13 @@ package cn.iocoder.yudao.module.ai.service.knowledge; */ public interface AiKnowledgeDocumentService { + + /** + * 创建文档 + * + * @param createReqVO 文档创建 Request VO + * @return 文档编号 + */ + Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 84ebb617e..caef4b802 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -1,7 +1,25 @@ package cn.iocoder.yudao.module.ai.service.knowledge; +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper; +import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; +import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum; +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.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; /** * AI 知识库-文档 Service 实现类 @@ -12,5 +30,55 @@ import org.springframework.stereotype.Service; @Slf4j public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentService { + @Resource + private AiKnowledgeDocumentMapper documentMapper; + @Resource + private AiKnowledgeSegmentMapper segmentMapper; + @Resource + private TokenTextSplitter tokenTextSplitter; + + @Resource + private AiEmbeddingService embeddingService; + + // TODO @xin 临时测试用,后续删 + @Value("classpath:/webapp/test/Fel.pdf") + private org.springframework.core.io.Resource data; + + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) { + AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class); + documentDO + //todo + .setTokens(0).setWordCount(0) + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setSliceStatus(AiKnowledgeDocumentStatusEnum.SUCCESS.getStatus()); + documentMapper.insert(documentDO); + + TikaDocumentReader loader = new TikaDocumentReader(data); + List documents = loader.get(); + Long documentId = documentDO.getId(); + if (CollUtil.isEmpty(documents)) { + log.info("文档内容为空"); + return documentId; + } + + // 文档分段 + List segments = tokenTextSplitter.apply(documents); + + List segmentDOList = CollectionUtils.convertList(segments, + segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId) + //todo + .setTokens(0).setWordCount(0) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + + // 分段内容入库 + segmentMapper.insertBatch(segmentDOList); + + //向量化并存储 + embeddingService.add(segments); + + return documentId; + } } From 238f603f690ef7e747b177729237af1a008b3056 Mon Sep 17 00:00:00 2001 From: xiaoxin <718949661@qq.com> Date: Thu, 15 Aug 2024 16:50:16 +0800 Subject: [PATCH 020/136] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91AI?= =?UTF-8?q?=20=E7=9F=A5=E8=AF=86=E5=BA=93=EF=BC=9A=E6=96=87=E6=A1=A3=20tok?= =?UTF-8?q?en=E3=80=81=E5=AD=97=E7=AC=A6=E6=95=B0=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/knowledge/AiEmbeddingService.java | 2 +- .../knowledge/AiEmbeddingServiceImpl.java | 3 +- .../AiKnowledgeDocumentServiceImpl.java | 29 ++++++++++++------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java index 9055cdc18..eee2f8044 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java @@ -21,7 +21,7 @@ public interface AiEmbeddingService { /** * 相似查询 * - * @param content 查询内容 + * @param request 查询实体 */ List similaritySearch(SearchRequest request); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java index a2c3e819d..2a6e75722 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java @@ -4,7 +4,6 @@ import jakarta.annotation.Resource; import org.springframework.ai.document.Document; import org.springframework.ai.vectorstore.RedisVectorStore; import org.springframework.ai.vectorstore.SearchRequest; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.List; @@ -21,6 +20,8 @@ public class AiEmbeddingServiceImpl implements AiEmbeddingService { private RedisVectorStore vectorStore; @Override +// @Async + // TODO xiaoxin 报错先注释 public void add(List documents) { vectorStore.add(documents); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index caef4b802..9ee5c4eed 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -14,12 +14,14 @@ 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.tokenizer.JTokkitTokenCountEstimator; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Objects; /** * AI 知识库-文档 Service 实现类 @@ -41,7 +43,9 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic @Resource private AiEmbeddingService embeddingService; - // TODO @xin 临时测试用,后续删 + private static final JTokkitTokenCountEstimator TOKEN_COUNT_ESTIMATOR = new JTokkitTokenCountEstimator(); + + // TODO xiaoxin 临时测试用,后续删 @Value("classpath:/webapp/test/Fel.pdf") private org.springframework.core.io.Resource data; @@ -49,18 +53,23 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic @Override @Transactional(rollbackFor = Exception.class) public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) { - AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class); - documentDO - //todo - .setTokens(0).setWordCount(0) - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setSliceStatus(AiKnowledgeDocumentStatusEnum.SUCCESS.getStatus()); - documentMapper.insert(documentDO); + // TODO xiaoxin 后续从 url 加载 TikaDocumentReader loader = new TikaDocumentReader(data); + // 加载文档 List documents = loader.get(); + Document document = CollUtil.getFirst(documents); + // TODO 芋艿 文档层面有没有可能会比较大,这两个字段是否可以从分段表计算得出? + Integer tokens = Objects.nonNull(document) ? TOKEN_COUNT_ESTIMATOR.estimate(document.getContent()) : 0; + Integer wordCount = Objects.nonNull(document) ? document.getContent().length() : 0; + + AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class); + documentDO.setTokens(tokens).setWordCount(wordCount) + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setSliceStatus(AiKnowledgeDocumentStatusEnum.SUCCESS.getStatus()); + // 文档记录入库 + documentMapper.insert(documentDO); Long documentId = documentDO.getId(); if (CollUtil.isEmpty(documents)) { - log.info("文档内容为空"); return documentId; } @@ -69,10 +78,8 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic List segmentDOList = CollectionUtils.convertList(segments, segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId) - //todo - .setTokens(0).setWordCount(0) + .setTokens(TOKEN_COUNT_ESTIMATOR.estimate(segment.getContent())).setWordCount(segment.getContent().length()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); - // 分段内容入库 segmentMapper.insertBatch(segmentDOList); From efc5ea23bfcc6fb5ddc211e9a45f824ed751fc8f Mon Sep 17 00:00:00 2001 From: scholar <1145227973@qq.com> Date: Thu, 15 Aug 2024 20:29:17 +0800 Subject: [PATCH 021/136] =?UTF-8?q?=E5=AE=8C=E6=88=90todo=E9=83=A8?= =?UTF-8?q?=E5=88=86=201=EF=BC=8C=E5=8D=8E=E4=B8=BA=E4=BA=91=E7=9F=AD?= =?UTF-8?q?=E4=BF=A1=E5=AE=9E=E7=8E=B0=E4=BC=98=E5=8C=96=EF=BC=8C=E5=8E=BB?= =?UTF-8?q?=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81VO;=202=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=8D=8E=E4=B8=BA=E4=BA=91=E7=9F=AD=E4=BF=A1=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=8D=95=E6=B5=8B=EF=BC=9B=203=EF=BC=8Cfix=E5=8D=8E?= =?UTF-8?q?=E4=B8=BA=E4=BA=91=E7=9F=AD=E4=BF=A1=E6=8E=A5=E6=94=B6=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=9B=9E=E8=B0=83=E7=9A=84bug=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/sms/SmsCallbackController.java | 13 +- .../sms/core/client/impl/HuaweiSmsClient.java | 154 +++++++----------- .../core/client/impl/HuaweiSmsClientTest.java | 114 +++++++++++++ 3 files changed, 182 insertions(+), 99 deletions(-) create mode 100644 yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java index 0bb406710..90cb763cc 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java @@ -5,16 +5,17 @@ import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum; import cn.iocoder.yudao.module.system.service.sms.SmsSendService; +import com.xingyuv.captcha.util.StreamUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletRequest; +import java.nio.charset.Charset; + import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 短信回调") @@ -50,10 +51,8 @@ public class SmsCallbackController { @PermitAll @Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档") @OperateLog(enable = false) - public CommonResult receiveHuaweiSmsStatus(HttpServletRequest request) throws Throwable { - String text = ServletUtils.getBody(request); - smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), text); + public CommonResult receiveHuaweiSmsStatus(@RequestBody String requestBody) throws Throwable { + smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), requestBody); return success(true); } - } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java index 4df820861..8465d0558 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java @@ -5,12 +5,11 @@ import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; -import cn.hutool.http.HttpRequest; -import cn.hutool.http.HttpResponse; 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.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 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,14 +17,14 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR 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 lombok.Data; import lombok.extern.slf4j.Slf4j; import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.net.URLEncoder; import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneId; import java.util.*; @@ -33,9 +32,6 @@ 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; /** @@ -47,9 +43,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE @Slf4j public class HuaweiSmsClient extends AbstractSmsClient { - /** - * 调用成功 code - */ public static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI public static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443"; public static final String SIGNEDHEADERS = "content-type;host;x-sdk-date"; @@ -78,13 +71,14 @@ public class HuaweiSmsClient extends AbstractSmsClient { List templateParas = CollectionUtils.convertList(templateParams, kv -> String.valueOf(kv.getValue())); - JSONObject JsonResponse = sendSmsRequest(sender,mobile,templateId,templateParas,statusCallBack); - SmsResponse smsResponse = getSmsSendResponse(JsonResponse); + JSONObject JsonResponse = request(sendLogId,sender,mobile,templateId,templateParas,statusCallBack); - return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString()); + return new SmsSendRespDTO().setSuccess("000000".equals(JsonResponse.getStr("code"))) + .setSerialNo(JsonResponse.getJSONArray("result").getJSONObject(0).getStr("smsMsgId")) + .setApiCode(JsonResponse.getJSONArray("result").getJSONObject(0).getStr("status")); } - JSONObject sendSmsRequest(String sender,String mobile,String templateId,List templateParas,String statusCallBack) throws UnsupportedEncodingException { + JSONObject request(Long sendLogId,String sender,String mobile,String templateId,List templateParas,String statusCallBack) throws UnsupportedEncodingException { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH); sdf.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -97,8 +91,7 @@ public class HuaweiSmsClient extends AbstractSmsClient { String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n" + "host:"+ HOST +"\n" + "x-sdk-date:" + sdkDate + "\n"; - //请求Body,不携带签名名称时,signature请填null - String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, null); + String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, sendLogId); if (null == body || body.isEmpty()) { return null; } @@ -118,26 +111,29 @@ public class HuaweiSmsClient extends AbstractSmsClient { + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature; // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response ************* - HttpResponse response = HttpRequest.post(URL) - .header("Content-Type", "application/x-www-form-urlencoded") - .header("X-Sdk-Date", sdkDate) - .header("host",HOST) - .header("Authorization", authorization) - .body(body) - .execute(); + TreeMap headers = new TreeMap<>(); + headers.put("Content-Type", "application/x-www-form-urlencoded"); + headers.put("X-Sdk-Date", sdkDate); + headers.put("host", HOST); + headers.put("Authorization", authorization); - return JSONUtil.parseObj(response.body()); - } - - private SmsResponse getSmsSendResponse(JSONObject resJson) { - SmsResponse smsResponse = new SmsResponse(); - smsResponse.setSuccess("000000".equals(resJson.getStr("code"))); - smsResponse.setData(resJson); - return smsResponse; + String responseBody = HttpUtils.post(URL, headers, body); + return JSONUtil.parseObj(responseBody); +// +// +// HttpResponse response = HttpRequest.post(URL) +// .header("Content-Type", "application/x-www-form-urlencoded") +// .header("X-Sdk-Date", sdkDate) +// .header("host",HOST) +// .header("Authorization", authorization) +// .body(body) +// .execute(); +// +// return JSONUtil.parseObj(response.body()); } static String buildRequestBody(String sender, String receiver, String templateId, List templateParas, - String statusCallBack, String signature) throws UnsupportedEncodingException { + String statusCallBack, Long sendLogId) throws UnsupportedEncodingException { if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty() || templateId.isEmpty()) { System.out.println("buildRequestBody(): sender, receiver or templateId is null."); @@ -150,7 +146,9 @@ public class HuaweiSmsClient extends AbstractSmsClient { appendToBody(body, "&templateId=", templateId); appendToBody(body, "&templateParas=", JsonUtils.toJsonString(templateParas)); appendToBody(body, "&statusCallback=", statusCallBack); - appendToBody(body, "&signature=", signature); + appendToBody(body, "&signature=", null); + appendToBody(body, "&extend=", String.valueOf(sendLogId)); + return body.toString(); } @@ -160,12 +158,35 @@ public class HuaweiSmsClient extends AbstractSmsClient { } } @Override - public List parseSmsReceiveStatus(String text) { - List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); - return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(Objects.equals(status.getStatus(),"DELIVRD")) - .setErrorCode(status.getStatus()).setErrorMsg(status.getStatus()) - .setMobile(status.getPhoneNumber()).setReceiveTime(status.getUpdateTime()) - .setSerialNo(status.getSmsMsgId())); + public List parseSmsReceiveStatus(String requestBody) { + + System.out.println("text in parseSmsReceiveStatus===== " + requestBody); + + Map params = new HashMap<>(); + try { + String[] pairs = requestBody.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8"); + String value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8"); + params.put(key, value); + } + } catch (Exception e) { + e.printStackTrace(); + } + + List respDTOS = new ArrayList<>(); + respDTOS.add(new SmsReceiveRespDTO() + .setSuccess("DELIVRD".equals(params.get("status"))) // 是否接收成功 + .setErrorCode(params.get("status")) // 状态报告编码 + .setErrorMsg(params.get("statusDesc")) + .setMobile(params.get("to")) // 手机号 + .setReceiveTime(LocalDateTime.ofInstant(Instant.parse(params.get("updateTime")), ZoneId.of("UTC"))) // 状态报告时间 + .setSerialNo(params.get("smsMsgId")) // 发送序列号 + .setLogId(Long.valueOf(params.get("extend")))//logId + ); + + return respDTOS; } @Override @@ -173,56 +194,5 @@ public class HuaweiSmsClient extends AbstractSmsClient { //华为短信模板查询和发送短信,是不同的两套key和secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现。 return new SmsTemplateRespDTO().setId(null).setContent(null) .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null); - } - - @Data - public static class SmsResponse { - - /** - * 是否成功 - */ - private boolean success; - - /** - * 厂商原返回体 - */ - private Object data; - - } - - - /** - * 短信接收状态 - * - * 参见 文档 - * - * @author scholar - */ - @Data - public static class SmsReceiveStatus { - - /** - * 本条状态报告对应的短信的接收方号码,仅当状态报告中携带了extend参数时才会同时携带该参数 - */ - @JsonProperty("to") - private String phoneNumber; - - /** - * 短信资源的更新时间,通常为短信平台接收短信状态报告的时间 - */ - @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) - private LocalDateTime updateTime; - - /** - * 短信状态报告枚举值 - */ - private String status; - - /** - * 发送短信成功时返回的短信唯一标识。 - */ - private String smsMsgId; - } - -} +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java new file mode 100644 index 000000000..e18a2b60d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java @@ -0,0 +1,114 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +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.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; +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.MockedStatic; + +import java.time.LocalDateTime; +import java.util.List; + +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; + +/** + * {@link HuaweiSmsClient} 的单元测试 + * + * @author scholar + */ +public class HuaweiSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString())// 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + + @InjectMocks + private HuaweiSmsClient smsClient = new HuaweiSmsClient(properties); + + @Test + public void testDoInit() { + // 调用 + smsClient.doInit(); + } + + @Test + public void testDoSendSms_success() throws Throwable { + + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString() + " " + randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); + + // mock 方法 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) + .thenReturn( + "{\"result\":[{\"originTo\":\"+86155****5678\",\"createTime\":\"2018-05-25T16:34:34Z\",\"from\":\"1069********0012\",\"smsMsgId\":\"d6e3cdd0-522b-4692-8304-a07553cdf591_8539659\",\"status\":\"000000\",\"countryId\":\"CN\",\"total\":2}],\"code\":\"000000\",\"description\":\"Success\"}\n" + ); + + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertTrue(result.getSuccess()); + assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo()); + assertEquals("000000", result.getApiCode()); + + } + } + + @Test + public void testDoSendSms_fail() throws Throwable { + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString() + " " + randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); + + // mock 方法 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) + .thenReturn( + "{\"result\":[{\"originTo\":\"+86155****5678\",\"createTime\":\"2018-05-25T16:34:34Z\",\"from\":\"1069********0012\",\"smsMsgId\":\"d6e3cdd0-522b-4692-8304-a07553cdf591_8539659\",\"status\":\"E200015\",\"countryId\":\"CN\",\"total\":2}],\"code\":\"E000000\",\"description\":\"Success\"}\n" + ); + + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertFalse(result.getSuccess()); + assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo()); + assertEquals("E200015", result.getApiCode()); + } + } + + @Test + public void testParseSmsReceiveStatus() { + // 准备参数 + String text = "sequence=1&total=1&statusDesc=%E7%94%A8%E6%88%B7%E5%B7%B2%E6%88%90%E5%8A%9F%E6%94%B6%E5%88%B0%E7%9F%AD%E4%BF%A1&updateTime=2024-08-15T03%3A00%3A34Z&source=2&smsMsgId=70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459&status=DELIVRD&extend=176"; + + // 调用 + List statuses = smsClient.parseSmsReceiveStatus(text); + // 断言 + assertEquals(1, statuses.size()); + assertTrue(statuses.getFirst().getSuccess()); + assertEquals("DELIVRD", statuses.getFirst().getErrorCode()); + assertEquals(LocalDateTime.of(2024, 8, 15, 3, 0, 34), statuses.getFirst().getReceiveTime()); + assertEquals("70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459", statuses.getFirst().getSerialNo()); + } + +} From 86a47481401067b1412ace9dc51660f727927901 Mon Sep 17 00:00:00 2001 From: tb Date: Thu, 15 Aug 2024 21:44:41 +0800 Subject: [PATCH 022/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=A2=9E=E5=8A=A0=E6=94=AF=E4=BB=98=E5=AE=9D?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=86=85=E5=AE=B9=E5=8A=A0=E5=AF=86=E6=94=AF?= =?UTF-8?q?=E6=8C=81=EF=BC=8C=E6=B3=A8=E6=84=8F=E9=9C=80=E8=A6=81=E5=92=8C?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E5=90=8C=E6=97=B6=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=20#IAFYDQ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/alipay/AlipayPayClientConfig.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java index 13f2885d4..3cb2bc242 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java @@ -26,6 +26,11 @@ public class AlipayPayClientConfig implements PayClientConfig { */ public static final Integer MODE_CERTIFICATE = 2; + /** + * 接口内容加密方式 - AES 加密 + */ + public static final String ENC_TYPE_AES = "AES"; + /** * 签名算法类型 - RSA */ @@ -92,6 +97,19 @@ public class AlipayPayClientConfig implements PayClientConfig { @NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class}) private String rootCertContent; + /** + * 接口内容加密方式,如果为空,将使用无加密方式 + * 如果要加密,目前支付宝只有 AES 一种加密方式 + * 支付宝开放平台 + * @see AlipayPayClientConfig#ENC_TYPE_AES + */ + private String encryptType; + + /** + * 接口内容加密的私钥 + */ + private String encryptKey; + public interface ModePublicKey { } From d2c7ec24459d25382aa61726e02f3ad007792173 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 18 Aug 2024 13:02:14 +0800 Subject: [PATCH 023/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E5=94=AE=E5=90=8E?= =?UTF-8?q?=E5=88=86=E9=A1=B5=E6=9F=A5=E8=AF=A2=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20userId=20=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/aftersale/vo/AfterSalePageReqVO.java | 3 +++ .../module/trade/dal/mysql/aftersale/AfterSaleMapper.java | 1 + 2 files changed, 4 insertions(+) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java index f74c84b8f..4b8756c7b 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java @@ -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; diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java index 68a09a82a..341dabc45 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java @@ -16,6 +16,7 @@ public interface AfterSaleMapper extends BaseMapperX { default PageResult selectPage(AfterSalePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(AfterSaleDO::getUserId, reqVO.getUserId()) .likeIfPresent(AfterSaleDO::getNo, reqVO.getNo()) .eqIfPresent(AfterSaleDO::getStatus, reqVO.getStatus()) .eqIfPresent(AfterSaleDO::getType, reqVO.getType()) From cd101ec7fc876e6b62b289b9603d260896ef9bd6 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 18 Aug 2024 13:07:30 +0800 Subject: [PATCH 024/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91=E6=94=AF=E4=BB=98=EF=BC=9A=E9=92=B1=E5=8C=85?= =?UTF-8?q?=E4=BD=99=E9=A2=9D=EF=BC=8C=E6=94=AF=E6=8C=81=20userId=20?= =?UTF-8?q?=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transaction/PayWalletTransactionPageReqVO.java | 11 ++++++++++- .../wallet/PayWalletTransactionServiceImpl.java | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java index 678649ce0..7491b9e50 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java @@ -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; + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletTransactionServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletTransactionServiceImpl.java index 76450c501..a2f3d92d6 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletTransactionServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletTransactionServiceImpl.java @@ -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 jakarta.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 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); } From 6eb40aa544d15f4586324f7cf2f408ac99e11d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E7=8E=84=E7=A4=BC?= <15732273052@139.com> Date: Sun, 18 Aug 2024 07:02:09 +0000 Subject: [PATCH 025/136] =?UTF-8?q?!1041=20=E6=94=AF=E4=BB=98=E5=BA=94?= =?UTF-8?q?=E7=94=A8=EF=BC=8C=E5=A2=9E=E5=8A=A0=20appKey=20=E6=A0=87?= =?UTF-8?q?=E8=AF=86=EF=BC=8C=E7=94=A8=E4=BA=8E=E4=B8=8D=E5=90=8C=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E6=96=B9=E7=9A=84=E6=A0=87=E8=AF=86=20*=20feat[yudao-?= =?UTF-8?q?module-pay]:=20=E6=9B=B4=E6=96=B0=E6=96=B0=E5=A2=9E=E5=92=8C?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=94=AF=E4=BB=98=E5=BA=94=E7=94=A8=E6=97=B6?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E9=80=BB=E8=BE=91=20*=20fix[yudao-module-tra?= =?UTF-8?q?de]:=20=E4=B8=BA=E6=94=AF=E4=BB=98=E5=BA=94=E7=94=A8=E6=A0=87?= =?UTF-8?q?=E8=AF=86=E6=8F=90=E4=BE=9B=E7=BC=BA=E7=9C=81=E5=80=BC=20*=20fi?= =?UTF-8?q?x[yudao-module-pay]:=20appKey=E6=B3=A8=E9=87=8A=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E7=BC=96=E7=A0=81=E6=9B=B4=E6=96=B0=E4=B8=BA=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E6=A0=87=E8=AF=86=20*=20feat[yudao-module-pay]:=20?= =?UTF-8?q?=E4=B8=BA=E6=94=AF=E4=BB=98=E5=BA=94=E7=94=A8=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E7=BC=96=E7=A0=81=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 18 +-- .../convert/order/TradeOrderConvert.java | 2 +- .../order/config/TradeOrderProperties.java | 13 +- .../order/TradeOrderUpdateServiceTest.java | 2 +- .../api/order/dto/PayOrderCreateReqDTO.java | 12 +- .../api/refund/dto/PayRefundCreateReqDTO.java | 11 +- .../transfer/dto/PayTransferCreateReqDTO.java | 10 +- .../module/pay/enums/ErrorCodeConstants.java | 1 + .../admin/app/vo/PayAppCreateReqVO.java | 5 + .../admin/app/vo/PayAppPageItemRespVO.java | 3 + .../admin/app/vo/PayAppPageReqVO.java | 3 + .../controller/admin/app/vo/PayAppRespVO.java | 8 +- .../admin/app/vo/PayAppUpdateReqVO.java | 5 + .../pay/dal/dataobject/app/PayAppDO.java | 4 + .../pay/dal/mysql/app/PayAppMapper.java | 6 +- .../module/pay/service/app/PayAppService.java | 16 ++- .../pay/service/app/PayAppServiceImpl.java | 63 ++++++++- .../service/demo/PayDemoOrderServiceImpl.java | 6 +- .../service/order/PayOrderServiceImpl.java | 4 +- .../service/refund/PayRefundServiceImpl.java | 19 +-- .../transfer/PayTransferServiceImpl.java | 14 +- .../wallet/PayWalletRechargeServiceImpl.java | 6 +- .../service/order/PayOrderServiceTest.java | 6 +- .../service/refund/PayRefundServiceTest.java | 16 +-- yudao-server/pom.xml | 120 +++++++++--------- .../src/main/resources/application-local.yaml | 40 +++--- .../src/main/resources/application.yaml | 2 +- 27 files changed, 259 insertions(+), 156 deletions(-) diff --git a/pom.xml b/pom.xml index 86dfebcc3..82de1e8b7 100644 --- a/pom.xml +++ b/pom.xml @@ -15,15 +15,15 @@ yudao-module-system yudao-module-infra - - - - - - - - - + yudao-module-member + yudao-module-bpm + yudao-module-report + yudao-module-mp + yudao-module-pay + yudao-module-mall + yudao-module-crm + yudao-module-erp + yudao-module-ai ${project.artifactId} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java index 9d788137b..aa36eeeec 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java @@ -101,7 +101,7 @@ public interface TradeOrderConvert { default PayOrderCreateReqDTO convert(TradeOrderDO order, List orderItems, TradeOrderProperties orderProperties) { PayOrderCreateReqDTO createReqDTO = new PayOrderCreateReqDTO() - .setAppId(orderProperties.getAppId()).setUserIp(order.getUserIp()); + .setAppKey(orderProperties.getAppKey()).setUserIp(order.getUserIp()); // 商户相关字段 createReqDTO.setMerchantOrderId(String.valueOf(order.getId())); String subject = orderItems.get(0).getSpuName(); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java index 1b564b06d..c88e93933 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java @@ -5,6 +5,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import jakarta.validation.constraints.NotNull; + import java.time.Duration; /** @@ -19,10 +20,16 @@ import java.time.Duration; public class TradeOrderProperties { /** - * 应用编号 + * 默认应用标识 */ - @NotNull(message = "应用编号不能为空") - private Long appId; + private static final String APP_KEY_DEFAULT = "mall"; + + /** + * 应用标识,用于区分不同的应用程序 + * 通过注解@NotNull确保应用标识不能为空 + */ + @NotNull(message = "应用标识不能为空") + private String appKey = APP_KEY_DEFAULT; /** * 支付超时时间 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java index e9677e665..fa15b7e52 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java @@ -99,7 +99,7 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest { @BeforeEach public void setUp() { - when(tradeOrderProperties.getAppId()).thenReturn(888L); + when(tradeOrderProperties.getAppKey()).thenReturn("demo"); when(tradeOrderProperties.getPayExpireTime()).thenReturn(Duration.ofDays(1)); when(tradeNoRedisDAO.generate(anyString())).thenReturn(IdUtil.randomUUID()); } diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java index a96048828..3a7b181be 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java @@ -1,11 +1,11 @@ package cn.iocoder.yudao.module.pay.api.order.dto; -import lombok.Data; -import org.hibernate.validator.constraints.Length; - 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 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 */ diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java index 48a6df504..6910fc2fe 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java @@ -1,11 +1,10 @@ package cn.iocoder.yudao.module.pay.api.refund.dto; -import lombok.Data; -import org.hibernate.validator.constraints.Length; - import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Length; /** * 退款单创建 Request DTO @@ -16,10 +15,10 @@ import jakarta.validation.constraints.NotNull; public class PayRefundCreateReqDTO { /** - * 应用编号 + * 应用标识 */ - @NotNull(message = "应用编号不能为空") - private Long appId; + @NotNull(message = "应用标识不能为空") + private String appKey; /** * 用户 IP */ diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java index e86733050..05159671b 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java @@ -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 lombok.Data; - import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import lombok.Data; + 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; diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java index 8b7a38ecf..131698e4a 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java @@ -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, "支付渠道的配置不存在"); diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java index 03cab7d3e..db0dcde8b 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.pay.controller.admin.app.vo; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import lombok.*; @Schema(description = "管理后台 - 支付应用信息创建 Request VO") @@ -8,4 +9,8 @@ import lombok.*; @ToString(callSuper = true) public class PayAppCreateReqVO extends PayAppBaseVO { + @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") + @NotNull(message = "应用标识不能为空") + private String appKey; + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java index 76b62003c..29931b14f 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java @@ -17,6 +17,9 @@ public class PayAppPageItemRespVO 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; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java index 94ade7ce6..7a9931ac5 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java @@ -20,6 +20,9 @@ public class PayAppPageReqVO extends PageParam { @Schema(description = "应用名", example = "小豆") private String name; + @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") + private String appKey; + @Schema(description = "开启状态", example = "0") private Integer status; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppRespVO.java index 9471a2f01..184e538e5 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppRespVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppRespVO.java @@ -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; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java index 68c559914..4ea50df27 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java @@ -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 jakarta.validation.constraints.*; @@ -13,4 +14,8 @@ public class PayAppUpdateReqVO extends PayAppBaseVO { @NotNull(message = "应用编号不能为空") private Long id; + @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") + @NotNull(message = "应用标识不能为空") + private String appKey; + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java index 8f3490fc7..456a40a21 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java @@ -31,6 +31,10 @@ public class PayAppDO extends BaseDO { */ @TableId private Long id; + /** + * 应用标识 + */ + private String appKey; /** * 应用名 */ diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/app/PayAppMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/app/PayAppMapper.java index c31dba551..07e190a57 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/app/PayAppMapper.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/app/PayAppMapper.java @@ -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 { default PageResult selectPage(PayAppPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .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); + } + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppService.java index c7a54bdaf..d348f5394 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppService.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppService.java @@ -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 java.util.Collection; import java.util.List; import java.util.Map; @@ -88,13 +88,13 @@ public interface PayAppService { * @return 商户 Map */ default Map getAppMap(Collection ids) { - List list = getAppList(ids); + List list = getAppList(ids); return CollectionUtils.convertMap(list, PayAppDO::getId); } /** * 支付应用的合法性 - * + *

* 如果不合法,抛出 {@link ServiceException} 业务异常 * * @param id 应用编号 @@ -102,4 +102,14 @@ public interface PayAppService { */ PayAppDO validPayApp(Long id); + /** + * 支付应用的合法性 + *

+ * 如果不合法,抛出 {@link ServiceException} 业务异常 + * + * @param appKey 应用标识 + * @return 应用 + */ + PayAppDO validPayApp(String appKey); + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java index 786b70c9f..9809b5057 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.pay.service.app; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 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; @@ -11,13 +13,14 @@ 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 jakarta.annotation.Resource; import java.util.Collection; import java.util.List; +import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; @@ -43,6 +46,8 @@ public class PayAppServiceImpl implements PayAppService { @Override public Long createApp(PayAppCreateReqVO createReqVO) { + // 验证appKey是否重复 + validateAppKeyDuplicate(null, createReqVO.getAppKey()); // 插入 PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO); appMapper.insert(app); @@ -54,6 +59,8 @@ public class PayAppServiceImpl implements PayAppService { public void updateApp(PayAppUpdateReqVO updateReqVO) { // 校验存在 validateAppExists(updateReqVO.getId()); + // 验证appKey是否重复 + validateAppKeyDuplicate(updateReqVO.getId(), updateReqVO.getAppKey()); // 更新 PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO); appMapper.updateById(updateObj); @@ -101,7 +108,7 @@ public class PayAppServiceImpl implements PayAppService { @Override public List getAppList() { - return appMapper.selectList(); + return appMapper.selectList(); } @Override @@ -110,8 +117,28 @@ 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 validatePayAppDO(app); + } + + @Override + public PayAppDO validPayApp(String appKey) { + PayAppDO app = appMapper.selectByAppKey(appKey); + // 校验支付应用数据是否存在以及可用 + return validatePayAppDO(app); + } + + /** + * 校验支付应用实体的有效性 + * 主要包括存在性检查和禁用状态检查 + * + * @param app 待校验的支付应用实体 + * @return 校验通过的支付应用实体 + * @throws IllegalArgumentException 如果支付应用实体不存在或已被禁用 + */ + private PayAppDO validatePayAppDO(PayAppDO app) { // 校验是否存在 if (app == null) { throw exception(ErrorCodeConstants.APP_NOT_FOUND); @@ -123,4 +150,32 @@ public class PayAppServiceImpl implements PayAppService { return app; } + + /** + * 校验应用密钥是否重复 + * 在新增或更新支付应用时,确保应用密钥(appKey)的唯一性 + * 如果是在新增情况下,检查数据库中是否已存在相同的appKey + * 如果是在更新情况下,检查数据库中是否存在除当前应用外的其他应用使用了相同的appKey + * + * @param payAppId 支付应用的ID,更新时使用,新增时可能为null + * @param payAppKey 支付应用的密钥,用于校验是否重复 + * @throws RuntimeException 如果发现appKey重复,抛出运行时异常 + */ + private void validateAppKeyDuplicate(Long payAppId, String payAppKey) { + // 新增时,校验appKey是否重复 + if (Objects.isNull(payAppId) && StrUtil.isNotBlank(payAppKey)) { + if (appMapper.selectCount(PayAppDO::getAppKey, payAppKey) > 0) { + throw exception(APP_KEY_EXISTS); + } + // 更新时,校验appKey是否重复 + } else if (Objects.nonNull(payAppId) && StrUtil.isNotBlank(payAppKey)) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); + queryWrapper.eq(PayAppDO::getAppKey, payAppKey) + .ne(PayAppDO::getId, payAppId); + if (appMapper.selectCount(queryWrapper) > 0) { + throw exception(APP_KEY_EXISTS); + } + } + } + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java index 817390537..c2067c838 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java @@ -47,7 +47,7 @@ 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()));// 价格信息 diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java index 11cd0fd48..31c1f8b55 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java @@ -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)); // 理论来说,不会出现这个情况 diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java index 360d00abf..e52d91f18 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java @@ -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 jakarta.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()); @@ -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) 调用,否则事务不生效 + @Transactional(rollbackFor = Exception.class) + // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效 public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) { // 情况一:退款成功 if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) { diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java index cf8fc3f5e..5ace5ab4b 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java @@ -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 jakarta.annotation.Resource; -import jakarta.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())) { diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java index 94c9fa611..b62c8cfcf 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java @@ -54,7 +54,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService { /** * TODO 芋艿:放到 payconfig */ - private static final Long WALLET_PAY_APP_ID = 8L; + private static final String WALLET_PAY_APP_KEY = "wallet"; private static final String WALLET_RECHARGE_ORDER_SUBJECT = "钱包余额充值"; @@ -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(WALLET_PAY_APP_KEY).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(WALLET_PAY_APP_KEY).setUserIp(userIp) .setMerchantOrderId(walletRechargeId) .setMerchantRefundId(refundId) .setReason("想退钱").setPrice(walletRecharge.getPayPrice())); diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java index 394e45d7f..b0c613af8 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java @@ -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,7 +239,7 @@ 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); diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java index 7429d6c58..131cb44cd 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java @@ -209,7 +209,7 @@ 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); @@ -232,7 +232,7 @@ 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); @@ -249,7 +249,7 @@ 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); @@ -268,7 +268,7 @@ 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); @@ -291,7 +291,7 @@ 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); @@ -315,7 +315,7 @@ 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)); @@ -347,7 +347,7 @@ 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)); @@ -391,7 +391,7 @@ 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)); diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml index 3b16fa192..b161ee79c 100644 --- a/yudao-server/pom.xml +++ b/yudao-server/pom.xml @@ -33,80 +33,80 @@ - - - - - + + cn.iocoder.boot + yudao-module-member-biz + ${revision} + - - - - - + + cn.iocoder.boot + yudao-module-report-biz + ${revision} + - - - - - + + cn.iocoder.boot + yudao-module-bpm-biz + ${revision} + - - - - - + + cn.iocoder.boot + yudao-module-pay-biz + ${revision} + - - - - - + + cn.iocoder.boot + yudao-module-mp-biz + ${revision} + - - - - - - - - - - - - - - - - - - - - + + cn.iocoder.boot + yudao-module-promotion-biz + ${revision} + + + cn.iocoder.boot + yudao-module-product-biz + ${revision} + + + cn.iocoder.boot + yudao-module-trade-biz + ${revision} + + + cn.iocoder.boot + yudao-module-statistics-biz + ${revision} + - - - - - + + cn.iocoder.boot + yudao-module-crm-biz + ${revision} + - - - - - + + cn.iocoder.boot + yudao-module-erp-biz + ${revision} + - - - - - + + cn.iocoder.boot + yudao-module-ai-biz + ${revision} + diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 0c27aeac8..9847131b0 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -45,7 +45,7 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://39.105.15.179:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # MySQL Connector/J 5.X 连接的示例 # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例 # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 @@ -54,26 +54,26 @@ spring: # url: jdbc:kingbase8://127.0.0.1:54321/test # 人大金仓 KingbaseES 连接的示例 # url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss 连接的示例 username: root - password: 123456 + password: 3WLiVUBEwTbvAfsh # username: sa # SQL Server 连接的示例 # password: Yudao@2024 # SQL Server 连接的示例 # username: SYSDBA # DM 连接的示例 # password: SYSDBA001 # DM 连接的示例 # username: root # OpenGauss 连接的示例 # password: Yudao@2024 # OpenGauss 连接的示例 - slave: # 模拟从库,可根据自己需要修改 - lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true - username: root - password: 123456 +# slave: # 模拟从库,可根据自己需要修改 +# lazy: true # 开启懒加载,保证启动速度 +# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true +# username: root +# password: 123456 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: redis: - host: 127.0.0.1 # 地址 + host: 39.105.15.179 # 地址 port: 6379 # 端口 database: 0 # 数据库索引 -# password: dev # 密码,建议生产环境开启 + password: 3WLiVUBEwTbvAfsh # 密码,建议生产环境开启 --- #################### 定时任务相关配置 #################### @@ -110,18 +110,18 @@ spring: # rocketmq 配置项,对应 RocketMQProperties 配置类 rocketmq: - name-server: 127.0.0.1:9876 # RocketMQ Namesrv + name-server: 117.72.39.77:9876 # RocketMQ Namesrv -spring: - # RabbitMQ 配置项,对应 RabbitProperties 配置类 - rabbitmq: - host: 127.0.0.1 # RabbitMQ 服务的地址 - port: 5672 # RabbitMQ 服务的端口 - username: rabbit # RabbitMQ 服务的账号 - password: rabbit # RabbitMQ 服务的密码 - # Kafka 配置项,对应 KafkaProperties 配置类 - kafka: - bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔 +#spring: +# # RabbitMQ 配置项,对应 RabbitProperties 配置类 +# rabbitmq: +# host: 127.0.0.1 # RabbitMQ 服务的地址 +# port: 5672 # RabbitMQ 服务的端口 +# username: rabbit # RabbitMQ 服务的账号 +# password: rabbit # RabbitMQ 服务的密码 +# # Kafka 配置项,对应 KafkaProperties 配置类 +# kafka: +# bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔 --- #################### 服务保障相关配置 #################### diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 609f85b82..f294b8010 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -309,7 +309,7 @@ yudao: end-code: 9999 # 这里配置 9999 的原因是,测试方便。 trade: order: - app-id: 1 # 商户编号 + app-key: mall pay-expire-time: 2h # 支付的过期时间 receive-expire-time: 14d # 收货的过期时间 comment-expire-time: 7d # 评论的过期时间 From 1dadfb8fba1eecdd27f43787d44ddd7c5912ff0f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 18 Aug 2024 15:30:35 +0800 Subject: [PATCH 026/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E6=94=AF=E4=BB=98=EF=BC=9A=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E5=BA=94=E7=94=A8=EF=BC=8C=E5=A2=9E=E5=8A=A0=20appKey=20?= =?UTF-8?q?=E6=A0=87=E8=AF=86=EF=BC=8C=E7=94=A8=E4=BA=8E=E4=B8=8D=E5=90=8C?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E6=96=B9=E7=9A=84=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 12 ++-- .../convert/order/TradeOrderConvert.java | 2 +- .../order/config/TradeOrderProperties.java | 15 ++-- .../order/TradeOrderUpdateServiceTest.java | 2 +- .../controller/admin/app/vo/PayAppBaseVO.java | 4 ++ .../admin/app/vo/PayAppCreateReqVO.java | 10 ++- .../admin/app/vo/PayAppPageItemRespVO.java | 3 - .../admin/app/vo/PayAppPageReqVO.java | 2 +- .../admin/app/vo/PayAppUpdateReqVO.java | 4 -- .../framework/pay/config/PayProperties.java | 8 +++ .../pay/service/app/PayAppServiceImpl.java | 69 +++++++------------ .../service/demo/PayDemoOrderServiceImpl.java | 2 +- .../service/refund/PayRefundServiceImpl.java | 2 +- .../wallet/PayWalletRechargeServiceImpl.java | 14 ++-- .../impl/alipay/AlipayPayClientConfig.java | 9 ++- yudao-server/pom.xml | 60 ++++++++-------- .../src/main/resources/application-local.yaml | 40 +++++------ .../src/main/resources/application.yaml | 1 - 18 files changed, 122 insertions(+), 137 deletions(-) diff --git a/pom.xml b/pom.xml index 82de1e8b7..4634d345e 100644 --- a/pom.xml +++ b/pom.xml @@ -16,14 +16,14 @@ yudao-module-system yudao-module-infra yudao-module-member - yudao-module-bpm - yudao-module-report - yudao-module-mp + + + yudao-module-pay yudao-module-mall - yudao-module-crm - yudao-module-erp - yudao-module-ai + + + ${project.artifactId} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java index aa36eeeec..d91969481 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java @@ -101,7 +101,7 @@ public interface TradeOrderConvert { default PayOrderCreateReqDTO convert(TradeOrderDO order, List orderItems, TradeOrderProperties orderProperties) { PayOrderCreateReqDTO createReqDTO = new PayOrderCreateReqDTO() - .setAppKey(orderProperties.getAppKey()).setUserIp(order.getUserIp()); + .setAppKey(orderProperties.getPayAppKey()).setUserIp(order.getUserIp()); // 商户相关字段 createReqDTO.setMerchantOrderId(String.valueOf(order.getId())); String subject = orderItems.get(0).getSpuName(); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java index c88e93933..0d7b271d9 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java @@ -1,5 +1,6 @@ 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; @@ -19,17 +20,15 @@ import java.time.Duration; @Validated public class TradeOrderProperties { - /** - * 默认应用标识 - */ - private static final String APP_KEY_DEFAULT = "mall"; + private static final String PAY_APP_KEY_DEFAULT = "mall"; /** - * 应用标识,用于区分不同的应用程序 - * 通过注解@NotNull确保应用标识不能为空 + * 支付应用标识 + * + * 在 pay 模块的 [支付管理 -> 应用信息] 里添加 */ - @NotNull(message = "应用标识不能为空") - private String appKey = APP_KEY_DEFAULT; + @NotEmpty(message = "Pay 应用标识不能为空") + private String payAppKey = PAY_APP_KEY_DEFAULT; /** * 支付超时时间 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java index fa15b7e52..fb19f074b 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java @@ -99,7 +99,7 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest { @BeforeEach public void setUp() { - when(tradeOrderProperties.getAppKey()).thenReturn("demo"); + when(tradeOrderProperties.getPayAppKey()).thenReturn("mall"); when(tradeOrderProperties.getPayExpireTime()).thenReturn(Duration.ofDays(1)); when(tradeNoRedisDAO.generate(anyString())).thenReturn(IdUtil.randomUUID()); } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java index a95242a9f..d6cabdc50 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java @@ -14,6 +14,10 @@ import jakarta.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; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java index db0dcde8b..f1a5dddaf 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.pay.controller.admin.app.vo; + import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; @Schema(description = "管理后台 - 支付应用信息创建 Request VO") @Data @@ -9,8 +11,4 @@ import lombok.*; @ToString(callSuper = true) public class PayAppCreateReqVO extends PayAppBaseVO { - @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") - @NotNull(message = "应用标识不能为空") - private String appKey; - } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java index 29931b14f..76b62003c 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java @@ -17,9 +17,6 @@ public class PayAppPageItemRespVO 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; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java index 7a9931ac5..e433c85e9 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java @@ -20,7 +20,7 @@ public class PayAppPageReqVO extends PageParam { @Schema(description = "应用名", example = "小豆") private String name; - @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") + @Schema(description = "应用标识", example = "yudao") private String appKey; @Schema(description = "开启状态", example = "0") diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java index 4ea50df27..c4e50bd44 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java @@ -14,8 +14,4 @@ public class PayAppUpdateReqVO extends PayAppBaseVO { @NotNull(message = "应用编号不能为空") private Long id; - @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") - @NotNull(message = "应用标识不能为空") - private String appKey; - } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java index 02254ca0b..d124bbfc5 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java @@ -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; + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java index 9809b5057..6774c442e 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java @@ -1,9 +1,7 @@ package cn.iocoder.yudao.module.pay.service.app; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 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; @@ -20,7 +18,6 @@ import org.springframework.validation.annotation.Validated; import java.util.Collection; import java.util.List; -import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; @@ -46,8 +43,9 @@ public class PayAppServiceImpl implements PayAppService { @Override public Long createApp(PayAppCreateReqVO createReqVO) { - // 验证appKey是否重复 - validateAppKeyDuplicate(null, createReqVO.getAppKey()); + // 验证 appKey 是否重复 + validateEmailUnique(null, createReqVO.getAppKey()); + // 插入 PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO); appMapper.insert(app); @@ -59,13 +57,28 @@ public class PayAppServiceImpl implements PayAppService { public void updateApp(PayAppUpdateReqVO updateReqVO) { // 校验存在 validateAppExists(updateReqVO.getId()); - // 验证appKey是否重复 - validateAppKeyDuplicate(updateReqVO.getId(), updateReqVO.getAppKey()); + // 验证 appKey 是否重复 + validateEmailUnique(updateReqVO.getId(), updateReqVO.getAppKey()); + // 更新 PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO); appMapper.updateById(updateObj); } + void validateEmailUnique(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) { // 校验商户存在 @@ -119,63 +132,31 @@ public class PayAppServiceImpl implements PayAppService { @Override public PayAppDO validPayApp(Long appId) { PayAppDO app = appMapper.selectById(appId); - // 校验支付应用数据是否存在以及可用 - return validatePayAppDO(app); + return validatePayApp(app); } @Override public PayAppDO validPayApp(String appKey) { PayAppDO app = appMapper.selectByAppKey(appKey); - // 校验支付应用数据是否存在以及可用 - return validatePayAppDO(app); + return validatePayApp(app); } /** - * 校验支付应用实体的有效性 - * 主要包括存在性检查和禁用状态检查 + * 校验支付应用实体的有效性:存在 + 开启 * * @param app 待校验的支付应用实体 * @return 校验通过的支付应用实体 - * @throws IllegalArgumentException 如果支付应用实体不存在或已被禁用 */ - private PayAppDO validatePayAppDO(PayAppDO app) { + 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; } - - /** - * 校验应用密钥是否重复 - * 在新增或更新支付应用时,确保应用密钥(appKey)的唯一性 - * 如果是在新增情况下,检查数据库中是否已存在相同的appKey - * 如果是在更新情况下,检查数据库中是否存在除当前应用外的其他应用使用了相同的appKey - * - * @param payAppId 支付应用的ID,更新时使用,新增时可能为null - * @param payAppKey 支付应用的密钥,用于校验是否重复 - * @throws RuntimeException 如果发现appKey重复,抛出运行时异常 - */ - private void validateAppKeyDuplicate(Long payAppId, String payAppKey) { - // 新增时,校验appKey是否重复 - if (Objects.isNull(payAppId) && StrUtil.isNotBlank(payAppKey)) { - if (appMapper.selectCount(PayAppDO::getAppKey, payAppKey) > 0) { - throw exception(APP_KEY_EXISTS); - } - // 更新时,校验appKey是否重复 - } else if (Objects.nonNull(payAppId) && StrUtil.isNotBlank(payAppKey)) { - LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); - queryWrapper.eq(PayAppDO::getAppKey, payAppKey) - .ne(PayAppDO::getId, payAppId); - if (appMapper.selectCount(queryWrapper) > 0) { - throw exception(APP_KEY_EXISTS); - } - } - } - } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java index c2067c838..29a9e9aec 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java @@ -43,7 +43,7 @@ import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; public class PayDemoOrderServiceImpl implements PayDemoOrderService { /** - * 接入的实力应用编号 + * 接入的支付应用标识 * * 从 [支付管理 -> 应用信息] 里添加 */ diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java index e52d91f18..39179dfa0 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java @@ -199,8 +199,8 @@ public class PayRefundServiceImpl implements PayRefundService { * @param channel 支付渠道 * @param notify 通知 */ - @Transactional(rollbackFor = Exception.class) // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效 + @Transactional(rollbackFor = Exception.class) public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) { // 情况一:退款成功 if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) { diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java index b62c8cfcf..98e32ec79 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java @@ -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 String WALLET_PAY_APP_KEY = "wallet"; - 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() - .setAppKey(WALLET_PAY_APP_KEY).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() - .setAppKey(WALLET_PAY_APP_KEY).setUserIp(userIp) + .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp) .setMerchantOrderId(walletRechargeId) .setMerchantRefundId(refundId) .setReason("想退钱").setPrice(walletRecharge.getPayPrice())); diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java index 3cb2bc242..9980fb71e 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java @@ -98,9 +98,12 @@ public class AlipayPayClientConfig implements PayClientConfig { private String rootCertContent; /** - * 接口内容加密方式,如果为空,将使用无加密方式 - * 如果要加密,目前支付宝只有 AES 一种加密方式 - * 支付宝开放平台 + * 接口内容加密方式 + * + * 1. 如果为空,将使用无加密方式 + * 2. 如果要加密,目前支付宝只有 AES 一种加密方式 + * + * @see 支付宝开放平台 * @see AlipayPayClientConfig#ENC_TYPE_AES */ private String encryptType; diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml index b161ee79c..d0c429aab 100644 --- a/yudao-server/pom.xml +++ b/yudao-server/pom.xml @@ -40,17 +40,17 @@ - - cn.iocoder.boot - yudao-module-report-biz - ${revision} - + + + + + - - cn.iocoder.boot - yudao-module-bpm-biz - ${revision} - + + + + + cn.iocoder.boot @@ -59,11 +59,11 @@ - - cn.iocoder.boot - yudao-module-mp-biz - ${revision} - + + + + + @@ -88,25 +88,25 @@ - - cn.iocoder.boot - yudao-module-crm-biz - ${revision} - + + + + + - - cn.iocoder.boot - yudao-module-erp-biz - ${revision} - + + + + + - - cn.iocoder.boot - yudao-module-ai-biz - ${revision} - + + + + + diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 9847131b0..0c27aeac8 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -45,7 +45,7 @@ spring: primary: master datasource: master: - url: jdbc:mysql://39.105.15.179:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # MySQL Connector/J 5.X 连接的示例 # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例 # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 @@ -54,26 +54,26 @@ spring: # url: jdbc:kingbase8://127.0.0.1:54321/test # 人大金仓 KingbaseES 连接的示例 # url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss 连接的示例 username: root - password: 3WLiVUBEwTbvAfsh + password: 123456 # username: sa # SQL Server 连接的示例 # password: Yudao@2024 # SQL Server 连接的示例 # username: SYSDBA # DM 连接的示例 # password: SYSDBA001 # DM 连接的示例 # username: root # OpenGauss 连接的示例 # password: Yudao@2024 # OpenGauss 连接的示例 -# slave: # 模拟从库,可根据自己需要修改 -# lazy: true # 开启懒加载,保证启动速度 -# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true -# username: root -# password: 123456 + slave: # 模拟从库,可根据自己需要修改 + lazy: true # 开启懒加载,保证启动速度 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true + username: root + password: 123456 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: redis: - host: 39.105.15.179 # 地址 + host: 127.0.0.1 # 地址 port: 6379 # 端口 database: 0 # 数据库索引 - password: 3WLiVUBEwTbvAfsh # 密码,建议生产环境开启 +# password: dev # 密码,建议生产环境开启 --- #################### 定时任务相关配置 #################### @@ -110,18 +110,18 @@ spring: # rocketmq 配置项,对应 RocketMQProperties 配置类 rocketmq: - name-server: 117.72.39.77:9876 # RocketMQ Namesrv + name-server: 127.0.0.1:9876 # RocketMQ Namesrv -#spring: -# # RabbitMQ 配置项,对应 RabbitProperties 配置类 -# rabbitmq: -# host: 127.0.0.1 # RabbitMQ 服务的地址 -# port: 5672 # RabbitMQ 服务的端口 -# username: rabbit # RabbitMQ 服务的账号 -# password: rabbit # RabbitMQ 服务的密码 -# # Kafka 配置项,对应 KafkaProperties 配置类 -# kafka: -# bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔 +spring: + # RabbitMQ 配置项,对应 RabbitProperties 配置类 + rabbitmq: + host: 127.0.0.1 # RabbitMQ 服务的地址 + port: 5672 # RabbitMQ 服务的端口 + username: rabbit # RabbitMQ 服务的账号 + password: rabbit # RabbitMQ 服务的密码 + # Kafka 配置项,对应 KafkaProperties 配置类 + kafka: + bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔 --- #################### 服务保障相关配置 #################### diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index f294b8010..7a42906f9 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -309,7 +309,6 @@ yudao: end-code: 9999 # 这里配置 9999 的原因是,测试方便。 trade: order: - app-key: mall pay-expire-time: 2h # 支付的过期时间 receive-expire-time: 14d # 收货的过期时间 comment-expire-time: 7d # 评论的过期时间 From 126aa1b40acef366bc21bc2a99e1eab1fad932ba Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 18 Aug 2024 16:31:14 +0800 Subject: [PATCH 027/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E6=94=AF=E4=BB=98=EF=BC=9A=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E5=BA=94=E7=94=A8=EF=BC=8C=E5=A2=9E=E5=8A=A0=20appKey=20?= =?UTF-8?q?=E6=A0=87=E8=AF=86=EF=BC=8C=E7=94=A8=E4=BA=8E=E4=B8=8D=E5=90=8C?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E6=96=B9=E7=9A=84=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/module/pay/service/app/PayAppServiceImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java index 6774c442e..c0e7558f1 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java @@ -44,7 +44,7 @@ public class PayAppServiceImpl implements PayAppService { @Override public Long createApp(PayAppCreateReqVO createReqVO) { // 验证 appKey 是否重复 - validateEmailUnique(null, createReqVO.getAppKey()); + validateAppKeyUnique(null, createReqVO.getAppKey()); // 插入 PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO); @@ -58,14 +58,14 @@ public class PayAppServiceImpl implements PayAppService { // 校验存在 validateAppExists(updateReqVO.getId()); // 验证 appKey 是否重复 - validateEmailUnique(updateReqVO.getId(), updateReqVO.getAppKey()); + validateAppKeyUnique(updateReqVO.getId(), updateReqVO.getAppKey()); // 更新 PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO); appMapper.updateById(updateObj); } - void validateEmailUnique(Long id, String appKey) { + void validateAppKeyUnique(Long id, String appKey) { PayAppDO app = appMapper.selectByAppKey(appKey); if (app == null) { return; From b632726a6915dca7bdec6cc351d18c7b90312199 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 18 Aug 2024 16:33:55 +0800 Subject: [PATCH 028/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E6=94=AF=E4=BB=98=EF=BC=9A=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E5=BA=94=E7=94=A8=EF=BC=8C=E5=A2=9E=E5=8A=A0=20appKey=20?= =?UTF-8?q?=E6=A0=87=E8=AF=86=EF=BC=8C=E7=94=A8=E4=BA=8E=E4=B8=8D=E5=90=8C?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E6=96=B9=E7=9A=84=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 6 ++--- yudao-server/pom.xml | 60 ++++++++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/pom.xml b/pom.xml index 4634d345e..86dfebcc3 100644 --- a/pom.xml +++ b/pom.xml @@ -15,12 +15,12 @@ yudao-module-system yudao-module-infra - yudao-module-member + - yudao-module-pay - yudao-module-mall + + diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml index d0c429aab..3b16fa192 100644 --- a/yudao-server/pom.xml +++ b/yudao-server/pom.xml @@ -33,11 +33,11 @@ - - cn.iocoder.boot - yudao-module-member-biz - ${revision} - + + + + + @@ -52,11 +52,11 @@ - - cn.iocoder.boot - yudao-module-pay-biz - ${revision} - + + + + + @@ -66,26 +66,26 @@ - - cn.iocoder.boot - yudao-module-promotion-biz - ${revision} - - - cn.iocoder.boot - yudao-module-product-biz - ${revision} - - - cn.iocoder.boot - yudao-module-trade-biz - ${revision} - - - cn.iocoder.boot - yudao-module-statistics-biz - ${revision} - + + + + + + + + + + + + + + + + + + + + From e850fbf675336c9ad46b84ea2d0dc5c5988f9561 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 18 Aug 2024 17:17:46 +0800 Subject: [PATCH 029/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E6=94=AF=E4=BB=98=EF=BC=9A=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E5=BA=94=E7=94=A8=EF=BC=8C=E5=A2=9E=E5=8A=A0=20appKey=20?= =?UTF-8?q?=E6=A0=87=E8=AF=86=EF=BC=8C=E7=94=A8=E4=BA=8E=E4=B8=8D=E5=90=8C?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E6=96=B9=E7=9A=84=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pay/service/refund/PayRefundServiceImpl.java | 2 +- .../pay/service/order/PayOrderServiceTest.java | 3 +++ .../pay/service/refund/PayRefundServiceTest.java | 16 ++++++++-------- .../src/test/resources/sql/create_tables.sql | 1 + 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java index 39179dfa0..8df7f8861 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java @@ -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()) diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java index b0c613af8..7fa0c8d90 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java @@ -243,6 +243,9 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest { // 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); diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java index 131cb44cd..c001336fc 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java @@ -212,7 +212,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { 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), @@ -235,7 +235,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { 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); @@ -252,7 +252,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { 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()) @@ -271,7 +271,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { 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()) @@ -294,7 +294,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { 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()) @@ -319,7 +319,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { .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()) @@ -351,7 +351,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { .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()) @@ -395,7 +395,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { .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()) diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql b/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql index 6ae2ce2d4..3f9f76417 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql @@ -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, From 5b7e637ebf9c0419ffd0516043af33210c3ed0b3 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 18 Aug 2024 19:52:48 +0800 Subject: [PATCH 030/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E7=9F=AD=E4=BF=A1=EF=BC=9A=E5=8D=8E=E4=B8=BA?= =?UTF-8?q?=E4=BA=91=E7=9A=84=E5=AE=9E=E7=8E=B0=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/sms/SmsCallbackController.java | 3 - .../core/client/impl/AbstractSmsClient.java | 6 - .../sms/core/client/impl/AliyunSmsClient.java | 9 +- .../client/impl/DebugDingTalkSmsClient.java | 4 - .../sms/core/client/impl/HuaweiSmsClient.java | 211 +++++++----------- .../core/client/impl/TencentSmsClient.java | 4 - .../core/client/impl/HuaweiSmsClientTest.java | 55 +++-- .../sms/core/client/impl/SmsClientTests.java | 51 +---- 8 files changed, 130 insertions(+), 213 deletions(-) diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java index 622c4f95b..28581073f 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java @@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum; import cn.iocoder.yudao.module.system.service.sms.SmsSendService; -import com.xingyuv.captcha.util.StreamUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.*; @@ -13,8 +12,6 @@ import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletRequest; -import java.nio.charset.Charset; - import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 短信回调") diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java index 3b6e0eb0d..a1883bfdf 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java @@ -26,15 +26,9 @@ public abstract class AbstractSmsClient implements SmsClient { * 初始化 */ public final void init() { - doInit(); log.debug("[init][配置({}) 初始化完成]", properties); } - /** - * 自定义初始化 - */ - protected abstract void doInit(); - public final void refresh(SmsChannelProperties properties) { // 判断是否更新 if (properties.equals(this.properties)) { diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java index f8158cdf2..558dbdef2 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java @@ -50,10 +50,6 @@ public class AliyunSmsClient extends AbstractSmsClient { Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); } - @Override - protected void doInit() { - } - @Override public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { @@ -80,7 +76,7 @@ public class AliyunSmsClient extends AbstractSmsClient { @Override public List parseSmsReceiveStatus(String text) { JSONArray statuses = JSONUtil.parseArray(text); - // 字段参考 + // 字段参考 https://help.aliyun.com/zh/sms/developer-reference/smsreport-2 return convertList(statuses, status -> { JSONObject statusObj = (JSONObject) status; return new SmsReceiveRespDTO() @@ -166,7 +162,8 @@ public class AliyunSmsClient extends AbstractSmsClient { String hashedRequestBody = DigestUtil.sha256Hex(requestBody); // 4. 构建 Authorization 签名 - String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody; + 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); // 计算签名 diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java index e9fcc6c41..6d2f2d017 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java @@ -36,10 +36,6 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient { Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); } - @Override - protected void doInit() { - } - @Override public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java index dfe5b2d45..4b073448b 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java @@ -1,13 +1,15 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.format.FastDateFormat; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.StrUtil; - import cn.hutool.crypto.SecureUtil; +import cn.hutool.http.HttpUtil; 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.CollectionUtils; import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; @@ -15,23 +17,19 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespD 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 lombok.extern.slf4j.Slf4j; import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; import java.net.URLEncoder; -import java.text.SimpleDateFormat; +import java.nio.charset.StandardCharsets; import java.time.Instant; +import java.time.LocalDateTime; import java.time.ZoneId; 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; -// todo @scholar:参考阿里云在优化下 /** * 华为短信客户端的实现类 * @@ -41,13 +39,11 @@ import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; @Slf4j public class HuaweiSmsClient extends AbstractSmsClient { - public static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI - public static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443"; - public static final String SIGNEDHEADERS = "content-type;host;x-sdk-date"; + private static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI + private static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443"; + private static final String SIGNEDHEADERS = "content-type;host;x-sdk-date"; - @Override - protected void doInit() { - } + private static final String RESPONSE_CODE_SUCCESS = "000000"; public HuaweiSmsClient(SmsChannelProperties properties) { super(properties); @@ -58,139 +54,96 @@ public class HuaweiSmsClient extends AbstractSmsClient { @Override public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { + // 1. 执行请求 // 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html - // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构 - // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。 - String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号 - String templateId = StrUtil.subBefore(apiTemplateId, " ", true); //模板ID - - //选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告 + // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构, + // 所以将 sender 通道号,拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"(空格为分隔符) + String sender = apiTemplateId.split(" ")[1]; // 中国大陆短信签名通道号或全球短信通道号 + String templateId = apiTemplateId.split(" ")[0]; //模板ID String statusCallBack = properties.getCallbackUrl(); + StringBuilder requestBody = new StringBuilder(); + appendToBody(requestBody, "from=", sender); + appendToBody(requestBody, "&to=", mobile); + appendToBody(requestBody, "&templateId=", templateId); + appendToBody(requestBody, "&templateParas=", JsonUtils.toJsonString( + convertList(templateParams, kv -> String.valueOf(kv.getValue())))); + appendToBody(requestBody, "&statusCallback=", statusCallBack); + appendToBody(requestBody, "&extend=", String.valueOf(sendLogId)); + JSONObject response = request("/sms/batchSendSms/v1/", "POST", requestBody.toString()); - List templateParas = CollectionUtils.convertList(templateParams, kv -> String.valueOf(kv.getValue())); - - JSONObject JsonResponse = request(sendLogId,sender,mobile,templateId,templateParas,statusCallBack); - - return new SmsSendRespDTO().setSuccess("000000".equals(JsonResponse.getStr("code"))) - .setSerialNo(JsonResponse.getJSONArray("result").getJSONObject(0).getStr("smsMsgId")) - .setApiCode(JsonResponse.getJSONArray("result").getJSONObject(0).getStr("status")); + // 2. 解析请求 + if (!response.containsKey("result")) { // 例如说:密钥不正确 + return new SmsSendRespDTO().setSuccess(false) + .setApiCode(response.getStr("code")) + .setApiMsg(response.getStr("description")); + } + JSONObject sendResult = response.getJSONArray("result").getJSONObject(0); + return new SmsSendRespDTO().setSuccess(RESPONSE_CODE_SUCCESS.equals(response.getStr("code"))) + .setSerialNo(sendResult.getStr("smsMsgId")).setApiCode(sendResult.getStr("status")); } - JSONObject request(Long sendLogId,String sender,String mobile,String templateId,List templateParas,String statusCallBack) throws UnsupportedEncodingException { - - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH); - sdf.setTimeZone(TimeZone.getTimeZone("UTC")); - String sdkDate = sdf.format(new Date()); - - // ************* 步骤 1:拼接规范请求串 ************* - String httpRequestMethod = "POST"; - String canonicalUri = "/sms/batchSendSms/v1/"; - String canonicalQueryString = "";//查询参数为空 - String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n" - + "host:"+ HOST +"\n" - + "x-sdk-date:" + sdkDate + "\n"; - String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, sendLogId); - if (null == body || body.isEmpty()) { - return null; - } - String hashedRequestBody = sha256Hex(body); - String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" - + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + hashedRequestBody; - - // ************* 步骤 2:拼接待签名字符串 ************* - String hashedCanonicalRequest = sha256Hex(canonicalRequest); - String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + hashedCanonicalRequest; - - // ************* 步骤 3:计算签名 ************* - String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); - - // ************* 步骤 4:拼接 Authorization ************* - String authorization = "SDK-HMAC-SHA256" + " " + "Access=" + properties.getApiKey() + ", " - + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature; - - // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response ************* + /** + * 请求华为云短信 + * + * @see https://support.huaweicloud.com/api-msgsms/sms_05_0046.html + * @param uri 请求 URI + * @param method 请求 Method + * @param requestBody 请求 Body + * @return 请求结果 + */ + private JSONObject request(String uri, String method, String requestBody) { + // 1.1 请求 Header TreeMap headers = new TreeMap<>(); headers.put("Content-Type", "application/x-www-form-urlencoded"); + String sdkDate = FastDateFormat.getInstance("yyyyMMdd'T'HHmmss'Z'", TimeZone.getTimeZone("UTC")).format(new Date()); headers.put("X-Sdk-Date", sdkDate); headers.put("host", HOST); - headers.put("Authorization", authorization); - String responseBody = HttpUtils.post(URL, headers, body); + // 1.2 构建签名 Header + String canonicalQueryString = ""; // 查询参数为空 + String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n" + + "host:"+ HOST +"\n" + "x-sdk-date:" + sdkDate + "\n"; + String canonicalRequest = method + "\n" + uri + "\n" + canonicalQueryString + "\n" + + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + sha256Hex(requestBody); + String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + sha256Hex(canonicalRequest); + String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名 + headers.put("Authorization", "SDK-HMAC-SHA256" + " " + "Access=" + properties.getApiKey() + + ", " + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature); + + // 2. 发起请求 + String responseBody = HttpUtils.post(URL, headers, requestBody); return JSONUtil.parseObj(responseBody); -// -// -// HttpResponse response = HttpRequest.post(URL) -// .header("Content-Type", "application/x-www-form-urlencoded") -// .header("X-Sdk-Date", sdkDate) -// .header("host",HOST) -// .header("Authorization", authorization) -// .body(body) -// .execute(); -// -// return JSONUtil.parseObj(response.body()); } - static String buildRequestBody(String sender, String receiver, String templateId, List templateParas, - String statusCallBack, Long sendLogId) throws UnsupportedEncodingException { - if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty() - || templateId.isEmpty()) { - System.out.println("buildRequestBody(): sender, receiver or templateId is null."); - return null; - } - - StringBuilder body = new StringBuilder(); - appendToBody(body, "from=", sender); - appendToBody(body, "&to=", receiver); - appendToBody(body, "&templateId=", templateId); - appendToBody(body, "&templateParas=", JsonUtils.toJsonString(templateParas)); - appendToBody(body, "&statusCallback=", statusCallBack); - appendToBody(body, "&signature=", null); - appendToBody(body, "&extend=", String.valueOf(sendLogId)); - - return body.toString(); - } - - private static void appendToBody(StringBuilder body, String key, String val) throws UnsupportedEncodingException { - if (null != val && !val.isEmpty()) { - body.append(key).append(URLEncoder.encode(val, "UTF-8")); - } - } @Override public List parseSmsReceiveStatus(String requestBody) { - - System.out.println("text in parseSmsReceiveStatus===== " + requestBody); - - Map params = new HashMap<>(); - try { - String[] pairs = requestBody.split("&"); - for (String pair : pairs) { - int idx = pair.indexOf("="); - String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8"); - String value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8"); - params.put(key, value); - } - } catch (Exception e) { - e.printStackTrace(); - } - - List respDTOS = new ArrayList<>(); - respDTOS.add(new SmsReceiveRespDTO() - .setSuccess("DELIVRD".equals(params.get("status"))) // 是否接收成功 - .setErrorCode(params.get("status")) // 状态报告编码 - .setErrorMsg(params.get("statusDesc")) - .setMobile(params.get("to")) // 手机号 - .setReceiveTime(LocalDateTime.ofInstant(Instant.parse(params.get("updateTime")), ZoneId.of("UTC"))) // 状态报告时间 - .setSerialNo(params.get("smsMsgId")) // 发送序列号 - .setLogId(Long.valueOf(params.get("extend")))//logId - ); - - return respDTOS; + Map params = HttpUtil.decodeParamMap(requestBody, StandardCharsets.UTF_8); + // 字段参考 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html + return ListUtil.of(new SmsReceiveRespDTO() + .setSuccess("DELIVRD".equals(params.get("status"))) // 是否接收成功 + .setErrorCode(params.get("status")) // 状态报告编码 + .setErrorMsg(params.get("statusDesc")) + .setMobile(params.get("to")) // 手机号 + .setReceiveTime(LocalDateTime.ofInstant(Instant.parse(params.get("updateTime")), ZoneId.of("UTC"))) // 状态报告时间 + .setSerialNo(params.get("smsMsgId")) // 发送序列号 + .setLogId(Long.valueOf(params.get("extend")))); // 用户序列号 } @Override public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { - //华为短信模板查询和发送短信,是不同的两套key和secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现。 - return new SmsTemplateRespDTO().setId(null).setContent(null) + // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构, + // 所以将 sender 通道号,拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"(空格为分隔符) + String[] strs = apiTemplateId.split(" "); + Assert.isTrue(strs.length == 2, "格式不正确,需要满足:apiTemplateId sender"); + return new SmsTemplateRespDTO().setId(strs[0]).setContent(null) .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null); } + + @SuppressWarnings("CharsetObjectCanBeUsed") + private static void appendToBody(StringBuilder body, String key, String value) throws UnsupportedEncodingException { + if (StrUtil.isNotEmpty(value)) { + body.append(key).append(URLEncoder.encode(value, CharsetUtil.CHARSET_UTF_8.name())); + } + } + } \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java index 23a01db24..bd603b543 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java @@ -56,10 +56,6 @@ public class TencentSmsClient extends AbstractSmsClient { validateSdkAppId(properties); } - @Override - protected void doInit() { - } - /** * 参数校验腾讯云的 SDK AppId * diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java index e18a2b60d..521c0ad18 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java @@ -36,15 +36,8 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { @InjectMocks private HuaweiSmsClient smsClient = new HuaweiSmsClient(properties); - @Test - public void testDoInit() { - // 调用 - smsClient.doInit(); - } - @Test public void testDoSendSms_success() throws Throwable { - try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { // 准备参数 Long sendLogId = randomLongId(); @@ -55,9 +48,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) - .thenReturn( - "{\"result\":[{\"originTo\":\"+86155****5678\",\"createTime\":\"2018-05-25T16:34:34Z\",\"from\":\"1069********0012\",\"smsMsgId\":\"d6e3cdd0-522b-4692-8304-a07553cdf591_8539659\",\"status\":\"000000\",\"countryId\":\"CN\",\"total\":2}],\"code\":\"000000\",\"description\":\"Success\"}\n" - ); + .thenReturn("{\"result\":[{\"originTo\":\"+86155****5678\",\"createTime\":\"2018-05-25T16:34:34Z\",\"from\":\"1069********0012\",\"smsMsgId\":\"d6e3cdd0-522b-4692-8304-a07553cdf591_8539659\",\"status\":\"000000\",\"countryId\":\"CN\",\"total\":2}],\"code\":\"000000\",\"description\":\"Success\"}\n"); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, @@ -66,12 +57,11 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { assertTrue(result.getSuccess()); assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo()); assertEquals("000000", result.getApiCode()); - } } @Test - public void testDoSendSms_fail() throws Throwable { + public void testDoSendSms_fail_01() throws Throwable { try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { // 准备参数 Long sendLogId = randomLongId(); @@ -82,17 +72,39 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) - .thenReturn( - "{\"result\":[{\"originTo\":\"+86155****5678\",\"createTime\":\"2018-05-25T16:34:34Z\",\"from\":\"1069********0012\",\"smsMsgId\":\"d6e3cdd0-522b-4692-8304-a07553cdf591_8539659\",\"status\":\"E200015\",\"countryId\":\"CN\",\"total\":2}],\"code\":\"E000000\",\"description\":\"Success\"}\n" - ); + .thenReturn("{\"result\":[{\"total\":1,\"originTo\":\"17321315478\",\"createTime\":\"2024-08-18T11:32:20Z\",\"from\":\"x8824060312575\",\"smsMsgId\":\"06e4b966-ad87-479f-8b74-f57fb7aafb60_304613461\",\"countryId\":\"CN\",\"status\":\"E200033\"}],\"code\":\"E000510\",\"description\":\"The SMS fails to be sent. For details, see status.\"}"); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); // 断言 assertFalse(result.getSuccess()); - assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo()); - assertEquals("E200015", result.getApiCode()); + assertEquals("06e4b966-ad87-479f-8b74-f57fb7aafb60_304613461", result.getSerialNo()); + assertEquals("E200033", result.getApiCode()); + } + } + + @Test + public void testDoSendSms_fail_02() throws Throwable { + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString() + " " + randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); + + // mock 方法 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) + .thenReturn("{\"code\":\"E000102\",\"description\":\"Invalid app_key.\"}"); + + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertFalse(result.getSuccess()); + assertEquals("E000102", result.getApiCode()); + assertEquals("Invalid app_key.", result.getApiMsg()); } } @@ -105,10 +117,11 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { List statuses = smsClient.parseSmsReceiveStatus(text); // 断言 assertEquals(1, statuses.size()); - assertTrue(statuses.getFirst().getSuccess()); - assertEquals("DELIVRD", statuses.getFirst().getErrorCode()); - assertEquals(LocalDateTime.of(2024, 8, 15, 3, 0, 34), statuses.getFirst().getReceiveTime()); - assertEquals("70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459", statuses.getFirst().getSerialNo()); + SmsReceiveRespDTO status = statuses.get(0); + assertTrue(status.getSuccess()); + assertEquals("DELIVRD", status.getErrorCode()); + assertEquals(LocalDateTime.of(2024, 8, 15, 3, 0, 34), status.getReceiveTime()); + assertEquals("70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459", status.getSerialNo()); } } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java index 6eb22af1b..cb6c6c9f4 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; 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.SmsClient; 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.property.SmsChannelProperties; @@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test; import java.util.List; /** - * 各种 {@link SmsClientTests 集成测试 + * 各种 {@link SmsClient} 的集成测试 * * @author 芋道源码 */ @@ -23,8 +23,8 @@ public class SmsClientTests { @Disabled public void testAliyunSmsClient_getSmsTemplate() throws Throwable { SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz"); + .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY")) + .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY")); AliyunSmsClient client = new AliyunSmsClient(properties); // 准备参数 String apiTemplateId = "SMS_207945135"; @@ -38,9 +38,9 @@ public class SmsClientTests { @Disabled public void testAliyunSmsClient_sendSms() throws Throwable { SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") - .setSignature("runpu"); + .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY")) + .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY")) + .setSignature("Ballcat"); AliyunSmsClient client = new AliyunSmsClient(properties); // 准备参数 Long sendLogId = System.currentTimeMillis(); @@ -52,35 +52,6 @@ public class SmsClientTests { System.out.println(sendRespDTO); } - @Test - @Disabled - public void testAliyunSmsClient_parseSmsReceiveStatus() { - SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz"); - AliyunSmsClient client = new AliyunSmsClient(properties); - // 准备参数 - String text = "[\n" + - " {\n" + - " \"phone_number\" : \"13900000001\",\n" + - " \"send_time\" : \"2017-01-01 11:12:13\",\n" + - " \"report_time\" : \"2017-02-02 22:23:24\",\n" + - " \"success\" : true,\n" + - " \"err_code\" : \"DELIVERED\",\n" + - " \"err_msg\" : \"用户接收成功\",\n" + - " \"sms_size\" : \"1\",\n" + - " \"biz_id\" : \"12345\",\n" + - " \"out_id\" : \"67890\"\n" + - " }\n" + - "]"; - // mock 方法 - - // 调用 - List statuses = client.parseSmsReceiveStatus(text); - // 打印结果 - System.out.println(statuses); - } - // ========== 腾讯云 ========== @Test @@ -123,14 +94,14 @@ public class SmsClientTests { @Disabled public void testHuaweiSmsClient_sendSms() throws Throwable { SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("123") - .setApiSecret("456") + .setApiKey(System.getenv("SMS_HUAWEI_ACCESS_KEY")) + .setApiSecret(System.getenv("SMS_HUAWEI_SECRET_KEY")) .setSignature("runpu"); HuaweiSmsClient client = new HuaweiSmsClient(properties); // 准备参数 Long sendLogId = System.currentTimeMillis(); - String mobile = "15601691323"; - String apiTemplateId = "xx test01"; + String mobile = "17321315478"; + String apiTemplateId = "3644cdab863546a3b718d488659a99ef x8824060312575"; List> templateParams = List.of(new KeyValue<>("code", "1024")); // 调用 SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); From a5f82fedb3a47bd54f4f34f2c7d923195edd5d01 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 18 Aug 2024 20:54:51 +0800 Subject: [PATCH 031/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E7=9F=AD=E4=BF=A1=EF=BC=9A=E8=85=BE=E8=AE=AF?= =?UTF-8?q?=E4=BA=91=E7=9A=84=E5=AE=9E=E7=8E=B0=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/client/impl/TencentSmsClient.java | 106 +++++++----------- .../sms/core/client/impl/SmsClientTests.java | 14 ++- .../client/impl/TencentSmsClientTest.java | 27 ++++- 3 files changed, 75 insertions(+), 72 deletions(-) diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java index bd603b543..ae3138362 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java @@ -1,7 +1,11 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; +import cn.hutool.core.date.format.FastDateFormat; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.HexUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; @@ -14,12 +18,8 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR 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.annotations.VisibleForTesting; -import jakarta.xml.bind.DatatypeConverter; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; import java.util.*; import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; @@ -34,6 +34,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. */ public class TencentSmsClient extends AbstractSmsClient { + private static final String HOST = "sms.tencentcloudapi.com"; private static final String VERSION = "2021-01-11"; private static final String REGION = "ap-guangzhou"; @@ -89,7 +90,7 @@ public class TencentSmsClient extends AbstractSmsClient { body.put("PhoneNumberSet", new String[]{mobile}); body.put("SmsSdkAppId", getSdkAppId()); body.put("SignName", properties.getSignature()); - body.put("TemplateId",apiTemplateId); + body.put("TemplateId", apiTemplateId); body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue()))); JSONObject response = request("SendSms", body); @@ -102,11 +103,11 @@ public class TencentSmsClient extends AbstractSmsClient { .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"))) + JSONObject sendResult = responseResult.getJSONArray("SendStatusSet").getJSONObject(0); + return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, sendResult.getStr("Code"))) .setApiRequestId(responseResult.getStr("RequestId")) - .setSerialNo(responseData.getStr("SerialNo")) - .setApiMsg(responseData.getStr("Message")); + .setSerialNo(sendResult.getStr("SerialNo")) + .setApiMsg(sendResult.getStr("Message")); } @Override @@ -133,14 +134,13 @@ public class TencentSmsClient extends AbstractSmsClient { 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); + // 2. 解析请求 + JSONObject statusResult = response.getJSONObject("Response") + .getJSONArray("DescribeTemplateStatusSet").getJSONObject(0); + return new SmsTemplateRespDTO().setId(apiTemplateId) + .setContent(statusResult.get("TemplateContent").toString()) + .setAuditStatus(convertSmsTemplateAuditStatus(statusResult.getInt("StatusCode"))) + .setAuditReason(statusResult.get("ReviewReply").toString()); } @VisibleForTesting @@ -163,63 +163,39 @@ public class TencentSmsClient extends AbstractSmsClient { * @return 请求结果 */ private JSONObject request(String action, TreeMap 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 = "/"; - String canonicalQueryString = ""; - - String canonicalHeaders = "content-type:application/json; charset=utf-8\n" - + "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:拼接待签名字符串 ************* - String credentialScope = date + "/" + "sms" + "/" + "tc3_request"; - String hashedCanonicalRequest = sha256Hex(canonicalRequest); - String stringToSign = "TC3-HMAC-SHA256" + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest; - - // ************* 步骤 3:计算签名 ************* - byte[] secretDate = hmac256(("TC3" + properties.getApiSecret()).getBytes(StandardCharsets.UTF_8), date); - byte[] secretService = hmac256(secretDate, "sms"); - byte[] secretSigning = hmac256(secretService, "tc3_request"); - String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase(); - - // ************* 步骤 4:拼接 Authorization ************* - String authorization = "TC3-HMAC-SHA256" + " " + "Credential=" + getApiKey() + "/" + credentialScope + ", " - + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature; - - // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response ************* + // 1.1 请求 Header Map headers = new HashMap<>(); - headers.put("Authorization", authorization); headers.put("Content-Type", "application/json; charset=utf-8"); - headers.put("Host", host); + headers.put("Host", HOST); headers.put("X-TC-Action", action); - headers.put("X-TC-Timestamp", timestamp); + Date now = new Date(); + String nowStr = FastDateFormat.getInstance("yyyy-MM-dd", TimeZone.getTimeZone("UTC")).format(now); + headers.put("X-TC-Timestamp", String.valueOf(now.getTime() / 1000)); headers.put("X-TC-Version", VERSION); headers.put("X-TC-Region", REGION); - String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(body)); + // 1.2 构建签名 Header + String canonicalQueryString = ""; + String canonicalHeaders = "content-type:application/json; charset=utf-8\n" + + "host:" + HOST + "\n" + "x-tc-action:" + action.toLowerCase() + "\n"; + String signedHeaders = "content-type;host;x-tc-action"; + String canonicalRequest = "POST" + "\n" + "/" + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + + signedHeaders + "\n" + sha256Hex(JSONUtil.toJsonStr(body)); + String credentialScope = nowStr + "/" + "sms" + "/" + "tc3_request"; + String stringToSign = "TC3-HMAC-SHA256" + "\n" + now.getTime() / 1000 + "\n" + credentialScope + "\n" + + sha256Hex(canonicalRequest); + byte[] secretService = hmac256(hmac256(("TC3" + properties.getApiSecret()).getBytes(StandardCharsets.UTF_8), nowStr), "sms"); + String signature = HexUtil.encodeHexStr(hmac256(hmac256(secretService, "tc3_request"), stringToSign)); + headers.put("Authorization", "TC3-HMAC-SHA256" + " " + "Credential=" + getApiKey() + "/" + credentialScope + ", " + + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature); + // 2. 发起请求 + String responseBody = HttpUtils.post("https://" + HOST, headers, JSONUtil.toJsonStr(body)); return JSONUtil.parseObj(responseBody); } - // 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 static byte[] hmac256(byte[] key, String msg) { + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, key).digest(msg); } + } \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java index cb6c6c9f4..3105c4369 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java @@ -57,15 +57,16 @@ public class SmsClientTests { @Test @Disabled public void testTencentSmsClient_sendSms() throws Throwable { + String sdkAppId = "1400500458"; SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId) + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY")) .setSignature("芋道源码"); TencentSmsClient client = new TencentSmsClient(properties); // 准备参数 Long sendLogId = System.currentTimeMillis(); String mobile = "15601691323"; - String apiTemplateId = "2136358"; + String apiTemplateId = "358212"; // 调用 SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024"))); // 打印结果 @@ -75,13 +76,14 @@ public class SmsClientTests { @Test @Disabled public void testTencentSmsClient_getSmsTemplate() throws Throwable { + String sdkAppId = "1400500458"; SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId) + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY")) .setSignature("芋道源码"); TencentSmsClient client = new TencentSmsClient(properties); // 准备参数 - String apiTemplateId = "2136358"; + String apiTemplateId = "358212"; // 调用 SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId); // 打印结果 diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java index b25540b44..060a34558 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java @@ -78,7 +78,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { } @Test - public void testDoSendSms_fail() throws Throwable { + public void testDoSendSms_fail_01() throws Throwable { try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { // 准备参数 Long sendLogId = randomLongId(); @@ -117,6 +117,31 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { } } + @Test + public void testDoSendSms_fail_02() throws Throwable { + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); + + // mock 方法 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) + .thenReturn("{\"Response\":{\"Error\":{\"Code\":\"AuthFailure.SecretIdNotFound\",\"Message\":\"The SecretId is not found, please ensure that your SecretId is correct.\"},\"RequestId\":\"2a88f82a-261c-4ac6-9fa9-c7d01aaa486a\"}}"); + + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertFalse(result.getSuccess()); + assertEquals("2a88f82a-261c-4ac6-9fa9-c7d01aaa486a", result.getApiRequestId()); + assertEquals("AuthFailure.SecretIdNotFound", result.getApiCode()); + assertEquals("The SecretId is not found, please ensure that your SecretId is correct.", result.getApiMsg()); + } + } + @Test public void testParseSmsReceiveStatus() { // 准备参数 From a685402688004f989886018942b80deaa4a50526 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 18 Aug 2024 21:04:15 +0800 Subject: [PATCH 032/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E7=9F=AD=E4=BF=A1=EF=BC=9A=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E7=9F=AD=E4=BF=A1=20channel=20=E7=9A=84=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sms/core/client/SmsClientFactory.java | 3 +- .../client/impl/SmsClientFactoryImpl.java | 3 +- .../service/sms/SmsChannelServiceImpl.java | 83 +++---------------- .../service/sms/SmsChannelServiceTest.java | 29 ++----- 4 files changed, 19 insertions(+), 99 deletions(-) diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java index a1133177f..ad878b78e 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java @@ -30,7 +30,8 @@ public interface SmsClientFactory { * 创建短信 Client * * @param properties 配置对象 + * @return 短信 Client */ - void createOrUpdateSmsClient(SmsChannelProperties properties); + SmsClient createOrUpdateSmsClient(SmsChannelProperties properties); } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java index 326cad058..dde1475d4 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java @@ -59,7 +59,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory { } @Override - public void createOrUpdateSmsClient(SmsChannelProperties properties) { + public SmsClient createOrUpdateSmsClient(SmsChannelProperties properties) { AbstractSmsClient client = channelIdClients.get(properties.getId()); if (client == null) { client = this.createSmsClient(properties); @@ -68,6 +68,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory { } else { client.refresh(properties); } + return client; } private AbstractSmsClient createSmsClient(SmsChannelProperties properties) { diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java index cca3741fe..5c6f36d8f 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java @@ -1,27 +1,21 @@ package cn.iocoder.yudao.module.system.service.sms; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient; -import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClientFactory; -import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import lombok.Getter; +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import jakarta.annotation.Resource; -import java.time.Duration; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; @@ -34,46 +28,6 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNE @Slf4j public class SmsChannelServiceImpl implements SmsChannelService { - /** - * {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory - */ - @Getter - private final LoadingCache idClientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L), - new CacheLoader() { - - @Override - public SmsClient load(Long id) { - // 查询,然后尝试刷新 - SmsChannelDO channel = smsChannelMapper.selectById(id); - if (channel != null) { - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); - smsClientFactory.createOrUpdateSmsClient(properties); - } - return smsClientFactory.getSmsClient(id); - } - - }); - - /** - * {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory - */ - @Getter - private final LoadingCache codeClientCache = buildAsyncReloadingCache(Duration.ofSeconds(60L), - new CacheLoader() { - - @Override - public SmsClient load(String code) { - // 查询,然后尝试刷新 - SmsChannelDO channel = smsChannelMapper.selectByCode(code); - if (channel != null) { - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); - smsClientFactory.createOrUpdateSmsClient(properties); - } - return smsClientFactory.getSmsClient(code); - } - - }); - @Resource private SmsClientFactory smsClientFactory; @@ -93,41 +47,22 @@ public class SmsChannelServiceImpl implements SmsChannelService { @Override public void updateSmsChannel(SmsChannelSaveReqVO updateReqVO) { // 校验存在 - SmsChannelDO channel = validateSmsChannelExists(updateReqVO.getId()); + validateSmsChannelExists(updateReqVO.getId()); // 更新 SmsChannelDO updateObj = BeanUtils.toBean(updateReqVO, SmsChannelDO.class); smsChannelMapper.updateById(updateObj); - - // 清空缓存 - clearCache(updateReqVO.getId(), channel.getCode()); } @Override public void deleteSmsChannel(Long id) { // 校验存在 - SmsChannelDO channel = validateSmsChannelExists(id); + validateSmsChannelExists(id); // 校验是否有在使用该账号的模版 if (smsTemplateService.getSmsTemplateCountByChannelId(id) > 0) { throw exception(SMS_CHANNEL_HAS_CHILDREN); } // 删除 smsChannelMapper.deleteById(id); - - // 清空缓存 - clearCache(id, channel.getCode()); - } - - /** - * 清空指定渠道编号的缓存 - * - * @param id 渠道编号 - * @param code 渠道编码 - */ - private void clearCache(Long id, String code) { - idClientCache.invalidate(id); - if (StrUtil.isNotEmpty(code)) { - codeClientCache.invalidate(code); - } } private SmsChannelDO validateSmsChannelExists(Long id) { @@ -155,12 +90,14 @@ public class SmsChannelServiceImpl implements SmsChannelService { @Override public SmsClient getSmsClient(Long id) { - return idClientCache.getUnchecked(id); + SmsChannelDO channel = smsChannelMapper.selectById(id); + SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); + return smsClientFactory.createOrUpdateSmsClient(properties); } @Override public SmsClient getSmsClient(String code) { - return codeClientCache.getUnchecked(code); + return smsClientFactory.getSmsClient(code); } } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java index 1cc9152c3..295911a17 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java @@ -57,9 +57,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { // 校验记录的属性是否正确 SmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId); assertPojoEquals(reqVO, smsChannel, "id"); - // 断言 cache - assertNull(smsChannelService.getIdClientCache().getIfPresent(smsChannel.getId())); - assertNull(smsChannelService.getCodeClientCache().getIfPresent(smsChannel.getCode())); } @Test @@ -79,9 +76,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { // 校验是否更新正确 SmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, smsChannel); - // 断言 cache - assertNull(smsChannelService.getIdClientCache().getIfPresent(smsChannel.getId())); - assertNull(smsChannelService.getCodeClientCache().getIfPresent(smsChannel.getCode())); } @Test @@ -105,9 +99,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { smsChannelService.deleteSmsChannel(id); // 校验数据不存在了 assertNull(smsChannelMapper.selectById(id)); - // 断言 cache - assertNull(smsChannelService.getIdClientCache().getIfPresent(dbSmsChannel.getId())); - assertNull(smsChannelService.getCodeClientCache().getIfPresent(dbSmsChannel.getCode())); } @Test @@ -196,29 +187,23 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { // mock 数据 SmsChannelDO channel = randomPojo(SmsChannelDO.class); smsChannelMapper.insert(channel); - // mock 参数 + // 准备参数 Long id = channel.getId(); // mock 方法 SmsClient mockClient = mock(SmsClient.class); - when(smsClientFactory.getSmsClient(eq(id))).thenReturn(mockClient); + SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); + when(smsClientFactory.createOrUpdateSmsClient(eq(properties))).thenReturn(mockClient); // 调用 SmsClient client = smsChannelService.getSmsClient(id); // 断言 assertSame(client, mockClient); - verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> { - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); - return properties.equals(arg); - })); } @Test public void testGetSmsClient_code() { - // mock 数据 - SmsChannelDO channel = randomPojo(SmsChannelDO.class); - smsChannelMapper.insert(channel); - // mock 参数 - String code = channel.getCode(); + // 准备参数 + String code = randomString(); // mock 方法 SmsClient mockClient = mock(SmsClient.class); when(smsClientFactory.getSmsClient(eq(code))).thenReturn(mockClient); @@ -227,10 +212,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { SmsClient client = smsChannelService.getSmsClient(code); // 断言 assertSame(client, mockClient); - verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> { - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); - return properties.equals(arg); - })); } } From c7ccb8286ac01238a28334bcfa966df357c4c190 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Mon, 19 Aug 2024 11:32:53 +0800 Subject: [PATCH 033/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91MALL:=20=E5=94=AE=E5=90=8E=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20userId=20=E6=A3=80=E7=B4=A2=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/aftersale/vo/AfterSalePageReqVO.java | 3 +++ .../module/trade/dal/mysql/aftersale/AfterSaleMapper.java | 1 + 2 files changed, 4 insertions(+) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java index f74c84b8f..f4b67fa5a 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java @@ -24,6 +24,9 @@ public class AfterSalePageReqVO extends PageParam { @Schema(description = "售后流水号", example = "202211190847450020500077") private String no; + @Schema(description = "用户编号", example = "1024") + private Long userId; + @Schema(description = "售后状态", example = "10") @InEnum(value = AfterSaleStatusEnum.class, message = "售后状态必须是 {value}") private Integer status; diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java index 68a09a82a..b5e507a91 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java @@ -17,6 +17,7 @@ public interface AfterSaleMapper extends BaseMapperX { default PageResult selectPage(AfterSalePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .likeIfPresent(AfterSaleDO::getNo, reqVO.getNo()) + .eqIfPresent(AfterSaleDO::getUserId, reqVO.getUserId()) .eqIfPresent(AfterSaleDO::getStatus, reqVO.getStatus()) .eqIfPresent(AfterSaleDO::getType, reqVO.getType()) .eqIfPresent(AfterSaleDO::getWay, reqVO.getWay()) From 2d29bf4e6f7486f1f2b4d359d6796d92e996ad54 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 19 Aug 2024 12:28:31 +0800 Subject: [PATCH 034/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E7=9F=AD=E4=BF=A1=EF=BC=9A=E5=8D=8E=E4=B8=BA?= =?UTF-8?q?=E4=BA=91=E7=9A=84=E5=AE=9E=E7=8E=B0=E4=BC=98=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E8=B0=83=E6=95=B4=20sender=20=E9=85=8D=E7=BD=AE=E5=88=B0=20acc?= =?UTF-8?q?essKey=20=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sms/core/client/impl/HuaweiSmsClient.java | 45 +++++++++++++------ .../core/client/impl/HuaweiSmsClientTest.java | 2 +- .../sms/core/client/impl/SmsClientTests.java | 5 ++- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java index 4b073448b..82f55395e 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java @@ -49,25 +49,43 @@ public class HuaweiSmsClient extends AbstractSmsClient { super(properties); Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + validateSender(properties); + } + + /** + * 参数校验华为云的 sender 通道号 + * + * 原因是:验华为云发放短信的时候,需要额外的参数 sender + * + * 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 + * + * @param properties 配置 + */ + private static void validateSender(SmsChannelProperties properties) { + String combineKey = properties.getApiKey(); + Assert.notEmpty(combineKey, "apiKey 不能为空"); + String[] keys = combineKey.trim().split(" "); + Assert.isTrue(keys.length == 2, "华为云短信 apiKey 配置格式错误,请配置 为[accessKeyId sender]"); + } + + private String getAccessKey() { + return StrUtil.subBefore(properties.getApiKey(), " ", true); + } + + private String getSender() { + return StrUtil.subAfter(properties.getApiKey(), " ", true); } @Override public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { - // 1. 执行请求 - // 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html - // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构, - // 所以将 sender 通道号,拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"(空格为分隔符) - String sender = apiTemplateId.split(" ")[1]; // 中国大陆短信签名通道号或全球短信通道号 - String templateId = apiTemplateId.split(" ")[0]; //模板ID - String statusCallBack = properties.getCallbackUrl(); StringBuilder requestBody = new StringBuilder(); - appendToBody(requestBody, "from=", sender); + appendToBody(requestBody, "from=", getSender()); appendToBody(requestBody, "&to=", mobile); - appendToBody(requestBody, "&templateId=", templateId); + appendToBody(requestBody, "&templateId=", apiTemplateId); appendToBody(requestBody, "&templateParas=", JsonUtils.toJsonString( convertList(templateParams, kv -> String.valueOf(kv.getValue())))); - appendToBody(requestBody, "&statusCallback=", statusCallBack); + appendToBody(requestBody, "&statusCallback=", properties.getCallbackUrl()); appendToBody(requestBody, "&extend=", String.valueOf(sendLogId)); JSONObject response = request("/sms/batchSendSms/v1/", "POST", requestBody.toString()); @@ -107,7 +125,7 @@ public class HuaweiSmsClient extends AbstractSmsClient { + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + sha256Hex(requestBody); String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + sha256Hex(canonicalRequest); String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名 - headers.put("Authorization", "SDK-HMAC-SHA256" + " " + "Access=" + properties.getApiKey() + headers.put("Authorization", "SDK-HMAC-SHA256" + " " + "Access=" + getAccessKey() + ", " + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature); // 2. 发起请求 @@ -131,11 +149,10 @@ public class HuaweiSmsClient extends AbstractSmsClient { @Override public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { - // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构, - // 所以将 sender 通道号,拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"(空格为分隔符) + // 华为短信模板查询和发送短信,是不同的两套 key 和 secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现 String[] strs = apiTemplateId.split(" "); Assert.isTrue(strs.length == 2, "格式不正确,需要满足:apiTemplateId sender"); - return new SmsTemplateRespDTO().setId(strs[0]).setContent(null) + return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(null) .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null); } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java index 521c0ad18..3f97412c8 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java @@ -29,7 +29,7 @@ import static org.mockito.Mockito.mockStatic; public class HuaweiSmsClientTest extends BaseMockitoUnitTest { private final SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey(randomString())// 随机一个 apiKey,避免构建报错 + .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错 .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 .setSignature("芋道源码"); diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java index 3105c4369..bc0dcf980 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java @@ -95,15 +95,16 @@ public class SmsClientTests { @Test @Disabled public void testHuaweiSmsClient_sendSms() throws Throwable { + String sender = "x8824060312575"; SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey(System.getenv("SMS_HUAWEI_ACCESS_KEY")) + .setApiKey(System.getenv("SMS_HUAWEI_ACCESS_KEY") + " " + sender) .setApiSecret(System.getenv("SMS_HUAWEI_SECRET_KEY")) .setSignature("runpu"); HuaweiSmsClient client = new HuaweiSmsClient(properties); // 准备参数 Long sendLogId = System.currentTimeMillis(); String mobile = "17321315478"; - String apiTemplateId = "3644cdab863546a3b718d488659a99ef x8824060312575"; + String apiTemplateId = "3644cdab863546a3b718d488659a99ef"; List> templateParams = List.of(new KeyValue<>("code", "1024")); // 调用 SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); From 11a6e8ebf75f1152df38756cc4bceae7620c38f4 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Mon, 19 Aug 2024 17:22:13 +0800 Subject: [PATCH 035/136] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91MAL?= =?UTF-8?q?L:=20=E6=9B=B4=E6=96=B0=E4=BC=9A=E5=91=98=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=99=E9=A2=9D=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao-module-member-biz/pom.xml | 5 +++ .../admin/user/MemberUserController.java | 14 ++++-- .../user/vo/MemberUserUpdateBalanceReqVO.java | 21 +++++++++ .../module/pay/api/wallet/PayWalletApi.java | 19 ++++++++ .../dto/PayWalletUpdateBalanceReqDTO.java | 23 ++++++++++ .../enums/wallet/PayWalletBizTypeEnum.java | 3 +- .../pay/api/wallet/PayWalletApiImpl.java | 43 +++++++++++++++++++ .../service/wallet/PayWalletServiceImpl.java | 5 ++- 8 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateBalanceReqVO.java create mode 100644 yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java create mode 100644 yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java create mode 100644 yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java diff --git a/yudao-module-member/yudao-module-member-biz/pom.xml b/yudao-module-member/yudao-module-member-biz/pom.xml index 3c9b81e65..368a3cba7 100644 --- a/yudao-module-member/yudao-module-member-biz/pom.xml +++ b/yudao-module-member/yudao-module-member-biz/pom.xml @@ -33,6 +33,11 @@ yudao-module-infra-api ${revision} + + cn.iocoder.boot + yudao-module-pay-api + ${revision} + diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java index c9c6c06e0..09978f6e0 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.member.controller.admin.user; import cn.hutool.core.collection.CollUtil; 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.member.controller.admin.user.vo.*; import cn.iocoder.yudao.module.member.convert.user.MemberUserConvert; import cn.iocoder.yudao.module.member.dal.dataobject.group.MemberGroupDO; @@ -15,15 +16,17 @@ import cn.iocoder.yudao.module.member.service.level.MemberLevelService; import cn.iocoder.yudao.module.member.service.point.MemberPointRecordService; import cn.iocoder.yudao.module.member.service.tag.MemberTagService; import cn.iocoder.yudao.module.member.service.user.MemberUserService; +import cn.iocoder.yudao.module.pay.api.wallet.PayWalletApi; +import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletUpdateBalanceReqDTO; 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.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; import java.util.Collection; import java.util.List; import java.util.Objects; @@ -50,6 +53,8 @@ public class MemberUserController { private MemberGroupService memberGroupService; @Resource private MemberPointRecordService memberPointRecordService; + @Resource + private PayWalletApi payWalletApi; @PutMapping("/update") @Operation(summary = "更新会员用户") @@ -79,8 +84,9 @@ public class MemberUserController { @PutMapping("/update-balance") @Operation(summary = "更新会员用户余额") @PreAuthorize("@ss.hasPermission('member:user:update-balance')") - public CommonResult updateUserBalance(@Valid @RequestBody Long id) { - // todo @jason:增加一个【修改余额】 + public CommonResult updateUserBalance(@Valid @RequestBody MemberUserUpdateBalanceReqVO updateReqVO) { + payWalletApi.updateBalance(BeanUtils.toBean(updateReqVO, PayWalletUpdateBalanceReqDTO.class) + .setUserId(updateReqVO.getId())); return success(true); } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateBalanceReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateBalanceReqVO.java new file mode 100644 index 000000000..fe694df67 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateBalanceReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.member.controller.admin.user.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.ToString; + +@Schema(description = "管理后台 - 用户修改余额 Request VO") +@Data +@ToString(callSuper = true) +public class MemberUserUpdateBalanceReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788") + @NotNull(message = "用户编号不能为空") + private Long id; + + @Schema(description = "变动余额,正数为增加,负数为减少", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "变动余额不能为空") + private Integer balance; + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java new file mode 100644 index 000000000..a99bafe93 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.pay.api.wallet; + +import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletUpdateBalanceReqDTO; + +/** + * 会员钱包 API 接口 + * + * @author HUIHUI + */ +public interface PayWalletApi { + + /** + * 更新钱包余额 + * + * @param reqDTO 请求 + */ + void updateBalance(PayWalletUpdateBalanceReqDTO reqDTO); + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java new file mode 100644 index 000000000..02a7756dd --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.pay.api.wallet.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 钱包余额更新 Request DTO + * + * @author HUIHUI + */ +@Data +public class PayWalletUpdateBalanceReqDTO { + + @NotNull(message = "用户编号不能为空") + private Long userId; + + /** + * 变动余额,正数为增加,负数为减少 + */ + @NotNull(message = "变动余额不能为空") + private Integer balance; + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java index 20e0a8b09..7892db543 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java @@ -18,7 +18,8 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable { RECHARGE(1, "充值"), RECHARGE_REFUND(2, "充值退款"), PAYMENT(3, "支付"), - PAYMENT_REFUND(4, "支付退款"); + PAYMENT_REFUND(4, "支付退款"), + UPDATE_BALANCE(5, "更新余额"); // TODO 后续增加 diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java new file mode 100644 index 000000000..9f94e1a8f --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.pay.api.wallet; + +import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletUpdateBalanceReqDTO; +import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; +import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.WALLET_NOT_FOUND; + +/** + * 会员钱包 API 实现类 + * + * @author HUIHUI + */ +@Service +@Validated +@Slf4j +public class PayWalletApiImpl implements PayWalletApi { + + @Resource + private PayWalletService payWalletService; + + @Override + public void updateBalance(PayWalletUpdateBalanceReqDTO reqDTO) { + // 获得用户钱包 + PayWalletDO wallet = payWalletService.getOrCreateWallet(reqDTO.getUserId(), MEMBER.getValue()); + if (wallet == null) { + log.error("[updateBalance],reqDTO({}) 用户钱包不存在.", reqDTO); + throw exception(WALLET_NOT_FOUND); + } + + // 更新钱包余额 + payWalletService.addWalletBalance(wallet.getId(), String.valueOf(reqDTO.getUserId()), + PayWalletBizTypeEnum.UPDATE_BALANCE, reqDTO.getBalance()); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java index 513786143..b844e3769 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java @@ -12,12 +12,12 @@ import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; import cn.iocoder.yudao.module.pay.service.order.PayOrderService; import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import jakarta.annotation.Resource; import java.time.LocalDateTime; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -176,6 +176,9 @@ public class PayWalletServiceImpl implements PayWalletService { walletMapper.updateWhenRecharge(payWallet.getId(), price); break; } + case UPDATE_BALANCE: // 更新余额 + walletMapper.updateWhenRecharge(payWallet.getId(), price); + break; default: { // TODO 其它类型待实现 throw new UnsupportedOperationException("待实现"); From 6cceab5ba4ed23c5d29ee9992449e8539156fc38 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 19 Aug 2024 19:24:36 +0800 Subject: [PATCH 036/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91AI=20=E5=A4=A7=E6=A8=A1=E5=9E=8B=EF=BC=9A?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vo/AiKnowledgeCreateMyReqVO.java | 7 ++----- .../vo/AiKnowledgeDocumentCreateReqVO.java | 5 +++-- .../vo/AiKnowledgeUpdateMyReqVO.java | 8 ++------ .../ai/dal/dataobject/image/AiImageDO.java | 2 +- .../knowledge/AiKnowledgeBaseDO.java | 8 ++++---- .../knowledge/AiKnowledgeDocumentDO.java | 8 +++++--- .../knowledge/AiKnowledgeSegmentDO.java | 10 ++++++---- .../dal/dataobject/mindmap/AiMindMapDO.java | 2 +- .../ai/dal/dataobject/music/AiMusicDO.java | 2 +- .../ai/dal/dataobject/write/AiWriteDO.java | 2 +- .../service/knowledge/AiEmbeddingService.java | 2 +- .../knowledge/AiEmbeddingServiceImpl.java | 2 ++ .../knowledge/AiKnowledgeBaseService.java | 2 +- .../knowledge/AiKnowledgeBaseServiceImpl.java | 18 ++++++++++------- .../knowledge/AiKnowledgeDocumentService.java | 1 - .../AiKnowledgeDocumentServiceImpl.java | 20 +++++++++---------- .../knowledge/AiKnowledgeSegmentService.java | 3 +-- .../AiKnowledgeSegmentServiceImpl.java | 3 +-- 18 files changed, 53 insertions(+), 52 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java index fa9161eba..ac94a4c15 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java @@ -7,9 +7,6 @@ import lombok.Data; import java.util.List; -/** - * @author xiaoxin - */ @Schema(description = "管理后台 - AI 知识库创建【我的】 Request VO") @Data public class AiKnowledgeCreateMyReqVO { @@ -21,10 +18,10 @@ public class AiKnowledgeCreateMyReqVO { @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "存储 ruoyi-vue-pro 操作文档") private String description; - @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]") + @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]") private List visibilityPermissions; - @Schema(description = "嵌入模型 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "嵌入模型不能为空") private Long modelId; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeDocumentCreateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeDocumentCreateReqVO.java index fa24eef72..10ad036b2 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeDocumentCreateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeDocumentCreateReqVO.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; +import org.hibernate.validator.constraints.URL; /** * @author xiaoxin @@ -12,7 +13,6 @@ import lombok.Data; @Data public class AiKnowledgeDocumentCreateReqVO { - @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204") @NotNull(message = "知识库编号不能为空") private Long knowledgeId; @@ -21,7 +21,8 @@ public class AiKnowledgeDocumentCreateReqVO { @NotBlank(message = "文档名称不能为空") private String name; - @Schema(description = "文档 url", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn") + @Schema(description = "文档 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn") + @URL(message = "文档 URL 格式不正确") private String url; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeUpdateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeUpdateMyReqVO.java index 2bc39d5db..e1f6a31af 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeUpdateMyReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeUpdateMyReqVO.java @@ -7,14 +7,10 @@ import lombok.Data; import java.util.List; -/** - * @author xiaoxin - */ @Schema(description = "管理后台 - AI 知识库创建【我的】 Request VO") @Data public class AiKnowledgeUpdateMyReqVO { - @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204") @NotNull(message = "知识库编号不能为空") private Long id; @@ -26,10 +22,10 @@ public class AiKnowledgeUpdateMyReqVO { @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "") private String description; - @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]") + @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]") private List visibilityPermissions; - @Schema(description = "嵌入模型 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "嵌入模型不能为空") private Long modelId; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java index 6768d904b..8584c5e14 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java @@ -32,7 +32,7 @@ public class AiImageDO extends BaseDO { /** * 编号 */ - @TableId(type = IdType.AUTO) + @TableId private Long id; /** diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java index 81cbf3ac9..d33114f2d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java @@ -1,9 +1,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; - import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -12,19 +10,20 @@ import lombok.Data; import java.util.List; +// TODO @xin:要不把 AiKnowledgeBaseDO 改成 AiKnowledgeDO。感觉 base 后缀,感觉有点奇怪(让人以为是基类)。然后,我们很多地方的外键编号,都是 knowledgeId /** * AI 知识库 DO * * @author xiaoxin */ -@TableName(value = "ai_knowledge_base") +@TableName(value = "ai_knowledge_base", autoResultMap = true) @Data public class AiKnowledgeBaseDO extends BaseDO { /** * 编号 */ - @TableId(type = IdType.AUTO) + @TableId private Long id; /** * 用户编号 @@ -40,6 +39,7 @@ public class AiKnowledgeBaseDO extends BaseDO { * 知识库描述 */ private String description; + // TODO @新:如果全部可见,需要怎么设置? /** * 可见权限,只能选择哪些人可见 */ diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java index 75d927cf0..486602509 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum; -import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @@ -20,10 +19,12 @@ public class AiKnowledgeDocumentDO extends BaseDO { /** * 编号 */ - @TableId(type = IdType.AUTO) + @TableId private Long id; /** * 知识库编号 + * + * 关联 {@link AiKnowledgeBaseDO#getId()} */ private Long knowledgeId; /** @@ -39,7 +40,7 @@ public class AiKnowledgeDocumentDO extends BaseDO { */ private String url; /** - * token数量 + * token 数量 */ private Integer tokens; /** @@ -59,4 +60,5 @@ public class AiKnowledgeDocumentDO extends BaseDO { * 枚举 {@link CommonStatusEnum} */ private Integer status; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java index 657a3739b..2032bfd5e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @@ -19,14 +18,17 @@ public class AiKnowledgeSegmentDO extends BaseDO { /** * 编号 */ - @TableId(type = IdType.AUTO) + @TableId private Long id; /** - * 向量库的id + * 向量库的编号 */ private String vectorId; + // TODO @新:knowledgeId 加个,会方便点 /** * 文档编号 + * + * 关联 {@link AiKnowledgeDocumentDO#getId()} */ private Long documentId; /** @@ -38,7 +40,7 @@ public class AiKnowledgeSegmentDO extends BaseDO { */ private Integer wordCount; /** - * token数量 + * token 数量 */ private Integer tokens; /** diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java index 0442a52d7..824881bf3 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java @@ -19,7 +19,7 @@ public class AiMindMapDO extends BaseDO { /** * 编号 */ - @TableId(type = IdType.AUTO) + @TableId private Long id; /** diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java index 8a6cbe828..97491ec4f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java @@ -25,7 +25,7 @@ public class AiMusicDO extends BaseDO { /** * 编号 */ - @TableId(type = IdType.AUTO) + @TableId private Long id; /** diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java index 752876f2a..5d2f6dcf1 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java @@ -20,7 +20,7 @@ public class AiWriteDO extends BaseDO { /** * 编号 */ - @TableId(type = IdType.AUTO) + @TableId private Long id; /** diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java index eee2f8044..ee4b3d03c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java @@ -17,11 +17,11 @@ public interface AiEmbeddingService { */ void add(List documents); - /** * 相似查询 * * @param request 查询实体 */ List similaritySearch(SearchRequest request); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java index 2a6e75722..689ccea03 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import java.util.List; +// TODO @xin:是不是不用 AiEmbeddingServiceImpl,直接 vectorStore 注入到需要的地方就好啦。通过 KnowledgeDocumentService 返回就好。 /** * AI 嵌入 Service 实现类 * @@ -30,4 +31,5 @@ public class AiEmbeddingServiceImpl implements AiEmbeddingService { public List similaritySearch(SearchRequest request) { return vectorStore.similaritySearch(request); } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseService.java index 7657ab748..be96b0918 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseService.java @@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdat */ public interface AiKnowledgeBaseService { - /** * 创建【我的】知识库 * @@ -27,4 +26,5 @@ public interface AiKnowledgeBaseService { * @param userId 用户编号 */ void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java index 63f4f53db..c208c92ba 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java @@ -32,33 +32,36 @@ public class AiKnowledgeBaseServiceImpl implements AiKnowledgeBaseService { @Resource private AiKnowledgeBaseMapper knowledgeBaseMapper; - @Override public Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId) { + // TODO @xin:貌似直接调用 chatModalService.validateChatModel(id) 完事,不用搞个方法 + // 1. 校验模型配置 AiChatModelDO model = validateChatModel(createReqVO.getModelId()); - AiKnowledgeBaseDO knowledgeBaseDO = BeanUtils.toBean(createReqVO, AiKnowledgeBaseDO.class); - knowledgeBaseDO.setModel(model.getModel()).setUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); - + // 2. 插入知识库 + // TODO @xin:不用 DO 结尾 + AiKnowledgeBaseDO knowledgeBaseDO = BeanUtils.toBean(createReqVO, AiKnowledgeBaseDO.class) + .setModel(model.getModel()).setUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); knowledgeBaseMapper.insert(knowledgeBaseDO); return knowledgeBaseDO.getId(); } @Override public void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId) { - + // 1.1 校验知识库存在 AiKnowledgeBaseDO knowledgeBaseDO = validateKnowledgeExists(updateReqVO.getId()); if (ObjUtil.notEqual(knowledgeBaseDO.getUserId(), userId)) { throw exception(KNOWLEDGE_NOT_EXISTS); } + // 1.2 校验模型配置 AiChatModelDO model = validateChatModel(updateReqVO.getModelId()); + + // 2. 更新知识库 AiKnowledgeBaseDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeBaseDO.class); updateDO.setModel(model.getModel()); - knowledgeBaseMapper.updateById(updateDO); } - private AiChatModelDO validateChatModel(Long id) { AiChatModelDO model = chatModalService.validateChatModel(id); Assert.notNull(model, "未找到对应嵌入模型"); @@ -72,4 +75,5 @@ public class AiKnowledgeBaseServiceImpl implements AiKnowledgeBaseService { } return knowledgeBase; } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 52c62abf7..82c4f7b91 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeDocum */ public interface AiKnowledgeDocumentService { - /** * 创建文档 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 9ee5c4eed..537033015 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -43,28 +43,30 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic @Resource private AiEmbeddingService embeddingService; + // TODO @xin:@Resource 注入 private static final JTokkitTokenCountEstimator TOKEN_COUNT_ESTIMATOR = new JTokkitTokenCountEstimator(); // TODO xiaoxin 临时测试用,后续删 @Value("classpath:/webapp/test/Fel.pdf") private org.springframework.core.io.Resource data; - + // TODO 芋艿:需要 review 下,代码格式; + // TODO @xin:最好有 1、/2、/3 这种,让代码更有层次感 @Override @Transactional(rollbackFor = Exception.class) public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) { - // TODO xiaoxin 后续从 url 加载 TikaDocumentReader loader = new TikaDocumentReader(data); // 加载文档 List documents = loader.get(); Document document = CollUtil.getFirst(documents); - // TODO 芋艿 文档层面有没有可能会比较大,这两个字段是否可以从分段表计算得出? + // TODO @xin:是不是不存在,就抛出异常呀;厚泽 return 呀; + // TODO 芋艿 文档层面有没有可能会比较大,这两个字段是否可以从分段表计算得出?回复:先直接算; Integer tokens = Objects.nonNull(document) ? TOKEN_COUNT_ESTIMATOR.estimate(document.getContent()) : 0; Integer wordCount = Objects.nonNull(document) ? document.getContent().length() : 0; - AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class); - documentDO.setTokens(tokens).setWordCount(wordCount) + AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class) + .setTokens(tokens).setWordCount(wordCount) .setStatus(CommonStatusEnum.ENABLE.getStatus()).setSliceStatus(AiKnowledgeDocumentStatusEnum.SUCCESS.getStatus()); // 文档记录入库 documentMapper.insert(documentDO); @@ -75,17 +77,15 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic // 文档分段 List segments = tokenTextSplitter.apply(documents); - + // 分段内容入库 List segmentDOList = CollectionUtils.convertList(segments, segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId) .setTokens(TOKEN_COUNT_ESTIMATOR.estimate(segment.getContent())).setWordCount(segment.getContent().length()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); - // 分段内容入库 segmentMapper.insertBatch(segmentDOList); - - //向量化并存储 + // 向量化并存储 embeddingService.add(segments); - return documentId; } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index 003ce5c96..7caea9ff4 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -1,11 +1,10 @@ package cn.iocoder.yudao.module.ai.service.knowledge; /** - * AI 知识库-分片 Service 接口 + * AI 知识库分片 Service 接口 * * @author xiaoxin */ public interface AiKnowledgeSegmentService { - } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index aa5facc36..226c5f8fb 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -4,7 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; /** - * AI 知识库-基础信息 Service 实现类 + * AI 知识库分片 Service 实现类 * * @author xiaoxin */ @@ -12,5 +12,4 @@ import org.springframework.stereotype.Service; @Slf4j public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService { - } From 12a80ca4b8d95c62ca5e288baff64f8c11933e30 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 19 Aug 2024 19:51:23 +0800 Subject: [PATCH 037/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91PAY=EF=BC=9A=E9=92=B1=E5=8C=85=E4=BD=99?= =?UTF-8?q?=E9=A2=9D=E4=BF=AE=E6=94=B9=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-module-member/yudao-module-member-biz/pom.xml | 1 + .../cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java | 1 + .../module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java | 1 + .../yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java | 2 -- .../iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java | 1 + 5 files changed, 4 insertions(+), 2 deletions(-) diff --git a/yudao-module-member/yudao-module-member-biz/pom.xml b/yudao-module-member/yudao-module-member-biz/pom.xml index 368a3cba7..165ab9f83 100644 --- a/yudao-module-member/yudao-module-member-biz/pom.xml +++ b/yudao-module-member/yudao-module-member-biz/pom.xml @@ -33,6 +33,7 @@ yudao-module-infra-api ${revision} + cn.iocoder.boot yudao-module-pay-api diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java index a99bafe93..7e7342f6b 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.pay.api.wallet; import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletUpdateBalanceReqDTO; +// TODO @puhui999:不在 MemberUserController 提供接口,而是 PayWalletController 增加。不然 member 耦合 pay 拉。 /** * 会员钱包 API 接口 * diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java index 02a7756dd..f10de79c5 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.pay.api.wallet.dto; import jakarta.validation.constraints.NotNull; import lombok.Data; +// TODO @puhui999:不在 MemberUserController 提供接口,而是 PayWalletController 增加。不然 member 耦合 pay 拉。 /** * 钱包余额更新 Request DTO * diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java index 7892db543..ae99128b9 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java @@ -21,8 +21,6 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable { PAYMENT_REFUND(4, "支付退款"), UPDATE_BALANCE(5, "更新余额"); - // TODO 后续增加 - /** * 业务分类 */ diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java index 9f94e1a8f..247a6c936 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java @@ -13,6 +13,7 @@ import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.WALLET_NOT_FOUND; +// @puhui999:不在 MemberUserController 提供接口,而是 PayWalletController 增加。不然 member 耦合 pay 拉。 /** * 会员钱包 API 实现类 * From 5c3a960403cec9ff736ce285202ed7ddad94e747 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 19 Aug 2024 19:51:32 +0800 Subject: [PATCH 038/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91PAY=EF=BC=9A=E9=92=B1=E5=8C=85=E4=BD=99?= =?UTF-8?q?=E9=A2=9D=E4=BF=AE=E6=94=B9=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/pay/controller/admin/wallet/PayWalletController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java index 15e381538..273ea22f2 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java @@ -48,4 +48,6 @@ public class PayWalletController { return success(PayWalletConvert.INSTANCE.convertPage(pageResult)); } + // TODO @puhui999:修改钱包余额,权限标识,记得加下噢 + } From 91240351ed55fdf89a9b2e6c49f7b0274a98143a Mon Sep 17 00:00:00 2001 From: puhui999 Date: Tue, 20 Aug 2024 15:50:07 +0800 Subject: [PATCH 039/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E:=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=AF=84=E5=AE=A1=20TODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/spu/dto/ProductSpuRespDTO.java | 9 ++++ .../TradeDeliveryPriceCalculator.java | 22 +++++++--- .../yudao-module-member-biz/pom.xml | 6 --- .../admin/user/MemberUserController.java | 14 ------ .../module/pay/api/wallet/PayWalletApi.java | 20 --------- .../dto/PayWalletUpdateBalanceReqDTO.java | 24 ---------- .../pay/api/wallet/PayWalletApiImpl.java | 44 ------------------- .../admin/wallet/PayWalletController.java | 25 +++++++++-- .../wallet/PayWalletUpdateBalanceReqVO.java | 10 ++--- 9 files changed, 50 insertions(+), 124 deletions(-) delete mode 100644 yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java delete mode 100644 yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java delete mode 100644 yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java rename yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateBalanceReqVO.java => yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java (67%) diff --git a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java index 707ccc338..60fb0bcd1 100644 --- a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java +++ b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.product.api.spu.dto; import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum; import lombok.Data; +import java.util.List; + /** * 商品 SPU 信息 Response DTO * @@ -68,6 +70,13 @@ public class ProductSpuRespDTO { // ========== 物流相关字段 ========= + /** + * 配送方式数组 + * + * 对应 DeliveryTypeEnum 枚举 + */ + private List deliveryTypes; + /** * 物流配置模板编号 * diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java index d9fed7aeb..9ab0fbabc 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java @@ -6,6 +6,8 @@ import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.module.member.api.address.MemberAddressApi; import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO; import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryExpressChargeModeEnum; @@ -17,18 +19,19 @@ import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplate import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO.OrderItem; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import jakarta.annotation.Resource; import java.util.List; import java.util.Map; import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; -import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PICK_UP_STORE_NOT_EXISTS; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND; /** * 运费的 {@link TradePriceCalculator} 实现类 @@ -49,13 +52,20 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { private DeliveryExpressTemplateService deliveryExpressTemplateService; @Resource private TradeConfigService tradeConfigService; + @Resource + private ProductSpuApi productSpuApi; @Override public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { if (param.getDeliveryType() == null) { return; } - // TODO @puhui999:需要校验,是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈 + // 校验是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈 + List spuList = productSpuApi.getSpuList(convertSet(result.getItems(), OrderItem::getSpuId)); + if (anyMatch(spuList, item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) { + return; + } + if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) { calculateByPickUp(param); } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) { @@ -124,7 +134,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { Map> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId); // 依次计算快递运费 for (Map.Entry> entry : template2ItemMap.entrySet()) { - Long templateId = entry.getKey(); + Long templateId = entry.getKey(); List orderItems = entry.getValue(); DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId); if (templateBO == null) { @@ -144,8 +154,8 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { /** * 按配送方式来计算运费 * - * @param orderItems SKU 商品项目 - * @param chargeMode 配送计费方式 + * @param orderItems SKU 商品项目 + * @param chargeMode 配送计费方式 * @param templateCharge 快递运费配置 */ private void calculateExpressFeeByChargeMode(List orderItems, Integer chargeMode, diff --git a/yudao-module-member/yudao-module-member-biz/pom.xml b/yudao-module-member/yudao-module-member-biz/pom.xml index 165ab9f83..3c9b81e65 100644 --- a/yudao-module-member/yudao-module-member-biz/pom.xml +++ b/yudao-module-member/yudao-module-member-biz/pom.xml @@ -33,12 +33,6 @@ yudao-module-infra-api ${revision} - - - cn.iocoder.boot - yudao-module-pay-api - ${revision} - diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java index 09978f6e0..6a642bbf0 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.member.controller.admin.user; import cn.hutool.core.collection.CollUtil; 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.member.controller.admin.user.vo.*; import cn.iocoder.yudao.module.member.convert.user.MemberUserConvert; import cn.iocoder.yudao.module.member.dal.dataobject.group.MemberGroupDO; @@ -16,8 +15,6 @@ import cn.iocoder.yudao.module.member.service.level.MemberLevelService; import cn.iocoder.yudao.module.member.service.point.MemberPointRecordService; import cn.iocoder.yudao.module.member.service.tag.MemberTagService; import cn.iocoder.yudao.module.member.service.user.MemberUserService; -import cn.iocoder.yudao.module.pay.api.wallet.PayWalletApi; -import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletUpdateBalanceReqDTO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -53,8 +50,6 @@ public class MemberUserController { private MemberGroupService memberGroupService; @Resource private MemberPointRecordService memberPointRecordService; - @Resource - private PayWalletApi payWalletApi; @PutMapping("/update") @Operation(summary = "更新会员用户") @@ -81,15 +76,6 @@ public class MemberUserController { return success(true); } - @PutMapping("/update-balance") - @Operation(summary = "更新会员用户余额") - @PreAuthorize("@ss.hasPermission('member:user:update-balance')") - public CommonResult updateUserBalance(@Valid @RequestBody MemberUserUpdateBalanceReqVO updateReqVO) { - payWalletApi.updateBalance(BeanUtils.toBean(updateReqVO, PayWalletUpdateBalanceReqDTO.class) - .setUserId(updateReqVO.getId())); - return success(true); - } - @GetMapping("/get") @Operation(summary = "获得会员用户") @Parameter(name = "id", description = "编号", required = true, example = "1024") diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java deleted file mode 100644 index 7e7342f6b..000000000 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.yudao.module.pay.api.wallet; - -import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletUpdateBalanceReqDTO; - -// TODO @puhui999:不在 MemberUserController 提供接口,而是 PayWalletController 增加。不然 member 耦合 pay 拉。 -/** - * 会员钱包 API 接口 - * - * @author HUIHUI - */ -public interface PayWalletApi { - - /** - * 更新钱包余额 - * - * @param reqDTO 请求 - */ - void updateBalance(PayWalletUpdateBalanceReqDTO reqDTO); - -} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java deleted file mode 100644 index f10de79c5..000000000 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletUpdateBalanceReqDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.yudao.module.pay.api.wallet.dto; - -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -// TODO @puhui999:不在 MemberUserController 提供接口,而是 PayWalletController 增加。不然 member 耦合 pay 拉。 -/** - * 钱包余额更新 Request DTO - * - * @author HUIHUI - */ -@Data -public class PayWalletUpdateBalanceReqDTO { - - @NotNull(message = "用户编号不能为空") - private Long userId; - - /** - * 变动余额,正数为增加,负数为减少 - */ - @NotNull(message = "变动余额不能为空") - private Integer balance; - -} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java deleted file mode 100644 index 247a6c936..000000000 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.yudao.module.pay.api.wallet; - -import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletUpdateBalanceReqDTO; -import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO; -import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; -import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; - -import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.WALLET_NOT_FOUND; - -// @puhui999:不在 MemberUserController 提供接口,而是 PayWalletController 增加。不然 member 耦合 pay 拉。 -/** - * 会员钱包 API 实现类 - * - * @author HUIHUI - */ -@Service -@Validated -@Slf4j -public class PayWalletApiImpl implements PayWalletApi { - - @Resource - private PayWalletService payWalletService; - - @Override - public void updateBalance(PayWalletUpdateBalanceReqDTO reqDTO) { - // 获得用户钱包 - PayWalletDO wallet = payWalletService.getOrCreateWallet(reqDTO.getUserId(), MEMBER.getValue()); - if (wallet == null) { - log.error("[updateBalance],reqDTO({}) 用户钱包不存在.", reqDTO); - throw exception(WALLET_NOT_FOUND); - } - - // 更新钱包余额 - payWalletService.addWalletBalance(wallet.getId(), String.valueOf(reqDTO.getUserId()), - PayWalletBizTypeEnum.UPDATE_BALANCE, reqDTO.getBalance()); - } - -} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java index 273ea22f2..54fa00419 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java @@ -4,9 +4,11 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO; +import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUpdateBalanceReqVO; import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserReqVO; import cn.iocoder.yudao.module.pay.convert.wallet.PayWalletConvert; import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -15,12 +17,12 @@ import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.WALLET_NOT_FOUND; @Tag(name = "管理后台 - 用户钱包") @RestController @@ -48,6 +50,21 @@ public class PayWalletController { return success(PayWalletConvert.INSTANCE.convertPage(pageResult)); } - // TODO @puhui999:修改钱包余额,权限标识,记得加下噢 + @PutMapping("/update-balance") + @Operation(summary = "更新会员用户余额") + @PreAuthorize("@ss.hasPermission('pay:wallet:update-balance')") + public CommonResult updateWalletBalance(@Valid @RequestBody PayWalletUpdateBalanceReqVO updateReqVO) { + // 获得用户钱包 + PayWalletDO wallet = payWalletService.getOrCreateWallet(updateReqVO.getUserId(), MEMBER.getValue()); + if (wallet == null) { + log.error("[updateWalletBalance],updateReqVO({}) 用户钱包不存在.", updateReqVO); + throw exception(WALLET_NOT_FOUND); + } + + // 更新钱包余额 + payWalletService.addWalletBalance(wallet.getId(), String.valueOf(updateReqVO.getUserId()), + PayWalletBizTypeEnum.UPDATE_BALANCE, updateReqVO.getBalance()); + return success(true); + } } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateBalanceReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java similarity index 67% rename from yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateBalanceReqVO.java rename to yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java index fe694df67..7569bca78 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateBalanceReqVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java @@ -1,18 +1,16 @@ -package cn.iocoder.yudao.module.member.controller.admin.user.vo; +package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; -import lombok.ToString; -@Schema(description = "管理后台 - 用户修改余额 Request VO") +@Schema(description = "管理后台 - 修改钱包余额 Request VO") @Data -@ToString(callSuper = true) -public class MemberUserUpdateBalanceReqVO { +public class PayWalletUpdateBalanceReqVO { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788") @NotNull(message = "用户编号不能为空") - private Long id; + private Long userId; @Schema(description = "变动余额,正数为增加,负数为减少", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") @NotNull(message = "变动余额不能为空") From aff0446a3848df3e271a104bacdfb1fdfac57c3c Mon Sep 17 00:00:00 2001 From: puhui999 Date: Tue, 20 Aug 2024 16:12:24 +0800 Subject: [PATCH 040/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E:=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=8F=96=E6=B6=88=E6=94=AF=E4=BB=98=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trade/enums/ErrorCodeConstants.java | 1 + .../order/TradeOrderUpdateServiceImpl.java | 24 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java index 33081d461..696eeba1b 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java @@ -35,6 +35,7 @@ public interface ErrorCodeConstants { ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】"); ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态"); ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单"); + ErrorCode ORDER_CANCEL_PAID_FAIL = new ErrorCode(1_011_000_033, "交易订单取消支付失败,原因:订单不是【{}】状态"); // ========== After Sale 模块 1-011-000-100 ========== ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在"); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index 945e36fc5..3a8d65fc8 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -18,6 +18,8 @@ import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO; +import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi; import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO; @@ -111,6 +113,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { private ProductCommentApi productCommentApi; @Resource public SocialClientApi socialClientApi; + @Resource + public PayRefundApi payRefundApi; @Resource private TradeOrderProperties tradeOrderProperties; @@ -855,14 +859,28 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @Override @Transactional(rollbackFor = Exception.class) public void cancelPaidOrder(Long userId, Long orderId) { - // TODO @puhui999:需要校验状态;已支付的情况下,才可以。 + // 1.1 检验订单存在 TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); if (order == null) { throw exception(ORDER_NOT_FOUND); } - cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL); + // 1.2 校验订单是否支付 + if (!order.getPayStatus()) { + throw exception(ORDER_CANCEL_PAID_FAIL, "已支付"); + } + // 1.3 校验订单是否已退款 + if (ObjUtil.equal(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) { + throw exception(ORDER_CANCEL_PAID_FAIL, "未退款"); + } - // TODO @puhui999:需要退款 + // 2.1 取消订单 + cancelOrder0(order, TradeOrderCancelTypeEnum.AFTER_SALE_CLOSE); + // 2.2 创建退款单 + payRefundApi.createRefund(new PayRefundCreateReqDTO() + .setAppKey(tradeOrderProperties.getPayAppKey()).setUserIp(getClientIP()) // 支付应用 + .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 + .setMerchantRefundId(String.valueOf(order.getId())) + .setReason("取消支付订单").setPrice(order.getPayPrice()));// 价格信息 } /** From 2b681f90ca2cf4a06cc97234f7c20d6d2a80175c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 20 Aug 2024 21:18:56 +0800 Subject: [PATCH 041/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E6=8B=BC=E5=9B=A2?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E7=9A=84=E5=8F=96=E6=B6=88=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java | 1 - .../module/trade/service/order/TradeOrderUpdateServiceImpl.java | 1 + .../service/price/calculator/TradeDeliveryPriceCalculator.java | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java index 744a7b8fd..f36f7bc95 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java @@ -28,7 +28,6 @@ public interface TradeOrderApi { */ TradeOrderRespDTO getOrder(Long id); - // TODO 芋艿:需要优化下; /** * 取消支付订单 * diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index 3a8d65fc8..c005781e3 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -859,6 +859,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @Override @Transactional(rollbackFor = Exception.class) public void cancelPaidOrder(Long userId, Long orderId) { + // TODO @puhui999:可能要加一个拼团取消;TradeOrderCancelTypeEnum.AFTER_SALE_CLOSE;然后参数传入下; // 1.1 检验订单存在 TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); if (order == null) { diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java index 9ab0fbabc..2fa0d44af 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java @@ -60,6 +60,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { if (param.getDeliveryType() == null) { return; } + // TODO @puhui999:1)TradePriceCalculateRespBO 传递进来 delveryType 配送方式,减少读取;2)如果不匹配,抛出业务异常; = = 不然就不扣钱啦。 // 校验是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈 List spuList = productSpuApi.getSpuList(convertSet(result.getItems(), OrderItem::getSpuId)); if (anyMatch(spuList, item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) { From 69695b118996b48e09d4ca14d5258a4575cf6b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Wed, 21 Aug 2024 06:39:33 +0800 Subject: [PATCH 042/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E5=A4=84=E7=90=86=E7=BB=9F=E8=AE=A1=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E5=AF=BC=E8=87=B4=E7=9A=84=E7=A9=BA=E6=8C=87=E9=92=88?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/MemberStatisticsServiceImpl.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/member/MemberStatisticsServiceImpl.java b/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/member/MemberStatisticsServiceImpl.java index 6e3200b77..2257c8d39 100644 --- a/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/member/MemberStatisticsServiceImpl.java +++ b/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/member/MemberStatisticsServiceImpl.java @@ -69,9 +69,18 @@ public class MemberStatisticsServiceImpl implements MemberStatisticsService { bo -> AreaUtils.getParentIdByType(bo.getAreaId(), AreaTypeEnum.PROVINCE), bo -> bo, (a, b) -> new MemberAreaStatisticsRespBO() - .setOrderCreateUserCount(a.getOrderCreateUserCount() + b.getOrderCreateUserCount()) - .setOrderPayUserCount(a.getOrderPayUserCount() + b.getOrderPayUserCount()) - .setOrderPayPrice(a.getOrderPayPrice() + b.getOrderPayPrice())); + .setOrderCreateUserCount( + (a.getOrderCreateUserCount() != null ? a.getOrderCreateUserCount() : 0) + + (b.getOrderCreateUserCount() != null ? b.getOrderCreateUserCount() : 0) + ) + .setOrderPayUserCount( + (a.getOrderPayUserCount() != null ? a.getOrderPayUserCount() : 0) + + (b.getOrderPayUserCount() != null ? b.getOrderPayUserCount() : 0) + ) + .setOrderPayPrice( + (a.getOrderPayPrice() != null ? a.getOrderPayPrice() : 0.0) + + (b.getOrderPayPrice() != null ? b.getOrderPayPrice() : 0.0) + ) // 拼接数据 List areaList = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area); areaList.add(new Area().setId(null).setName("未知")); From 9b2ec3d341689c83c4c4ec9585713e293a1ff4c1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 21 Aug 2024 21:54:12 +0800 Subject: [PATCH 043/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E5=A4=84=E7=90=86=E7=BB=9F=E8=AE=A1=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E5=AF=BC=E8=87=B4=E7=9A=84=E7=A9=BA=E6=8C=87=E9=92=88?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/MemberStatisticsServiceImpl.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/member/MemberStatisticsServiceImpl.java b/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/member/MemberStatisticsServiceImpl.java index 2257c8d39..15e46ff18 100644 --- a/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/member/MemberStatisticsServiceImpl.java +++ b/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/member/MemberStatisticsServiceImpl.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.statistics.service.member; import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.ip.core.Area; import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum; @@ -15,10 +16,10 @@ import cn.iocoder.yudao.module.statistics.service.pay.PayWalletStatisticsService import cn.iocoder.yudao.module.statistics.service.pay.bo.RechargeSummaryRespBO; import cn.iocoder.yudao.module.statistics.service.trade.TradeOrderStatisticsService; import cn.iocoder.yudao.module.statistics.service.trade.TradeStatisticsService; +import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import jakarta.annotation.Resource; import java.time.Duration; import java.time.LocalDateTime; import java.util.List; @@ -69,18 +70,12 @@ public class MemberStatisticsServiceImpl implements MemberStatisticsService { bo -> AreaUtils.getParentIdByType(bo.getAreaId(), AreaTypeEnum.PROVINCE), bo -> bo, (a, b) -> new MemberAreaStatisticsRespBO() - .setOrderCreateUserCount( - (a.getOrderCreateUserCount() != null ? a.getOrderCreateUserCount() : 0) + - (b.getOrderCreateUserCount() != null ? b.getOrderCreateUserCount() : 0) - ) - .setOrderPayUserCount( - (a.getOrderPayUserCount() != null ? a.getOrderPayUserCount() : 0) + - (b.getOrderPayUserCount() != null ? b.getOrderPayUserCount() : 0) - ) - .setOrderPayPrice( - (a.getOrderPayPrice() != null ? a.getOrderPayPrice() : 0.0) + - (b.getOrderPayPrice() != null ? b.getOrderPayPrice() : 0.0) - ) + .setOrderCreateUserCount(ObjectUtil.defaultIfNull(a.getOrderCreateUserCount(), 0) + + ObjectUtil.defaultIfNull(b.getOrderCreateUserCount(), 0)) + .setOrderPayUserCount(ObjectUtil.defaultIfNull(a.getOrderPayUserCount(), 0) + + ObjectUtil.defaultIfNull(b.getOrderPayUserCount(), 0)) + .setOrderPayPrice(ObjectUtil.defaultIfNull(a.getOrderPayPrice(), 0) + + ObjectUtil.defaultIfNull(b.getOrderPayPrice(), 0))); // 拼接数据 List areaList = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area); areaList.add(new Area().setId(null).setName("未知")); From 96bffc41997a2b33502e3ca7b2cc26cca1be6640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Thu, 22 Aug 2024 14:47:32 +0800 Subject: [PATCH 044/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E5=BF=AB=E9=80=92=E9=B8=9F=E9=A1=BA=E4=B8=B0?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E9=9C=80=E8=A6=81CustomerName?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../delivery/core/client/dto/ExpressTrackQueryReqDTO.java | 5 +++++ .../core/client/dto/kdniao/KdNiaoExpressQueryReqDTO.java | 6 ++++++ .../trade/service/order/TradeOrderQueryServiceImpl.java | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java index 34ad0128d..16662d89d 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java @@ -28,4 +28,9 @@ public class ExpressTrackQueryReqDTO { */ private String phone; + /** + * 自定义名称(顺丰专用) + */ + private String customerName; + } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryReqDTO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryReqDTO.java index bcb6e3353..049dcd6f0 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryReqDTO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryReqDTO.java @@ -29,4 +29,10 @@ public class KdNiaoExpressQueryReqDTO { @JsonProperty("OrderCode") private String orderNo; + /** + * 自定义名称(顺丰专用) + */ + @JsonProperty("CustomerName") + private String customerName; + } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java index af8e9e139..e96f79a72 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java @@ -219,7 +219,7 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService { public List getExpressTrackList(String code, String logisticsNo, String receiverMobile) { return expressClientFactory.getDefaultExpressClient().getExpressTrackList( new ExpressTrackQueryReqDTO().setExpressCode(code).setLogisticsNo(logisticsNo) - .setPhone(receiverMobile)); + .setPhone(receiverMobile).setCustomerName(StrUtil.subSuf(receiverMobile, receiverMobile.length() - 4))); } // =================== Order Item =================== From 32e25cf4a24dd855a741832d98677863dff2091e Mon Sep 17 00:00:00 2001 From: puhui999 Date: Thu, 22 Aug 2024 18:23:54 +0800 Subject: [PATCH 045/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E:=20=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E6=B4=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/PromotionProductScopeEnum.java | 7 ++- .../admin/reward/vo/RewardActivityBaseVO.java | 40 +++++++++++----- .../app/activity/AppActivityController.java | 47 +++++++++---------- .../dataobject/reward/RewardActivityDO.java | 10 +++- .../reward/RewardActivityServiceImpl.java | 43 +++++++++++++---- .../aftersale/vo/AfterSalePageReqVO.java | 3 -- .../dal/mysql/aftersale/AfterSaleMapper.java | 1 - 7 files changed, 96 insertions(+), 55 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java index 882dc4aee..98e2ac7c9 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java @@ -15,10 +15,9 @@ import java.util.Arrays; @AllArgsConstructor public enum PromotionProductScopeEnum implements IntArrayValuable { - ALL(1, "通用券"), // 全部商品 - SPU(2, "商品券"), // 指定商品 - CATEGORY(3, "品类券"), // 指定品类 - ; + ALL(1, "全部商品"), + SPU(2, "指定商品"), + CATEGORY(3, "指定品类"); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionProductScopeEnum::getScope).toArray(); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java index ae7a9f0bd..c6bb4eae1 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java @@ -1,23 +1,22 @@ package cn.iocoder.yudao.module.promotion.controller.admin.reward.vo; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import org.springframework.format.annotation.DateTimeFormat; - import jakarta.validation.Valid; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.Future; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; +import lombok.Data; + import java.time.LocalDateTime; import java.util.List; - -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import java.util.Objects; /** * 满减送活动 Base VO,提供给添加、修改、详细的子 VO 使用 @@ -32,12 +31,10 @@ public class RewardActivityBaseVO { @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "开始时间不能为空") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime startTime; @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "结束时间不能为空") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Future(message = "结束时间必须大于当前时间") private LocalDateTime endTime; @@ -54,8 +51,8 @@ public class RewardActivityBaseVO { @InEnum(value = PromotionProductScopeEnum.class, message = "商品范围必须是 {value}") private Integer productScope; - @Schema(description = "商品 SPU 编号的数组", example = "1,2,3") - private List productSpuIds; + @Schema(description = "商品范围编号的数组", example = "[1, 3]") + private List productScopeValues; /** * 优惠规则的数组 @@ -63,6 +60,13 @@ public class RewardActivityBaseVO { @Valid // 校验下子对象 private List rules; + @AssertTrue(message = "商品范围编号的数组不能为空") + @JsonIgnore + public boolean isProductScopeValuesValid() { + return Objects.equals(productScope, PromotionProductScopeEnum.ALL.getScope()) // 全部范围时,可以为空 + || CollUtil.isNotEmpty(productScopeValues); + } + @Schema(description = "优惠规则") @Data public static class Rule { @@ -76,12 +80,20 @@ public class RewardActivityBaseVO { private Integer discountPrice; @Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "规则是否包邮不能为空") private Boolean freeDelivery; + @Schema(description = "是否赠送积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "规则是否赠送积分不能为空") + private Boolean givePoint; + @Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") - @Min(value = 1L, message = "赠送的积分必须大于等于 1") private Integer point; + @Schema(description = "是否赠送优惠券", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "规则是否赠送优惠券不能为空") + private Boolean giveCoupon; + @Schema(description = "赠送的优惠劵编号的数组", example = "1,2,3") private List couponIds; @@ -91,7 +103,13 @@ public class RewardActivityBaseVO { @AssertTrue(message = "优惠劵和数量必须一一对应") @JsonIgnore public boolean isCouponCountsValid() { - return CollUtil.size(couponCounts) == CollUtil.size(couponCounts); + return BooleanUtil.isFalse(givePoint) || CollUtil.size(couponIds) == CollUtil.size(couponCounts); + } + + @AssertTrue(message = "赠送的积分不能小于 1") + @JsonIgnore + public boolean isPointValid() { + return BooleanUtil.isFalse(givePoint) || (point != null && point >= 1); } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java index 4ec685aab..a59ff7df1 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java @@ -9,9 +9,7 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityD import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO; -import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO; -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; import cn.iocoder.yudao.module.promotion.service.bargain.BargainActivityService; import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService; @@ -30,7 +28,6 @@ import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; import java.util.*; -import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; @@ -145,28 +142,28 @@ public class AppActivityController { } private void getRewardActivities(Collection spuIds, LocalDateTime now, List activityList) { - // TODO @puhui999:有 3 范围,不只 spuId,还有 categoryId,全部 - List rewardActivityList = rewardActivityService.getRewardActivityBySpuIdsAndStatusAndDateTimeLt( - spuIds, PromotionActivityStatusEnum.RUN.getStatus(), now); - if (CollUtil.isEmpty(rewardActivityList)) { - return; - } - - Map> spuIdAndActivityMap = spuIds.stream() - .collect(Collectors.toMap( - spuId -> spuId, - spuId -> rewardActivityList.stream() - .filter(activity -> activity.getProductSpuIds().contains(spuId)) - .max(Comparator.comparing(RewardActivityDO::getCreateTime)))); - for (Long supId : spuIdAndActivityMap.keySet()) { - if (spuIdAndActivityMap.get(supId).isEmpty()) { - continue; - } - - RewardActivityDO rewardActivityDO = spuIdAndActivityMap.get(supId).get(); - activityList.add(new AppActivityRespVO(rewardActivityDO.getId(), PromotionTypeEnum.REWARD_ACTIVITY.getType(), - rewardActivityDO.getName(), supId, rewardActivityDO.getStartTime(), rewardActivityDO.getEndTime())); - } + // TODO @puhui999:有 3 范围,不只 spuId,还有 categoryId,全部,下次 fix + //List rewardActivityList = rewardActivityService.getRewardActivityBySpuIdsAndStatusAndDateTimeLt( + // spuIds, PromotionActivityStatusEnum.RUN.getStatus(), now); + //if (CollUtil.isEmpty(rewardActivityList)) { + // return; + //} + // + //Map> spuIdAndActivityMap = spuIds.stream() + // .collect(Collectors.toMap( + // spuId -> spuId, + // spuId -> rewardActivityList.stream() + // .filter(activity -> activity.getProductSpuIds().contains(spuId)) + // .max(Comparator.comparing(RewardActivityDO::getCreateTime)))); + //for (Long supId : spuIdAndActivityMap.keySet()) { + // if (spuIdAndActivityMap.get(supId).isEmpty()) { + // continue; + // } + // + // RewardActivityDO rewardActivityDO = spuIdAndActivityMap.get(supId).get(); + // activityList.add(new AppActivityRespVO(rewardActivityDO.getId(), PromotionTypeEnum.REWARD_ACTIVITY.getType(), + // rewardActivityDO.getName(), supId, rewardActivityDO.getStartTime(), rewardActivityDO.getEndTime())); + //} } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java index d94533e8c..507225ffd 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java @@ -71,7 +71,7 @@ public class RewardActivityDO extends BaseDO { * 商品 SPU 编号的数组 */ @TableField(typeHandler = LongListTypeHandler.class) - private List productSpuIds; + private List productScopeValues; /** * 优惠规则的数组 */ @@ -99,10 +99,18 @@ public class RewardActivityDO extends BaseDO { * 是否包邮 */ private Boolean freeDelivery; + /** + * 是否赠送积分 + */ + private Boolean givePoint; /** * 赠送的积分 */ private Integer point; + /** + * 是否赠送优惠券 + */ + private Boolean giveCoupon; /** * 赠送的优惠劵编号的数组 */ diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java index e896eab92..855d72c34 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java @@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.promotion.service.reward; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi; +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; @@ -10,6 +12,7 @@ import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert; import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper; import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import cn.iocoder.yudao.module.promotion.util.PromotionUtils; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; @@ -19,6 +22,7 @@ import java.time.LocalDateTime; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; @@ -37,12 +41,19 @@ public class RewardActivityServiceImpl implements RewardActivityService { @Resource private RewardActivityMapper rewardActivityMapper; + @Resource + private ProductCategoryApi productCategoryApi; + @Resource + private ProductSpuApi productSpuApi; + @Override public Long createRewardActivity(RewardActivityCreateReqVO createReqVO) { - // 校验商品是否冲突 - validateRewardActivitySpuConflicts(null, createReqVO.getProductSpuIds()); + // 1.1 校验商品范围 + validateProductScope(createReqVO.getProductScope(), createReqVO.getProductScopeValues()); + // 1.2 校验商品是否冲突 + //validateRewardActivitySpuConflicts(null, createReqVO.getProductSpuIds()); - // 插入 + // 2. 插入 RewardActivityDO rewardActivity = RewardActivityConvert.INSTANCE.convert(createReqVO) .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime())); rewardActivityMapper.insert(rewardActivity); @@ -52,15 +63,17 @@ public class RewardActivityServiceImpl implements RewardActivityService { @Override public void updateRewardActivity(RewardActivityUpdateReqVO updateReqVO) { - // 校验存在 + // 1.1 校验存在 RewardActivityDO dbRewardActivity = validateRewardActivityExists(updateReqVO.getId()); if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能修改噢 throw exception(REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED); } - // 校验商品是否冲突 - validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO.getProductSpuIds()); + // 1.2 校验商品范围 + validateProductScope(updateReqVO.getProductScope(), updateReqVO.getProductScopeValues()); + // 1.3 校验商品是否冲突 + //validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO.getProductSpuIds()); - // 更新 + // 2. 更新 RewardActivityDO updateObj = RewardActivityConvert.INSTANCE.convert(updateReqVO) .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime())); rewardActivityMapper.updateById(updateObj); @@ -103,7 +116,7 @@ public class RewardActivityServiceImpl implements RewardActivityService { } // TODO @芋艿:逻辑有问题,需要优化;要分成全场、和指定来校验; - + // TODO @puhui999: 下次提交 fix /** * 校验商品参加的活动是否冲突 * @@ -126,6 +139,14 @@ public class RewardActivityServiceImpl implements RewardActivityService { } } + private void validateProductScope(Integer productScope, List productScopeValues) { + if (Objects.equals(PromotionProductScopeEnum.SPU.getScope(), productScope)) { + productSpuApi.validateSpuList(productScopeValues); + } else if (Objects.equals(PromotionProductScopeEnum.CATEGORY.getScope(), productScope)) { + productCategoryApi.validateCategoryList(productScopeValues); + } + } + /** * 获得商品参加的满减送活动的数组 * @@ -135,8 +156,10 @@ public class RewardActivityServiceImpl implements RewardActivityService { */ private List getRewardActivityListBySpuIds(Collection spuIds, Collection statuses) { - List list = rewardActivityMapper.selectListByStatus(statuses); - return CollUtil.filter(list, activity -> CollUtil.containsAny(activity.getProductSpuIds(), spuIds)); + // TODO @puhui999: 下次 fix + //List list = rewardActivityMapper.selectListByStatus(statuses); + //return CollUtil.filter(list, activity -> CollUtil.containsAny(activity.getProductSpuIds(), spuIds)); + return List.of(); } @Override diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java index 119370ace..4b8756c7b 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java @@ -27,9 +27,6 @@ public class AfterSalePageReqVO extends PageParam { @Schema(description = "售后流水号", example = "202211190847450020500077") private String no; - @Schema(description = "用户编号", example = "1024") - private Long userId; - @Schema(description = "售后状态", example = "10") @InEnum(value = AfterSaleStatusEnum.class, message = "售后状态必须是 {value}") private Integer status; diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java index 846bb31f9..341dabc45 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java @@ -18,7 +18,6 @@ public interface AfterSaleMapper extends BaseMapperX { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(AfterSaleDO::getUserId, reqVO.getUserId()) .likeIfPresent(AfterSaleDO::getNo, reqVO.getNo()) - .eqIfPresent(AfterSaleDO::getUserId, reqVO.getUserId()) .eqIfPresent(AfterSaleDO::getStatus, reqVO.getStatus()) .eqIfPresent(AfterSaleDO::getType, reqVO.getType()) .eqIfPresent(AfterSaleDO::getWay, reqVO.getWay()) From 8de0de615f5fe5bb0617d950fb6e57f650efdcf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Fri, 23 Aug 2024 15:23:51 +0800 Subject: [PATCH 046/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=94=AE=E5=90=8E=E9=80=80=E8=B4=A7=E9=80=80?= =?UTF-8?q?=E6=AC=BE=E6=97=B6=E9=A6=96=E5=85=88=E8=BF=94=E8=BF=98=E6=8A=B5?= =?UTF-8?q?=E6=89=A3=E7=A7=AF=E5=88=86=EF=BC=8C=E7=84=B6=E5=90=8E=E5=86=8D?= =?UTF-8?q?=E6=89=A3=E5=87=8F=E8=B5=A0=E9=80=81=E7=A7=AF=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/handler/TradeMemberPointOrderHandler.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeMemberPointOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeMemberPointOrderHandler.java index db41eb9de..8f99a987e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeMemberPointOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeMemberPointOrderHandler.java @@ -78,17 +78,15 @@ public class TradeMemberPointOrderHandler implements TradeOrderHandler { @Override public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) { - // 扣减(回滚)积分(订单赠送) - reducePoint(order.getUserId(), orderItem.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE_CANCEL_ITEM, - orderItem.getId()); // 增加(回滚)积分(订单抵扣) - addPoint(order.getUserId(), orderItem.getUsePoint(), MemberPointBizTypeEnum.ORDER_USE_CANCEL_ITEM, - orderItem.getId()); + addPoint(order.getUserId(), orderItem.getUsePoint(), MemberPointBizTypeEnum.ORDER_USE_CANCEL_ITEM, orderItem.getId()); + + // 扣减(回滚)积分(订单赠送) + reducePoint(order.getUserId(), orderItem.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE_CANCEL_ITEM, orderItem.getId()); // 扣减(回滚)用户经验 AfterSaleDO afterSale = afterSaleService.getAfterSale(orderItem.getAfterSaleId()); - memberLevelApi.reduceExperience(order.getUserId(), afterSale.getRefundPrice(), - MemberExperienceBizTypeEnum.ORDER_GIVE_CANCEL_ITEM.getType(), String.valueOf(orderItem.getId())); + memberLevelApi.reduceExperience(order.getUserId(), afterSale.getRefundPrice(), MemberExperienceBizTypeEnum.ORDER_GIVE_CANCEL_ITEM.getType(), String.valueOf(orderItem.getId())); } /** From 76f0bd816c93282f29faea3360e11fd7eafdba55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Fri, 23 Aug 2024 15:24:41 +0800 Subject: [PATCH 047/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E4=BF=AE=E5=A4=8D=E9=80=80=E6=AC=BE=E6=97=B6?= =?UTF-8?q?=E5=BA=94=E7=94=A8AppKey=E4=B8=BA=E7=A9=BA=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/module/trade/convert/aftersale/AfterSaleConvert.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java index fd759c625..086cb6370 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java @@ -43,7 +43,8 @@ public interface AfterSaleConvert { @Mapping(source = "afterSale.orderId", target = "merchantOrderId"), @Mapping(source = "afterSale.id", target = "merchantRefundId"), @Mapping(source = "afterSale.applyReason", target = "reason"), - @Mapping(source = "afterSale.refundPrice", target = "price") + @Mapping(source = "afterSale.refundPrice", target = "price"), + @Mapping(source = "orderProperties.payAppKey", target = "appKey") }) PayRefundCreateReqDTO convert(String userIp, AfterSaleDO afterSale, TradeOrderProperties orderProperties); From 94effa87a302dd04f12356b2632c7df3147056a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Fri, 23 Aug 2024 16:32:25 +0800 Subject: [PATCH 048/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E8=AE=A2=E5=8D=95=E6=97=A5=E5=BF=97=E6=A0=B9?= =?UTF-8?q?=E6=8D=AE=E5=88=9B=E5=BB=BA=E6=97=B6=E9=97=B4=E5=80=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trade/dal/mysql/aftersale/AfterSaleLogMapper.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java index c0ec91c6d..5a71ed812 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.dal.mysql.aftersale; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleLogDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; import java.util.List; @@ -10,7 +11,10 @@ import java.util.List; public interface AfterSaleLogMapper extends BaseMapperX { default List selectListByAfterSaleId(Long afterSaleId) { - return selectList(AfterSaleLogDO::getAfterSaleId, afterSaleId); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(AfterSaleLogDO::getAfterSaleId, afterSaleId); + queryWrapper.orderByDesc(AfterSaleLogDO::getCreateTime); + return selectList(queryWrapper); } } From 427cf8e4f6dfc63913e3830efd56dfd8683da28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Fri, 23 Aug 2024 16:42:06 +0800 Subject: [PATCH 049/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E8=AE=A2=E5=8D=95=E6=97=A5=E5=BF=97=E6=A0=B9?= =?UTF-8?q?=E6=8D=AE=E5=88=9B=E5=BB=BA=E6=97=B6=E9=97=B4=E5=80=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/trade/dal/mysql/order/TradeOrderLogMapper.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderLogMapper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderLogMapper.java index 7788030ff..94d693f69 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderLogMapper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderLogMapper.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.dal.mysql.order; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderLogDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; import java.util.List; @@ -10,7 +11,10 @@ import java.util.List; public interface TradeOrderLogMapper extends BaseMapperX { default List selectListByOrderId(Long orderId) { - return selectList(TradeOrderLogDO::getOrderId, orderId); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TradeOrderLogDO::getOrderId, orderId); + queryWrapper.orderByDesc(TradeOrderLogDO::getCreateTime); + return selectList(queryWrapper); } } From e36671255a9f0c0f6f71691a75974cbdd11d9327 Mon Sep 17 00:00:00 2001 From: "LAPTOP-00JMG2HE\\George Wei" Date: Fri, 23 Aug 2024 18:41:28 +0800 Subject: [PATCH 050/136] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=89=A7=E8=A1=8Cmvn?= =?UTF-8?q?=20test=E6=97=B6=E5=A4=9A=E4=B8=AATestCase=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=E3=80=82=E5=A4=B1=E8=B4=A5=E5=8E=9F?= =?UTF-8?q?=E5=9B=A0=EF=BC=9A1)=E4=BD=BF=E7=94=A8=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E6=AF=94=E8=BE=83POJO=E5=AF=B9=E8=B1=A1=E6=97=B6=E6=9C=AA?= =?UTF-8?q?=E5=BF=BD=E7=95=A5expiresTime=E3=80=81createTime=E3=80=81update?= =?UTF-8?q?Time=E5=B1=9E=E6=80=A7=EF=BC=9B2)=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=95=B0=E6=8D=AE=E5=BA=93=E6=97=B6=E6=9C=AA?= =?UTF-8?q?=E4=BB=A5UTF8=E7=BC=96=E7=A0=81=E8=AF=BB=E5=8F=96=E8=84=9A?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/job/JobLogServiceImplTest.java | 2 +- .../logger/ApiAccessLogServiceImplTest.java | 2 +- .../logger/ApiErrorLogServiceImplTest.java | 2 +- .../test/resources/application-unit-test.yaml | 1 + .../oauth2/OAuth2ApproveServiceImplTest.java | 2 +- .../oauth2/OAuth2CodeServiceImplTest.java | 4 ++-- .../oauth2/OAuth2TokenServiceImplTest.java | 18 +++++++++--------- 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImplTest.java index d3342eeec..0ef0f6692 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImplTest.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImplTest.java @@ -114,7 +114,7 @@ public class JobLogServiceImplTest extends BaseDbUnitTest { assertEquals(1, count); List logs = jobLogMapper.selectList(); assertEquals(1, logs.size()); - assertEquals(log02, logs.get(0)); + assertPojoEquals(log02, logs.get(0), "createTime", "updateTime"); } @Test diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImplTest.java index 660f3d38f..7ad09e19b 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImplTest.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImplTest.java @@ -91,7 +91,7 @@ public class ApiAccessLogServiceImplTest extends BaseDbUnitTest { assertEquals(1, count); List logs = apiAccessLogMapper.selectList(); assertEquals(1, logs.size()); - assertEquals(log02, logs.get(0)); + assertPojoEquals(log02, logs.get(0), "createTime", "updateTime"); } @Test diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImplTest.java index 66514e0d4..0a3c20994 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImplTest.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImplTest.java @@ -157,7 +157,7 @@ public class ApiErrorLogServiceImplTest extends BaseDbUnitTest { assertEquals(1, count); List logs = apiErrorLogMapper.selectList(); assertEquals(1, logs.size()); - assertEquals(log02, logs.get(0)); + assertPojoEquals(log02, logs.get(0), "createTime", "updateTime"); } } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/resources/application-unit-test.yaml b/yudao-module-infra/yudao-module-infra-biz/src/test/resources/application-unit-test.yaml index d88a15a60..5ce2d4b5f 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/test/resources/application-unit-test.yaml +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/resources/application-unit-test.yaml @@ -19,6 +19,7 @@ spring: sql: init: schema-locations: classpath:/sql/create_tables.sql + encoding: UTF-8 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java index 91a96769d..05432275c 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java @@ -209,7 +209,7 @@ public class OAuth2ApproveServiceImplTest extends BaseDbUnitTest { List result = oauth2ApproveService.getApproveList(userId, userType, clientId); // 断言 assertEquals(1, result.size()); - assertPojoEquals(approve, result.get(0)); + assertPojoEquals(approve, result.get(0), "expiresTime"); } @Test diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java index 2601ffc97..c052576f1 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java @@ -50,7 +50,7 @@ class OAuth2CodeServiceImplTest extends BaseDbUnitTest { scopes, redirectUri, state); // 断言 OAuth2CodeDO dbCodeDO = oauth2CodeMapper.selectByCode(codeDO.getCode()); - assertPojoEquals(codeDO, dbCodeDO, "createTime", "updateTime", "deleted"); + assertPojoEquals(codeDO, dbCodeDO, "expiresTime", "createTime", "updateTime", "deleted"); assertEquals(userId, codeDO.getUserId()); assertEquals(userType, codeDO.getUserType()); assertEquals(clientId, codeDO.getClientId()); @@ -92,7 +92,7 @@ class OAuth2CodeServiceImplTest extends BaseDbUnitTest { // 调用 OAuth2CodeDO result = oauth2CodeService.consumeAuthorizationCode(code); - assertPojoEquals(codeDO, result); + assertPojoEquals(codeDO, result, "expiresTime"); assertNull(oauth2CodeMapper.selectByCode(code)); } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java index 8f2f63cae..a3070648f 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java @@ -77,7 +77,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, userType, clientId, scopes); // 断言访问令牌 OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken()); - assertPojoEquals(accessTokenDO, dbAccessTokenDO, "createTime", "updateTime", "deleted"); + assertPojoEquals(accessTokenDO, dbAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted"); assertEquals(userId, accessTokenDO.getUserId()); assertEquals(userType, accessTokenDO.getUserType()); assertEquals(2, accessTokenDO.getUserInfo().size()); @@ -88,7 +88,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { assertFalse(DateUtils.isExpired(accessTokenDO.getExpiresTime())); // 断言访问令牌的缓存 OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken()); - assertPojoEquals(accessTokenDO, redisAccessTokenDO, "createTime", "updateTime", "deleted"); + assertPojoEquals(accessTokenDO, redisAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted"); // 断言刷新令牌 OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectList().get(0); assertPojoEquals(accessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted"); @@ -177,13 +177,13 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { assertNull(oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken())); // 断言,新的访问令牌 OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(newAccessTokenDO.getAccessToken()); - assertPojoEquals(newAccessTokenDO, dbAccessTokenDO, "createTime", "updateTime", "deleted"); + assertPojoEquals(newAccessTokenDO, dbAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted"); assertPojoEquals(newAccessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater"); assertFalse(DateUtils.isExpired(newAccessTokenDO.getExpiresTime())); // 断言,新的访问令牌的缓存 OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(newAccessTokenDO.getAccessToken()); - assertPojoEquals(newAccessTokenDO, redisAccessTokenDO, "createTime", "updateTime", "deleted"); + assertPojoEquals(newAccessTokenDO, redisAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted"); } @Test @@ -198,9 +198,9 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { // 调用 OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken); // 断言 - assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted", + assertPojoEquals(accessTokenDO, result, "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater"); - assertPojoEquals(accessTokenDO, oauth2AccessTokenRedisDAO.get(accessToken), "createTime", "updateTime", "deleted", + assertPojoEquals(accessTokenDO, oauth2AccessTokenRedisDAO.get(accessToken), "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater"); } @@ -237,7 +237,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { // 调研,并断言 OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken); // 断言 - assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted", + assertPojoEquals(accessTokenDO, result, "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater"); } @@ -259,7 +259,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { oauth2RefreshTokenMapper.insert(refreshTokenDO); // 调用 OAuth2AccessTokenDO result = oauth2TokenService.removeAccessToken(accessTokenDO.getAccessToken()); - assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted", + assertPojoEquals(accessTokenDO, result, "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater"); // 断言数据 assertNull(oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken())); @@ -297,7 +297,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); - assertPojoEquals(dbAccessToken, pageResult.getList().get(0)); + assertPojoEquals(dbAccessToken, pageResult.getList().get(0), "expiresTime"); } } From f5706972a09c1f1aa47952144734ed3c63677740 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 23 Aug 2024 19:08:10 +0800 Subject: [PATCH 051/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E6=B4=BB=E5=8A=A8=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/reward/vo/RewardActivityBaseVO.java | 14 +++++++------- .../dal/dataobject/reward/RewardActivityDO.java | 2 ++ .../service/reward/RewardActivityServiceImpl.java | 1 + .../reward/RewardActivityServiceImplTest.java | 1 + 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java index c6bb4eae1..d498b5e9f 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java @@ -60,13 +60,6 @@ public class RewardActivityBaseVO { @Valid // 校验下子对象 private List rules; - @AssertTrue(message = "商品范围编号的数组不能为空") - @JsonIgnore - public boolean isProductScopeValuesValid() { - return Objects.equals(productScope, PromotionProductScopeEnum.ALL.getScope()) // 全部范围时,可以为空 - || CollUtil.isNotEmpty(productScopeValues); - } - @Schema(description = "优惠规则") @Data public static class Rule { @@ -114,4 +107,11 @@ public class RewardActivityBaseVO { } + @AssertTrue(message = "商品范围编号的数组不能为空") + @JsonIgnore + public boolean isProductScopeValuesValid() { + return Objects.equals(productScope, PromotionProductScopeEnum.ALL.getScope()) // 全部范围时,可以为空 + || CollUtil.isNotEmpty(productScopeValues); + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java index 507225ffd..98d3e8d81 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java @@ -99,6 +99,7 @@ public class RewardActivityDO extends BaseDO { * 是否包邮 */ private Boolean freeDelivery; + // TODO @puhui999:是不是大于零,就认为赠送积分哈;简洁一点; /** * 是否赠送积分 */ @@ -107,6 +108,7 @@ public class RewardActivityDO extends BaseDO { * 赠送的积分 */ private Integer point; + // TODO @puhui999:非空,就认为赠送优惠劵 /** * 是否赠送优惠券 */ diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java index 855d72c34..98fa990c1 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java @@ -82,6 +82,7 @@ public class RewardActivityServiceImpl implements RewardActivityService { @Override public void closeRewardActivity(Long id) { // 校验存在 + // TODO @puhui999:去掉 PromotionActivityStatusEnum,使用 CommonStatus 作为状态哈。开启,关闭 RewardActivityDO dbRewardActivity = validateRewardActivityExists(id); if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能关闭噢 throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java b/yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java index f2297abf6..ca8d85fa7 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java @@ -190,6 +190,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { @Test public void testGetRewardActivities_product() { // mock 数据 + // TODO @puhui999:有单测的问题,也一起瞅瞅 RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L))); rewardActivityMapper.insert(productActivity01); From 4e1d7d0877b84eb50de4227023d4b59576287f18 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 23 Aug 2024 19:49:40 +0800 Subject: [PATCH 052/136] =?UTF-8?q?=E3=80=90=E4=BC=98=E5=8C=96=E3=80=91?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90=EF=BC=9A=E9=92=88=E5=AF=B9?= =?UTF-8?q?=20element-plus=20=E7=9A=84=20checkbox=E3=80=81radio=20?= =?UTF-8?q?=E7=9A=84=20label=20=3D>=20value=20=E7=9A=84=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vue3/views/components/form_sub_erp.vue.vm | 11 +++++----- .../views/components/form_sub_normal.vue.vm | 22 +++++++++---------- .../resources/codegen/vue3/views/form.vue.vm | 11 +++++----- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm index 3996a9caa..81cd9775e 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm @@ -64,12 +64,11 @@ - {{ dict.label }} - + :label="dict.label" + :value="dict.value" + /> #else##没数据字典 - 请选择字典生成 + #end @@ -85,7 +84,7 @@ {{ dict.label }} #else##没数据字典 - 请选择字典生成 + 请选择字典生成 #end diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm index dbd03569e..3fa1effb2 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm @@ -92,12 +92,11 @@ - {{ dict.label }} - + :label="dict.label" + :value="dict.value" + /> #else##没数据字典 - 请选择字典生成 + #end @@ -117,7 +116,7 @@ {{ dict.label }} #else##没数据字典 - 请选择字典生成 + 请选择字典生成 #end @@ -219,12 +218,11 @@ - {{ dict.label }} - + :label="dict.label" + :value="dict.value" + /> #else##没数据字典 - 请选择字典生成 + #end @@ -240,7 +238,7 @@ {{ dict.label }} #else##没数据字典 - 请选择字典生成 + 请选择字典生成 #end diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm index 8e3596b4f..e37474b85 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm @@ -75,12 +75,11 @@ - {{ dict.label }} - + :label="dict.label" + :value="dict.value" + /> #else##没数据字典 - 请选择字典生成 + #end @@ -96,7 +95,7 @@ {{ dict.label }} #else##没数据字典 - 请选择字典生成 + 请选择字典生成 #end From 02562cb77d46aa8cbfaac7e32066419341bcd209 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 23 Aug 2024 20:01:32 +0800 Subject: [PATCH 053/136] =?UTF-8?q?bugfix:=20S3=20=E5=AE=A2=E6=9C=8D?= =?UTF-8?q?=E7=AB=AF=20VirtualStyle=20=E5=88=A4=E6=96=AD=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/framework/file/core/client/s3/S3FileClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java index 43ff2733b..29f6fc34f 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java @@ -91,7 +91,7 @@ public class S3FileClient extends AbstractFileClient { * 开启 VirtualStyle 模式 */ private void enableVirtualStyleEndpoint() { - if (StrUtil.containsAll(config.getEndpoint(), + if (StrUtil.containsAny(config.getEndpoint(), S3FileClientConfig.ENDPOINT_TENCENT, // 腾讯云 https://cloud.tencent.com/document/product/436/41284 S3FileClientConfig.ENDPOINT_VOLCES)) { // 火山云 https://www.volcengine.com/docs/6349/1288493 client.enableVirtualStyleEndpoint(); From 7384b5c3f6d6fada19bf8927375c6f59e7f81b8e Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 23 Aug 2024 20:06:36 +0800 Subject: [PATCH 054/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E8=AF=84=E4=BB=B7=E5=81=B6=E7=8E=B0=E8=AE=A2=E5=8D=95=E5=95=86?= =?UTF-8?q?=E5=93=81=E5=B7=B2=E8=AF=84=E4=BB=B7=EF=BC=88=E5=AE=9E=E9=99=85?= =?UTF-8?q?=E6=9C=AA=E8=AF=84=E4=BB=B7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/service/comment/ProductCommentServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java index 83c8e93a1..f12345416 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java @@ -67,7 +67,7 @@ public class ProductCommentServiceImpl implements ProductCommentService { // 校验 SPU ProductSpuDO spu = validateSpu(sku.getSpuId()); // 校验评论 - validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderId()); + validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderItemId()); // 获取用户详细信息 MemberUserRespDTO user = memberUserApi.getUser(createReqDTO.getUserId()); From 0f51229954daef96f1b4e0f952a55ab00fa5acf2 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 23 Aug 2024 20:16:24 +0800 Subject: [PATCH 055/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E5=BF=AB=E9=80=92?= =?UTF-8?q?=E9=B8=9F=E7=9B=B8=E5=85=B3=E7=9A=84=E4=BF=AE=E5=A4=8D=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trade/dal/mysql/aftersale/AfterSaleLogMapper.java | 7 +++---- .../module/trade/dal/mysql/order/TradeOrderLogMapper.java | 7 +++---- .../trade/service/order/TradeOrderQueryServiceImpl.java | 1 + .../order/handler/TradeMemberPointOrderHandler.java | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java index 5a71ed812..d5453c946 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java @@ -11,10 +11,9 @@ import java.util.List; public interface AfterSaleLogMapper extends BaseMapperX { default List selectListByAfterSaleId(Long afterSaleId) { - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(AfterSaleLogDO::getAfterSaleId, afterSaleId); - queryWrapper.orderByDesc(AfterSaleLogDO::getCreateTime); - return selectList(queryWrapper); + return selectList(new LambdaQueryWrapper() + .eq(AfterSaleLogDO::getAfterSaleId, afterSaleId) + .orderByDesc(AfterSaleLogDO::getCreateTime)); } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderLogMapper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderLogMapper.java index 94d693f69..135f6864c 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderLogMapper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderLogMapper.java @@ -11,10 +11,9 @@ import java.util.List; public interface TradeOrderLogMapper extends BaseMapperX { default List selectListByOrderId(Long orderId) { - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(TradeOrderLogDO::getOrderId, orderId); - queryWrapper.orderByDesc(TradeOrderLogDO::getCreateTime); - return selectList(queryWrapper); + return selectList(new LambdaQueryWrapper() + .eq(TradeOrderLogDO::getOrderId, orderId) + .orderByDesc(TradeOrderLogDO::getCreateTime)); } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java index e96f79a72..350156031 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java @@ -219,6 +219,7 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService { public List getExpressTrackList(String code, String logisticsNo, String receiverMobile) { return expressClientFactory.getDefaultExpressClient().getExpressTrackList( new ExpressTrackQueryReqDTO().setExpressCode(code).setLogisticsNo(logisticsNo) + // TODO @卢越:1)为什么 customerName 使用 mobile 哈?2)如果使用 mobile,其实可以考虑通过 phone 计算下 .setPhone(receiverMobile).setCustomerName(StrUtil.subSuf(receiverMobile, receiverMobile.length() - 4))); } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeMemberPointOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeMemberPointOrderHandler.java index 8f99a987e..88e1ce4f7 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeMemberPointOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeMemberPointOrderHandler.java @@ -80,13 +80,13 @@ public class TradeMemberPointOrderHandler implements TradeOrderHandler { public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) { // 增加(回滚)积分(订单抵扣) addPoint(order.getUserId(), orderItem.getUsePoint(), MemberPointBizTypeEnum.ORDER_USE_CANCEL_ITEM, orderItem.getId()); - // 扣减(回滚)积分(订单赠送) reducePoint(order.getUserId(), orderItem.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE_CANCEL_ITEM, orderItem.getId()); // 扣减(回滚)用户经验 AfterSaleDO afterSale = afterSaleService.getAfterSale(orderItem.getAfterSaleId()); - memberLevelApi.reduceExperience(order.getUserId(), afterSale.getRefundPrice(), MemberExperienceBizTypeEnum.ORDER_GIVE_CANCEL_ITEM.getType(), String.valueOf(orderItem.getId())); + memberLevelApi.reduceExperience(order.getUserId(), afterSale.getRefundPrice(), + MemberExperienceBizTypeEnum.ORDER_GIVE_CANCEL_ITEM.getType(), String.valueOf(orderItem.getId())); } /** From 45a4108138c377de19fd39aeff5b5b7748f5ab83 Mon Sep 17 00:00:00 2001 From: "LAPTOP-00JMG2HE\\George Wei" Date: Sat, 24 Aug 2024 22:05:41 +0800 Subject: [PATCH 056/136] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BF=BD=E7=95=A5exp?= =?UTF-8?q?iresTime=E3=80=81createTime=E5=92=8CupdateTime=E7=9A=84TODO?= =?UTF-8?q?=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/infra/service/job/JobLogServiceImplTest.java | 1 + .../service/logger/ApiAccessLogServiceImplTest.java | 1 + .../infra/service/logger/ApiErrorLogServiceImplTest.java | 1 + .../service/oauth2/OAuth2ApproveServiceImplTest.java | 1 + .../system/service/oauth2/OAuth2CodeServiceImplTest.java | 2 ++ .../service/oauth2/OAuth2TokenServiceImplTest.java | 9 +++++++++ 6 files changed, 15 insertions(+) diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImplTest.java index 0ef0f6692..a6bd3c8c8 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImplTest.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImplTest.java @@ -114,6 +114,7 @@ public class JobLogServiceImplTest extends BaseDbUnitTest { assertEquals(1, count); List logs = jobLogMapper.selectList(); assertEquals(1, logs.size()); + // TODO @芋艿:createTime updateTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(log02, logs.get(0), "createTime", "updateTime"); } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImplTest.java index 7ad09e19b..a1e1f64a6 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImplTest.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImplTest.java @@ -91,6 +91,7 @@ public class ApiAccessLogServiceImplTest extends BaseDbUnitTest { assertEquals(1, count); List logs = apiAccessLogMapper.selectList(); assertEquals(1, logs.size()); + // TODO @芋艿:createTime updateTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(log02, logs.get(0), "createTime", "updateTime"); } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImplTest.java index 0a3c20994..3ae291935 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImplTest.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImplTest.java @@ -157,6 +157,7 @@ public class ApiErrorLogServiceImplTest extends BaseDbUnitTest { assertEquals(1, count); List logs = apiErrorLogMapper.selectList(); assertEquals(1, logs.size()); + // TODO @芋艿:createTime updateTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(log02, logs.get(0), "createTime", "updateTime"); } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java index 05432275c..142201c29 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java @@ -209,6 +209,7 @@ public class OAuth2ApproveServiceImplTest extends BaseDbUnitTest { List result = oauth2ApproveService.getApproveList(userId, userType, clientId); // 断言 assertEquals(1, result.size()); + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(approve, result.get(0), "expiresTime"); } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java index c052576f1..cba9d3e5d 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java @@ -50,6 +50,7 @@ class OAuth2CodeServiceImplTest extends BaseDbUnitTest { scopes, redirectUri, state); // 断言 OAuth2CodeDO dbCodeDO = oauth2CodeMapper.selectByCode(codeDO.getCode()); + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(codeDO, dbCodeDO, "expiresTime", "createTime", "updateTime", "deleted"); assertEquals(userId, codeDO.getUserId()); assertEquals(userType, codeDO.getUserType()); @@ -92,6 +93,7 @@ class OAuth2CodeServiceImplTest extends BaseDbUnitTest { // 调用 OAuth2CodeDO result = oauth2CodeService.consumeAuthorizationCode(code); + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(codeDO, result, "expiresTime"); assertNull(oauth2CodeMapper.selectByCode(code)); } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java index a3070648f..c548940d6 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java @@ -77,6 +77,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, userType, clientId, scopes); // 断言访问令牌 OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken()); + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(accessTokenDO, dbAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted"); assertEquals(userId, accessTokenDO.getUserId()); assertEquals(userType, accessTokenDO.getUserType()); @@ -88,6 +89,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { assertFalse(DateUtils.isExpired(accessTokenDO.getExpiresTime())); // 断言访问令牌的缓存 OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken()); + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(accessTokenDO, redisAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted"); // 断言刷新令牌 OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectList().get(0); @@ -177,12 +179,14 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { assertNull(oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken())); // 断言,新的访问令牌 OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(newAccessTokenDO.getAccessToken()); + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(newAccessTokenDO, dbAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted"); assertPojoEquals(newAccessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater"); assertFalse(DateUtils.isExpired(newAccessTokenDO.getExpiresTime())); // 断言,新的访问令牌的缓存 OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(newAccessTokenDO.getAccessToken()); + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(newAccessTokenDO, redisAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted"); } @@ -198,8 +202,10 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { // 调用 OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken); // 断言 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(accessTokenDO, result, "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater"); + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(accessTokenDO, oauth2AccessTokenRedisDAO.get(accessToken), "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater"); } @@ -237,6 +243,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { // 调研,并断言 OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken); // 断言 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(accessTokenDO, result, "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater"); } @@ -259,6 +266,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { oauth2RefreshTokenMapper.insert(refreshTokenDO); // 调用 OAuth2AccessTokenDO result = oauth2TokenService.removeAccessToken(accessTokenDO.getAccessToken()); + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(accessTokenDO, result, "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater"); // 断言数据 @@ -297,6 +305,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。 assertPojoEquals(dbAccessToken, pageResult.getList().get(0), "expiresTime"); } From d4e4207ae9e10797cbd8128368d7c2e217970d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Sun, 25 Aug 2024 08:39:44 +0800 Subject: [PATCH 057/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=AF=B9customerName=E5=86=85=E9=83=A8?= =?UTF-8?q?=E5=B1=8F=E8=94=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/dto/ExpressTrackQueryReqDTO.java | 20 ++++++++++++++++++- .../order/TradeOrderQueryServiceImpl.java | 13 +++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java index 16662d89d..b9fe1bd18 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.trade.framework.delivery.core.client.dto; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO; import lombok.Data; @@ -13,7 +14,7 @@ public class ExpressTrackQueryReqDTO { /** * 快递公司编码 - * + *

* 对应 {@link DeliveryExpressDO#getCode()} */ private String expressCode; @@ -33,4 +34,21 @@ public class ExpressTrackQueryReqDTO { */ private String customerName; + public ExpressTrackQueryReqDTO setExpressCode(String expressCode) { + this.expressCode = expressCode; + updateCustomerName(); + return this; // 返回实体对象 + } + + public ExpressTrackQueryReqDTO setPhone(String phone) { + this.phone = phone; + updateCustomerName(); + return this; // 返回实体对象 + } + + private void updateCustomerName() { + if ("SF".equals(expressCode) && phone != null && phone.length() >= 4) { + this.customerName = phone.substring(phone.length() - 4); + } + } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java index 350156031..a77c04f0e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java @@ -206,7 +206,7 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService { /** * 查询物流轨迹 - * + *

* 缓存的目的:考虑及时性要求不高,但是每次调用需要钱 * * @param code 快递公司编码 @@ -217,10 +217,13 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService { @Cacheable(cacheNames = RedisKeyConstants.EXPRESS_TRACK, key = "#code + '-' + #logisticsNo + '-' + #receiverMobile", condition = "#result != null && #result.length() > 0") public List getExpressTrackList(String code, String logisticsNo, String receiverMobile) { - return expressClientFactory.getDefaultExpressClient().getExpressTrackList( - new ExpressTrackQueryReqDTO().setExpressCode(code).setLogisticsNo(logisticsNo) - // TODO @卢越:1)为什么 customerName 使用 mobile 哈?2)如果使用 mobile,其实可以考虑通过 phone 计算下 - .setPhone(receiverMobile).setCustomerName(StrUtil.subSuf(receiverMobile, receiverMobile.length() - 4))); + return expressClientFactory.getDefaultExpressClient() + .getExpressTrackList( + new ExpressTrackQueryReqDTO() + .setExpressCode(code) + .setLogisticsNo(logisticsNo) + .setPhone(receiverMobile) + ); } // =================== Order Item =================== From 9bb231eb7338f7dc4649d0f8ef219de43f1ed259 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 25 Aug 2024 11:00:45 +0800 Subject: [PATCH 058/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=20SF=20=E7=89=A9=E6=B5=81=EF=BC=8C=E9=9C=80=E8=A6=81=20Custome?= =?UTF-8?q?rName=20=E7=9A=84=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/dto/ExpressTrackQueryReqDTO.java | 18 ------------------ .../impl/kdniao/KdNiaoExpressClient.java | 7 +++++++ .../order/TradeOrderQueryServiceImpl.java | 9 ++------- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java index b9fe1bd18..ab3820796 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.trade.framework.delivery.core.client.dto; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO; import lombok.Data; @@ -34,21 +33,4 @@ public class ExpressTrackQueryReqDTO { */ private String customerName; - public ExpressTrackQueryReqDTO setExpressCode(String expressCode) { - this.expressCode = expressCode; - updateCustomerName(); - return this; // 返回实体对象 - } - - public ExpressTrackQueryReqDTO setPhone(String phone) { - this.phone = phone; - updateCustomerName(); - return this; // 返回实体对象 - } - - private void updateCustomerName() { - if ("SF".equals(expressCode) && phone != null && phone.length() >= 4) { - this.customerName = phone.substring(phone.length() - 4); - } - } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java index 1f1116882..24cf8e6ed 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.trade.framework.delivery.core.client.impl.kdniao import cn.hutool.core.codec.Base64; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.net.URLEncodeUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.trade.framework.delivery.config.TradeExpressProperties; @@ -60,6 +62,11 @@ public class KdNiaoExpressClient implements ExpressClient { // 发起请求 KdNiaoExpressQueryReqDTO requestDTO = INSTANCE.convert(reqDTO) .setExpressCode(reqDTO.getExpressCode().toUpperCase()); + if (ObjUtil.equal(requestDTO.getExpressCode(), "SF") + && StrUtil.isBlank(reqDTO.getCustomerName()) + && StrUtil.length(reqDTO.getPhone()) >= 4) { + requestDTO.setCustomerName(StrUtil.subSufByLength(reqDTO.getPhone(), 4)); + } KdNiaoExpressQueryRespDTO respDTO = httpRequest(REAL_TIME_QUERY_URL, REAL_TIME_FREE_REQ_TYPE, requestDTO, KdNiaoExpressQueryRespDTO.class); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java index a77c04f0e..68c549891 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java @@ -217,13 +217,8 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService { @Cacheable(cacheNames = RedisKeyConstants.EXPRESS_TRACK, key = "#code + '-' + #logisticsNo + '-' + #receiverMobile", condition = "#result != null && #result.length() > 0") public List getExpressTrackList(String code, String logisticsNo, String receiverMobile) { - return expressClientFactory.getDefaultExpressClient() - .getExpressTrackList( - new ExpressTrackQueryReqDTO() - .setExpressCode(code) - .setLogisticsNo(logisticsNo) - .setPhone(receiverMobile) - ); + return expressClientFactory.getDefaultExpressClient().getExpressTrackList(new ExpressTrackQueryReqDTO() + .setExpressCode(code).setLogisticsNo(logisticsNo).setPhone(receiverMobile)); } // =================== Order Item =================== From ee9a733c725fbb35e9dc8250bbb2685e5ebfe19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Mon, 26 Aug 2024 10:05:35 +0800 Subject: [PATCH 059/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E4=BF=AE=E5=A4=8D=E5=95=86=E5=9F=8E=E5=8F=91?= =?UTF-8?q?=E8=B4=A7=E5=90=8E=E8=AE=A2=E5=8D=95=E6=97=A5=E5=BF=97=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E6=98=BE=E7=A4=BA=E5=BF=AB=E9=80=92=E5=85=AC=E5=8F=B8?= =?UTF-8?q?=E5=90=8D=E7=A7=B0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/trade/enums/order/TradeOrderOperateTypeEnum.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderOperateTypeEnum.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderOperateTypeEnum.java index 695cb41ce..986d06437 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderOperateTypeEnum.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderOperateTypeEnum.java @@ -17,7 +17,7 @@ public enum TradeOrderOperateTypeEnum { ADMIN_UPDATE_PRICE(2, "订单价格 {oldPayPrice} 修改,调整价格 {adjustPrice},实际支付金额为 {newPayPrice} 元"), MEMBER_PAY(10, "用户付款成功"), ADMIN_UPDATE_ADDRESS(11, "收货地址修改"), - ADMIN_DELIVERY(20, "已发货,快递公司:{deliveryName},快递单号:{logisticsNo}"), + ADMIN_DELIVERY(20, "已发货,快递公司:{expressName},快递单号:{logisticsNo}"), MEMBER_RECEIVE(30, "用户已收货"), SYSTEM_RECEIVE(31, "到期未收货,系统自动确认收货"), ADMIN_PICK_UP_RECEIVE(32, "管理员自提收货"), From 9d26381dcc36545138504b64f39d40956c9dd821 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 26 Aug 2024 12:32:54 +0800 Subject: [PATCH 060/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=A2=9E=E5=BC=BA=20JDK17=E3=80=81JDK8=20?= =?UTF-8?q?=E4=B9=8B=E9=97=B4=E7=9A=84=E5=85=BC=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/core/model/deepseek/DeepSeekChatModel.java | 5 +++-- .../framework/ai/core/model/xinghuo/XingHuoChatModel.java | 5 +++-- .../audio/transcription/TongYiAudioTranscriptionModel.java | 3 ++- .../com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java | 3 ++- .../ai/tongyi/embedding/TongYiTextEmbeddingModel.java | 3 ++- .../cn/iocoder/yudao/framework/ai/music/SunoApiTests.java | 3 ++- .../statistics/CrmStatisticsPerformanceServiceImpl.java | 3 ++- .../framework/sms/core/client/impl/SmsClientTests.java | 7 ++++--- 8 files changed, 20 insertions(+), 12 deletions(-) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java index 1437404e8..e3097b83a 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.ai.core.model.deepseek; +import cn.hutool.core.collection.ListUtil; import cn.hutool.core.lang.Assert; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.metadata.ChatGenerationMetadata; @@ -70,12 +71,12 @@ public class DeepSeekChatModel implements ChatModel { OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody(); if (chatCompletion == null) { log.warn("No chat completion returned for prompt: {}", prompt); - return new ChatResponse(List.of()); + return new ChatResponse(ListUtil.of()); } List choices = chatCompletion.choices(); if (choices == null) { log.warn("No choices returned for prompt: {}", prompt); - return new ChatResponse(List.of()); + return new ChatResponse(ListUtil.of()); } // 2. 转换 ChatResponse 返回 diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java index 60284bf2f..501d916db 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.ai.core.model.xinghuo; +import cn.hutool.core.collection.ListUtil; import cn.hutool.core.lang.Assert; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.metadata.ChatGenerationMetadata; @@ -72,12 +73,12 @@ public class XingHuoChatModel implements ChatModel { OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody(); if (chatCompletion == null) { log.warn("No chat completion returned for prompt: {}", prompt); - return new ChatResponse(List.of()); + return new ChatResponse(ListUtil.of()); } List choices = chatCompletion.choices(); if (choices == null) { log.warn("No choices returned for prompt: {}", prompt); - return new ChatResponse(List.of()); + return new ChatResponse(ListUtil.of()); } // 2. 转换 ChatResponse 返回 diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java index 2068feeb5..0f0dca9c0 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java @@ -16,6 +16,7 @@ package com.alibaba.cloud.ai.tongyi.audio.transcription; +import cn.hutool.core.collection.ListUtil; import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels; import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionPrompt; import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionResponse; @@ -82,7 +83,7 @@ public class TongYiAudioTranscriptionModel try { transcriptionParam = TranscriptionParam.builder() .model(AudioTranscriptionModels.Paraformer_V1) - .fileUrls(List.of(String.valueOf(instructions.getURL()))) + .fileUrls(ListUtil.of(String.valueOf(instructions.getURL()))) .build(); } catch (IOException e) { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java index c29ffbdfb..11328a02e 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java @@ -16,6 +16,7 @@ package com.alibaba.cloud.ai.tongyi.chat; +import cn.hutool.core.collection.ListUtil; import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException; import com.alibaba.dashscope.aigc.conversation.ConversationParam; import com.alibaba.dashscope.aigc.generation.Generation; @@ -207,7 +208,7 @@ public class TongYiChatModel extends .getChoices() .get(0) )); - return new ChatResponse(List.of(gen)); + return new ChatResponse(ListUtil.of(gen)); }) ) .publishOn(Schedulers.parallel()); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java index ce92dae07..99a356fe8 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java @@ -16,6 +16,7 @@ package com.alibaba.cloud.ai.tongyi.embedding; +import cn.hutool.core.collection.ListUtil; import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException; import com.alibaba.cloud.ai.tongyi.metadata.TongYiTextEmbeddingResponseMetadata; import com.alibaba.dashscope.embeddings.TextEmbedding; @@ -100,7 +101,7 @@ public class TongYiTextEmbeddingModel extends AbstractEmbeddingModel { return this.call( new EmbeddingRequest( - List.of(document.getFormattedContent(this.metadataMode)), + ListUtil.of(document.getFormattedContent(this.metadataMode)), null) ).getResults().stream() .map(Embedding::getOutput) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoApiTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoApiTests.java index ed8ecc6c6..2d80fcf06 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoApiTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoApiTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.ai.music; +import cn.hutool.core.collection.ListUtil; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -66,7 +67,7 @@ public class SunoApiTests { String id = "584729e5-0fe9-4157-86da-1b4803ff42bf"; // 调用方法 - List musicList = sunoApi.getMusicList(List.of(id)); + List musicList = sunoApi.getMusicList(ListUtil.of(id)); // 打印结果 System.out.println(musicList); } diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java index 1e7e3bbb2..ae8a46ab9 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java +++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.crm.service.statistics; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceReqVO; import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO; @@ -106,7 +107,7 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform private List getUserIds(CrmStatisticsPerformanceReqVO reqVO) { // 情况一:选中某个用户 if (ObjUtil.isNotNull(reqVO.getUserId())) { - return List.of(reqVO.getUserId()); + return ListUtil.of(reqVO.getUserId()); } // 情况二:选中某个部门 // 2.1 获得部门列表 diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java index 6eb22af1b..b1e96f195 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java @@ -1,5 +1,6 @@ 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; @@ -47,7 +48,7 @@ public class SmsClientTests { String mobile = "15601691323"; String apiTemplateId = "SMS_207945135"; // 调用 - SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024"))); + SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024"))); // 打印结果 System.out.println(sendRespDTO); } @@ -96,7 +97,7 @@ public class SmsClientTests { String mobile = "15601691323"; String apiTemplateId = "2136358"; // 调用 - SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024"))); + SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024"))); // 打印结果 System.out.println(sendRespDTO); } @@ -131,7 +132,7 @@ public class SmsClientTests { Long sendLogId = System.currentTimeMillis(); String mobile = "15601691323"; String apiTemplateId = "xx test01"; - List> templateParams = List.of(new KeyValue<>("code", "1024")); + List> templateParams = ListUtil.of(new KeyValue<>("code", "1024")); // 调用 SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); // 打印结果 From 4f7ac969feec4fc61f403f3aa2e605473900e544 Mon Sep 17 00:00:00 2001 From: scholar <1145227973@qq.com> Date: Mon, 26 Aug 2024 15:52:41 +0800 Subject: [PATCH 061/136] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=83=E7=89=9B?= =?UTF-8?q?=E4=BA=91=E7=9F=AD=E4=BF=A1=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/common/util/http/HttpUtils.java | 42 ++++- .../admin/sms/SmsCallbackController.java | 17 +- .../sms/core/client/impl/QiniuSmsClient.java | 172 ++++++++++++++++++ .../sms/core/enums/SmsChannelEnum.java | 2 + .../core/client/impl/QiniuSmsClientTest.java | 128 +++++++++++++ .../sms/core/client/impl/SmsClientTests.java | 138 +++++++------- 6 files changed, 428 insertions(+), 71 deletions(-) create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java index 9a39a7a4e..1697d097f 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java @@ -5,6 +5,8 @@ import cn.hutool.core.map.TableMap; import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -109,7 +111,7 @@ public class HttpUtils { authorization = Base64.decodeStr(authorization); clientId = StrUtil.subBefore(authorization, ":", false); clientSecret = StrUtil.subAfter(authorization, ":", false); - // 再从 Param 中获取 + // 再从 Param 中获取 } else { clientId = request.getParameter("client_id"); clientSecret = request.getParameter("client_secret"); @@ -122,5 +124,43 @@ public class HttpUtils { return null; } + /** + * HTTP post 请求,基于 {@link cn.hutool.http.HttpUtil} 实现 + * + * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数 + * + * @param url URL + * @param headers 请求头 + * @param requestBody 请求体 + * @return 请求结果 + */ + public static String post(String url, Map headers, String requestBody) { + + try (HttpResponse response = HttpRequest.post(url) + .addHeaders(headers) + .body(requestBody) + .execute()) { + return response.body(); + } + } + + /** + * HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现 + * + * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数 + * + * @param url URL + * @param headers 请求头 + * @return 请求结果 + */ + public static String get(String url, Map headers) { + + try (HttpResponse response = HttpRequest.get(url) + .addHeaders(headers) + .execute()) { + return response.body(); + } + } } + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java index 90cb763cc..f4712f0ab 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java @@ -2,10 +2,8 @@ package cn.iocoder.yudao.module.system.controller.admin.sms; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; -import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum; import cn.iocoder.yudao.module.system.service.sms.SmsSendService; -import com.xingyuv.captcha.util.StreamUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.*; @@ -14,8 +12,6 @@ import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletRequest; -import java.nio.charset.Charset; - import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 短信回调") @@ -29,7 +25,6 @@ public class SmsCallbackController { @PostMapping("/aliyun") @PermitAll @Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/document_detail/120998.html 文档") - @OperateLog(enable = false) public CommonResult receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable { String text = ServletUtils.getBody(request); smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text); @@ -39,7 +34,6 @@ public class SmsCallbackController { @PostMapping("/tencent") @PermitAll @Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/52077 文档") - @OperateLog(enable = false) public CommonResult receiveTencentSmsStatus(HttpServletRequest request) throws Throwable { String text = ServletUtils.getBody(request); smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text); @@ -50,9 +44,18 @@ public class SmsCallbackController { @PostMapping("/huawei") @PermitAll @Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档") - @OperateLog(enable = false) public CommonResult receiveHuaweiSmsStatus(@RequestBody String requestBody) throws Throwable { smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), requestBody); return success(true); } + + @PostMapping("/qiniu") + @PermitAll + @Operation(summary = "七牛云短信的回调", description = "参见 https://developer.qiniu.com/sms/5910/message-push 文档") + public CommonResult receiveQiniuSmsStatus(@RequestBody String requestBody) throws Throwable { + smsSendService.receiveSmsStatus(SmsChannelEnum.QINIU.getCode(), requestBody); + return success(true); + } + } + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java new file mode 100644 index 000000000..c0a2b60ac --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java @@ -0,0 +1,172 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.crypto.digest.HMac; +import cn.hutool.crypto.digest.HmacAlgorithm; +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.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.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 七牛云短信客户端的实现类 + * + * @author scholar + * @since 2024/08/26 15:35 + */ +@Slf4j +public class QiniuSmsClient extends AbstractSmsClient { + + private static final String HOST = "sms.qiniuapi.com"; + + private static final String PATH = "/v1/message/single"; + + private static final String TEMPLATE_PATH = "/v1/template"; + + public QiniuSmsClient(SmsChannelProperties properties) { + super(properties); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + } + @Override + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, + List> templateParams) throws Throwable { + + // 1. 执行请求 + // 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages + LinkedHashMap body = new LinkedHashMap<>(); + Map paramsMap = templateParams.stream() + .collect(Collectors.toMap(KeyValue::getKey, KeyValue::getValue)); + + body.put("template_id", apiTemplateId); + body.put("mobile", mobile); + body.put("parameters", paramsMap); + body.put("seq", Long.toString(sendLogId)); + + JSONObject response = request("POST", body, null); + // 2. 解析请求 + return new SmsSendRespDTO().setSuccess(response.containsKey("message_id")) + .setSerialNo(response.getStr("message_id")); + } + + + /** + * 请求七牛云短信 + * + * @see + * @param httpMethod http请求方法 + * @param queryParams 请求参数 + * @return 请求结果 + */ + private JSONObject request(String httpMethod, LinkedHashMap body, Map queryParams) { + + String signature = ""; + String templateIdPath = ""; + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + String signDate = dateFormat.format(new Date()); + + //请求头 + Map header = new HashMap<>(4); + header.put("HOST", HOST); + header.put("Authorization", signature); + header.put("Content-Type", "application/json"); + header.put("X-Qiniu-Date", signDate); + + String responseBody =""; + if(Objects.equals(httpMethod, "POST")){ + header.put("Authorization", getSignature(httpMethod, HOST, PATH, JSONUtil.toJsonStr(body), signDate)); + responseBody = HttpUtils.post("https://" + HOST + PATH, header, JSONUtil.toJsonStr(body)); + }else { // GET + templateIdPath = TEMPLATE_PATH + "/" + queryParams.get("template_id"); + header.put("Authorization", getSignature(httpMethod, HOST, templateIdPath, null, signDate)); + responseBody = HttpUtils.get("https://" + HOST + templateIdPath, header); + } + return JSONUtil.parseObj(responseBody); + } + + public String getSignature(String method, String host, String path, String body, String signDate) { + + StringBuilder dataToSign = new StringBuilder(); + dataToSign.append(method.toUpperCase()).append(" ").append(path); + dataToSign.append("\nHost: ").append(host); + dataToSign.append("\n").append("Content-Type").append(": ").append("application/json"); + dataToSign.append("\n").append("X-Qiniu-Date").append(": ").append(signDate); + dataToSign.append("\n\n"); + if (ObjectUtil.isNotEmpty(body)) { + dataToSign.append(body); + } + HMac hMac = new HMac(HmacAlgorithm.HmacSHA1, properties.getApiSecret().getBytes(StandardCharsets.UTF_8)); + byte[] signData = hMac.digest(dataToSign.toString().getBytes(StandardCharsets.UTF_8)); + String encodedSignature = Base64.getEncoder().encodeToString(signData); + + return "Qiniu " + properties.getApiKey() + ":" + encodedSignature; + } + + @Override + public List parseSmsReceiveStatus(String text) { + + JSONObject status = JSONUtil.parseObj(text); + //字段参考 https://developer.qiniu.com/sms/5910/message-push + return ListUtil.of(new SmsReceiveRespDTO() + .setSuccess("DELIVRD".equals(status.getJSONArray("items").getJSONObject(0).getStr("status"))) // 是否接收成功 + .setErrorMsg(status.getJSONArray("items").getJSONObject(0).getStr("status")) + .setMobile(status.getJSONArray("items").getJSONObject(0).getStr("mobile")) // 手机号 + .setReceiveTime(LocalDateTimeUtil.of(status.getJSONArray("items").getJSONObject(0).getLong("delivrd_at")*1000L)) + .setSerialNo(status.getJSONArray("items").getJSONObject(0).getStr("message_id")) // 发送序列号 + .setLogId(Long.valueOf(status.getJSONArray("items").getJSONObject(0).getStr("seq")))); // logId + } + + @Override + public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { + // 1. 执行请求 + // 参考链接 https://developer.qiniu.com/sms/5969/query-a-single-template + HashMap queryParam = new HashMap<>(); + queryParam.put("template_id", apiTemplateId); + JSONObject response = request("GET", null, queryParam); + + // 2.1 请求失败 + String status = response.getStr("audit_status"); + if (!Objects.equals(status, "passed")) { + log.error("[getSmsTemplate][模版编号({}) 响应不正确({})]", apiTemplateId, response); + return null; + } + // 2.2 请求成功 + return new SmsTemplateRespDTO() + .setId(response.getStr("id")) + .setContent(response.getStr("template")) + .setAuditStatus(convertSmsTemplateAuditStatus(response.getStr("audit_status"))) + .setAuditReason(response.getStr("reject_reason")); + } + + @VisibleForTesting + Integer convertSmsTemplateAuditStatus(String templateStatus) { + + if(Objects.equals(templateStatus, "passed")){ + return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + }else { + throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus)); + } + } +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java index 88f578a18..cbbde696b 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java @@ -18,6 +18,7 @@ public enum SmsChannelEnum { ALIYUN("ALIYUN", "阿里云"), TENCENT("TENCENT", "腾讯云"), HUAWEI("HUAWEI", "华为云"), + QINIU("QINIU", "七牛云"), ; /** @@ -34,3 +35,4 @@ public enum SmsChannelEnum { } } + diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java new file mode 100644 index 000000000..c64c39470 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java @@ -0,0 +1,128 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +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.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.MockedStatic; + +import java.time.LocalDateTime; +import java.util.List; + +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; + +/** + * {@link QiniuSmsClient} 的单元测试 + * + * @author scholar + */ +public class QiniuSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString())// 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + + @InjectMocks + private QiniuSmsClient smsClient = new QiniuSmsClient(properties); + + @Test + public void testDoInit() { + // 调用 + smsClient.doInit(); + } + + @Test + public void testDoSendSms_success() throws Throwable { + + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString() + " " + randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); + // mock 方法 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) + .thenReturn( + "{\"message_id\":\"17245678901\"}" + ); + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertTrue(result.getSuccess()); + assertEquals("17245678901", result.getSerialNo()); + } + } + + @Test + public void testDoSendSms_fail() throws Throwable { + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString() + " " + randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); + + // mock 方法 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) + .thenReturn( + "{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}" + ); + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertFalse(result.getSuccess()); + } + } + + @Test + public void testGetSmsTemplate() throws Throwable { + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + String apiTemplateId = randomString(); + // mock 方法 + httpUtilsMockedStatic.when(() -> HttpUtils.get(anyString(), anyMap())) + .thenReturn("{\"audit_status\":\"passed\",\"created_at\":1724231187,\"description\":\"\",\"disable_broadcast\":false,\"disable_broadcast_reason\":\"\",\"disable_reason\":\"\",\"disabled\":false,\"id\":\"1826184073773596672\",\"is_oversea\":false,\"name\":\"dd\",\"parameters\":[\"code\"],\"reject_reason\":\"\",\"signature_id\":\"1826099896017498112\",\"signature_text\":\"yudao\",\"template\":\"您的验证码为:${code}\",\"type\":\"verification\",\"uid\":1383022432,\"updated_at\":1724288561,\"variable_count\":0}"); + // 调用 + SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId); + // 断言 + assertEquals("1826184073773596672", result.getId()); + assertEquals("您的验证码为:${code}", result.getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus()); + assertEquals("", result.getAuditReason()); + } + } + + @Test + public void testParseSmsReceiveStatus() { + // 准备参数 + String text = "{\"items\":[{\"mobile\":\"18881234567\",\"message_id\":\"10135515063508004167\",\"status\":\"DELIVRD\",\"delivrd_at\":1724591666,\"error\":\"DELIVRD\",\"seq\":\"123\"}]}"; + // 调用 + List statuses = smsClient.parseSmsReceiveStatus(text); + // 断言 + assertEquals(1, statuses.size()); + assertTrue(statuses.getFirst().getSuccess()); + assertEquals("DELIVRD", statuses.getFirst().getErrorMsg()); + assertEquals(LocalDateTime.of(2024, 8, 25, 21, 14, 26), statuses.getFirst().getReceiveTime()); + assertEquals("18881234567", statuses.getFirst().getMobile()); + assertEquals("10135515063508004167", statuses.getFirst().getSerialNo()); + assertEquals(123, statuses.getFirst().getLogId()); + } + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java index f1db141e8..3752e5763 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; 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.SmsClient; 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.property.SmsChannelProperties; @@ -11,39 +11,19 @@ import org.junit.jupiter.api.Test; import java.util.List; /** - * 各种 {@link SmsClientTests 集成测试 + * 各种 {@link SmsClient} 的集成测试 * * @author 芋道源码 */ public class SmsClientTests { - @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> templateParams = List.of(new KeyValue<>("code", "1024")); - // 调用 - SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); - // 打印结果 - System.out.println(smsSendRespDTO); - } - // ========== 阿里云 ========== - @Test @Disabled public void testAliyunSmsClient_getSmsTemplate() throws Throwable { SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz"); + .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY")) + .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY")); AliyunSmsClient client = new AliyunSmsClient(properties); // 准备参数 String apiTemplateId = "SMS_207945135"; @@ -57,9 +37,9 @@ public class SmsClientTests { @Disabled public void testAliyunSmsClient_sendSms() throws Throwable { SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") - .setSignature("runpu"); + .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY")) + .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY")) + .setSignature("Ballcat"); AliyunSmsClient client = new AliyunSmsClient(properties); // 准备参数 Long sendLogId = System.currentTimeMillis(); @@ -71,49 +51,21 @@ public class SmsClientTests { System.out.println(sendRespDTO); } - @Test - @Disabled - public void testAliyunSmsClient_parseSmsReceiveStatus() { - SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz"); - AliyunSmsClient client = new AliyunSmsClient(properties); - // 准备参数 - String text = "[\n" + - " {\n" + - " \"phone_number\" : \"13900000001\",\n" + - " \"send_time\" : \"2017-01-01 11:12:13\",\n" + - " \"report_time\" : \"2017-02-02 22:23:24\",\n" + - " \"success\" : true,\n" + - " \"err_code\" : \"DELIVERED\",\n" + - " \"err_msg\" : \"用户接收成功\",\n" + - " \"sms_size\" : \"1\",\n" + - " \"biz_id\" : \"12345\",\n" + - " \"out_id\" : \"67890\"\n" + - " }\n" + - "]"; - // mock 方法 - - // 调用 - List statuses = client.parseSmsReceiveStatus(text); - // 打印结果 - System.out.println(statuses); - } - // ========== 腾讯云 ========== @Test @Disabled public void testTencentSmsClient_sendSms() throws Throwable { + String sdkAppId = "1400500458"; SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId) + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY")) .setSignature("芋道源码"); TencentSmsClient client = new TencentSmsClient(properties); // 准备参数 Long sendLogId = System.currentTimeMillis(); String mobile = "15601691323"; - String apiTemplateId = "2136358"; + String apiTemplateId = "358212"; // 调用 SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024"))); // 打印结果 @@ -123,17 +75,77 @@ public class SmsClientTests { @Test @Disabled public void testTencentSmsClient_getSmsTemplate() throws Throwable { + String sdkAppId = "1400500458"; SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId) + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY")) .setSignature("芋道源码"); TencentSmsClient client = new TencentSmsClient(properties); // 准备参数 - String apiTemplateId = "2136358"; + String apiTemplateId = "358212"; // 调用 SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId); // 打印结果 System.out.println(template); } -} + // ========== 华为云 ========== + + @Test + @Disabled + public void testHuaweiSmsClient_sendSms() throws Throwable { + String sender = "x8824060312575"; + SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(System.getenv("SMS_HUAWEI_ACCESS_KEY") + " " + sender) + .setApiSecret(System.getenv("SMS_HUAWEI_SECRET_KEY")) + .setSignature("runpu"); + HuaweiSmsClient client = new HuaweiSmsClient(properties); + // 准备参数 + Long sendLogId = System.currentTimeMillis(); + String mobile = "17321315478"; + String apiTemplateId = "3644cdab863546a3b718d488659a99ef"; + List> templateParams = List.of(new KeyValue<>("code", "1024")); + // 调用 + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + // 打印结果 + System.out.println(smsSendRespDTO); + } + + // ========== 七牛云 ========== + + @Test + @Disabled + public void testQiniuSmsClient_sendSms() throws Throwable { + + SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey("SMS_QINIU_ACCESS_KEY") + .setApiSecret("SMS_QINIU_SECRET_KEY"); + QiniuSmsClient client = new QiniuSmsClient(properties); + // 准备参数 + Long sendLogId = System.currentTimeMillis(); + String mobile = "17321315478"; + String apiTemplateId = "3644cdab863546a3b718d488659a99ef"; + List> templateParams = List.of(new KeyValue<>("code", "1122")); + // 调用 + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + // 打印结果 + System.out.println(smsSendRespDTO); + } + + @Test + @Disabled + public void testQiniuSmsClient_getSmsTemplate() throws Throwable { + + SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey("SMS_QINIU_ACCESS_KEY") + .setApiSecret("SMS_QINIU_SECRET_KEY"); + QiniuSmsClient client = new QiniuSmsClient(properties); + // 准备参数 + String apiTemplateId = "3644cdab863546a3b718d488659a99ef"; + // 调用 + SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId); + // 打印结果 + System.out.println(template); + } + +} \ No newline at end of file From 1e4cc953d342a61b848bf2f529201b98125c9a15 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 26 Aug 2024 22:13:16 +0800 Subject: [PATCH 062/136] =?UTF-8?q?=E3=80=90BUG=E3=80=91=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E9=99=90=E6=97=B6=E6=8A=98=E6=89=A3=E6=9B=B4=E6=96=B0=E6=97=B6?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E8=AE=BE=E7=BD=AE=E6=B4=BB=E5=8A=A8=E5=BC=80?= =?UTF-8?q?=E5=A7=8B=E6=97=B6=E9=97=B4=E7=82=B9=E5=92=8C=E6=B4=BB=E5=8A=A8?= =?UTF-8?q?=E7=BB=93=E6=9D=9F=E6=97=B6=E9=97=B4=E7=82=B9=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/discount/DiscountActivityServiceImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java index 25872d8e8..0c995267b 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java @@ -104,7 +104,10 @@ public class DiscountActivityServiceImpl implements DiscountActivityService { } // 计算新增的记录 List newDiscountProducts = convertList(updateReqVO.getProducts(), - product -> DiscountActivityConvert.INSTANCE.convert(product).setActivityId(updateReqVO.getId())); + product -> DiscountActivityConvert.INSTANCE.convert(product) + .setActivityId(updateReqVO.getId()) + .setActivityStartTime(updateReqVO.getStartTime()) + .setActivityEndTime(updateReqVO.getEndTime())); newDiscountProducts.removeIf(product -> dbDiscountProducts.stream().anyMatch( dbProduct -> DiscountActivityConvert.INSTANCE.isEquals(dbProduct, product))); // 如果匹配到,说明是更新的 if (CollectionUtil.isNotEmpty(newDiscountProducts)) { From 4868ef795cec7c990a001ec2ed335865530b63f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Tue, 27 Aug 2024 10:17:58 +0800 Subject: [PATCH 063/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E=E4=BC=98=E6=83=A0=E5=88=B8?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=B7=BB=E5=8A=A0=E6=8F=8F=E8=BF=B0=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/coupon/vo/template/CouponTemplateBaseVO.java | 3 +++ .../promotion/dal/dataobject/coupon/CouponTemplateDO.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java index 419a3f443..b41b96119 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java @@ -95,6 +95,9 @@ public class CouponTemplateBaseVO { @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用 private Integer discountLimitPrice; + @Schema(description = "优惠券描述", example = "限时优惠!使用优惠券即可享受全场商品 8 折优惠,快来抢购吧!") // 单位:分,仅在 discountType 为 PERCENT 使用 + private String description; + @AssertTrue(message = "商品范围编号的数组不能为空") @JsonIgnore public boolean isProductScopeValuesValid() { diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java index ad4ebab9b..761b42c2d 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java @@ -11,6 +11,7 @@ import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -163,4 +164,6 @@ public class CouponTemplateDO extends BaseDO { // TODO 芋艿:领取开始时间、领取结束时间 // TODO 芋艿:要不要加描述 + @Schema(description = "优惠券描述", example = "限时优惠!使用优惠券即可享受全场商品 8 折优惠,快来抢购吧!") // 单位:分,仅在 discountType 为 PERCENT 使用 + private String description; } From 841ad5875cb0ff01510a5dfb66bb8154794246c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Tue, 27 Aug 2024 10:38:53 +0800 Subject: [PATCH 064/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=B0=8F=E7=A8=8B=E5=BA=8F=E7=AB=AF=E5=95=86?= =?UTF-8?q?=E5=9F=8E=E4=BC=98=E6=83=A0=E5=88=B8=E6=A8=A1=E6=9D=BF=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=8F=8F=E8=BF=B0=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/coupon/vo/template/AppCouponTemplateRespVO.java | 3 +++ .../promotion/dal/dataobject/coupon/CouponTemplateDO.java | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java index a2967ac32..5c3cc5508 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java @@ -67,4 +67,7 @@ public class AppCouponTemplateRespVO { @Schema(description = "是否可以领取", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean canTake; + @Schema(description = "优惠券描述", example = "限时优惠!使用优惠券即可享受全场商品 8 折优惠,快来抢购吧!") // 单位:分,仅在 discountType 为 PERCENT 使用 + private String description; + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java index 761b42c2d..bdf80e677 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java @@ -163,7 +163,6 @@ public class CouponTemplateDO extends BaseDO { // TODO 芋艿:领取开始时间、领取结束时间 - // TODO 芋艿:要不要加描述 @Schema(description = "优惠券描述", example = "限时优惠!使用优惠券即可享受全场商品 8 折优惠,快来抢购吧!") // 单位:分,仅在 discountType 为 PERCENT 使用 private String description; } From 4d6ea0043f7be1e434884bcdce1beb09b3569ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Tue, 27 Aug 2024 11:50:46 +0800 Subject: [PATCH 065/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E4=BC=98=E6=83=A0=E5=88=B8=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/coupon/vo/template/CouponTemplateBaseVO.java | 2 +- .../app/coupon/vo/template/AppCouponTemplateRespVO.java | 2 +- .../promotion/dal/dataobject/coupon/CouponTemplateDO.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java index b41b96119..715982aa0 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java @@ -95,7 +95,7 @@ public class CouponTemplateBaseVO { @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用 private Integer discountLimitPrice; - @Schema(description = "优惠券描述", example = "限时优惠!使用优惠券即可享受全场商品 8 折优惠,快来抢购吧!") // 单位:分,仅在 discountType 为 PERCENT 使用 + @Schema(description = "优惠券说明", example = "优惠券使用说明") // 单位:分,仅在 discountType 为 PERCENT 使用 private String description; @AssertTrue(message = "商品范围编号的数组不能为空") diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java index 5c3cc5508..8ca62935e 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java @@ -67,7 +67,7 @@ public class AppCouponTemplateRespVO { @Schema(description = "是否可以领取", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean canTake; - @Schema(description = "优惠券描述", example = "限时优惠!使用优惠券即可享受全场商品 8 折优惠,快来抢购吧!") // 单位:分,仅在 discountType 为 PERCENT 使用 + @Schema(description = "优惠券说明", example = "优惠券使用说明") // 单位:分,仅在 discountType 为 PERCENT 使用 private String description; } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java index bdf80e677..93970ab9b 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java @@ -163,6 +163,6 @@ public class CouponTemplateDO extends BaseDO { // TODO 芋艿:领取开始时间、领取结束时间 - @Schema(description = "优惠券描述", example = "限时优惠!使用优惠券即可享受全场商品 8 折优惠,快来抢购吧!") // 单位:分,仅在 discountType 为 PERCENT 使用 + @Schema(description = "优惠券说明", example = "优惠券使用说明") // 单位:分,仅在 discountType 为 PERCENT 使用 private String description; } From 710f29d911f4310b78d4666fe4631f9879f3f263 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Tue, 27 Aug 2024 16:46:04 +0800 Subject: [PATCH 066/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E:=20=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E6=B4=BB=E5=8A=A8=20CRUD=20=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/RewardActivityMatchRespDTO.java | 48 ++++++- .../promotion/enums/ErrorCodeConstants.java | 3 +- .../common/PromotionProductScopeEnum.java | 13 ++ .../app/activity/AppActivityController.java | 74 +++++++--- .../dataobject/reward/RewardActivityDO.java | 4 +- .../mysql/reward/RewardActivityMapper.java | 12 +- .../service/reward/RewardActivityService.java | 3 +- .../reward/RewardActivityServiceImpl.java | 100 +++++--------- .../reward/RewardActivityServiceImplTest.java | 128 +++++++++--------- .../TradeRewardActivityPriceCalculator.java | 4 +- ...radeRewardActivityPriceCalculatorTest.java | 6 +- 11 files changed, 220 insertions(+), 175 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java index 6ae71a1d9..a174637af 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java @@ -1,8 +1,12 @@ package cn.iocoder.yudao.module.promotion.api.reward.dto; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import lombok.Data; +import java.io.Serializable; +import java.time.LocalDateTime; import java.util.List; /** @@ -21,28 +25,50 @@ public class RewardActivityMatchRespDTO { * 活动标题 */ private String name; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 开始时间 + */ + private LocalDateTime startTime; + /** + * 结束时间 + */ + private LocalDateTime endTime; + /** + * 备注 + */ + private String remark; /** * 条件类型 * * 枚举 {@link PromotionConditionTypeEnum} */ private Integer conditionType; + /** + * 商品范围 + * + * 枚举 {@link PromotionProductScopeEnum} + */ + private Integer productScope; + /** + * 商品 SPU 编号的数组 + */ + private List productScopeValues; /** * 优惠规则的数组 */ private List rules; - /** - * 商品 SPU 编号的数组 - */ - private List spuIds; - - // TODO 芋艿:后面 RewardActivityRespDTO 有了之后,Rule 可以放过去 /** * 优惠规则 */ @Data - public static class Rule { + public static class Rule implements Serializable { /** * 优惠门槛 @@ -59,10 +85,18 @@ public class RewardActivityMatchRespDTO { * 是否包邮 */ private Boolean freeDelivery; + /** + * 是否赠送积分 + */ + private Boolean givePoint; /** * 赠送的积分 */ private Integer point; + /** + * 是否赠送优惠券 + */ + private Boolean giveCoupon; /** * 赠送的优惠劵编号的数组 */ diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java index 8cebd6e13..e1efb9c91 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java @@ -44,7 +44,8 @@ public interface ErrorCodeConstants { ErrorCode REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_002, "满减送活动已关闭,不能修改"); ErrorCode REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_006_003, "满减送活动未关闭,不能删除"); ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_004, "满减送活动已关闭,不能重复关闭"); - ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END = new ErrorCode(1_013_006_005, "满减送活动已结束,不能关闭"); + ErrorCode REWARD_ACTIVITY_SCOPE_ALL_EXISTS = new ErrorCode(1_013_006_005, "已存在商品范围为全场的满减送活动"); + ErrorCode REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS = new ErrorCode(1_013_006_006, "存在商品类型参加了其它满减送活动"); // ========== TODO 空着 1-013-007-000 ============ diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java index 98e2ac7c9..c082e190f 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.promotion.enums.common; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; @@ -35,4 +36,16 @@ public enum PromotionProductScopeEnum implements IntArrayValuable { return ARRAYS; } + public static boolean isAll(Integer scope) { + return ObjUtil.equal(scope, ALL.scope); + } + + public static boolean isSpu(Integer scope) { + return ObjUtil.equal(scope, SPU.scope); + } + + public static boolean isCategory(Integer scope) { + return ObjUtil.equal(scope, CATEGORY.scope); + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java index a59ff7df1..fe15c0f71 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java @@ -2,14 +2,19 @@ package cn.iocoder.yudao.module.promotion.controller.app.activity; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; import cn.iocoder.yudao.module.promotion.controller.app.activity.vo.AppActivityRespVO; import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO; +import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; import cn.iocoder.yudao.module.promotion.service.bargain.BargainActivityService; import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService; @@ -48,6 +53,8 @@ public class AppActivityController { private DiscountActivityService discountActivityService; @Resource private RewardActivityService rewardActivityService; + @Resource + private ProductSpuApi productSpuApi; @GetMapping("/list-by-spu-id") @Operation(summary = "获得单个商品,近期参与的每个活动") @@ -141,29 +148,52 @@ public class AppActivityController { item.getName(), productMap.get(item.getId()), item.getStartTime(), item.getEndTime()))); } + private static void buildAppActivityRespVO(RewardActivityDO rewardActivity, Collection spuIds, + List activityList) { + for (Long spuId : spuIds) { + // 校验商品是否已经加入过活动 + if (anyMatch(activityList, appActivity -> ObjUtil.equal(appActivity.getId(), rewardActivity.getId()) && + ObjUtil.equal(appActivity.getSpuId(), spuId))) { + continue; + } + activityList.add(new AppActivityRespVO(rewardActivity.getId(), + PromotionTypeEnum.REWARD_ACTIVITY.getType(), rewardActivity.getName(), spuId, + rewardActivity.getStartTime(), rewardActivity.getEndTime())); + } + } + private void getRewardActivities(Collection spuIds, LocalDateTime now, List activityList) { - // TODO @puhui999:有 3 范围,不只 spuId,还有 categoryId,全部,下次 fix - //List rewardActivityList = rewardActivityService.getRewardActivityBySpuIdsAndStatusAndDateTimeLt( - // spuIds, PromotionActivityStatusEnum.RUN.getStatus(), now); - //if (CollUtil.isEmpty(rewardActivityList)) { - // return; - //} - // - //Map> spuIdAndActivityMap = spuIds.stream() - // .collect(Collectors.toMap( - // spuId -> spuId, - // spuId -> rewardActivityList.stream() - // .filter(activity -> activity.getProductSpuIds().contains(spuId)) - // .max(Comparator.comparing(RewardActivityDO::getCreateTime)))); - //for (Long supId : spuIdAndActivityMap.keySet()) { - // if (spuIdAndActivityMap.get(supId).isEmpty()) { - // continue; - // } - // - // RewardActivityDO rewardActivityDO = spuIdAndActivityMap.get(supId).get(); - // activityList.add(new AppActivityRespVO(rewardActivityDO.getId(), PromotionTypeEnum.REWARD_ACTIVITY.getType(), - // rewardActivityDO.getName(), supId, rewardActivityDO.getStartTime(), rewardActivityDO.getEndTime())); - //} + // 1.1 获得所有的活动 + List rewardActivityList = rewardActivityService.getRewardActivityByStatusAndDateTimeLt( + CommonStatusEnum.ENABLE.getStatus(), now); + if (CollUtil.isEmpty(rewardActivityList)) { + return; + } + // 1.2 获得所有的商品信息 + List spuList = productSpuApi.getSpuList(spuIds); + if (CollUtil.isEmpty(spuList)) { + return; + } + + // 2. 构建活动 + for (RewardActivityDO rewardActivity : rewardActivityList) { + // 情况一:所有商品都能参加 + if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) { + buildAppActivityRespVO(rewardActivity, spuIds, activityList); + } + // 情况二:指定商品参加 + if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) { + List fSpuIds = spuList.stream().map(ProductSpuRespDTO::getId).filter(id -> + rewardActivity.getProductScopeValues().contains(id)).toList(); + buildAppActivityRespVO(rewardActivity, fSpuIds, activityList); + } + // 情况三:指定商品类型参加 + if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) { + List fSpuIds = spuList.stream().filter(spuItem -> rewardActivity.getProductScopeValues() + .contains(spuItem.getCategoryId())).map(ProductSpuRespDTO::getId).toList(); + buildAppActivityRespVO(rewardActivity, fSpuIds, activityList); + } + } } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java index 98d3e8d81..9a7135063 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.promotion.dal.dataobject.reward; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; @@ -40,7 +40,7 @@ public class RewardActivityDO extends BaseDO { /** * 状态 * - * 枚举 {@link PromotionActivityStatusEnum} + * 枚举 {@link CommonStatusEnum} */ private Integer status; /** diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java index ca9e9668f..915696967 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java @@ -30,10 +30,6 @@ public interface RewardActivityMapper extends BaseMapperX { .orderByDesc(RewardActivityDO::getId)); } - default List selectListByStatus(Collection statuses) { - return selectList(RewardActivityDO::getStatus, statuses); - } - default List selectListByProductScopeAndStatus(Integer productScope, Integer status) { return selectList(new LambdaQueryWrapperX() .eq(RewardActivityDO::getProductScope, productScope) @@ -53,16 +49,16 @@ public interface RewardActivityMapper extends BaseMapperX { * 获取指定活动编号的活动列表且 * 开始时间和结束时间小于给定时间 dateTime 的活动列表 * - * @param ids 活动编号 + * @param status 状态 * @param dateTime 指定日期 * @return 活动列表 */ - default List selectListByIdsAndDateTimeLt(Collection ids, LocalDateTime dateTime) { + default List selectListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) { return selectList(new LambdaQueryWrapperX() - .in(RewardActivityDO::getId, ids) + .eq(RewardActivityDO::getStatus, status) .lt(RewardActivityDO::getStartTime, dateTime) .gt(RewardActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动 - .orderByDesc(RewardActivityDO::getCreateTime) + .orderByAsc(RewardActivityDO::getStartTime) ); } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java index e2e225608..1d4b978e9 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java @@ -75,11 +75,10 @@ public interface RewardActivityService { /** * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录 * - * @param spuIds spu 编号 * @param status 状态 * @param dateTime 当前日期时间 * @return 满减送活动列表 */ - List getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection spuIds, Integer status, LocalDateTime dateTime); + List getRewardActivityByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime); } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java index 98fa990c1..a6a865eab 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java @@ -1,17 +1,18 @@ package cn.iocoder.yudao.module.promotion.service.reward; -import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi; import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; +import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityBaseVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert; import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper; -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import cn.iocoder.yudao.module.promotion.util.PromotionUtils; import jakarta.annotation.Resource; @@ -20,14 +21,13 @@ import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Objects; +import static cn.hutool.core.collection.CollUtil.intersectionDistinct; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; -import static java.util.Arrays.asList; /** * 满减送活动 Service 实现类 @@ -51,7 +51,7 @@ public class RewardActivityServiceImpl implements RewardActivityService { // 1.1 校验商品范围 validateProductScope(createReqVO.getProductScope(), createReqVO.getProductScopeValues()); // 1.2 校验商品是否冲突 - //validateRewardActivitySpuConflicts(null, createReqVO.getProductSpuIds()); + validateRewardActivitySpuConflicts(null, createReqVO); // 2. 插入 RewardActivityDO rewardActivity = RewardActivityConvert.INSTANCE.convert(createReqVO) @@ -65,13 +65,13 @@ public class RewardActivityServiceImpl implements RewardActivityService { public void updateRewardActivity(RewardActivityUpdateReqVO updateReqVO) { // 1.1 校验存在 RewardActivityDO dbRewardActivity = validateRewardActivityExists(updateReqVO.getId()); - if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能修改噢 + if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动,不能修改噢 throw exception(REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED); } // 1.2 校验商品范围 validateProductScope(updateReqVO.getProductScope(), updateReqVO.getProductScopeValues()); // 1.3 校验商品是否冲突 - //validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO.getProductSpuIds()); + validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO); // 2. 更新 RewardActivityDO updateObj = RewardActivityConvert.INSTANCE.convert(updateReqVO) @@ -82,17 +82,13 @@ public class RewardActivityServiceImpl implements RewardActivityService { @Override public void closeRewardActivity(Long id) { // 校验存在 - // TODO @puhui999:去掉 PromotionActivityStatusEnum,使用 CommonStatus 作为状态哈。开启,关闭 RewardActivityDO dbRewardActivity = validateRewardActivityExists(id); - if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能关闭噢 + if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动,不能关闭噢 throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED); } - if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.END.getStatus())) { // 已关闭的活动,不能关闭噢 - throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END); - } // 更新 - RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()); + RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus()); rewardActivityMapper.updateById(updateObj); } @@ -100,7 +96,7 @@ public class RewardActivityServiceImpl implements RewardActivityService { public void deleteRewardActivity(Long id) { // 校验存在 RewardActivityDO dbRewardActivity = validateRewardActivityExists(id); - if (!dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 未关闭的活动,不能删除噢 + if (dbRewardActivity.getStatus().equals(CommonStatusEnum.ENABLE.getStatus())) { // 未关闭的活动,不能删除噢 throw exception(REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED); } @@ -116,27 +112,30 @@ public class RewardActivityServiceImpl implements RewardActivityService { return activity; } - // TODO @芋艿:逻辑有问题,需要优化;要分成全场、和指定来校验; - // TODO @puhui999: 下次提交 fix /** * 校验商品参加的活动是否冲突 * - * @param id 活动编号 - * @param spuIds 商品 SPU 编号数组 + * @param id 活动编号 + * @param rewardActivity 请求 */ - private void validateRewardActivitySpuConflicts(Long id, Collection spuIds) { - if (CollUtil.isEmpty(spuIds)) { - return; - } - // 查询商品参加的活动 - List rewardActivityList = getRewardActivityListBySpuIds(spuIds, - asList(PromotionActivityStatusEnum.WAIT.getStatus(), PromotionActivityStatusEnum.RUN.getStatus())); + private void validateRewardActivitySpuConflicts(Long id, RewardActivityBaseVO rewardActivity) { + List list = rewardActivityMapper.selectList(RewardActivityDO::getProductScope, + rewardActivity.getProductScope(), RewardActivityDO::getStatus, CommonStatusEnum.ENABLE.getStatus()); if (id != null) { // 排除自己这个活动 - rewardActivityList.removeIf(activity -> id.equals(activity.getId())); + list.removeIf(activity -> id.equals(activity.getId())); } - // 如果非空,则说明冲突 - if (CollUtil.isNotEmpty(rewardActivityList)) { - throw exception(REWARD_ACTIVITY_SPU_CONFLICTS); + + // 情况一:全部商品参加 + if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope()) && !list.isEmpty()) { + throw exception(REWARD_ACTIVITY_SCOPE_ALL_EXISTS); + } + if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) || // 情况二:指定商品参加 + PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) { // 情况三:指定商品类型参加 + if (anyMatch(list, item -> !intersectionDistinct(item.getProductScopeValues(), + rewardActivity.getProductScopeValues()).isEmpty())) { + throw exception(PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) ? + REWARD_ACTIVITY_SPU_CONFLICTS : REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS); + } } } @@ -148,21 +147,6 @@ public class RewardActivityServiceImpl implements RewardActivityService { } } - /** - * 获得商品参加的满减送活动的数组 - * - * @param spuIds 商品 SPU 编号数组 - * @param statuses 活动状态数组 - * @return 商品参加的满减送活动的数组 - */ - private List getRewardActivityListBySpuIds(Collection spuIds, - Collection statuses) { - // TODO @puhui999: 下次 fix - //List list = rewardActivityMapper.selectListByStatus(statuses); - //return CollUtil.filter(list, activity -> CollUtil.containsAny(activity.getProductSpuIds(), spuIds)); - return List.of(); - } - @Override public RewardActivityDO getRewardActivity(Long id) { return rewardActivityMapper.selectById(id); @@ -176,31 +160,13 @@ public class RewardActivityServiceImpl implements RewardActivityService { @Override public List getMatchRewardActivityList(Collection spuIds) { // TODO 芋艿:待实现;先指定,然后再全局的; -// // 如果有全局活动,则直接选择它 -// List allActivities = rewardActivityMapper.selectListByProductScopeAndStatus( -// PromotionProductScopeEnum.ALL.getScope(), PromotionActivityStatusEnum.RUN.getStatus()); -// if (CollUtil.isNotEmpty(allActivities)) { -// return MapUtil.builder(allActivities.get(0), spuIds).build(); -// } -// -// // 查询某个活动参加的活动 -// List productActivityList = getRewardActivityListBySpuIds(spuIds, -// singleton(PromotionActivityStatusEnum.RUN.getStatus())); -// return convertMap(productActivityList, activity -> activity, -// rewardActivityDO -> intersectionDistinct(rewardActivityDO.getProductSpuIds(), spuIds)); // 求交集返回 - return null; + List list = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, CommonStatusEnum.ENABLE.getStatus()); + return BeanUtils.toBean(list, RewardActivityMatchRespDTO.class); } @Override - public List getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection spuIds, Integer status, LocalDateTime dateTime) { - // 1. 查询出指定 spuId 的 spu 参加的活动 - List rewardActivityList = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, status); - if (CollUtil.isEmpty(rewardActivityList)) { - return Collections.emptyList(); - } - - // 2. 查询活动详情 - return rewardActivityMapper.selectListByIdsAndDateTimeLt(convertSet(rewardActivityList, RewardActivityDO::getId), dateTime); + public List getRewardActivityByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) { + return rewardActivityMapper.selectListByStatusAndDateTimeLt(status, dateTime); } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java b/yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java index ca8d85fa7..7e7cf14db 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java @@ -1,21 +1,23 @@ package cn.iocoder.yudao.module.promotion.service.reward; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper; -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; +import jakarta.annotation.Resource; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; -import jakarta.annotation.Resource; import java.time.Duration; +import java.util.List; import java.util.Set; import static cn.hutool.core.util.RandomUtil.randomEle; @@ -27,15 +29,15 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServic import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.REWARD_ACTIVITY_NOT_EXISTS; -import static java.util.Arrays.asList; +import static com.google.common.primitives.Longs.asList; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.*; /** -* {@link RewardActivityServiceImpl} 的单元测试类 -* -* @author 芋道源码 -*/ + * {@link RewardActivityServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ @Disabled // TODO 芋艿:后续 fix 补充的单测 @Import(RewardActivityServiceImpl.class) public class RewardActivityServiceImplTest extends BaseDbUnitTest { @@ -63,7 +65,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { // 校验记录的属性是否正确 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(rewardActivityId); assertPojoEquals(reqVO, rewardActivity, "rules"); - assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus()); + assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus()); for (int i = 0; i < reqVO.getRules().size(); i++) { assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i)); } @@ -72,7 +74,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { @Test public void testUpdateRewardActivity_success() { // mock 数据 - RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus())); + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 // 准备参数 RewardActivityUpdateReqVO reqVO = randomPojo(RewardActivityUpdateReqVO.class, o -> { @@ -88,7 +90,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { // 校验是否更新正确 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, rewardActivity, "rules"); - assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus()); + assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus()); for (int i = 0; i < reqVO.getRules().size(); i++) { assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i)); } @@ -97,7 +99,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { @Test public void testCloseRewardActivity() { // mock 数据 - RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus())); + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbRewardActivity.getId(); @@ -106,7 +108,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { rewardActivityService.closeRewardActivity(id); // 校验状态 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(id); - assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.CLOSE.getStatus()); + assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus()); } @Test @@ -121,15 +123,15 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { @Test public void testDeleteRewardActivity_success() { // mock 数据 - RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus())); + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbRewardActivity.getId(); // 调用 rewardActivityService.deleteRewardActivity(id); - // 校验数据不存在了 - assertNull(rewardActivityMapper.selectById(id)); + // 校验数据不存在了 + assertNull(rewardActivityMapper.selectById(id)); } @Test @@ -143,78 +145,82 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest { @Test public void testGetRewardActivityPage() { - // mock 数据 - RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到 - o.setName("芋艿"); - o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()); - }); - rewardActivityMapper.insert(dbRewardActivity); - // 测试 name 不匹配 - rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆"))); - // 测试 status 不匹配 - rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()))); - // 准备参数 - RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO(); - reqVO.setName("芋艿"); - reqVO.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()); + // mock 数据 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到 + o.setName("芋艿"); + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + }); + rewardActivityMapper.insert(dbRewardActivity); + // 测试 name 不匹配 + rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆"))); + // 测试 status 不匹配 + rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()))); + // 准备参数 + RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO(); + reqVO.setName("芋艿"); + reqVO.setStatus(CommonStatusEnum.DISABLE.getStatus()); - // 调用 - PageResult pageResult = rewardActivityService.getRewardActivityPage(reqVO); - // 断言 - assertEquals(1, pageResult.getTotal()); - assertEquals(1, pageResult.getList().size()); - assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules"); + // 调用 + PageResult pageResult = rewardActivityService.getRewardActivityPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules"); } @Test public void testGetRewardActivities_all() { // mock 数据 - RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) + RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()) .setProductScope(PromotionProductScopeEnum.ALL.getScope())); rewardActivityMapper.insert(allActivity); - RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) - .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L))); + RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))); rewardActivityMapper.insert(productActivity); // 准备参数 Set spuIds = asSet(1L, 2L); // 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList - //Map> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds); + List matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds); // 断言 - //assertEquals(matchRewardActivities.size(), 1); - //Map.Entry> next = matchRewardActivities.entrySet().iterator().next(); - //assertPojoEquals(next.getKey(), allActivity); - //assertEquals(next.getValue(), spuIds); + assertEquals(matchRewardActivityList.size(), 1); + matchRewardActivityList.forEach((activity) -> { + if (activity.getId().equals(productActivity.getId())) { + assertPojoEquals(activity, productActivity); + assertEquals(activity.getProductScopeValues(), asList(1L, 2L)); + } else { + fail(); + } + }); } @Test public void testGetRewardActivities_product() { // mock 数据 - // TODO @puhui999:有单测的问题,也一起瞅瞅 - RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) - .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L))); + RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))); rewardActivityMapper.insert(productActivity01); - RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) - .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(singletonList(3L))); + RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L))); rewardActivityMapper.insert(productActivity02); // 准备参数 Set spuIds = asSet(1L, 2L, 3L); // 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList - //Map> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds); + List matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds); // 断言 - //assertEquals(matchRewardActivities.size(), 2); - //matchRewardActivities.forEach((activity, activitySpuIds) -> { - // if (activity.getId().equals(productActivity01.getId())) { - // assertPojoEquals(activity, productActivity01); - // assertEquals(activitySpuIds, asSet(1L, 2L)); - // } else if (activity.getId().equals(productActivity02.getId())) { - // assertPojoEquals(activity, productActivity02); - // assertEquals(activitySpuIds, asSet(3L)); - // } else { - // fail(); - // } - //}); + assertEquals(matchRewardActivityList.size(), 2); + matchRewardActivityList.forEach((activity) -> { + if (activity.getId().equals(productActivity01.getId())) { + assertPojoEquals(activity, productActivity01); + assertEquals(activity.getProductScopeValues(), asList(1L, 2L)); + } else if (activity.getId().equals(productActivity02.getId())) { + assertPojoEquals(activity, productActivity02); + assertEquals(activity.getProductScopeValues(), singletonList(3L)); + } else { + fail(); + } + }); } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java index 4374783d2..0c25dcb30 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -10,10 +10,10 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; +import jakarta.annotation.Resource; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import jakarta.annotation.Resource; import java.util.List; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; @@ -96,7 +96,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator private List filterMatchCouponOrderItems(TradePriceCalculateRespBO result, RewardActivityMatchRespDTO rewardActivity) { return filterList(result.getItems(), - orderItem -> CollUtil.contains(rewardActivity.getSpuIds(), orderItem.getSpuId())); + orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getSpuId())); } /** diff --git a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java index de72ed616..219ae727e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java @@ -63,10 +63,10 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest // mock 方法(限时折扣 DiscountActivity 信息) when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L, 3L)))).thenReturn(asList( randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号") - .setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType()) + .setProductScopeValues(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType()) .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(200).setDiscountPrice(70)))), randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号") - .setSpuIds(singletonList(3L)).setConditionType(PromotionConditionTypeEnum.COUNT.getType()) + .setProductScopeValues(singletonList(3L)).setConditionType(PromotionConditionTypeEnum.COUNT.getType()) .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10), new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60), // 最大可满足,因为是 4 个 new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100)))) @@ -175,7 +175,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest // mock 方法(限时折扣 DiscountActivity 信息) when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L)))).thenReturn(singletonList( randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号") - .setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType()) + .setProductScopeValues(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType()) .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(351).setDiscountPrice(70)))) )); From 63fcc699306f40270ed3ffe048b9b4bcbb3226e6 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Tue, 27 Aug 2024 16:49:02 +0800 Subject: [PATCH 067/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E:=20=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../combination/CombinationRecordServiceImpl.java | 9 +++++++-- .../yudao/module/trade/api/order/TradeOrderApi.java | 8 +++++--- .../trade/enums/order/TradeOrderCancelTypeEnum.java | 3 ++- .../module/trade/api/order/TradeOrderApiImpl.java | 7 ++++--- .../trade/service/order/TradeOrderUpdateService.java | 11 ++++++----- .../service/order/TradeOrderUpdateServiceImpl.java | 8 ++++---- 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java index cb70b8ea9..c1449d60d 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java @@ -27,6 +27,7 @@ import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStat import cn.iocoder.yudao.module.system.api.social.SocialClientApi; import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum; import jakarta.annotation.Nullable; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -37,7 +38,10 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; @@ -335,7 +339,8 @@ public class CombinationRecordServiceImpl implements CombinationRecordService { List headAndRecords = updateBatchCombinationRecords(headRecord, CombinationRecordStatusEnum.FAILED); // 2. 订单取消 - headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId())); + headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId(), + TradeOrderCancelTypeEnum.COMBINATION_CLOSE)); } /** diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java index f36f7bc95..d21e88a44 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.trade.api.order; import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum; import java.util.Collection; import java.util.List; @@ -31,9 +32,10 @@ public interface TradeOrderApi { /** * 取消支付订单 * - * @param userId 用户编号 - * @param orderId 订单编号 + * @param userId 用户编号 + * @param orderId 订单编号 + * @param cancelTypeEnum 取消类型 */ - void cancelPaidOrder(Long userId, Long orderId); + void cancelPaidOrder(Long userId, Long orderId, TradeOrderCancelTypeEnum cancelTypeEnum); } diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderCancelTypeEnum.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderCancelTypeEnum.java index 8ec1e9b16..cfd25468f 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderCancelTypeEnum.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderCancelTypeEnum.java @@ -17,7 +17,8 @@ public enum TradeOrderCancelTypeEnum implements IntArrayValuable { PAY_TIMEOUT(10, "超时未支付"), AFTER_SALE_CLOSE(20, "退款关闭"), - MEMBER_CANCEL(30, "买家取消"); + MEMBER_CANCEL(30, "买家取消"), + COMBINATION_CLOSE(40, "拼团关闭"); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderCancelTypeEnum::getType).toArray(); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java index 7426585d9..edb675f29 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java @@ -2,12 +2,13 @@ package cn.iocoder.yudao.module.trade.api.order; import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO; import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum; import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService; import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService; +import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import jakarta.annotation.Resource; import java.util.Collection; import java.util.List; @@ -36,8 +37,8 @@ public class TradeOrderApiImpl implements TradeOrderApi { } @Override - public void cancelPaidOrder(Long userId, Long orderId) { - tradeOrderUpdateService.cancelPaidOrder(userId, orderId); + public void cancelPaidOrder(Long userId, Long orderId, TradeOrderCancelTypeEnum cancelTypeEnum) { + tradeOrderUpdateService.cancelPaidOrder(userId, orderId, cancelTypeEnum); } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java index e16a08bd7..b38decc17 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.trade.service.order; -import cn.iocoder.yudao.framework.common.enums.TerminalEnum; import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderRemarkReqVO; import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderUpdateAddressReqVO; @@ -10,7 +9,7 @@ import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettle import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO; import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; - +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum; import jakarta.validation.constraints.NotNull; /** @@ -188,12 +187,14 @@ public interface TradeOrderUpdateService { void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId); // TODO 芋艿:拼团取消,不调这个接口哈; + /** * 取消支付订单 * - * @param userId 用户编号 - * @param orderId 订单编号 + * @param userId 用户编号 + * @param orderId 订单编号 + * @param cancelTypeEnum 取消类型 */ - void cancelPaidOrder(Long userId, Long orderId); + void cancelPaidOrder(Long userId, Long orderId, TradeOrderCancelTypeEnum cancelTypeEnum); } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index c005781e3..36195c117 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -858,13 +858,13 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @Override @Transactional(rollbackFor = Exception.class) - public void cancelPaidOrder(Long userId, Long orderId) { - // TODO @puhui999:可能要加一个拼团取消;TradeOrderCancelTypeEnum.AFTER_SALE_CLOSE;然后参数传入下; + public void cancelPaidOrder(Long userId, Long orderId, TradeOrderCancelTypeEnum cancelTypeEnum) { // 1.1 检验订单存在 TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); if (order == null) { throw exception(ORDER_NOT_FOUND); } + // 1.2 校验订单是否支付 if (!order.getPayStatus()) { throw exception(ORDER_CANCEL_PAID_FAIL, "已支付"); @@ -875,13 +875,13 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { } // 2.1 取消订单 - cancelOrder0(order, TradeOrderCancelTypeEnum.AFTER_SALE_CLOSE); + cancelOrder0(order, cancelTypeEnum); // 2.2 创建退款单 payRefundApi.createRefund(new PayRefundCreateReqDTO() .setAppKey(tradeOrderProperties.getPayAppKey()).setUserIp(getClientIP()) // 支付应用 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 .setMerchantRefundId(String.valueOf(order.getId())) - .setReason("取消支付订单").setPrice(order.getPayPrice()));// 价格信息 + .setReason(cancelTypeEnum.getName()).setPrice(order.getPayPrice()));// 价格信息 } /** From 1c1abae5bbf8357fb32044e766cb92b06603dcfe Mon Sep 17 00:00:00 2001 From: scholar <1145227973@qq.com> Date: Wed, 28 Aug 2024 10:51:19 +0800 Subject: [PATCH 068/136] =?UTF-8?q?=E4=B8=83=E7=89=9B=E4=BA=91=E7=9F=AD?= =?UTF-8?q?=E4=BF=A1=E5=AE=9E=E7=8E=B0=EF=BC=8C=E8=AF=84=E5=AE=A1=E6=84=8F?= =?UTF-8?q?=E8=A7=81=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/common/util/http/HttpUtils.java | 2 - .../sms/core/client/impl/QiniuSmsClient.java | 85 ++++++++----------- .../core/client/impl/QiniuSmsClientTest.java | 25 ++++-- .../sms/core/client/impl/SmsClientTests.java | 2 - 4 files changed, 51 insertions(+), 63 deletions(-) diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java index 1697d097f..456b4007e 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java @@ -135,7 +135,6 @@ public class HttpUtils { * @return 请求结果 */ public static String post(String url, Map headers, String requestBody) { - try (HttpResponse response = HttpRequest.post(url) .addHeaders(headers) .body(requestBody) @@ -154,7 +153,6 @@ public class HttpUtils { * @return 请求结果 */ public static String get(String url, Map headers) { - try (HttpResponse response = HttpRequest.get(url) .addHeaders(headers) .execute()) { diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java index c0a2b60ac..4fbb8649d 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java @@ -1,10 +1,13 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; +import cn.hutool.core.collection.CollStreamUtil; import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; -import cn.hutool.crypto.digest.HMac; +import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.digest.HmacAlgorithm; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; @@ -18,11 +21,7 @@ import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProp import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; -import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; - import java.util.*; -import java.util.stream.Collectors; /** * 七牛云短信客户端的实现类 @@ -45,69 +44,60 @@ public class QiniuSmsClient extends AbstractSmsClient { Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); } - @Override protected void doInit() { } - @Override + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { - // 1. 执行请求 // 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages LinkedHashMap body = new LinkedHashMap<>(); - Map paramsMap = templateParams.stream() - .collect(Collectors.toMap(KeyValue::getKey, KeyValue::getValue)); - body.put("template_id", apiTemplateId); body.put("mobile", mobile); - body.put("parameters", paramsMap); + body.put("parameters", CollStreamUtil.toMap(templateParams, KeyValue::getKey, KeyValue::getValue)); body.put("seq", Long.toString(sendLogId)); - JSONObject response = request("POST", body, null); + JSONObject response = request("POST", body, PATH); // 2. 解析请求 + if (ObjectUtil.isNotEmpty(response.getStr("error"))){//短信请求失败 + return new SmsSendRespDTO().setSuccess(false) + .setApiCode(response.getStr("error")) + .setApiRequestId(response.getStr("request_id")) + .setApiMsg(response.getStr("message")); + } + return new SmsSendRespDTO().setSuccess(response.containsKey("message_id")) .setSerialNo(response.getStr("message_id")); } - /** * 请求七牛云短信 * * @see * @param httpMethod http请求方法 - * @param queryParams 请求参数 + * @param body http请求消息体 + * @param path URL path * @return 请求结果 */ - private JSONObject request(String httpMethod, LinkedHashMap body, Map queryParams) { - - String signature = ""; - String templateIdPath = ""; - - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - String signDate = dateFormat.format(new Date()); - + private JSONObject request(String httpMethod, LinkedHashMap body, String path) { + String signDate = DateUtil.date().setTimeZone(TimeZone.getTimeZone("UTC")).toString("yyyyMMdd'T'HHmmss'Z'"); //请求头 Map header = new HashMap<>(4); header.put("HOST", HOST); - header.put("Authorization", signature); + header.put("Authorization", getSignature(httpMethod, HOST, path, body != null ? JSONUtil.toJsonStr(body) : "", signDate)); header.put("Content-Type", "application/json"); header.put("X-Qiniu-Date", signDate); String responseBody =""; - if(Objects.equals(httpMethod, "POST")){ - header.put("Authorization", getSignature(httpMethod, HOST, PATH, JSONUtil.toJsonStr(body), signDate)); - responseBody = HttpUtils.post("https://" + HOST + PATH, header, JSONUtil.toJsonStr(body)); - }else { // GET - templateIdPath = TEMPLATE_PATH + "/" + queryParams.get("template_id"); - header.put("Authorization", getSignature(httpMethod, HOST, templateIdPath, null, signDate)); - responseBody = HttpUtils.get("https://" + HOST + templateIdPath, header); + if (Objects.equals(httpMethod, "POST")){// POST 发送短消息用POST请求 + responseBody = HttpUtils.post("https://" + HOST + path, header, JSONUtil.toJsonStr(body)); + }else { // GET 查询template状态用GET请求 + responseBody = HttpUtils.get("https://" + HOST + path, header); } return JSONUtil.parseObj(responseBody); } public String getSignature(String method, String host, String path, String body, String signDate) { - StringBuilder dataToSign = new StringBuilder(); dataToSign.append(method.toUpperCase()).append(" ").append(path); dataToSign.append("\nHost: ").append(host); @@ -117,18 +107,15 @@ public class QiniuSmsClient extends AbstractSmsClient { if (ObjectUtil.isNotEmpty(body)) { dataToSign.append(body); } - HMac hMac = new HMac(HmacAlgorithm.HmacSHA1, properties.getApiSecret().getBytes(StandardCharsets.UTF_8)); - byte[] signData = hMac.digest(dataToSign.toString().getBytes(StandardCharsets.UTF_8)); - String encodedSignature = Base64.getEncoder().encodeToString(signData); + String encodedSignature = SecureUtil.hmac(HmacAlgorithm.HmacSHA1, properties.getApiSecret()).digestBase64(dataToSign.toString(), true); return "Qiniu " + properties.getApiKey() + ":" + encodedSignature; } @Override public List parseSmsReceiveStatus(String text) { - JSONObject status = JSONUtil.parseObj(text); - //字段参考 https://developer.qiniu.com/sms/5910/message-push + // 字段参考 https://developer.qiniu.com/sms/5910/message-push return ListUtil.of(new SmsReceiveRespDTO() .setSuccess("DELIVRD".equals(status.getJSONArray("items").getJSONObject(0).getStr("status"))) // 是否接收成功 .setErrorMsg(status.getJSONArray("items").getJSONObject(0).getStr("status")) @@ -142,16 +129,13 @@ public class QiniuSmsClient extends AbstractSmsClient { public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { // 1. 执行请求 // 参考链接 https://developer.qiniu.com/sms/5969/query-a-single-template - HashMap queryParam = new HashMap<>(); - queryParam.put("template_id", apiTemplateId); - JSONObject response = request("GET", null, queryParam); - + JSONObject response = request("GET", null, TEMPLATE_PATH + "/" + apiTemplateId); // 2.1 请求失败 - String status = response.getStr("audit_status"); - if (!Objects.equals(status, "passed")) { + if (ObjUtil.notEqual(response.getStr("audit_status"), "passed")) { log.error("[getSmsTemplate][模版编号({}) 响应不正确({})]", apiTemplateId, response); return null; } + // 2.2 请求成功 return new SmsTemplateRespDTO() .setId(response.getStr("id")) @@ -162,11 +146,12 @@ public class QiniuSmsClient extends AbstractSmsClient { @VisibleForTesting Integer convertSmsTemplateAuditStatus(String templateStatus) { - - if(Objects.equals(templateStatus, "passed")){ - return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); - }else { - throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus)); - } + return switch (templateStatus) { + case "passed" -> SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + case "reviewing" -> SmsTemplateAuditStatusEnum.CHECKING.getStatus(); + case "rejected" -> SmsTemplateAuditStatusEnum.FAIL.getStatus(); + case null, default -> + throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus)); + }; } } diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java index c64c39470..c3e896695 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java @@ -29,7 +29,6 @@ import static org.mockito.Mockito.mockStatic; * @author scholar */ public class QiniuSmsClientTest extends BaseMockitoUnitTest { - private final SmsChannelProperties properties = new SmsChannelProperties() .setApiKey(randomString())// 随机一个 apiKey,避免构建报错 .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 @@ -46,7 +45,6 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { @Test public void testDoSendSms_success() throws Throwable { - try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { // 准备参数 Long sendLogId = randomLongId(); @@ -56,9 +54,7 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) - .thenReturn( - "{\"message_id\":\"17245678901\"}" - ); + .thenReturn("{\"message_id\":\"17245678901\"}"); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); @@ -77,17 +73,17 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { String apiTemplateId = randomString() + " " + randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); - // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) - .thenReturn( - "{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}" - ); + .thenReturn("{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}"); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); // 断言 assertFalse(result.getSuccess()); + assertEquals("BadToken", result.getApiCode()); + assertEquals("Your authorization token is invalid", result.getApiMsg()); + assertEquals("etziWcJFo1C8Ne8X", result.getApiRequestId()); } } @@ -125,4 +121,15 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { assertEquals(123, statuses.getFirst().getLogId()); } + @Test + public void testConvertSmsTemplateAuditStatus() { + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), + smsClient.convertSmsTemplateAuditStatus("passed")); + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), + smsClient.convertSmsTemplateAuditStatus("reviewing")); + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), + smsClient.convertSmsTemplateAuditStatus("rejected")); + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("unknown"), + "未知审核状态(3)"); + } } \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java index 3752e5763..4f003ebaf 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java @@ -116,7 +116,6 @@ public class SmsClientTests { @Test @Disabled public void testQiniuSmsClient_sendSms() throws Throwable { - SmsChannelProperties properties = new SmsChannelProperties() .setApiKey("SMS_QINIU_ACCESS_KEY") .setApiSecret("SMS_QINIU_SECRET_KEY"); @@ -135,7 +134,6 @@ public class SmsClientTests { @Test @Disabled public void testQiniuSmsClient_getSmsTemplate() throws Throwable { - SmsChannelProperties properties = new SmsChannelProperties() .setApiKey("SMS_QINIU_ACCESS_KEY") .setApiSecret("SMS_QINIU_SECRET_KEY"); From cccad2c6c1974dc7c98fcfaa12c86321bab7bf6f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 28 Aug 2024 13:19:36 +0800 Subject: [PATCH 069/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E6=8B=BC=E5=9B=A2=E7=9A=84=E6=94=AF=E4=BB=98=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/PromotionProductScopeEnum.java | 8 ++--- .../app/activity/AppActivityController.java | 35 ++++++++++--------- .../service/reward/RewardActivityService.java | 2 +- .../reward/RewardActivityServiceImpl.java | 2 +- .../order/TradeOrderUpdateService.java | 7 ++-- .../order/TradeOrderUpdateServiceImpl.java | 7 ++-- .../TradeRewardActivityPriceCalculator.java | 7 ++-- 7 files changed, 35 insertions(+), 33 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java index c082e190f..4a95cb1fa 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java @@ -1,11 +1,11 @@ package cn.iocoder.yudao.module.promotion.enums.common; -import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; +import java.util.Objects; /** * 营销的商品范围枚举 @@ -37,15 +37,15 @@ public enum PromotionProductScopeEnum implements IntArrayValuable { } public static boolean isAll(Integer scope) { - return ObjUtil.equal(scope, ALL.scope); + return Objects.equals(scope, ALL.scope); } public static boolean isSpu(Integer scope) { - return ObjUtil.equal(scope, SPU.scope); + return Objects.equals(scope, SPU.scope); } public static boolean isCategory(Integer scope) { - return ObjUtil.equal(scope, CATEGORY.scope); + return Objects.equals(scope, CATEGORY.scope); } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java index fe15c0f71..fae7fa54d 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java @@ -53,6 +53,7 @@ public class AppActivityController { private DiscountActivityService discountActivityService; @Resource private RewardActivityService rewardActivityService; + @Resource private ProductSpuApi productSpuApi; @@ -91,7 +92,7 @@ public class AppActivityController { // 4. 限时折扣活动 getDiscountActivities(spuIds, now, activityList); // 5. 满减送活动 - getRewardActivities(spuIds, now, activityList); + getRewardActivityList(spuIds, now, activityList); return activityList; } @@ -148,23 +149,9 @@ public class AppActivityController { item.getName(), productMap.get(item.getId()), item.getStartTime(), item.getEndTime()))); } - private static void buildAppActivityRespVO(RewardActivityDO rewardActivity, Collection spuIds, - List activityList) { - for (Long spuId : spuIds) { - // 校验商品是否已经加入过活动 - if (anyMatch(activityList, appActivity -> ObjUtil.equal(appActivity.getId(), rewardActivity.getId()) && - ObjUtil.equal(appActivity.getSpuId(), spuId))) { - continue; - } - activityList.add(new AppActivityRespVO(rewardActivity.getId(), - PromotionTypeEnum.REWARD_ACTIVITY.getType(), rewardActivity.getName(), spuId, - rewardActivity.getStartTime(), rewardActivity.getEndTime())); - } - } - - private void getRewardActivities(Collection spuIds, LocalDateTime now, List activityList) { + private void getRewardActivityList(Collection spuIds, LocalDateTime now, List activityList) { // 1.1 获得所有的活动 - List rewardActivityList = rewardActivityService.getRewardActivityByStatusAndDateTimeLt( + List rewardActivityList = rewardActivityService.getRewardActivityListByStatusAndDateTimeLt( CommonStatusEnum.ENABLE.getStatus(), now); if (CollUtil.isEmpty(rewardActivityList)) { return; @@ -196,4 +183,18 @@ public class AppActivityController { } } + private static void buildAppActivityRespVO(RewardActivityDO rewardActivity, Collection spuIds, + List activityList) { + for (Long spuId : spuIds) { + // 校验商品是否已经加入过活动 + if (anyMatch(activityList, appActivity -> ObjUtil.equal(appActivity.getId(), rewardActivity.getId()) && + ObjUtil.equal(appActivity.getSpuId(), spuId))) { + continue; + } + activityList.add(new AppActivityRespVO(rewardActivity.getId(), + PromotionTypeEnum.REWARD_ACTIVITY.getType(), rewardActivity.getName(), spuId, + rewardActivity.getStartTime(), rewardActivity.getEndTime())); + } + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java index 1d4b978e9..27cc86c33 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java @@ -79,6 +79,6 @@ public interface RewardActivityService { * @param dateTime 当前日期时间 * @return 满减送活动列表 */ - List getRewardActivityByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime); + List getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime); } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java index a6a865eab..1ad0ae48f 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java @@ -165,7 +165,7 @@ public class RewardActivityServiceImpl implements RewardActivityService { } @Override - public List getRewardActivityByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) { + public List getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) { return rewardActivityMapper.selectListByStatusAndDateTimeLt(status, dateTime); } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java index b38decc17..d03826924 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java @@ -186,15 +186,14 @@ public interface TradeOrderUpdateService { */ void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId); - // TODO 芋艿:拼团取消,不调这个接口哈; - + // TODO @puhui999:不传递枚举哈。因为 rpc 不好支持。 /** * 取消支付订单 * * @param userId 用户编号 * @param orderId 订单编号 - * @param cancelTypeEnum 取消类型 + * @param cancelType 取消类型 */ - void cancelPaidOrder(Long userId, Long orderId, TradeOrderCancelTypeEnum cancelTypeEnum); + void cancelPaidOrder(Long userId, Long orderId, TradeOrderCancelTypeEnum cancelType); } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index 36195c117..3eda99411 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -858,7 +858,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @Override @Transactional(rollbackFor = Exception.class) - public void cancelPaidOrder(Long userId, Long orderId, TradeOrderCancelTypeEnum cancelTypeEnum) { + public void cancelPaidOrder(Long userId, Long orderId, TradeOrderCancelTypeEnum cancelType) { + // TODO @puhui999:这里校验下 cancelType 只允许拼团关闭; // 1.1 检验订单存在 TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); if (order == null) { @@ -875,13 +876,13 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { } // 2.1 取消订单 - cancelOrder0(order, cancelTypeEnum); + cancelOrder0(order, cancelType); // 2.2 创建退款单 payRefundApi.createRefund(new PayRefundCreateReqDTO() .setAppKey(tradeOrderProperties.getPayAppKey()).setUserIp(getClientIP()) // 支付应用 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 .setMerchantRefundId(String.valueOf(order.getId())) - .setReason(cancelTypeEnum.getName()).setPrice(order.getPayPrice()));// 价格信息 + .setReason(cancelType.getName()).setPrice(order.getPayPrice()));// 价格信息 } /** diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java index 0c25dcb30..490c2aea7 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -52,7 +52,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result, RewardActivityMatchRespDTO rewardActivity) { // 1.1 获得满减送的订单项(商品)列表 - List orderItems = filterMatchCouponOrderItems(result, rewardActivity); + List orderItems = filterMatchActivityOrderItems(result, rewardActivity); if (CollUtil.isEmpty(orderItems)) { return; } @@ -93,8 +93,9 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator * @param rewardActivity 满减送活动 * @return 订单项(商品)列表 */ - private List filterMatchCouponOrderItems(TradePriceCalculateRespBO result, - RewardActivityMatchRespDTO rewardActivity) { + private List filterMatchActivityOrderItems(TradePriceCalculateRespBO result, + RewardActivityMatchRespDTO rewardActivity) { + // TODO @puhui999:是不是得根据类型过滤哈 return filterList(result.getItems(), orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getSpuId())); } From 10c0b86f9b88c795f73ba506b6ac8d8727addbff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=AE=87=E5=BA=86?= Date: Wed, 28 Aug 2024 05:58:19 +0000 Subject: [PATCH 070/136] =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E7=A7=9F=E6=88=B7Job=E9=94=99=E8=AF=AF=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 杨宇庆 --- .../iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java | 1 + 1 file changed, 1 insertion(+) diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java index 732a0732e..76fd98ecb 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java @@ -46,6 +46,7 @@ public class TenantJobAspect { try { joinPoint.proceed(); } catch (Throwable e) { + log.error("occur error while executing job with tenant {}", tenantId, e); results.put(tenantId, ExceptionUtil.getRootCauseMessage(e)); } }); From ed2296e4c772724004d92b0b0dc3b209cd70a78b Mon Sep 17 00:00:00 2001 From: xiaoxin <718949661@qq.com> Date: Wed, 28 Aug 2024 16:01:10 +0800 Subject: [PATCH 071/136] =?UTF-8?q?=E3=80=90=E8=A7=A3=E5=86=B3todo?= =?UTF-8?q?=E3=80=91AI=20=E7=9F=A5=E8=AF=86=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../knowledge/AiKnowledgeController.java | 4 +-- ...nowledgeBaseDO.java => AiKnowledgeDO.java} | 5 ++- .../knowledge/AiKnowledgeDocumentDO.java | 2 +- .../knowledge/AiKnowledgeBaseMapper.java | 4 +-- .../service/knowledge/AiEmbeddingService.java | 27 -------------- .../knowledge/AiEmbeddingServiceImpl.java | 35 ------------------- .../AiKnowledgeDocumentServiceImpl.java | 24 ++++++------- ...seService.java => AiKnowledgeService.java} | 2 +- ...eImpl.java => AiKnowledgeServiceImpl.java} | 31 ++++++---------- .../ai/config/YudaoAiAutoConfiguration.java | 10 +++++- 10 files changed, 39 insertions(+), 105 deletions(-) rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/{AiKnowledgeBaseDO.java => AiKnowledgeDO.java} (81%) delete mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java delete mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/{AiKnowledgeBaseService.java => AiKnowledgeService.java} (94%) rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/{AiKnowledgeBaseServiceImpl.java => AiKnowledgeServiceImpl.java} (66%) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java index 9d9c99a9a..9eae6b70c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeCreateMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdateMyReqVO; -import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeBaseService; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; @@ -19,7 +19,7 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti public class AiKnowledgeController { @Resource - private AiKnowledgeBaseService knowledgeBaseService; + private AiKnowledgeService knowledgeBaseService; @PostMapping("/create-my") @Operation(summary = "创建【我的】知识库") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java similarity index 81% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java index d33114f2d..89e7486dc 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeBaseDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java @@ -10,15 +10,14 @@ import lombok.Data; import java.util.List; -// TODO @xin:要不把 AiKnowledgeBaseDO 改成 AiKnowledgeDO。感觉 base 后缀,感觉有点奇怪(让人以为是基类)。然后,我们很多地方的外键编号,都是 knowledgeId /** * AI 知识库 DO * * @author xiaoxin */ -@TableName(value = "ai_knowledge_base", autoResultMap = true) +@TableName(value = "ai_knowledge", autoResultMap = true) @Data -public class AiKnowledgeBaseDO extends BaseDO { +public class AiKnowledgeDO extends BaseDO { /** * 编号 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java index 486602509..c5e526cce 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java @@ -24,7 +24,7 @@ public class AiKnowledgeDocumentDO extends BaseDO { /** * 知识库编号 * - * 关联 {@link AiKnowledgeBaseDO#getId()} + * 关联 {@link AiKnowledgeDO#getId()} */ private Long knowledgeId; /** diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java index cad90fcfe..710b65429 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeBaseDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import org.apache.ibatis.annotations.Mapper; /** @@ -10,5 +10,5 @@ import org.apache.ibatis.annotations.Mapper; * @author xiaoxin */ @Mapper -public interface AiKnowledgeBaseMapper extends BaseMapperX { +public interface AiKnowledgeBaseMapper extends BaseMapperX { } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java deleted file mode 100644 index ee4b3d03c..000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingService.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.iocoder.yudao.module.ai.service.knowledge; - -import org.springframework.ai.document.Document; -import org.springframework.ai.vectorstore.SearchRequest; - -import java.util.List; - -/** - * AI 嵌入 Service 接口 - * - * @author xiaoxin - */ -public interface AiEmbeddingService { - - /** - * 向量化文档并存储 - */ - void add(List documents); - - /** - * 相似查询 - * - * @param request 查询实体 - */ - List similaritySearch(SearchRequest request); - -} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java deleted file mode 100644 index 689ccea03..000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiEmbeddingServiceImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.module.ai.service.knowledge; - -import jakarta.annotation.Resource; -import org.springframework.ai.document.Document; -import org.springframework.ai.vectorstore.RedisVectorStore; -import org.springframework.ai.vectorstore.SearchRequest; -import org.springframework.stereotype.Service; - -import java.util.List; - -// TODO @xin:是不是不用 AiEmbeddingServiceImpl,直接 vectorStore 注入到需要的地方就好啦。通过 KnowledgeDocumentService 返回就好。 -/** - * AI 嵌入 Service 实现类 - * - * @author xiaoxin - */ -@Service -public class AiEmbeddingServiceImpl implements AiEmbeddingService { - - @Resource - private RedisVectorStore vectorStore; - - @Override -// @Async - // TODO xiaoxin 报错先注释 - public void add(List documents) { - vectorStore.add(documents); - } - - @Override - public List similaritySearch(SearchRequest request) { - return vectorStore.similaritySearch(request); - } - -} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 537033015..69a73d6f7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -14,8 +14,9 @@ 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.tokenizer.JTokkitTokenCountEstimator; +import org.springframework.ai.tokenizer.TokenCountEstimator; import org.springframework.ai.transformer.splitter.TokenTextSplitter; +import org.springframework.ai.vectorstore.RedisVectorStore; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -39,52 +40,49 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic @Resource private TokenTextSplitter tokenTextSplitter; - @Resource - private AiEmbeddingService embeddingService; + private TokenCountEstimator TOKEN_COUNT_ESTIMATOR; + @Resource + private RedisVectorStore vectorStore; - // TODO @xin:@Resource 注入 - private static final JTokkitTokenCountEstimator TOKEN_COUNT_ESTIMATOR = new JTokkitTokenCountEstimator(); // TODO xiaoxin 临时测试用,后续删 @Value("classpath:/webapp/test/Fel.pdf") private org.springframework.core.io.Resource data; // TODO 芋艿:需要 review 下,代码格式; - // TODO @xin:最好有 1、/2、/3 这种,让代码更有层次感 @Override @Transactional(rollbackFor = Exception.class) public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) { // TODO xiaoxin 后续从 url 加载 TikaDocumentReader loader = new TikaDocumentReader(data); - // 加载文档 + // 1.1 加载文档 List documents = loader.get(); Document document = CollUtil.getFirst(documents); // TODO @xin:是不是不存在,就抛出异常呀;厚泽 return 呀; - // TODO 芋艿 文档层面有没有可能会比较大,这两个字段是否可以从分段表计算得出?回复:先直接算; Integer tokens = Objects.nonNull(document) ? TOKEN_COUNT_ESTIMATOR.estimate(document.getContent()) : 0; Integer wordCount = Objects.nonNull(document) ? document.getContent().length() : 0; AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class) .setTokens(tokens).setWordCount(wordCount) .setStatus(CommonStatusEnum.ENABLE.getStatus()).setSliceStatus(AiKnowledgeDocumentStatusEnum.SUCCESS.getStatus()); - // 文档记录入库 + // 1.2 文档记录入库 documentMapper.insert(documentDO); Long documentId = documentDO.getId(); if (CollUtil.isEmpty(documents)) { return documentId; } - // 文档分段 + // 2.1 文档分段 List segments = tokenTextSplitter.apply(documents); - // 分段内容入库 + // 2.2 分段内容入库 List segmentDOList = CollectionUtils.convertList(segments, segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId) .setTokens(TOKEN_COUNT_ESTIMATOR.estimate(segment.getContent())).setWordCount(segment.getContent().length()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); segmentMapper.insertBatch(segmentDOList); - // 向量化并存储 - embeddingService.add(segments); + // 3 向量化并存储 + vectorStore.add(segments); return documentId; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java similarity index 94% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseService.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java index be96b0918..91b0c9b3e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java @@ -7,7 +7,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdat * * @author xiaoxin */ -public interface AiKnowledgeBaseService { +public interface AiKnowledgeService { /** * 创建【我的】知识库 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java similarity index 66% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java index c208c92ba..a981b877f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeBaseServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java @@ -1,12 +1,11 @@ package cn.iocoder.yudao.module.ai.service.knowledge; -import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeCreateMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdateMyReqVO; -import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeBaseDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeBaseMapper; import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; @@ -24,7 +23,7 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_NOT_ */ @Service @Slf4j -public class AiKnowledgeBaseServiceImpl implements AiKnowledgeBaseService { +public class AiKnowledgeServiceImpl implements AiKnowledgeService { @Resource private AiChatModelService chatModalService; @@ -34,42 +33,34 @@ public class AiKnowledgeBaseServiceImpl implements AiKnowledgeBaseService { @Override public Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId) { - // TODO @xin:貌似直接调用 chatModalService.validateChatModel(id) 完事,不用搞个方法 // 1. 校验模型配置 - AiChatModelDO model = validateChatModel(createReqVO.getModelId()); + AiChatModelDO model = chatModalService.validateChatModel(createReqVO.getModelId()); // 2. 插入知识库 - // TODO @xin:不用 DO 结尾 - AiKnowledgeBaseDO knowledgeBaseDO = BeanUtils.toBean(createReqVO, AiKnowledgeBaseDO.class) + AiKnowledgeDO knowledgeBase = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class) .setModel(model.getModel()).setUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); - knowledgeBaseMapper.insert(knowledgeBaseDO); - return knowledgeBaseDO.getId(); + knowledgeBaseMapper.insert(knowledgeBase); + return knowledgeBase.getId(); } @Override public void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId) { // 1.1 校验知识库存在 - AiKnowledgeBaseDO knowledgeBaseDO = validateKnowledgeExists(updateReqVO.getId()); + AiKnowledgeDO knowledgeBaseDO = validateKnowledgeExists(updateReqVO.getId()); if (ObjUtil.notEqual(knowledgeBaseDO.getUserId(), userId)) { throw exception(KNOWLEDGE_NOT_EXISTS); } // 1.2 校验模型配置 - AiChatModelDO model = validateChatModel(updateReqVO.getModelId()); + AiChatModelDO model = chatModalService.validateChatModel(updateReqVO.getModelId()); // 2. 更新知识库 - AiKnowledgeBaseDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeBaseDO.class); + AiKnowledgeDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class); updateDO.setModel(model.getModel()); knowledgeBaseMapper.updateById(updateDO); } - private AiChatModelDO validateChatModel(Long id) { - AiChatModelDO model = chatModalService.validateChatModel(id); - Assert.notNull(model, "未找到对应嵌入模型"); - return model; - } - - public AiKnowledgeBaseDO validateKnowledgeExists(Long id) { - AiKnowledgeBaseDO knowledgeBase = knowledgeBaseMapper.selectById(id); + public AiKnowledgeDO validateKnowledgeExists(Long id) { + AiKnowledgeDO knowledgeBase = knowledgeBaseMapper.selectById(id); if (knowledgeBase == null) { throw exception(KNOWLEDGE_NOT_EXISTS); } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 543444fdd..8566a0941 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -13,6 +13,8 @@ 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.tokenizer.JTokkitTokenCountEstimator; +import org.springframework.ai.tokenizer.TokenCountEstimator; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.ai.transformers.TransformersEmbeddingModel; import org.springframework.ai.vectorstore.RedisVectorStore; @@ -90,7 +92,7 @@ public class YudaoAiAutoConfiguration { } /** - * 我们启动有加载很多 Embedding 模型,不晓得取哪个好,先 new 个 TransformersEmbeddingModel 跑 + * TODO @xin 抽离出去,根据具体模型走 */ @Bean @Lazy // TODO 芋艿:临时注释,避免无法启动 @@ -114,4 +116,10 @@ public class YudaoAiAutoConfiguration { return new TokenTextSplitter(500, 100, 5, 10000, true); } + @Bean + @Lazy // TODO 芋艿:临时注释,避免无法启动 + public TokenCountEstimator tokenCountEstimator() { + return new JTokkitTokenCountEstimator(); + } + } \ No newline at end of file From 024109dac9237d06766daf8f1aa0e5347b5bf361 Mon Sep 17 00:00:00 2001 From: xiaoxin <718949661@qq.com> Date: Thu, 29 Aug 2024 10:12:47 +0800 Subject: [PATCH 072/136] =?UTF-8?q?=E3=80=90=E4=BC=98=E5=8C=96=E3=80=91AI?= =?UTF-8?q?=20=E7=9F=A5=E8=AF=86=E5=BA=93:=20=E4=BB=8Eurl=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E6=96=87=E6=A1=A3=E8=B5=84=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...BaseMapper.java => AiKnowledgeMapper.java} | 2 +- .../AiKnowledgeDocumentServiceImpl.java | 24 ++++++++++++------ .../knowledge/AiKnowledgeServiceImpl.java | 4 +-- .../src/main/resources/webapp/test/Fel.pdf | Bin 352908 -> 0 bytes 4 files changed, 19 insertions(+), 11 deletions(-) rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/{AiKnowledgeBaseMapper.java => AiKnowledgeMapper.java} (80%) delete mode 100755 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/resources/webapp/test/Fel.pdf diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java similarity index 80% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java index 710b65429..41e71ccad 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeBaseMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java @@ -10,5 +10,5 @@ import org.apache.ibatis.annotations.Mapper; * @author xiaoxin */ @Mapper -public interface AiKnowledgeBaseMapper extends BaseMapperX { +public interface AiKnowledgeMapper extends BaseMapperX { } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 69a73d6f7..2af8b9d90 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.hutool.core.collection.CollUtil; +import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -17,7 +18,7 @@ import org.springframework.ai.reader.tika.TikaDocumentReader; import org.springframework.ai.tokenizer.TokenCountEstimator; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.ai.vectorstore.RedisVectorStore; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ByteArrayResource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -46,17 +47,14 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic private RedisVectorStore vectorStore; - // TODO xiaoxin 临时测试用,后续删 - @Value("classpath:/webapp/test/Fel.pdf") - private org.springframework.core.io.Resource data; - // TODO 芋艿:需要 review 下,代码格式; @Override @Transactional(rollbackFor = Exception.class) public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) { - // TODO xiaoxin 后续从 url 加载 - TikaDocumentReader loader = new TikaDocumentReader(data); - // 1.1 加载文档 + // 1.1 下载文档 + String url = createReqVO.getUrl(); + TikaDocumentReader loader = new TikaDocumentReader(downloadFile(url)); + // 1.2 加载文档 List documents = loader.get(); Document document = CollUtil.getFirst(documents); // TODO @xin:是不是不存在,就抛出异常呀;厚泽 return 呀; @@ -86,4 +84,14 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic return documentId; } + private org.springframework.core.io.Resource downloadFile(String url) { + try { + byte[] bytes = HttpUtil.downloadBytes(url); + return new ByteArrayResource(bytes); + } catch (Exception e) { + log.error("[downloadFile][url({}) 下载失败]", url, e); + throw new RuntimeException(e); + } + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java index a981b877f..5889bcef7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java @@ -7,7 +7,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeCreat import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdateMyReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; -import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeBaseMapper; +import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper; import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -29,7 +29,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { private AiChatModelService chatModalService; @Resource - private AiKnowledgeBaseMapper knowledgeBaseMapper; + private AiKnowledgeMapper knowledgeBaseMapper; @Override public Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId) { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/resources/webapp/test/Fel.pdf b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/resources/webapp/test/Fel.pdf deleted file mode 100755 index 405b67fedada1d989b18a7d81df1149fc7ebec01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 352908 zcmbrl1yo&4(!ik26i~|-JQu9IA$UyB6}k%IBiBiIir-Vp}DEZzrIBO z^(DoqYU<&l3ukI)0<6dOPklZa8YvAu~Y zoPYqFvx}3dp)H(8R+XB9^_&p8f~y8OG=C!swK&wOXlTai7+IoVxNCchdP-kCyH!34 z^=C+>Y5r&Wjx)|nhMeL&Xr!DsjAzhb408(m1v(RV3^(kdm+{=)hmDI)&iwH3ZIvrY zQXG@R$7s2Dy#7=USSpnlV5XBA0?Zg z^A-5JboS1ulrxVNi9t-eV#Ho)Q^BrCS;2--aSHLEIUP$PgdY?U%VcT z;%emL>0tUVIKwe&SegJ(%fiVG$0%uPX>Q>{#LUSF$0%ay;;d-uBx-N#U~gw?=R(8< z$0%xVWACKmU}$X0C}!$rX>6)2A@cu-^ncp|_g`E1Pu!nn(7oL#FhnHZSHD$wY!%-Wh zIirf7<7S+hrOQ99WeY{MX1+e&~W9!?^AGcS=pkb-pqSmOwFJI;ac1XgX0VX#;Y=SP`_-%7oF_Z^mPdfaHQ1uGee#sF z8SQf95*(g(3aWmK76Xb5X1zG8g7Yvbu>xtEGN>}5BN142bX3O|MsRp|9@pi}p6pud z?|^7DCXd-ymw}V%RS=K=&91ax1<=Tp?yV00=w0UB>@~61WTb)m)`7?4|BS#dd2+PJAc{Vn#X)53wK&jO{06w%QpFlq<-~z zn_P&LsHShb@Sob&&yiR55~|ftg!gPzeC$+)=2@HN^`E7<2G>iOFyZUX6f)h#M!H&Iqu3LX~NT+w}ZK)2ZoVl@RMySB#dBuHx;ek=vDC2MLU8< z7{Q)6-Z5qcv=Tm!MB{uncGv_zGLOr)x}X51f@Z2Wyj=3QRw7zGag})U^y(y4=gj59 zkUCmF$onWh4;C&~vfJ^MF7B7%fd?M~y}5RQbMA-^|H&Dpq3{%=52;h@>Y1T*!?$@c zO?&Q5J9kMer%=e^qgtnCO1{PI-D6D|fLi#UEw}?D#U9BIu0r-dA+&mN2yIN%IOD#W z7yy(K@ppbS{A9h;EavA!ExGU%s3=`Sp0bWlAvqOjK7zxGm70P_-g06dl>cggAOWja z;o247gp6b@^@c0(8iikD@hI*eNVxDN+$@pYgryQBFzr!tw?4}VVgqXGq})uAT&Fr! zR~s)VT(gf(&J%V=QPkN%5FJdL?BUBINx3XEX$qF}6^-n5UFo;TfjAK;^5m$bR-Cv@ zQB!JMoKjM2goUZnjz^RH&;z{1Jg`d%aO4pW!E@T-HGBxII38Z*M;v# z=H9*1gnB?>P#(s2$b;YfJ1F{#vA@ywZ#?q&>q6M~cun*-Z{aiaVWCteMtq#OeN;wm z)>IAvo<`W3sJRm{4Z*WU!;Zy7!rzt>NlHI;)V4`$+XmoBunL%^gR-1k`TY+h{eoro zgM1secXco$CJvG|7IzB)18rX)IuIId^ke%*)8Z*3w^?J8#V!?paA=)CF?{9lI1s;c zh4#W#zxri#56=AY>Pk4V8sGynaJUH(!*-5bL=@^$KN-emPfnuU3krS^u#q36T6ZU8 zee7=Wt>@mmgq_?;`^}&*Ep35+yjG*g8Y9V4puu-vtnt2BNSJaLW`G z4{K7O@dR|>-`N`lNg+bP*TY2N;eB&f&%r^&rVK@~&=xVH8pW>rJQ#AGh&*M=Qyt8N zLp=PnzO-Apcrv;d7>oc8yCa?4m@|lvcyNOq4^s^u1GfG#q_i2jVr>X>?>Q5D6+}le z3XK$H!I8NQ!##L`H)?K){uRLn8Uup2Pr@auE%((Msb2+^<{J_eDhbLU{NQJ&G&(|& zTAWkHR2e+6&>M&Md}72y^f8Ec(6=vo6429fwr^A=6~adQ4!i=Ld!h z&=8v#iHUe`1)~I55W*t;I2~z0OT*94v|_<#mh3Jj4ydJ#iqb=zk|^R&?k+kJGtOQN z$O|GQBpjj4ds#hOIL3zc6J4a9lSmtOYE|`={|0h;=)yyMez;vi1S)JHvy3!L7L?IP zv*p?xBrjJwyLwhaNH<^vc#=;-#E|;w_`Ho|NQGC#eG6~ozCa@+a0-Fz;9A&(t787g z^+V3aaKQQL!;2Ho4~22qKusJ9?2gRJ)ibxl82tmfU{K|35DM&=-#bpNNx#=1Xo+mC z(@ZY=>Nf3uhBCnP!AucVwu7B`IEu*!*nL8W_^!yK`jou$Yuc(wI$F+f8Q4>4Jvn`>BTdiKgj#e=h~tIRucxqO=@5jL=aO_s%EVhIKNWzVV3 zmn2XTp;*dwIP;TRBfxFBwWa961FtvWMRoG0NQ)7%rU8D4?Xe|#4N}s>Igm$ zug)~7Wzh^vOrJbpNhk7v;LlWX7hM_J4#@eIFbvVldq_P!QZq;wbNSPP#V|{d62>f| zpv(?7XgZQKtHY#hxJRd=?+7g#3AI5WT*b0 z=n0`yqfb61ylBUM#@ZZ(*JJiS78l*z5g0K>39yUVamLC{Xun8&dc-;K>Xa{4d9|F@ zN`dx8ZDO$HeAvSYF34k_yJPF;i($A7J5<{q@79p8k+J?t^{22we?s*55taAHG|mri zuW!dNTS(I*zKovGRY&rAcPq=jI2`r~#B6!ia;ZpGKJ3k5vykr)QQ;rnrl-xn%m`eL zJ@^J4;nN3V;>lo@&ueG`-L1#jtP!9s$49nR3o%H&ovV5i?SHObEe_axdZmfn3-m9% z-C8&EtLF>3k+cr+5F-qlUSaL-dOdJ=T_0&mOV>90UVy1>_55yj)4+g4z30M{?5o9e zY%o*V6nI{SP*UTo>3&ht^yU{UFk$BG^2t}kQ)xq;L(JG$dzm2i1IT9d+UXn5I4GhI zuc(+@nWh`>*X8t+wP+73s?KRLtD6*WptaUsom%rny>`B$NdQ6~>85pt5_n?O+`%Gw zYshu>#tDvjY5j}5rq#314f@oI_hw@b0<~tgH_IgAvPgEO+`;_HCh7I`5kfC5KJuB#RjtOq5gi9ZsM5wB(hO77;r5>%qIUG|n|6H?y zKrR8>pD)}8TrvGOmaU>sHFy%o}RDfQ0 z7sFAHU2;9u_roxuA#ij$rGi^1WwuNfytMso!{?Rz+MB1qYNTR+KAV=* zw6DsqUEUthRN|vG*o?2u-F$5i`ocy5zK0}EcZhHk)+U*xIa0@cMP-mX7@RJMRY$F| zfL^=6^T^f(x$VzE5ZiZeDGT=`x-ZVZiWdtuL71da7;CnJ@@`JLtSih2Y-{i)jQEdI zZ_FRAuT==gvs9?*RsVj=tS^w~H>nvvuEYBjUc@7}Td^Ble3^Mnl?ysxPArP=ik2!R z$IAgX&|L~A$gwyiO1XHvfRPlj_z$h+=p%6XA8yTZakzH-FMcU_=WZ4&2wVt%zdH2$ z(5hOne7^BQvl$;O3}{-|nyn|e0yuI)#9Uiz60;JZ$Yxmm%VzPR?(8tf%^J=)+BS2| z0D61sHxM|(6^JHLGR>KyB!E1fO;fdsKD1G>IJuKR|MgViZFGt|c0L8rNsz*5n-(%V zMA(y$fY`sa5ZvqK#**&9r>DIRnI(AXtF=GoObZl4_DxuG8*8#)a1}eLz<4-hgu+hY z0TQ5kbL<)0A;Z>dJ~1jv$sZI!Q4dQM;0yeKS+iqDNq|~coz8{}+#Sh0Jpk?iPZ)2) zYytq#>!fwXba=mHK}88#NqTH-j!FS2sC=tQ{oax%Xn!fYVlKcJ2hHp~PVHzy9Vldj zfzoKaSQ_6Iu0Kw}c2A;aci43SP3i};Fr3ad>!qCs&tii&(4-gxlZ0T}xkGshJK8Ff z9z{MGBXFT93D!(a;$LGWLSBjas+z7X)8{=@?=8O_1u8hd3%SHdWS=bWdx$-OTOvde zm((fRyY9a(B`KWPs91U_%tAv=I<@lQAzoMtYj!R;Sqe@wNzbym{>V|g_=Cv!P;v*U zUZK`+&$Ak{>~Z_pYP|hPym8&mPz;Mq5&E;(Ryu1(PZo$)rhF-2{W@fE&BgdR83OvL zp#IIg5-~}zv5AOHYZQDwPHh+gvdRU#lgZzn`L{SPUJMqWWR;BhU4N!5i_cp%yxszi zlu+z6|4w|oSIPoqA8agneqBBUZ}(1{<)iCFowrup`FP%^VB)X*au-n~1QGzBqJbFa z+_kXHI(gWIUv4gC?e8RlAgX!Ze>X1}-&Am@IOW%$vYd9(iLroMd?z&ByxLviPjb_t zSL3pOVIu)ql`kz4C7umaoF!jM#?5weQhY9nS>>W(CvEM2z1L|oYV(y(D-5SN&W^WN z=k6TDi5td*CB5w1Gi9utq&xj_8T?Xc116U?dJ!Ai7ig5IxVk*5 z9^}3uVF9#PAFyXhiXgqjlOACg>|U7P?(O(~LY&B5&XKfp-*h1RNn?zoF!mNkL_;XN ze+zJX3xUD#AS1rQx*6ze+Nob4CxtW<=?Zs7;YVRSY7dlDnl;7|BKo1{2RTrL3yf44 zgzdMH!^0*MdG-_K`xY@@cGi}M%2c>qIS!!VeZIxH56plO7n7jc87G5>jqV9IkZz34 z4`dEHEjH{cb!cM7nqo{kb|4+voD-G|DsYa|#gL6BliJ3Bd^Ggf2~E%Kk+zZEKJ=sP zTx6>=xafee58%$jp}`}|UGsruj6#*l4WV^`M!|Nfbu|jquUxd2)^cHLEEBVO4C`7& z8Sm>21E{1=14inwD>@Oi0|Fd&SoVo)5Q`;iqz3(6o?Q{Hbn@&(OeyIyXA~`%ju8&0 zgCSWGA?44y8d*k2A$s*W=grevjXZ3OD7kV!I6i^y7v%G-n0j= z=3?lw&9*lVO5}Q}nZV+~*agqdPt}S-HMxYb(gB`FujYgVabCiuy@Yqmhl@IkC`vp* zP(1(KnP|vu4uI*^$+>n2+p`@$!$+>|le=T5X6g>y)X^{Qr%Au~*T4^?V!3JI zPUHQU_VzbCWbETeCqKVJ=jvGjRiuAii^62VpUSw=&o&a9Kk50l*FB$a?vvXLOtgdh zv~T7%sG62-Za8_*Zp7_9SFG)Q0RJbz?;jhbKRv!ofA>9oZ65JGJ>|^ww@>29mT!BK z6sP9j@{rU`}(pQjdsR+ z1L>~oNchHqnn7UZl=gG#==`HKAzLHB%XntPiK9k9wByA}zz?l6;P{^fOgZ@+#zT?fSeymDUzHh%DWk!$g>Fk4f7P^|)?E0Tv^)Z$v@A+& zIox{GEnEUHh*A&kU*oY7KA(=UV~Z&MHni4SDi3_ki)D?p(rEzZ{t*pV(G0OMI0+ZTxygZQ11reQD~p4NcyY7G=bX<0=COY_;~!HKM{eKMf4NGCE9_`yPf#V8tE zY2Z+3P^W)SENYJr0w)k#_FXKv++&~{Y&7Cuy@-(%IP?8klK=1^0|*ekiUb8P%E`Ys zD9Q2ROy1)|27HmmfOo?V_0bSefFt1x1SAK7ev!tUCGEsVih0SAiIXiA5NE5THI)Fw zj2sb;VnPm+d}&<(T&;397k1(aB4E-p(CGRi0Nq=QzwMxW0l>|~N!#IDs`%WpTWGk% zo+MduoB_U9K0g%M%>#f*{LUER_`=u-Kr;BS7_wG;2{qKNQJuxRW>|(rxJPhG$!237 zYTwk>3+{A&4K<*AOvPCXH4z^pfd?1MVhvHRMQ<2s@?&hzHDteO>qn67{K| zxa(ul+VyIT1Oka*0*Nl4kVLp)bpH1|CosMEH_yq;#r*F?Co|K3XFC7M$TI_z`v31K z`FTxU8`Ur98yw#KZtMpBDUEa?xNy6AtL@q8ybtE8er>hdRjTzZBFW?f7RX;J*aY%= zCHgYO^#d?>f&`iQ(37wT!--Lh(a@4HJz1EqhEuPXAfkC5&c_^%Ce|%kGsPRE4o=sP zxDR-p_g~j%J+TaIz@EP$Ki(p}LG|~r>wC}(zC$oQbm!Fk%ALIh&%_-^pbMU0@ zS+z?!_Lrj}a$l{=%vN$q<7w`LjLZn>5bHW}i0y#zENqaZR)^%S2~KmgOm(v7n%ked zyl81fD8v>?a)h!X*UiRnYYs)ulJ~4+VYbjIBDBtA2q*OW1MGHbjE|6qA@k8pyS zdMQ0Yk8Vx>8eneQjbdkHa*qq{w9-L|fgKQ`jV_Y7 zhH4`)fzp-m78%d$_3$>4drtE{GBjvYT{?1DXWg2Mh0QgaCnb#@D@u6Pc)P(tiQdw6 zCHYr35^EZYhUPoK9}=rV7ESbB2uWW6A zN$hKFlXoaXMjmc}X)Ro1h6VM=&zFTmPZS_YE{(7Yn&P^+DwI)6M5KtTs0vz=A0S*7 z`zIeOq|pRa`d<2OTl**VBBXD5JB#oP&ru*bBOUZ;vohIN=cY zo_EraflHWg7XVw~DJinsMOy=X)y76Ftlcw>`REt(q^Un{Zlw`?%+C*2i}b%Fv5)N- zAX40fo*BUux%KVY85e4$1(l&;oY?wSL0)~~sZ?_Nq_g&Gq@dPFIkz+Fl%LSAcL?>= z4D=TcKbEcJC^uWDVRG~FA6td)LwN|x z5fUYn@5jvKHv_`yaqNgAXOJ`q&F_ZOCqK(Y;ub+I5OLcY+r(a71I7itc3751;7bsV zJE*v<0NX*1gSsSw8qb)!yGJ2w3t@1_D4g#)YHXn5U%k3 zv^@!ug>JiQX=)v*WJXq7~sUm$36=kFD}4+Z|OhWmA6=I%i?mh!Clcl?2o6 zgQP}OF)yj~zThn0dy4CXqkr%#x_lOA5n0|rsXUsA_KBG&4n-Q2?d^g5{ zI<7k&!xesOT%)_EK~kYrf<41f#{nvz@qF4?ieh+;sH!5gWSS*^RVqk|xot*!zWVGlS zMxRShgnZ~7THtsqqW|5Oe$mVi9lx34;}7=sxRHFWxA`jlh89{z{RP6Vn1&|Es(pBh z_rn)~A?>0w$W_Jsb&3JmzMWOUV99v^YNR%*SdhDRow5TrUh+U3W zGG!4bKYG1nSvvVOX)>@4&huB3U3A7O`vdtQ9GvBr=6VPMHPSoFUq6CW8yh2-<=WY* z;})0e31^$tFb#~%h<|0U@DMjv&I~@AMk$R52tA)J)~WWEPDN|-Qa!1MV!U3B&<2x^ z2X0F>XWLufc65NSdK9` zP0OjtGS^i@QRdlj9Rxt(a3I4N$+S|DRwz|cch&cW!U?$@H=Zb}hndP5LRw8EBe=gaG znE!SH-2Gs#(_%+!EYl~0U2hBrg+>2_jc?_1$c9Uv91 z%H)UuZyTEK`+D_2y4xiB))5$>u@hIk>rf8Djk z%h8Kp`Y@=dg|zHW8+T*fUOY#$Dwhw7vE9I@Zrrnu+X&7RIvTQS;a)k6Z0s*6xHG+D ziPIUDjN;b86vdTHe;m;a3O{j%KdA9dR{DtGy>xw6_;7zhIM`kF_VbQqeKRqla7*1#y88+PRd1QZfTQ9Fko#)|{9de%0Ovh}1 zr|{`)svUc!++Va2)l2!;o5{pNSm^^sPpFv?1;Y5f(A%sgpI^Gud*6JOXCA3>3UzLSHY23;@qRx)9V&J&bS~22@ z@5evCxR9R^K#hm4FEgwPm)f4V@$LDr$U?E2lin;LI9K`lFsrEhl$X6y8@tAyQJA%x zH`>n~Ql4rn&{`uHE9~UGaV>pEJCnZQU^+63z0)7ADzlMEvQ@K^@(w0V8U8sI(CGf9 z^HTc!Pz29_U_jXVVbX8p+2<{0#r|#LMcEhx!r=ocpbm^Ex24}pd{UUd+vLS9^4u8u zkDjxEdM3WFfc15{h2%w70#m<881|Pmb0^cZUKhD^I@reYY>)N|{aJ$V(dqDGkgH*t z{RaDG^&I`h-^MK={l1SIqQAZMw6jBt(zKq|3O#h4SzG)65TLuIDC%o=L6bItRKF?U zb5>Z(NOQ6_VLfT{==c$fwU;x{!MoEc*h)HZ!p=rcoNZ|*6 zWMs%ji)w>kX()W@`micpNKnXtKgzDbPsQqGpWGVsk@`vF&eE*t+PU;YP18INtkezL z@CmyGceW9hT+bf17k{&i>3izgj5#Gk#y#}4Wj(uTohB${>!<;XuPNrRm}-8O@?kZm z8~)R5zx@p}h&jfu733No5=+uQepptrV{D9Uco$5=B~g|&eBRRQK>QAciI8e=WW4DZ z%8ON=tZ)rBaO}i5ziUJ-?abU?AE{WOGd9A4Pg+2uo^gSqSHrKx8Z4-ziC-A3K4vvj zsb?T>2|GB)(9>RFudPKtpQ66^Xe(3k;;*Zs!z^r(Iy+gO0SB(6p8t~@gVQkIpP1K+ z4t=4vT+GxmW9_4(`E|)u}*L^+sFLlyLwhT^NQoo+J=i(+PBM$`^O)8k7_Bz zQR?GWL8d>xQ&(TBOQ4;;M40y3pSU5w^lmQW#@L~zskHapAF1%R-u<2?DuKFcW;#?S1SV*Cp^OTvHB-kBhi0Q zNQx}?LButcG>=qP3G;De-uncfH`z1{zw*uYy+VGg-$T9pagad$>Ca&B_wTr~mI~)- z6AQLWE$JnYP|RWV#)+)Blpq~|melV^lkjB`mkrno>0kng7t_vV5-S%sYz&?bXZdU! zB45X5!AK|aZI&Tre=`?o#EZ;F^eDP9brhqpni7Uav8Q>mu_)A(P2cNvyv^M1su-XL zQU%ttD;iXKfs>n=%34|y3}H=}kCUROG;f7CfLUob)?x0RysX`>2?=QO$^$I zHdrJ%Vd$vRlfdw$3V-3Jj)6XZqCaV<7MY3yIRY%r0K`y=k&sW8>@jw}~Id3#5yMT(ZHrv8d~v@x>CUU$nKDbA7c8$f@G+`vYuMl2Q9xma}O>_H9I< z>Oktv0rIMmM&Xvt%aDdLK}reip-)0x&R z*(GB{PwVV@S{FgmlA{hSNax>K(Wf9}rM)_${5846THG?}D2s?R*2nF+L#3OF?Y z8>%xP8#V-+M<*rn#?nj;r7`S?Y|P7$BH~R0aT-gQnJaBBQng%9t;)|zt`#=rMG2+f zgh*O7=;>24V9V}fkEn&)KRBIlrfq`b{0tO5!++LKSWJh@cdfYj5 z*ZQ+%iNkXmEEO*ecNG*(@SZBgrd>_OnC;0C)|&KkM9!z1%tjIXe;rlU*-+LMaG@a}an!{(VH7^sd^yyD#L zjxo9a4Z7tH6Zdr_c=^khH-En$FXY@YJ8-r8_C>KP6$Q`@o9cAS=iqq= zUA%KW$9c)guNmJH$@_e;W^FKRCEk#%GETAocTW75Q36RO3p*F*znLd1kY)l4wTW2& zMT%Mf9_0KlQp^e@y#G+s|KFsTorvYXNb!HOUsfQK{-+C7;0ZlpV;4(%J2*xaMm1$A zIN%4Bg^P;=4ZA<}6_@~Zgo*x+{x6^Zhs?A7$C<SF7FsnCJ3StgRHg@{-xT@hU`c*&eeLC!&!qh$*MqblARpk2 zaX*{kQ+@>|bEcezbQ+#~w*G(^8segy@DBVD6<&E-vl=DVIiKyjaU&)so;5Va*nMB1 z`QC1m`jPp3zwL9y?_Se+`SEr+dCrcc-SF;7lKD7l=LrLW?}y&w)8KW3B?H0RxYt^tVCTabwEKo@n+?G|(s{?Jf5rvh zb6@8qU)zIPZF;n>O$kCKgZ<6!IHur3UwOoKcZNgb_>?^t-8frx#kUVfxmV2hBbVi7 zq_dz{pPS)Kd(yXO#?5O&GXEVyeGlm2urrwtk?Zr{`cJe8gfF8%U-=|00{6Gq=!Kqt zC#Vd0A?rO9^}{yR;c3WY&e}je4x30Q*ZV3)Op1_WE`45~+_Ff#Ag#s#3#)=IM-{ ze3a(i(ha=DFvgq(%a7vAod;Cge_*V z3fWg>wM$pzI8gh@^TWhXka6f~ZHz2MLnO?L>xJ=Y2&jVX_O=sqUuj_L2Z*U{d7KB? zIVg@)j|-JpiV%e|(G#0Q9Ak#r_YawFJeLjf$Y8dYL%)IxYw$I0M4!A|YI6_n%$go} zOHBUMtWRqZ%_47`<^ciSr&#ScjX*rfbKM`COktKwE!xzq5`7PE>V#*L3&a1+6Uu4y zs?9x74}S8=f=i_oc7=Lcmed40Vf}svPxAoFi;~dS6zfOyu zyJNlY9%Veg19t77+H2l46$9NJt7c7@8tz#_NE}Nb^n>u(7_6kH^irU|7;0CrquDYb zbFK^^Gr-zLFPMO*sBx&LznsSCqou-sh-niU!AB?_k)<(0M+vBk1O|!muJN`kz?hAc*;%P|@4}EGOrE zgLe?|4Fw3ztP6twM-X5fLItxA?3l-Kf<~W{@@ACG>4ejvU=|WSvCqP23d3qiC(;XF zeDt4m(=iCcLZd8Xd}7}Q(GrIBI{%k!7R`aOfs>vpoXEUJO;Td=<+U8A3RKe#tVakw ze-^fGt}CWDqn!WvNh7=|N`62aLT)~`L{2ta$*u%yxNr1|bDHO6lmH}5H0oOtlI0csr5?s<)!gt^9H=zCi3JUQCd@q zV?{p^H80xOujYYtj*`;yyxHx$@aSo6H9v(y=Rm3SomhS?OUnnwjV*$~lL^BpbnOmQ zd$^IC6M<(b#^xgm1*9`_#~$ZpB(TOE-ugy|@a)EZg(NR^nZuTmF98i~8W0(06H!-m zN|`Z{nV8)0JY$TMk?#`@ZWDV;l{n-4m>ys=!aZ=-lp?60n{RN8Eno)%c6;tPq&q_yC z|IUOY?<*s3fmy*gq|wU{%H^`D1q(S215MxgL|dlYHH&c$H|vO{uH#`2WUtPdhz#5P z^}YNW_HsTvZ)#qpFemgMbK0Jjzb2Hw6apnORmZ=dF5+$V_Vs=3sNUD%fcANqQnzNG zmRQ^oriE)l3t9}M1{;Sta5i3!)e2vv(70MMy;zO*(~Er$MI|0#uGz=Q&wg;J*Cewe zR!L^>R7HgB1u>Y$^z^Ru+ZOKMLBY%_smPD@;6U8WUk?#yA+ASv0)1S%cxLE&ztI7p zhC=GDATH3Wu`C*jd#`~|dBri69JxF-`7QWedN`>t&=u-3KFrqcCLPCvlcnyBb)PqEX0_{jrvRxk+vh5yE3?euaDYk3 z{`Hb7chMZo!7?JNO4jwDqbt#so1bUJ-Ed5UqIhhw3Q&aUSO8+3CN z&REhumK1X9r*~es_4(+#ectAhb!~o*{Q#9KbtMwJToZ7cYEkGMinZj)_FL7i`s>pSF!fHuqy;Kl`E9r}`-iiI%eWf?DjidHbLHMFMMYvT3o7C@v|S^aZ}D#y=5O zW9D?VE|4Of`=nPV^YzIm$WWaSV(K{C{^2Xwj+)O90_Ac3+-ezf($Kg5Rd&uYz>uF? zh&75DFMxQdOON+9P-AR^T(!T99R=)+V>=-DpyQCR!8OAuIKnRp7XQeK!7I8pv~0?c zHY~$tu#eqDm-(RX<1RklSGdRgJzL=LqZw)SXM)dNclFbl;F^Dy9h0@QnY&*YgJSoC z$&qmB-2v~M0@Hl>;MihIO2^ptIys~B*S=MKB>hX&Foki#r}LwkuP+WQ)xEOeove2< zA4&bUTNBW4?GRCn7JUjI-samba@8)UC9NO+gpc%h;*nLyledY*$CL0?dq0#d{fCnA zbaf4_V(qFV_k~YTV@;Tr%RPTEdOi**Yq&X@zACq=hW3AoWFK_K@#aQqLR?$IQ?qy5 zLY5D^=jA9KhN5F_3Kfi7ZF8Dm@#GiSKT3FoE(9#W8t0*7WxvNg?S`=Og#-6ulgx04 zD>{kMY>8aDUpr?2rqYVbqK*`>Y6~}abh9<#{;H{doh5>KgdX3dg@zxe&VOZDjKX}& z*9cg3>1_t?dJc>Ry&MjV^J}#p#ybe9_}NK2RHBVCs;4t>U~p)ieP?2y3+2b3AJ`>l z7`|bPSAoNesCRA~VEuv;`58T*VH;XiZtFaMC&N|MX3SW@eOx=z|Hq#;wl>LC95`)L z!qrmm9KP&DP9@_*jM~nHeArr3EB#G$TcSq)B3a93l+o9dOrr)3hU=dmzGw{w@9)3F zrO`e)?CEb7g-&r}Jf)S(z_bZl13co&BIg%em@`Its&NjZbbsI34N!;gi=52gNBp1i z*#N?M=NUBw&wIR+LxG&h?F**OP=cWScrNK&cd7)@qInW~Ic6@likD!JXVWmB@ zR!cKWl`0cPClI@WjQHJBDrcP(3c?eozaX^$`Npu{t%U0@*5|{?0h53RzqKz%AP{NG zvF~^b!oLXLj?%=6Hz%SOi2Vmf{BNOoj>Dlzt2^U0p4q+zJK#(R;#RPI19K@c$`!e5v1Z+M4pm9H2=kND#Ny|593qsv+=!p*m`O#l|1Q6$zp~BD8CAC zRqq7Xg>;5|sbHG0wFqeCSYoVoZNJ&3?!%t(9|lkyvnp6bA!kkmq|?&2%+Lm8&|gdN zTCDL?^XqI$p(^<=dVihC^C+fs{`>Lk4z229eu)>-NDp8kAXnndr*o3D>hR>-8Ds^L zE;k{ma_}iP&)rz#$c8zOtit}W15c4*I%sq)L3 z7K7z4OVtc*bgiFe`!U2>aJHvxwD`6IEDgI);ISuMZ5It$7U~&pN2P9Bw2f{_NO++e zZz6PSEyfFo`?}b!evg>1VQamdw;!o36`KUow?iolyjHP zp!a^!?(hA8<}T>@VXh$G?QvE53+dfObkzx;lOcq7F|zE9Si{N1wW{TnmXsix@ym+5 zlz&e$o;C}PuV+WjFE+UuOFVyC!Pw2uBfs5~b+LC{@I@MD;|>Er9ahOTR#VYT1(R=9 zQuSY+y)Qa0b1yz_OW)t;1wB?jd~PY-{XfosUwo`4&VF<~?j$Ivd;u#R-Y=Qw$s=~0 zSF=alPMGS#lK}DrUa&WZ5)`gzmU^Wy6eLQK&M=f%c29qAjeq$!mWfikM}H5rEF96F z0cTB!asl)z-e`O)LKgl(U;kRR+~v|~vhy&#T_EeH{R?{}S~S6Vqu}u&cBRP@bo)d#Ea%IHFD?|w97*DbQEOl#5 zBUdfMC!&7WD@&2G`JwImd(@n=4oCD?ZPfBw14k)V?P6gmeHd#Sd2C(x{Kr1OUe?KT zr$1BtVrJ7qL%W=}Q8I!_4bG`tuwhN2PeDJ;D|t!m~FT^rIq{EYxnZwhoBunj22gah`8(L$O`qY z<6F-+slv?fUqT&WO|aIK(8&8V=8N$9j;7YRsDWUA$X6q!e{s?0=U7-nh+>i#y=N-( z!sdt3zvJtI*Z905qM>!f(m3W@+sN-UP%^pld5t#oTe-!vo+U*?COFy&qd!qhnsY}_ zIGQVtL6%lAX;grzf!B);;s?wQ+wPo;G%HijBK!wdU-?vtrC0GXU=51Zc=iwO@AlIc zRpa!HEV`U^NT`yb8mnYeBGN4h5HAp|zQpWF(=1@`m9V}G zF_y^}p{X%-NB+x>n^h1Qd@$K^iZny#@_y)n1@MQvIoo@4philhtVuGYBPhst4q@q@ zm6~tj8nXw}i%AeNrXd=Rx1q{iT@UkTGi>&C?*M1zrmB13>sN*F-H2ljOi z?e;ew3KObyWar31R4UkeLB5{9kNUh<1LJdpoF=o3a)-PSgZ(s|#zWFjX26f42wzK3 z*$ZLR=b_cO06e?K7=>*)z|@xi@Vm?p=yuIx*}44p;QB@&X6e}eh1utfM1TafUcLb_ zTg4p&9(wl)X9yW%%*tBcDX!M~3_1aCpt5-%9ucbj{`X7)@{VoO&;9{0dP(F$*%vAlBmAiRtI$F2J<2T78EXXO5m!=tc7SOQki&xJpexW`knDsnhs-Ty@$Jt$e zY>ph_y$jQ-0$|Zx$*m^6wpd~qI zZl^5eIBu%FIPEwsG?>0F+vXv*BQIsmd1GCLuOh^~2hegi`yB`)qPnev1_0H)?g;@6 zf8q{81aw6Jy8wt2a5F4CX5~)M3)=5im_SSiG@c>Qz8G~Wr^EYOQTQz;o3b~h#NREY zlw^T2B%_CWRe@4?748H8eVf8Sk8`w7Ai!?B9I6jGPT4e^Kmh$KeGs=ry`_6LIna{C z#a)08RO11WgWiRAJ_T-rXIIe!NZ5UAC$!{PBr6?MK?L?=#RVuJm(pxD1LU_`1bJ-q z^WcrTFx^R%${KtameX0ko+jFDA`7^m*RoKOw?1lq? zE(oFkk?25u^W$YQ{Aa><6;++NJj7p!b3#Cn8L5H)3uKsFK#+-m13_l{4Gjn~eqo>+ z(cS_|e_k?ap9v4L;_MEQ`LXJzj9__L4jyl zbgQqx_WrQvijRgvI9n81Y;13%kPl%q96V*tzp6*KX_T80fL4R&83z)B<$k=+`l6GVTO5*mSo8TNMsXYdX3^An@! zplXUUDi~8ZR_1(Oh7!p%1c;YoGTKEwYw*C)8pTtmkNc+<&_SK8Z~>f~_pBw5;4C)* zNUOD0TVsaLo!e8Yo@=~v{2d_NhaE^qND_>ro~)k_w`?PObDYUgdagojUp7nH4$D8Q zuTsvi9pvw4tS&O_ZHVrG`piyWTYJ6$z~fdj`mPs{A9j}kjQH+5uB01s!^#(SE`udg zM{p^CmvK`fhlV}Dq`SNRqSFO6 zTouQ$oj~S zd9t7PfRera{h%BYz4=4Ly%?O6Lt?$eJ(2@!#ecDFhQ8a-md5JXA#eF1ra;^0im##U z_3bn)<7)t|`+>pS&OTnvPBhSk*K4sx$Ekfj2U4bY+NESO+`2Kg48bY9`(>A3C(r;rVY5rL=&AYc0wmcHUz`+%}yrz2Y<2gDX zGb)N>^Z?6!b?ead<`T2u^ucxK{Vt6(nH>3pO}EYk`pzk9hLbdhEhT)Fn{0Y!?Nr4m zacdH=wn!B{5kTkwx(L+TtTSD08RGH(pzSTg;^?}rK_p1f;BLV+xNCsm?ruRFcXvo| z*IO=%+)2#v9Z*3`G2?kw(A==xL8chsh^< z)XKlozmK($dM>tA+TY?h{P>C4R6EaG2-7>bfwlTR(2GQBqxGb%pPH=jJyyOMSbdau{obrr{~<=H~)PYQC(# z*x1rtxt5g#{r2Oc2R&!|UYUe%LuPHkmk=4vGH2Kt?Wk_%FQrR93p74L zqXx51Xm(#(V`v=9BhFzf%yISi>0fox1^?cknU*P1${$w%aK6l~$a_|!*m`C(KeVxO zdAejeNB>XgP2smWstwXD7b+OR$?6yK-bN9WdBLq+z4Bf>P9HClK@z4Z4 zjQZ6Lf>{;*bIRnsqVn*ZHOxw*F2{I-20i8YtHv3=cM5&B2JhMAb+Is}=Jhi_13W~C zGX!W9j+RXO4l!Jpx<`DFQ%<%7P-GyPm&bgU=SH?+D$O3?^dr-ZMRBay-S$O?rtyh8`^TvS4=9h*ASwLwbYj_bYZ$_3$uL+w$olG4+ z%cR~tehUm&1O>xRjctE^T7AR@PW5{h5|n+B*pA~b)g*VWHq?x2NM7W$2Y3`NKr+~Q zlAk4Bunb$7PA{2-a>sA9z=s|c3blw6Y_s=4rC{eB=Wd7r_q`s=Ks^8ibH#G)XKEl= zFO$F$uUxqnp8nGkQKsIyxzx;V8qtz*$MX2s`|cKJgZ%z%Fg+izi#063UoBk!?P))i z;@LgeAp6dyHfSLjFT1}R!dJ&q%{yT<%l$V9L`NEoO{C9h`WQB5qu3ab=d|f%W zf8flOBvg>;Y@fd+%mfjADY%ZHYwDSLq9BKeYk!m^`kShlBJIy5umL#vM_PM|pyRTi zxROx+CKhbYtOj8u*AXRFuwNhR#bhDOGWWA8>`xsbjQ;;12`Kk-B=?(Tk;MYemBk+) zP47bJ7+k9^YrKPJ=78Z3v3;aejRzw|_1V$iZU01X# zmn;@DmmdzE7;9qgd;g!#ftM9t$a*>t7a!MuoCBQy=ZyiJ|6?WnKa0)(Y_0SEwawIg z_Sz(^jK<(6w`nJq?(Olg@kF+CR`e5;H4THu{NXb4w(gTUq%GtcD-?LnjJG{~$fywm zeK)j@)SI%WsMAEJeWCc`E$^R^wgLhT40h*kb24{tH8h^R{p+QF{mIVG`6HBDc2YL` zPD3f6S?}4S%jkJS@De?>&FISGu5B|Q;OR!aicG$$ zO~8H4psRG)-Ggk6;ApAU_yFLWmXz87O z3UJ;X{cF#x_2_yEPOI|vIR%sHuaS{f1bF{B@W=BPZfD;dB+^>bBK5zXC?;LM%_e%C z(L=dlD<;We(h+_(@bhv~DD-Ij4wfoR=>sj1qd+vFA5<$;xPnD}*Rw@I2!Y*pX+ zgG2ibSPy5xS9Y%aGJ9VIGeR_*W+}GKG0rj5kXFQ%E>_xjtb8 zY3aLHQj2|H&0`+xKkVwO|BdOwR=n?a5h5-_9IT#h;(b(sJf56fq&18ll{l6Vk3>wN zOfBV9?D7Z^_x_Xawk_aWT< z#acf2z2#IP^QX$7mr-O82g+Uib-U(0IpdGR;m%Ww1J^Q%(uLXFgSN-Vb0+_Q`H#KB z-c)=nbGv*mb@yTxW(>H#V(a*dI4 z_x|!^r*2G*nA=0qFhy~>|GHcc74tx{Xic$IzHp=3XP+gP{&E5=kdUb`IDNE z{JP70Gj>+nWcrRG=oc(lbzX#YK?5)?Ky%1hQgj=$kF&a%TVAt2Yu#!+65`U*lAX2OkIM!}ztv`*l|tHdv*M8l7@GEq zX#t^kX~tf{!qg4%CY-nZS2ZCfN_XqV^A_pGl}} zyPk5vzbV}NC&N|sxD|Jn$`e?dwUVj*Z0NFm(zBEYe@Yz~(SmTkWx69p>{{VUqJNDynjd?LlfB|y4sdpuy zIU;c;E%{||RV3`~vYU-;Sn9h^XW|F_t{V3GrK!2*?DZp{}nozVlJ^H!(z#0Nd~c6ysc^>)3&=#c_Q?b-93j!Ktx*3 z>yGK!e;VlK%9Y1lcUrLW4xHR&XAdt2YZrj_ccgqFL(yZ@E3SSv7sc`T%%gPA?#SkY^KSp3aI*V%=@ZB1CJl{a=GTGG?fvrN)(g& zfQ5g#(WI&5%1JuhVlFHIP3?hf+v_I;_!r;2BW+ZnvX@G2gR5%j*Vul7Ia^E5F=QMX z#Y4il3aT%Il(}(_D(T9{X1Rg3px#bLa}dkwt6vsX#DTauT&QU$#mPkr`{)M6Tm{i7k8^Q4 zBH53NM-=Xf6}?MOF8zP%SYjv%CwMDgW9K2mSuBt&Sf+3Q#n%$b9-L_+zx^6NBt}Y@ zr95v_zJn&cavs7~MeE_pztQ{4KK}ahuG9TKitP5BL!|1~o}c=NBYxUiK^aP$&Y;N{ z?<7CDbQ1Bsm^`h2l%pUwtondEve8Cea*?I)#_rKCeeI=t!N1@@S%b$F8Nt_b=+ zzGSkFVDft)FZKk0Vde*hnem$XSlFC@X$6UEjiNgSNXKxINGPuyNXBye7DFYH^i^El zf8Gl;kqSu!PxvX=FoH1D53;oSU>ZIVK4PEIP^QjYd zRdeX^vJ#_sV2b%Ce5Dzd@n{1Jwi5Rn{v(!rP9XTb8a-{U2lynr)=QpRdAIl7o`Cl2 z#Irc%du)G;WbS)BFVYRUNjew-o3(R)#$xZCKDlTWTk=a*bcn8;YG^8d_lo!QDZ7fk zGqcm#yj(aeyAUH`$PH+q5WXoK9zqbS_dpWTU&y_KDcXXsCW5E;^er5qyG6*2`lK51 z*J9<-^VfBmkouV6S}go^-#y*1!{{snRErfI$ii+>*@|qG$(~6L;PTR2GBv>(C>BbV-7&=aZaXo6UXh$Q#g=GM|V>zb$YxvWTQ3*oz$tSaDT2zzgi`=lU z1r+ltoQ6)~Q5=e#Z~eGFBanmlS!Xd|e@5zo)Uj}K5xOpxl%Ae0MYf(hb_;LQnhm(t zuX3l!nRcX%&9TU1>URe;KZE9Ev$QQgoDU3O?yL!?OTuq3@(mL^;Ped3pH9gyRTxZ5qq!fQU5J+TQ=AogEAARGSuDgQav0fqO z7%~);{nFrqHVkZm;4iotOY$*Bocr_QZSdcFdYNj~+~`C$Hzs@tads$f*e+1*rHh!R zbb@OvjW|BpNhKose&BZ3#iD+Lmy?6K)521&%at{A$?qosROk_Ho`HVY!rlozuGLm0 z>5)ZHBDCVLn4%CBlVB+kT6>YrMh2Rr3Vm?lLZ(&ta+J*!-n^IJrLP%TVRQ%0k`45n zd}CJYXM6wkMY*lgDf>AHnl?n*@Sdn!EwhK$16jvzH7@afVa{=s958h?0Pd;!tTG}+ zu$K5TnSEtup}n}!x5rKCoUPhp4=k`fWaeym7s&p6{{8ccu@5P5<;R;s%C~{dRYnw8 zI7`xpAB3jRoPKE=b|vaII;CnEHE&$D*1D}lCa6hGhi&K-a3*Aq%vuW7EbLB7t|vJa z2~)DxgC$@;Dx41U0{QUC|3z_Tz0Z6s9-MGvq*uz_cE|?z!RQxq@&0k56`i-Nq*th( zMem~dU=2ad5!|C1Kaxp<=0yB<3nGC6E+-fllYxz31NVTz^7fU@=ZpySWZ- z#e?}+w>h!yQs=t2f-3=P-a2uZls8#(jkA!`FZBsnPNwTEg+76VliQw`GbD>~b!`tk zfCu@}6;*Q#ce^g@A(s+Xt=)??QsCWHhytM?KVl%_50~HY!N&>BuXyELD=k7U?qwaN z5~SwM9|{eYH4*^eX^1e{ZKygQLOOTiEqWv3I*(av15^X|hV8F`X=3_6@~oo47A{Eo z!FnM5X)f#?O&hFITojkAwx-ftcfXJgLP539`%+rBwJ7IZCemIcbRrwM*}t3anPbrWP6)Sy~?syms3C5}I5*c|it9Za{=-$>_22 zIA{1TR@b7^5@HH(c4G`nyhPbSx4}DW6 z;?{*r0~*|>DA(@zwI^$Lo)z<*Z?uT|cF`KsEQ0@*sWeS4r$A(DhWWLf7CD#6)Aa`H z{2A7*Z*HMtLq+dO2W4!@T{rlE5-iBoke%h$i>+9HvN~)V%Rh0!dfV(H{|lp-9n|o0 z&mfEIvzhH;2LmpyaC>MTE0QlzdPQyqZgoLZCgZgbi`ll@eCZMq?JWmb1F&_FX%a(h z9U6vOm5N{GD$6YhC2Q8h=oC1CFskBT65TsPRD+m7E?N&dXuH=>C3yi15YdKMI7cR3 z&;_6mI`KAvBnXIwt+IkZfGn@0WwZXzG)Ls*g&MgHYlvej%rY&6uL^x-6k2B4lJ!u% zf-?yETh}U3m4F1xY=a7`H3V{@FVHE!{K{UzW-2GC))fD?fk{_BE{ zDyYe+k$J~r{u!a_(&kwxA)?#zBgpG3rwWYIKIQIa%b)y0VE0aM1&mHF^~{LSD!c4t z6m*6L4&5YF?Iv9Hul!MP^HVXU%Hj%e#B*-Pq)DERh49V-KNE=cR2opTJ6xx6=~=HD zO`!qfmU69?95x=~GU9u7^upKD;cs$Gr;A#D?c>-bpg1k`TG)8We&8wl&GdC z55+cG1E6Z94m*Z?(rIiB?3d1fpeWiL_Qs?KK|*G^Yo; z=I(C~wr^)VBR?7@TQHC_F;9}k383pnHY8UrV25NRD+2ZHk|s_3hUtigQ3Lw30y#;^S8NX3*-Q3KT*_4K#?4JM!&u$zf5e zv>aV}=NE`a*@k&05j7MoE;`xbzqg%m{T9c1LByoor)~gH2{IdKWXV}XbOestt^Ddz zKL@@QTvmll_th}rO3Bm&#Kpa&s~mO^jl=-@rYuf^%v*A}EJSmj_b#8E1kH;|Y9ZOq zf^X-L?oRiOI}PF}x+N2-mCm~b><1wVUlHX$*6KE|W616>+9*Bvyy2R}L zMed4%GtTLuau1}1^OyqV3(pM{U@9oQCNd`k!XOC96Nr*Pgg4~(`BVqPP|7v3S0*Z-)R$+!^=@+#H*tm92 zhFoFkGGXy}LPnb(&<(S^?!1@3N(q;!fQrf#1(sLSp{@d)PBhuAMuqiE!7%%6_EVWE zsjVvGp?v>F5W8v`GntXLdiHY4da8)bkjzwP(Vf<>)cys;L`>+1Qx}|!-d?sR0 zQ^RjB@b0*Is-Pa=Blenuw2yYff9L?txQ9@=^npokQf6SSxW9@-L@iZ#be#~=M9cc@ z0-{xy^Qk^1U3}*kc!)t<7QL(TB)Pc5qsleIR1dc@0xMn4XBTbIO=nvuKz0rkSW47j zkzVK=7}IlYAR~a6S|sR~`pjw#sTiLA(!q@)N= z`>UkUqU3uZZS9@yzeu4D;glCorLVUiO3ikkO8?t}r~Xyy_=kH1w5@y?r!zAb342u2 zc;Q~xO6JON&Fe7?3VBcl>mpY*!k|SFz%JC?VnCcn@S%I;@c8fb@>d=edY@dC7@+$@ zY$$#$Mb-X$EVu3|)AS$qZ~WDDGX*NSEpIxmg^N zdYCO2p}76wA#y{KIQh`s@%{qqG;qgz--q~k{IAnb55J@SD`DCLOQX(Khb*re{a&*H z^H%7R)z5#mLJ%hqMKU+qXcY`*pfjdC|V-?$-Jt(QX)MjIL}JGD|~iL>mE7gTcY%DR=V89RIxR znn3>6dz=jgXd+(uhDk;0)%1EFxOmHT`Ht>iq4h70D8dWHTj zcAJs(N`280(Of8`o!`{Mf5HZIbl^F&aJYJ0!12ju+lAY&oAhZh+wiVFjBrPsN8pqaNm<;!`}C9_8CD;PyV`RTQv6N|8`+Xy_S#0}($!-b&j>z_67kPsO!CP9p$I zWX<9w;E8Xrh6_Vc$6rnd+Ce*pPYg)zQ`&_*Z@-;7?0;I$IvYl{nL!c;k)-V}@-NyH zcg?>35>4hF^9vlt)ujWQ9&eV}XhGuGuj$i^k z7kI`3A}*WVi!B72gShZy#mjbz49g_uYQ01AFPP%gX@UhugJ3;T+BEDTwkKVJ_aSe3 z1*KQ=V(s7+yi%oXh~q8!9HHV&ng^f(`wduEM+yOjXa^>qjVZlR6AZ{a&Bc;0pUhR2 zL_h%n!K2Lz@{^SW{5Q_Bi6LvV==6buHRtE(HW%sFfC24H=Odp_0VH&WKU5*(EJsIy zZCx&wy9Yn$)tr>@zw1=~HnKH)ric0M=S{T#JI6n2)C1=PJ2N1Kp}>txA>vQKz#^;R z+5qhIM{l{Zjms$MpKb9~_e{fQa|h5yiT6w|xiD<96N61K+*whu?gvpEZ})oOJDjom zg1wQ!$F1~!zM#_}{~0M;b-9J7*!)*NxfW-bU&9!^GB99finv2DuGQs;4&-WFdYi@)%AMU{G-q3X^xaj>MCxjR zUB=)OKiEeZK4&`tN#H+2sEZ77SAk;iNB|32qv$}5F<5siAtZFoh8g0ns^l_s?XGcG zg#;^@Fpik~=yx&|LR&GJDlq$lAE10 zpd9^mCY1vGXDn}C1036#9WaDHR+UQtTHf*y0SVX2QL8!?MDS|kFB~4oYB!uyO?op% zsu5tB8}3vn4#)Rscl^nBY>Q)pm*6vv#ZQN2*La{y;(r)GD} zY~V)#g_uL~VZ$4-ASP|d-$fUPzkdAB@HD@F#pNUKufAK#2XVWlVb}rWc zcptd_$9`S^hc-pvf^3xV?;ZyKdLOv{|NX?)yn$Q=wUiermXaU}qS=-cF;P@Rv@zOs zhpU^%6{YkWdDb3u(g{(|Xs^$oKW^5&D(5xDGyn8E9#y9f30X5j`I|IT1Sa%|8Aj7E z%X^hRb|jU2SGUKw(^I}(SFPL}f!Gh%k9X_B8(Ky}&t+9rTUB_yT^u|Q)qvwP;E{2& ztc~>9@9}RH?rs(FGmG)f;X-&ozn785yxYCmjp$+2p@ND&I;Bndue(vL!!mcotqr48 z0i({ldo4<*mb>|LOQ*;FIAa*x{bYlWN4lb)GmyP`|pq7sIcLoQFAP_kMrm=x2^jLrta zWK89AV;D5LQzNSJclK6=-m&pG;5jb}#Bi_-2?+&bMd{7^H%yhMZxVwAH1E6I=VM6v z9r@N5?iZPHcKxyCg%I^ob3N~N=Kl(BRp+7>`ur)^8YG)E5^@RY9v30Lb$B|NpWJA< z1ZN947`5IW&7*I$-wjiKpv&$l>U4NhhjE^qq9O4H#ctj+w1V7}LkH(nFI}TwUsK3H z=r&_bXTSW9jc^xlZI8Cq3Jk?}6|=#$=)ZQFXJjiWyT>tdlgAU8e+~u;@jNUg+jnwv zm$T=2SUSvk4D|`#b+YQK&ys1m`#m@Xv^xpwExL8L(~ZuEj^K`e-CCMyvTu9uf8R!W zdwt(^KKJ)s*YWCcq>gyvy0L*Y7J-Q~^g8=;*qc{!D?XtWHAUM_bBVtqs}l0yjFHSa zJ9U&!`LtDBk@)-sq0FP-Vq<(C`WVg}K}`3F?!8LBdd65y*iabz2aL$i`XAYQQzg)n zHIV64%H%B;-~X2VA2Tt+<_{%(c^s*qHi5ITrCXXqKUadpm`c=C z6FkFjMDa1q42O(=I{&b0(7WA4*=cL0kynB#f|=nugys&0;#LLZRU4N4dMZ`?`E@+b zT}rn85sMn_!rvJu82Po*>)OM8ffGKQ?DvXf=%NJW!EfKIUGw)UOvOx~{&lbE_o=8G|g5;J*c~F=gi01AQ5u|*dSdfh0YJ442OxR1|a94$j zFbrjH>z9bj(uiVBC{wl=l)mg<@(z_SsvPop@)Vt@O?od6a~!#vHu>pwrRLlm)>W9s z=rO4dtmkMEPdZm@V~`92UtiFwxMA_oJGTVi#`d^}%agISmdo3Qku$w5xN2s{w43R* z45K=mZi^p%W~V4LT{c3rN*UzZh9a-RKkJD9j@`9hO3N&EN>5OcVoBYlL*=1=Cz6nA zAUkz(kf&Dnw&^;jBx&eG#CmHj8(>O9$#K@PF-=^IGA&wDR4%vC4r6EP1~TqA|rx z%B?4p7{deH3-2L5bn2Pv&qg1MBwg}n4!^l?piSxYe=PpzOnz0cFwvy_ zylEMgXi}NKY0TC$E(F7H3_IvWEy;G+L}Nx8m-_57TV%K^TqPlz|ya_r%)*SJaYUJV`cA!}vpHtIXOB9re{ z$f#t{uo&%3*jCEoa4?t4h#K9*F^*(?!HQOq}=JCznA7rPUc*XEDD;3mmAW+i33$3St@nJet4f=%R53Le;WFNnO^`8us}Y36^cJ3~Yx zoVon#H|(Ri5Q>dW3iLti2{76_+HQlpVw6h}9pG$FQY!l&ede>e^07USW8$$1wgn>> zQ^DVJX*d>t^Y~e^=~<_`y_IOHI%|nbPo6)ZTQE3IT^BR5 zpFJ9mqhOuRyQdd5jLIu;Qgl_6o!#_!zG?p0TEwZ1V6dh$W`7rkQ@vJad!iJ#jwk#s z`7%5rZbDB!r`t&J6C-f9f6dL=MqNQPGH7Z!

Fijw^)$D%QILN=)zF{%qagD!ovQOM zZW0D7S3MTb`p^fKgWIBSz&OT$m@Rl7fek6Jdz8_S+Z)n9iaMWsJ-eIfN-B3fppn;4 zt?VDBZp~de*Rj)55N=_Te)lWKog0gd$Vx7131veBRK(fu!8S`LV_k!m^#`~9i@Wu7 zA5k+yCVHXuTw?Kb<}$0(rd-r8M$2f1#B0fP<-cM5Oj`@l((EqzuNBn}`(8U`D)$Hi z`=HBODEILqyB@!TTM*xAl?@1;9SBlDD(?i)EqEdy~DDWYQ zi7s=-h{+QkXC^9#vzmaH6=@#0Y%Nrz6URuCxRgZq zlB56QJ;;neK1T(7j>@d0N9T2hCaK-K+2U!1+VPsJl(0CvuU65}V$ zVow^%oZDsj2_}&rl00WH<5WM-?!kpD(-X#^sQu@*hM?oI)tNl{WPwooL;M0T7*W{C z?!ZXK01V8S<{UUW zrG*TINa@3fnh79H-)ATND&YRnKkx-THS30s^hclGtaeuW*WImhHhbNq(+7JA1*;l( zh6MojGR?2zhQK(>ks9_2Kr|alMTHI{re}JE_)x8ssGm$~QoF5m8m;+EoH6poD5S zya2!iue6x;;1JnM6h>15Q8-VSZY9R#F(8H{y5bEh^Ho<3?fsj0?*50*G6e=|m?Lj! z#&mtGBd2hI{3k#dCFdekL!k5WHCFsM9#++ht;;$P81mqfsbs|j`T;FzRzmSij2)w= z!8wCdL|V>tiA?!M4gEFAEpgdcX`JF{JLBjJ>(|d1-7Dg3KqK`IDZ| zS&-{T5_UQ{`Ej}u6f$2mzmXVX@K+LVr_eBB-H!K=dm4a|)E*k1zeRcvbQT5#+C%5_ zpKuzAWhJ)5N#^VD6%z@ggQ{5L+e<35Ru4Fvlil&@`J-eC^w!w3vV`?hy}#A=D8`c0 zct(a3+eZHjlua;!ii-RbT5p1lt;`sq_I|f);k?w-iZ!V0EU&C;@>kH-gLvC0?#`Fz zCNVPijE~$PPNm}sS^cz4>NMI}AC3#so{#pq_C^96nng*aK?yMyLu+e&ro9S@4XPVd4Sx zerwnWzFxk$rf-YDrKpd`D<$sBf?U+P_-yj02ugsNoOD|&(jZB)&Y4epM(KQ@w&dox zdjfP+fX1$4n@q7U7H{sY!uj+Q5`@YZwQQck7t8mIW%{U8FI8F3EFf(&eUi!V?UO%b zqA@dO2aReUs>7Fz=GUj2FPj;xOgk1QV~vgK#!2$DB#V!|&dH~cDxmx~tiGalt18M) z{xe=yurY!x(t$VT{%nDZ~VHNpsjw`N~s_0blH|$5PsNuUca9a647_X#HSUIKWd2S zr_PS5;I6+;<&dnIt1M@-AV?Xrduk*G>v&hMA^p;gJ){CAH47&4-$~_m3v47t%mn7H zXKJ7bttJ)Nmh9ysYhS}wY>Q6$!U3Id6ALQjbuUs9%Rse2xWlI4dsQ5H-7h~xW!?Rl z%*dB0r$5k^GkcvdTce~qYiVN1b7zKq->8`M^H%u1v`L1L-@x&4Gxd#(L;B6x`mfB1 zv5M4;w{*WNj%8e=gwgY6KEh`HuJuQjJ)5r(Y1aOMGJG`5y(aW-|RL3uTvHwVjZmOjIlFh2;h6a3o$7`z#zMr-+&*RT}`|ANNd3P_@d`ud&k0pf4(KV)}c2PQwVr@hzs zNU)L({95Hr)E`jaF!|kt_sx>g{XcBVc1cKMH92Pf_F68)g^`NfIBW{&f5<-9`nBhh@d z{5WHhF)X4WVw|&0HvVSCg}Ia?KnmY1iHeN{XXRiFqc%V7>ZA zGH4PQlU11X&y-J~8dF;CS17+U8GA@I3H7cQIHBbD%Pn9y$N?Io{DS6UQU$K;8`S}HH*nm<$7$(*S zxty5JB#*99!wq_#mZat`NSeG$`A?dl5*yQ=rtLB!im^7-2EiWve!O^r6)=vUgA=Vo zIvIG%BC#Yg$~VM(i6`Q7nz2yp3}B?!n>-LYC1Iy!mlood?IY>BI>OyPbfo-vbd^LF z?ZUm@+UR2NY6nDx=PsK)s~FMYw*G4Z2WwJCK#sqfT{Qa+xtnR01cb!Gr2^B2`D^64M;nrvP zn;_&2DOw9=fOrx|SY?EwuPf0G_iQZe4RBX*yY@i5w3UD;~^~ z?Ny}w7&Yy{3V<`I9T`qklI!la^P~fT?w)9XL)5{Xle*Z2Pe~xGWw?PzhKDf7?28Ou z>_|@ITGWJ366X}gg1#SE?%0BDfkawb2ak3|NhJZJU8SZXS@$ga@$Pv=9~_2n^>%1D zgBHQqPWPU?)tZqj-^;Kp2s`xI!r6}7ZRRAaeHfY5T!2+7Z!zik+-3MC%PPQrym8%_ z`ylRm-(t|Q)*yp9ra`wd&?@iPCZu1!tp9}`R?my^piEe-&Jls{5O7&wT>hL+VO& zL;A;O{CN`WfZwj>WmHEu`Blk?LgTRKW%0*<-d@XwukD(;v8NS3+$E%6rI{vQGm`Zy z*tje-?=>|?G5nTbND+J?slJILg_QgoL9yQ9VI0JNJ99y3V~S7OzhC0lDF>=1UYtqu zADC}J&>o_|{V&V|uK}1(LSI;Ir1d?LQiukP!2{V;uXKYekm>hhH`nVfCyUni_h9xT*@qC>il>i`+8}C-#>9GT06Y#KX*H5=3b^4BtTT;5hhO;g{|ui}Xbx zgoi~e;&ot||aUb;>|1*}8LY7>F!f|S%h<@i|-|UDsLNl$6wM+1M z_?yV`(-`sGQluM3+;2!}Hoh@o{SIxZQ!N9@|KXMY)bsjl2WY&^f=3jccojbZ+xb zaQj7D%q_3)TG)Riy|%56{uMxN_l|x5FQS~|CU_Y5lq$=vvbaMS`BY*cbWEA(UpmIy z45dAsVfW8GWU)b2{2puiha%}m|M#7EFpilHu$RnSQ!ioCc4Dhk=C5e`6W3~UoV18gNuisJkWm|EbSO%*vkDi%U~kRk_Cwz(Ze7zChff#w?kD5dPf z8E{V=6dsfp24nm6DX2DMIFGTJsUhjT9At!ACrq7}SSm9k zYzy9wF2fI%qJrTAs!xy^SODU>GJ)coZol z|1CT&uZ)=y5_euQg{==(do<`9;|q<&Z-EB*#SDaNNPlr3rl-!7B?y;(}#HDy_W zr?cqpZV^Ib`(ll!*)6eSRJ{VV#BoRb_ke=uVM3j z1*#vT1t0kD7E)`OpLHS1TSzHpFirAUHlZh+M0U^n3p3lUaXtxDcIZ?kfAsSVgh z%$wIs6}J21#_dP4Z%z8~$4O>;6Ej8raW>jVxpEWn<5GHdeg2JS=y|+(U7IU%PP6Fm zN!0asK61qG*+@2~^MUr2$eGc?Q|ix-uBRWln7#-1YXz^!Zl*h`F{7mj{&ZBYJ$MA4 z`s1rcPQF{9y+jT02&{8Mpxb2}s$8pT!uefMan~WKeV1?eC9n-mU zDq9n;H@k%SKh7yMm*IBB-%at8mVBWpE-HS+z6ur};+eA#SU);m&KAiNIh2juI{({o z-u0UM`Jy`xF~`VfCk}0y)MKFX%Fm1E4$#jHV@2Bh-X1I0Yh{KUFbHoPR6X1{GP)al z*Bm2;Lr>oBxpY|R*Es(ABVFOHW70+VanLM4^vq0T&7oLGd#4;B2zYkZcSr4s5Zxvq z0Q9$7F!^!51Fm=SIHjVDBDH;%tGSG^S!{gGA&4P<-|a6QTR?ItxIOg&)Z7RcTj-Ei1K0cwBu16^I6CM z!}o#%{T%hLU-JWLk5mo;mDkY|I7j7fApf*g`j%DV!P;NvulVpx`}|MO(NS-Y>w+(v zYRp2drCZ#KmwxStTd402JG@@HOFCXlHtUkr^H1WG^FN=Mfi)!AE5jW)Dgig*At$uR1-N39*c0CT9P=U6EaxgX_)(mfX=##}W@pa2O}%qI_T_DYZMkgXxuC-P zSA@f(vITH`^v;2SZ#i`Fh3Ct{C6J+BhJI-phLO%r@G}_&eaWJ4g)*)OwR_rcc+Snp zx*FakD3`$v60%ZU{$+0av^n_0A6(h}!|<(NGNPhCeo2{Wbzn*by6=L{n)KD6*$OQz z?{SH5_J;r$*vGRleY?Vh#mA~B@N29&-B>HTQGMHyL2Q2tC!(^!r%P&97N>P6Q!`l z;&XVr>S^xq96b53CWgA`()e^0SPdN>fVmfDOT8^;v0!NUOPt|^?_6f_h@Snen3 zh`HwLv@ja(Mwqav5Mup~b)Ai5+2?k&b7;S*nwo<_@Ip&6bB*NQTMSUrQopBKrYF0s z=wyu$j6PdsU@aAycXlhRloB~ArvQYb$N)_IiRk_~b1SE1?8NiJt$xR3iDUAjzVdnX z-gvGb{b<+l=A*~E{Wl0Fv<9u;cLxf2&J|;|W!4F3?@0M6w1$kmjaSZ**f-T>#f<(BmleL3-8Msp}{y|-?&OOtmgk;zs*ZZpAb zsmnIpiutr4oTvALRHSaJoas;;)#LHx$AxlzcA@Xts83FDZ%)&wlHrq?|IO1uM~5JD z+U~x?C4zfS!?Demdk=gKH??XOpQ>j4Ug26$sj%j5%+p;`k?^UUgcj)qSMKLwFebzMc_(Kz+08!43Q`*%6+nUJsNT{X-{c2xo%1)XZ}Qup6IYvIkydT_Ctcny3>-QM?A=5y>V_NXd&O35 zrKWCruNtsFy*&Ymg}1Pox5O#{zm0ZE?2mCOkQ(aaFTLP|cj@?eqqKK-{=aB@3#ho3 zZhtTd!8N!`NPq+l(6~czmxkaHtZ{dD55e7nH8c`DxVr@!cXxN4&b{~h-dpQ?GxOH^ z&#YeE=bS!LRlBxT?S1M8TQ4Cedmr;52+wnC;OSjfb6lCvWmfM#E%_?nZM*H_9h^s! zVZy8y(mCF9WH5Z$ZI+zY;P1_xS2lxcn%!6d*IXTuR?#k3ok%KEhkl$$!R@xKcMsmR z%!@7S3y%{X#QruV8|mIZj-Cr_ErdAIM!#`=;=Rr~7aZ|vdFI{LUB~cmB@gxy30^EZUrXRP zI%zFou8WNbtG9Vwf1i5OxrXiX3e#JXrIiYo*K{iWMXTPDC57H*UPyJ7TZ0@=n)>{i zA(_CNJ3LY!Kbp4HGux#jOYH0AxHcE+B2 zt!GuY1cefO%jk^OSh%dV0l4+;kt9W9Q9YgdJ?Oup%nL(wrmTkA0q6_9X+au%NuTl9 zi%Eii1n5u%;Z^5nKFBpVbGYt(rcMp;zAj{)J%99GijVVsP0h;rWH2ZBTULy(h-nKi z4H?e*v**m-wOSwfR*i$hBm8@&rdgicu9kC5#M$svxB4Ltrm98P>D^CBnjtZYkE^t9 zd)Y$XU8wI5IPTzRZ6B>WUD`fsUy<3}lXgkp5-9L?buacHud3CZ#!uvF>pUNDkKu3@ z+`1~2x(Y{E9bLTD4FM%D%4423vJFJ=ugw7&XJ{`2nq!k>L=<;b6B7IPnAEUBbOoG6 z!y=}E0_tlAzn6@p(J&|Sq%R7{1QXOT=AkNeD_RrcYX!W-CW!uS<) zAI!aS4bwbJ+*HCQ>bTfZ-z@X5(c6sz_K6L-idwyUk7s&mUbxm&uJ$&l^pMce2t!tr z%1Rzco2m(D6b2l!ygWq06c)4ue{?|?BU%;LsC{g7fNL{WOIVwv+MO;5u3gJ{+u3e* zmp`!8pV)3w$*(79k3QHvek6!iQ9+}YI zfmbH^sGc2f=$dnEFYi&*M@tmEAmg|3fd-G4=3yALY?^k=J1Hh@9^DfI(z!%8o?N%Q zpB+Q@*AXYU6^$}6(YQ{CJXmy8TRtdIND@>Tlfo-KvL5NFdAFsa7<))3egE9fkx^^M z@SxRDA)(uzykQ8eo9=m)B>Y*9K#T2$R$zi-%C4MSVzgbA-qxs{6KpKo*rf=$O7 z_Tjj01I+QdqdM|x4i28fezJW^Ydf#WJc^A$rpf{sEo0o&mzdhn4!{QgEdX!-wtFg%Z(vV;J6NbvYK-8_l zVRxqQ>bqN+6{}Zf_W^Ly815eoCHOq1ihFHSA~tU#C||}VJ0$w*B-?kqBA6dSU?|EU zf~y;jz#p-Jza1;L!ke)hxhj3qR(Ncvx4H3#tZoZVd9ByUE!MgCG}(&V^g9Ex7-uh2 zx0Wb89Snh4n54;H#tIRN1jiP#!-j0vT{yfUT`B4(S!l+RXfFD2AKZ5DF7(!`zy(jg zZY^`C551PwB+d&q#olHJizu=CQ#bW5tMR-gkFD8sGmlbtzFJ|y(e;f$3G@~Ih&G@w zubjE+I{aFmn!YYlQqd$NalW0Cwg-Ek&I3XE%|{;4;_P^ANGWYU;cd~7{_gL!7h*-p z;EO9Br5ktQ?#rzFA~W2&_XSWR)K_{r#QF#8z0C5th=ikhO5(Yo%c~BX zPCr-cNN;%8Iqul(@X-#1e6Y~+UEr{n)VDl-Er|-ovNW}17AKl8Ie8$-4Rv$JvY$M8 zxz+qac#F`(Jw6Biil$`+yJhFFxe9J9v#cNHWxTu2SG6M}%*F4@Dvyf}3% zlV|j!OxS&^>4-(rHKUgSirEpr2M=3BH)_OMNe7|HAW=Z?AufMmSsS4V0xb<|8UH{6=5A5<__4w~e?cGowte(c@Pv{N+z@89XD*6jM6pp9q|Z{NpjAY=G}R z61V!@7Qe70%2MLpD@^ybp;MpC*M*p#lh=nMS-53q?aCJ$MWzf@BKQFOSS+-<1insL zld7>s`vB_?jgoW7dN0W&NL$mDR(D2KcH@+WU&=?!Hz1d-hw|WZm5c;r_RB&>?&D4k z;$NAavUzp*ygg2-msH(5-Q;*wG3~2N(;(DNnl4Oeq|L~En5-;cmKyWhpVL#DzH5<> zpFs=0y~FvSU}u^)%u^0`q=Z3)mi;RGUHn<%s~8IZx&2DbK#{y2<3?i9NHYM{cF}4Z zCrEolDUXl{z%)Pk`W5!ei*Jq~C-k;A=CIQ{e(&}6k!N$0tWt+)6P>WGr7z3WbUPQ+ z^9}sb`78r~)#DU=?30Wtc_Fn(;2b}8>Wv_lUpt7)xWn0Gez&gfp_&7A4V&FHPj8)1w#Q87L4?IEa{|<6p;&hOpd1>L4=E*<-E;3B#7q18jCk`o`)3*7 znGQI8knw@1lIj1ws?mzVU9#?lXTL>dYg_1Kbj;!=H)ajiBumL#=mMvFR$<2bO&x=U zTKNbvtV*3Tu`GrcEOB|j`g*jXWV7Am%<7$7mmvh9sUs-DyYu1+w=I1~dL`ksP{xk) z@P6t|^|y2aTemd}4Kv+IQ-tGuk81C!i3d5)zE@SayaqGsJ8r>VcHAL9J}b%ouD4V_ zXZggZ(#H*Y=uZKz+egC!`J=e@(Ww%!&Cd)W5l7WGl4Cy`Xr%H~aqOcr-$f}a^x-!% z_+5yAiYEqMpOL5`y_-RDmZ#KmJi3N|7?+s*IZ+HO2+M=iv&Sq-lBSx3ZeX>(LO_@- zi8Osdso2ftTh2DjsQi}z(4U}XwbE60%4dW&eKVmO&1YD|n|%9T#>eRKBYR%-xtdw9 zLTpfvWtGZq-N9ur!6~PN=lndph)&62EbkScKAE4zRB&)!dQJ&ra6l5nCHkOVO5RwA_F*KMayHGd|>!tL%wFdI8??Kb_aV+{8c5D=UDV{a@M99RI&KuQ2en z|ALI;_)m|O^MB{D)@Y1IkbcHpDy^E8LKP42`&3^5_jW#T7orZ;5me-)s;GK<%ECjp z6DSr>XUCa*`K>F!6tjWu36Z=xY(*IBok$Z>=32WCHLU@T-s9fkQgJF-p$pN>6KFf_ z*y|X&zgOlTzfy>KBZ2>|)N#d9zClXPDwt|}15uqnfjQAsj=@_mX>_CHY_c)Q4fgcQFEd<9>DFck?2Z%pt68Ct^f zVu#cKmWk3*GlT6~y>~J>8554xafGDyt=}$Rp4sD(w>9De-PrAgA_S1(njM7GX57nX zF4CUU3JC;vdV5gx*8%F0%mIURsYi zj{YVIf&OlSxbGe7YjB%89MIt{k^&?*#t;F=;DRB>dZq7yq<7FI)hOMa^mueQkWs`y z3NKL}b>3|Ot?EL}VWD}76tgiQ%AhmcJdVToh;DgHFMakBua&cM^1y;r^Q_;OMWrpM zgy;kYwKVF0XGJDH+r*77;1Fi{fPWyTRjrRv1;2x??aS6Eb6ubw{(;pcU;e9CqJvKq z4K~`R^@H-!bV(^Veh#Cz=D`;Ejju0^si^O&jO`A*Cx00>Wn4yka~jwk*M5=tybXYX zprpNW_Li=yEy7rEddbszCjxQSEq~;E@?6PJZM5Bf3y&lHIZ#WXwkxap%#&kn)^U8? z6~TXB+han77?+Np^&A%?PiAe)>b#D{=d8JZOVH`1$5MyUwMF1ql=6Cq4yI9Egrc9s zRAi8=$=o3}u0Z;He$=)pn<~cP{6Q|=?ZW7L*Ol-Vd#8cinOug&)(4BxxSO-WqU1)7 z^^DS?&$c2Fp=VUGbk%HLAM>~arv#PB6HJ2~W(FJx_O->degy4~Ohez=-8P~`+mO3( zk%{w$V3W_(KbCVw&*8;Fo!y^T1qCPjSI6t?}Pfn~H$$uoxy@k7U2a{A_9$9VDTN9@u4 zUosO=&m*<_r8ec)|8>evP4&+yJ2jv^vBbu^Jy#dv-VKJ)fgm?=c{^ak1Q(5|8<1&e z!alwToP~xl?kI}wKx`&SWs#g|N)%eHC_Gks!j4YOKIF@j>i35D*Hm3p8a)14?Q?u$ zc%K0AyAm>$zLQz=5aY++r5;bbY+uCO(LvGJUew0Q*2db{+L4kAg;~_b(#BrJ*1*V^S) zi5({PmO57Q9d=l07=@mblTnPo}bq`e~W!DkyHbj-v2%e0Z;6zilv!_`&sXaVe zix1xCv2H0t|J=Fg$cHUO;Hm3H1E?8O zIx5`C3$?kA^R)*6r0<tFY!2mpOeSWm@fZ#iCJwM%#)*9 zolig=2@0e#>JXvju^pH8?M5rB>?0u8Z`kJ_V! z(UyM=v_d~dH_SB2bEmG= zdX1?OB-a-Lhws*=B_#NA)mrBfaeACzbNP%zsuY1NAcNvoI3M1r!`YPA9n<%xqg4GC zUx(BzI|&+K>pnn-7V~U{g4hM1wisHEto)mQuHv(>ZALWG^mtrjrgBs?RNqA6k!L0XL;akMH*+MyRoc@eiv<|ak$VGGR}XmO zXSu>i33`Ft=eIOJe=y4aRI3B-8Fg72P7N30I0A#fzZ#Ub=^`44K^}>*UcOfCnJ+}B zHXdm`ER(+2J^Tn*sRjT0c#o~1Ob0 zr&TC8`A&M+MMVsP3ZkDIHuHFFrnIu;MVofAGbb6Q2*Drc%iKiG|2TM9YD> zZ99$`F=an+{D>;;C@zr zVfz5%iv&twXR;6UO-b?INMz({l_Wy+>w*hgiuggtH#$t+Zes?7ueW5w#kZ@5Ms0-` z)5n|8)d8n>VZ9(l@!C)u6-P-9iZgMd0|$}Vwv=D=!1Fk8@DpjkF93hS$2lZSk=jO2 zqY3dI5Z7xB%pJ7Iar7|gO){E*p{6k~ZJL{bEVTP-_U+U#VVXZRs{%<_oq|VM!7gY_ z7A-qv*~q)Jg6i-zoGQ6(!`*YaKfbs3Z@h*wXuhx zvljzrKjmG!*VOCbT+m&EtimHmlg;FMUhZUM?(u(E@?%RKC7}g)o>YQ+0x2@Pb;ENj zm*>vDMePW7zm*U_is=Sf>XnzM+@@;;I`;*LgNfrKVilf<>Qk3A&>I?vnX-34O^(QD z@LyFCAJH?u%`3xUlFQzc6-~k8(rV*|3-rEv^AHL@B{Y`J(0KfLSOYyjZ&^iO4D~u^ zx@OfPs`f#}i8Ov~!I8x8ck+6FVNPc@n);MGi< zbDm-Rb;9KK1LUHCHGw*jYCT>@tcSUw&!%-wua-F(K&PF zb4EvIZ4-n-)&~sSVTzo}35=_2fAT{lxkIlD5N(AIINh6RTjI-8B1JceJ{ zgA1d^uOfv8XOqVnCDHlC+S)cJ z2ZmbH0V#?a)5Wm!V*ZTlu0_JwEyKX7uhmU)-5>^bzfxB5Kt5Zzq-}-zWpr{nSzchf z;Phf6%hQ*3a|0jG*zLw6FU@3ss+}Ik1BGdL>S@QP*UZ$b4Ah#lBP8n6U+OKW^BRPG zUuh;`sG^I;JBl?Ow!u41(5M5d+n}z-9nCgZ^G;q{4~+v|*7qAD0HPj=t`jo4g)EN? zKfCLy>vd|is4yII1Pt*&iHo8ePDk**W6atOhWV7cX0l)GEwo-dI??5)20Dd=b)!6x z5r6mcp4GYfHzw4{g>)tLxlapbAkF9Se`Ql@5S- zWl|MVuBic4>%{M%UY{!jwW=p~&q}~6+vAB2GhFYaVZ*tmXnOCK^Y01dGXs4$Xf~VI zi!14mN7(A2H%JO(=P26%K|RD-Ef=3{v-&7izz|8*06(C$FNzM`TBI9Ka>3mQ;5s-> z9y{T3&Ld%{Rx&$vtaht{Lf**A^2DoHq!xi|C&qq0 zE<)!O>^wJraCcP{OeYFpmgg`6468{^D3VOYzKvb&b8V6+Ex02#+hbLI^*zPmz>fh56Q?P=S4}ahMdDgZR+8Aa;}+SY2p!13DFveaKZ$! zR(HJXD4oELc@7MEOBk!cw}8hiuOH`GMS^K1uD(P6%e%R2Yq_j)mXvqOd>C9(c!qB? z*6*W5$jEx~I-{=d`;|9GzYVGb#Vw+JgeH2TNPCM=(yaq8=ghlpkpD6PxaDb1q3hfE zykQ3v8nu-DSUDaj>7g&&8>N#(z2buu%bS z$!UQGY>ZHuDh(C~_rPxWe;6;gZ>V*tV-#=n6Ay6OLtXW?Zms6Z= zdZ&oxZ3PT2+V}CSIn{Z1(k8!3YFuq!&keTypIAe{U&t_G20D$DDv3pl zQ)B7WCL+S2KjN|w?YlwZu&;ZAA>hNt#wwi}8+Ja(U8yp)cZ=#xnZ~eWnT8y*dMT`q ze5Cmx@my6o=8wQQd2jMQyb_{*ypr8b*mJ(ch2*IYOU8qDKX-%3(?Z1R4FS}!CpK$W zrsCS4*~Wv3!ju8cMH+HEw{Dc`r6%vxOAC_dfpH@8m&ripzBHEj>- zIEK}_y{T`F-$16!M$8I8$; z2c&YMx*F?QsJnbMDM~JL<|gTmxIfYcabRB=e#qo&txw(W!$xqm10Yw&x~8L62}KqR z@_9cb4|nq?fu$&9PGz~&hPQ99dBiqhVDS`zJvSWWllF>e$B}GS|*J2 z@B}7)%7EA;Rk=HjaMO-W&{`P{td!%mj!3LLb1lr0!(wr}-h7aKJ8Z2od9^Ahm4CDcrkRdt?EK?t>p;{!T)OWgCd|%dIrYM+7-A8nJUm9 z6`E35cqRS*uy%q|vW869vdHE8ONbi<(nhuTp|)LQG#?^Vryl2XO0Ixojb3I~@Fq zfugl;588JtKx*&rH)rcr<(M?WX-ApMdWP5I&YkSon_pT_!>O$c&f{s3`(w%DZ0S{| zGSsu6{s7N5%sy~vH#IToCIVXA1@xMdDSSIBF9d)%3szOUi%sqyqc?AU5=ujV#N@lu zJcvRg$rD5k8En$l9B|7sz_;$4baBH+8oi04+D%M?O;aYE34&qSf7df~yt-UsO7dba zAGOl#=Y?=-&+_4=l~WZshcoSmm@SJxFKSKzUy&A*YCd~6hG5z`jR9~19Df^p`Ff|`S~MaZ!OFD+l% zXvhyA2QZD~`gO0|2d|@Kf{>(XPLC<~#$MA^&y|Q|gyU$2!R?OF zJ35PsaqJWK_d@w?9xFVFO7|ra?pih{v$K{x`x>S2rdR$G9dYqy%xZC%iEI7m#oFkP zq6_nsQ7!;o=s~%rA%cREL~@oX8xo$kf}cRR1-t%X?2>w#A;+^SujDgIQZ=vqrs-8U z^lL}Z7fSJocJ#8eTN4+;To40)lfOEksf9`40nZWD^Ud$cdPa<-lfj=$DgNN%(IwJd zPl1|YON3`OsV|W-fA^f{M>QNbFBzy%|D?AOxupmU=c>x1F<$6{`rv4B^EKVh@ zfT_eZFn4`TC%R{!xo$m@%=2ex{rwQ4XA(+gZ`KR6lQmFJLO0CN{1Fwd1hC(J5wtHj z^+$q{`TB%}+Hg;CYLp5f>E1G2)b8fyiW48GVoq3`>RAaBZ#D+c*73QZ#`Sw=mxs`{ z>U_5^Ey7ur&WI%AVVfMv(YEI1UWUi{J4Dp1d8 z<{Z&(5cYOQm_+|Gas6i!9l*)O`tM2f|6F;A^MAMU5=2eT0!Nr|Ng9O} zX-$Ii<|SOhPhl6fWnK%auwp4C+;JxNqD%&eWUe*y%Tu2jy%kJ4k@BVHdy1xujsA#{ zgOPXhho$H`L^EC-ZmwC^DKVzXLUaUJ#Z*ud6)Gy@Sl@Em?DH3$_s8ujz9Pt`T-= zB@%t!LX_~pHA0eg9zjFeY=nQRIgDwYvSs6lI-EokQi4#jpXUQT*86m6$u@JYW{Jv;xao%m`tRt z<{+4lr2yyyEY}droujWkZM>Wv*`qB~ZLtJYPh}DMT@L$CiD2ZnnW`~L0*;7hG#b5K z2d+wUG)~-xtISUyHyh8L!OyBBQAXatF9NKWh%$xbEyS$wE_IBSmq~{LD5p?ngtKw| z;z`8dn3n-6c=M{Xnesddfvmkoab9WR92pbId(hwPW!cD1A2d>X@Tw)ph~(B$QX}(- z;!j?wfq`L>Y4Nx8LF1wq{-1ZX5!{2t_1(DO=(Lc6 zUwp(xS^ZBU{Z+yrg0Qpwn*{%&8>}AxW0VZQ$@1?g87m9m|Dz}w*B`*Vzew_5QL+rG z_+PTP-RnJ3B(RTo-nbb%_MeR;JMrH~uuaSULhErF8P=HE%j+kM5Vp5J8S;)tGC0ws z-UeccAW{9_{)hq6mr}vyM{q@V*aAu5@-nR4m!zZ~z1DRegMhYYjmw0Vxb@SUrJ7DB z2YN}Q(^o|*_`jDEGT@#qt5Q$7v`VtC%c$=WOIkKWLU|1R{HacDR~ZNCEbHb?x<%7# zZ=-p%I|~!vYlwu(M)y*ScCC&InfD0lUe+)NIlTaS&FY5KWPn?)L_&YPNcxl^FOfIy z%D0u;B;m%IQVJ{?O5`IsQPX=ewEcd=e4tk{TH_@-`;l(KpkEi63Y!3dH)EuiB74!A z`dxw6arVe;Nvn$R^73*DULd{VHVU>#lP^~^5uGLki2FH?ZDR1uXSN}ZV)L{(0 z15=(IS-eD^KPyU7e*MC+1Z0ZyStZ?=Qq2h?xF~ycQ~@|JnNdX=L$sg1Y1^l<&2aD4!(%#o z$U4>jbCE{T=&98ku4QBi1y8J)II3p|((FR^d7P`t{|8Xy!EUDYb+Pn7Q2Wq}WE=!G zIDL7-Av9lZ0~WRyCc*7O6?2ERf~94(?I+5@>#S$-UnGKL+}s9A^E&cnn{NhfxlEF> zHzBD2Y;mMhTeDJ4NMiS{P{nr_%VyDvnfLX&4ke+|ql!N}XWV$U<4fpfr<}SbH@uT`FiuI}#XX~YxtSKJqJG=wKSaU=bz*?J4tjzQj!eF9LV?KBk$?lrtgrC)zh#8?(G z5Ym2a*33rOfuDdJtoYVJO5j^g*J4>3SJvjSjqw9AA^kx>fYESFJZwc{W#Kr-!zLH9 z6wSkgI&6`L3*bgf^^y(fSFfFHGm`JcO4Xt@!4$He>QS?p)Ov#*t9XH)lPj-pE_LS~-m zKTCR<1e&_y*Hg240(*WeSrj!Z(%;+EyIA^7Y{W-CUN85r&=5`^TgA_PboN|)>|f#PcgcOvaWL7e0Y6yNLCZ~_nW1+zf%L`%KT zly%Jb5krqJRX;Rm7-8gmxe}g7)XJsHr8Iki@o* zE3?yJ^?g@Lf(Y`ue-u|7EK=><7!~}|0+FC;QD9Up^w2E5!oTA5z7dwHOArCDKrx;o z*ejdor$DqwXSP5*){MH@d@x|~_(RjyJ}vGHP3j9OY>}7>ed;N@rZz2H)+2;3e%X|k zhCU2PdK%O%2E!-k6IvNikbqwMrxbggj`irD-TI-vJyF*+OBOSZA*cc3Xb5o2#qdiX z$;#XYW@x3LVEpI2@1n)Y%^H>-eF}NjVz1fTC(w0D{UCxI@HM^x5vHmaKWtEUto&A&zgT{ z#J$-_uHFCA-Rl6(U`zy-KBuGSI&QWod3NmB~ zd_nb-5ubhlXDw4E$TC^^!#bqCO;#B4(B(y1&f2U-6ymi12rM_P(e(21apB2W@utsu zXu`u+4J2U{1l0T#e+B)x3DE=w8yPkWB`7isZBxYj_lcOdaFwWysJ zS@+gY->Qzc`u4Q_*0XZ|!l7(z)n}YCQg*ejZ>`v2^ox;S1&4Y#t z6Yf5(b}jS}TZjE!NAg~oop_I{I|f>t1Zv$IOP-GDL<>-YcE6U+o>>g=HOe=>L9Nv_ zHXHa|PMe^?k)N9o?Rotnn%9m|i9{r=t@H5}G3UNf`6Wp;9+qn{O?V&_ipr)0w~}$Q zHQ7_{=LEgKDV5%opf@rUHl`6JZbbM#8o_a$4)~1AEs6Ch?U!eDg}~IL7N2aE#$yD5U+srg5gPQLfzh_J4*#!wm(9EkI^e7 z2~x1cWA0J*#~iMtPkvqpzN0@;(j_5WSLKUGCj;{8So zJ$9}UHH?HUNW*J2t#YirCF1fg-&~9dNVJJfWLz68?p36IuluMX+<(~wt=ZIAETw)g z0kas$RG!``b@l;qG4hTW)}-Brjf=dyvs#3X{C>75B$8->$W|VNpRO?AUt)Ru0b(Q*x+Z3ku%N^l4Q~q2AzR z1Z`HJ>`N|5Bdw|sq78DDs@E>D!L5)%t~ECy4`r<@jL)~gY1cs>!^iuy|P(#1a;koEte6M1GGNP52+=vG1+U-zfXSUx(N z1on!{%q!si+=9(!-d|?@t|f{%+`+ z!bW}7ZgU`(Pcm2Z8=PFawZTpWZSVVDVy3T+i+8bkIgsmi%3Rs$rg4F+(MObz3em3b zTM3)P%7qR7G0Z7WT`2ytC2)#JvFTZNm7=m{HJZkAWkZyt7~4DJ7gP7s>Zs4K#4op_ z%SV7Ne8Su_FvO|k{IkO*{0PB55Qa$0l`5|At3o8SXMEQ&nX+#dQ4^t@?#+|04pl;X&97&aArYmX+Y^6?L)#ymIU45&2BLxRx1 zeYpFOB;e}4-`vEvIPFe>(d+pN*K&}){CshR?~5>2=>XZ3wTml=EJuDO@mnRf$d9n~ zC@VQVc3HR+U#VY{^9X0yYX{Z5G-gZd#LGVo>^ z+_J~nc47F-zYN;lx!*T+BfyszT~Cho*7*fTnSUVG=vE%-*w0m4$*QO;Zp z&d5O2>a$s&+#I}jfv)fGAIYue8jM&|vfn>nUP2F@PqF6<(Gj0TRNSA-{7^)6n$B{) z>SN=A-40Dz(uPxY|MHN>abM)X@c_tUT3cHiD)&@ypf769^!Cyy1B=r0jH+fGc{1Ad zed}KrYnD%=DEbf;Ct&3Q>tF6kBo_lqYP6YfI3r%MM_>alXTt`NCP8}BAHacFHkg-4 z!unIp$06l*A>oAIu8oK=Zw_TZ?0x-}Q(T3)r? zQSb6@-L`@cvv8{L<@p>Wj&Xn~VTrS=x8K(-{NNUKQao(ZSu6uAfzEwOkrmI2eZ+&+ zY7M|515XR1u19m`83yxl?&v93 zI>9$uiUhc*V8S1PJpTNTh&3xJMKoq1UZhS0vZvu0w-48CLaV;*nOv;S^IX}ikKTf} z=8dwDYzubaO=G)5_#a0~p*u?)*+81}wgsDRq^3|yIg*ld3ZCG=H&w)I{6I)x<@;|Z z(W4PKz5E+inkK3cWPUgE@^R}0Cy!&=FZc4Fs!SWnqDjuy_eU$S(_`O)#vKH7v;@g)-yh&6Q)f7cY1?EDOU~yl+-=T(tl9<}sj*AaxLCS$~Gy^sm zukh1polTuqnpXq|qn-N(ZDDF!9ZxerO>c2q`4Z~I1lv(}u>TarIldvA*M(p6`JHtG zM?H_75!9b?h9Z%12lCc=LUBzb{Si-oHFIPRDS-28OX6uL%|~h_d*U&0_fd%~MS`s) zh>SJL4W}8;hxkP+YFU3;0*MkqUG6Urwg>b0>^Hk9I5*a7)d&P3{GT;c>>gHvqn?}w zE%VtVV%Be5n(WhkYjEU~p6J|PnrM+f1HhFowzwUbWeq6j>U&WWSD) z7UNE5xr|eUY|V3McvReUrYiY^(}B)>Le8=6>f^u^u>1}z7#y%y7H&bFJ}-(PlOba3 z`}Oq%o$*Mzz5}HYkWz!xD0q=l{3$?$i{M+v4>x)TaTwwSU;XN-BKr>6<)A-w*R+h{ z;|H5dav_Mk;P!OQ*$2Qwo@Ob|?un_s=%XU7hX)0?Q5xy#PED5Iyl>L^GrcB(t}(1nfQOF(uGWx?g6j{(M<3fa&TQ?}TGdMeIAQ2Io!_|~ zUg7L~OR%mROtZexM_PVux02G2qlP)sS66fsKTKqe<9iSKZ1mpdGEosSq&s`vAkc%d ztXADC@bm&Ui*!?L!ZB_ErO+aW>I6ASG*rh)c^3jX*ceIWp4g?^hmg4t`%pH*6f#bN zI?K?a#o4+Uu_&*OPYtBJZK%$H3sK*3i`3S>61xu={oKZq5N-ES{$3=wYij3R-;`k0 zH62EMUUJyZasm)lL8lF?U9csTquIXCUUVmpU&K+ZXvz6|lIqRj6lc6rJp=q^JLTOS z=+n_^-EQ6Qlqa)X&Eg|4q_J~U%)tKm@dK?5@q1Ncme9AuTmXkxQP3H5)@?JPJtU?i zpXxTzRhwI-ofT`c&Cc(XLg^eYEl9gs`{Qe2%X25RJP!&wKV}CY6;BQS>QnbncRr2HL zcfoPeQg|ZDGcIwZEsyb2(!~(+P2(5=d3e>)NicCB@iOSPicX*R33!)C?@lkRyj6`{ zIGBe(avk@^+|?ta`Jp}%x%&94*WHWRus~aNnl596Umf|Lp@V3SK{DQ{|PU=#NUaq2^r_*wfqdu*SCi8gf)D#Yft=C&WzDKlH@hU&6tmTz+6~25V zuvBF!FBYWTtI6coKCdJ(ZVyO~nUGbC3b+oKm(hcqkQw0=VGLtGO=&{#|_vU=WpZi;Ff zWYbeD*o$bJ*bbWK+tfWz1qcWFkW#H(H()wnR#`Rt(l@S^t%%v|BFbhkqwL7m@#-xF zz@+L7XlC_$LDgR`&J>vP`RbO6@(&(zyLvPg-{e>F_C%XW!c&m_hxc$Lk~<6IaRfu zz&A0?Seu=TB-Ote*A8ulynfI&BO^@?tYoBJO^$RKzjm!dMPyvoZ~`}`?H^IGsD}dJ zEqr8_1`3o!n)4ErZy88M2x+gAjo5Qfyn2W?z4n&G^{RAzf`#uN?B|H~f7SfEgw<7*&xs>;7-A?n_)n%=eyD-PY4kqYsV;Bf>X-@8UOYjC}?CV+8P2c+` z%nHyRFG@el-ShG9M)?Rv97Fu!&Tc1XQaEkSRMLM#_^v9Hpo_2>N1gMHTwpV5&{Fv0 zgPA7G9K90@%Fd5+COBR|YjYN>vcTByK4cQS79I$x9}-fpkH@r2@hh|a5O;q?mrdkOw(0@tzHm;v02 z(kTWkg<2oj(6$`u7ys2VIeQ|nc46Ff{{-0bw1huhM^SAy_;kqay(AWeAtw?6(PGd= zm3F=M7Dep=y{JKt6RpdE9uRPLei|qQEtXpXCzlzo@^1n&{Ngsth5| zysxjhFA8gjx%j~uRwa2M-BLIAENh_HaFgx)ng^c?dEU#Z*R$E;Os`qE-g9wta%FyU z@mQEGfUVGub;fZiT>QAeJU-C%6O9_I^Dq=!F4_tHFV@~FtgdET8w~F5P9V6uJAvR3 z++BjZyE}y7?hA(?!QI{6-QE3Px4nE0PEaU? zog45cTh1^O|5v|HPf4tX$F$hLOnA8@nYc;4zQl5i$T@G_f z=uy{XCvHxv!RZ#6oOMr}A5QMzMP*0+NVC1uirr&LmEX4 z-^l?gbmj<) z47>6k-aa9xp5FOx1a}*d>WrBz=-;zMXAqyO#q`%~Uc79OiVAzUFmp-a%>|X;IG%wC z_4(X%^ILq8PGQbB=g-1Ai?N6n=sygwj~@IS+UfiKEP(ET-r)r4PqW&;e4h2-y!SXc)TtFTFA82CfKAg;nBpa9)(}Ss!OYN7 zrypP#K^d(&>4iMI?*+sm+tkYK-E(PIgygG&O}Oeb8fxLaXS~XCWH?`6-$DtpRUE%7 zuo`@FG_R{R;TmF8nc%T@{Lc23^%{&Alpr3O&VfZ96G=byg`G>|Fw{Gu7joF$tK(5` z+q132;cOf60b12W!qRY~d`jYlxf%jO=xefTjr@t=(vPF5B|Ta@?M6Ql zy4*Cw*G1tTl_?Cz}~f$$I&n?Z|-4>E|y2~JpA4m!>+3zKVGa%twhs}s_ET4Io}3UU)=uZ~}m ze)MEZLX#>&Wz7+A((YAVpc&q0uZAdRwIm>sfV|9Hy5`pV^9n^~OFk?b$hd9sPcS#p zCADf4v?j0(qwHSDb(?OIMYE^Td7Fp?CsqQ!a4m8}N_f`pFFJ~+^pzNWvP`UJMx@^! zO#il*+`$@DZA>t8(om?B^XPXxNS8J`rdp~eB%@vqbf}U;wJ_5Dsnm_7rftW^dDv>Y zuH<@yb%aG28-#eV7g(9WbWNhS)a|u;6OIShRici+g`&rV^DJFdjdfN-rVWPnL$?~Ab2{v>+Uj##b5X8ER_q&l{pC94WXPF#=k<_xp_+!y7v0eDZGxe3fNm>A?@$Q( zI*ai1UtyHf%9}Ld%;w?D@hYW`Yt+-4DH^}$ic}u2l*XI3+4_z3<%sceTlVMsz^gL` z*UxD@z6DYu@!VXnnfQ$dvv&?Ts@1A`=GL;I|0vU`R0(pUsm1*KeXl%QYxxwB1VXNM z_3U+G{`Af)3HhLJ8)nWm>@`Q%(*7v|56`)Jjx(2RC~cImI%u1-c`&;^_qvIl(!GXj zWdA`V;tC`d^tS^x{gk83ApK19c zh}D%BW-Ep#(;qp&10rpXqM7T4s(m>jo{~!qoX7c@-ZYhtj}IjLXLJ8vPfXXmA_kyW zsTWb?Ikb3|rhvMU%qB>TnHF^n(;~WB&?vw}C@wuVBMRe>VCbd`{ia0<*W2TiT*KMz z6TJHbRdSggG3!a3huHmMP$S=XW6Y$P{iO;XM}xsAgg&xOqt}If&1&Y+uV+_c%iq`P zSud~&#m!wm{j8Oi53jF>nN@%ulTE^?PJI^4A|&sTan!7J2rT!_GnlF(lqY^6P>yy?jH>9kys^oxe0d?NI40b7omJSAv-%im4fZE2IowmPkTJmLUh@?X?V#!0fD#KI^X|7aUw%KG zI6w#ml;F)KeiLX|Wx!y1pvdGT;}4$jSI_axnAa^m8q(Qo9LkJnLTpc4mo$Hw*UyQz zrF7r4_#Om-!ZV<`X0m@35AuULET^xRuNCO-vg0ugu41Z7s^m_nMpg(sf(sOGKSxP+7Y#u_=Qkr}NS; zDE`mouB7mQJw$K|R$PUgm*?^l^I6?t&Bv9WP*g-`(Z$a$%-!1h+0AtcYw<1069~I` zO&;G@ceSp25D<4_lJM!T!R;a8lgql?>RF$Jl_G9Iww5g%)bj~2y&>?|3BRJW{l63wwS@RH?JVwN$U^# zZ5U9hz%XO)`u%GzZ?lCcr2gW&z=Y=_w>4_0ztj{bR`DZo*}GKvXtl4yng_{eOD7gl zgk+5)?kUkZ>oWG2=J}r1^RK_)P~LCWk7wBhWrZA7#5HZ7cEo>EEKPqd9}+m!5n1_4 zwn4LWe9JlC)CmQ9fpfo7eg&!Qeb&7{s&OcE2KE!MM8C)*zpDJ*#UrUGSM*8_%B^$+ z5luF^U5)k%iT)Q^2`F8`t^4YsBR~B;(p9P1`unMEMf1qFPVa9XtQ^ciq@*7a3jEE$7a;GCV-*N?n*+zpGC zmZIVs<+gvkRgE;tQ33p%pcN(W;6{b3uiF>9gMKj=;G%h*Nl@z*J6B#}DLAFsvr76z z=oHh~h}{g%shSQ%T>sZ8^S^tvQmb?!R}7{LMHd3qGTV8zXAU2a}4DGsy7w*?9$dg&VjoD zSZY8N6me2jUQOYB6$36zG#0)muq%`G+AcQ#rXls(&`E`c5}L?Bco9u5Gq!a9ER#K#6-uES!Tq`l3kpA zY&~13sL2CK-YdOquxA%HHU=10pqF=EJ{GfYCz$YZ*4&dq^~BsmcDJ{A-lg`a9nny= zFeb-z9pn;F)8(vNzEhrx_|hN%lD+=ab=SKy^#3CKn((?~tW}h0^!wR-E^lI>UMr3%$e=*PU z0p;XRi2f51lbEf=&I<98w5eG7a*Q7iU2p|$>m)1%}6E|COTV>Ji@S+Qf)4)n*h}YpQi@(Od-@0kU#fz^ns8^Y$toLx=xgS6U9e9ZOn!_$2}t{Gsm~5W=_zu~0Xu zCg+P$;|2e~U#vImnxebTV$Szb%MB*P)QG5C;?-|K}Jd#Uc zp2PsrS>!DU+v>tjJ$X)bMOkvG(Zlx0k3nPLU{}Z&;VNcBl)4itQ*!f|4=_Ozcva;* zO(gpQ%k?0!91oj<1u%jxbRMnW4*1wSyC$vc;ly+YbY9i}LBNcl5#jiU2|bDQRB{><}=Jh*)7jNi}G@7%nL+u@Mo7nNjF1qs{!_;FvS?H!r9!0euWlhbuv zbGuEWuz#?_%nt5Oqg!`A^r}p0@(?(WebKeDNKp7~ox^0B4-mhIqJJ9&*smC^;IH(X+PH}2G)A`tOkv8 zW&+G+@Y0GXZW18cG6U=*3VzgUaQXWBdI{yrDpE6bXP(6-l>6|3JDQ8&_ebuOtI&k! z4yGI$r*JCT(`$APZQBXq1Py=JB8=*~GEzFz+*7R|@as99w)Ub1!VASLL6JjYU(Ao!m zvD~ya6ELzSLJXNfEOVo&=R-H)C445*t(ZOtikHAVYm1tz2QGJoUofh?|=c~2~R+j4M04e&bKVxC&i)+vH^ux)Dm~l}gcWf@?Q@YvIwClYn_@^Wy--@)wuCuE!3A5F_6S-Jgh6K& zQeEzZp4>Vz1eC-IzWPFEe1xqLwA^waEjGQdzWM&f>1$&tx%ur8c`{LJinJxxdl;P7(Tcy;0m&-=ZZA{ikIRE4s(2`J$A^fW70gC1# zuIWzd7`RR2%7CqtOXANj0_>r+TZHjOfPejaP4a^;z=1(kVmsybUueZ!JYl3fN-2sl zT9lt)3~Cl57s=NCj6`{7eZGlk>YhZIo5b2FKPcziL2A|~QYsP%2V4GewgWu>Q9ysK z(?{5)tr|Y&w>o#7HEKIt^qqXpVEn7jGBjts6mTLAbvq}=+Z)V?pm$#8EbIETh8!&U z((6{+X{xW~JC8IU`vCUC<}2=I)lpWxq}IGv^v~ZYClAN$1L=L?6p&noAW)0VFs`SC z)I^qn1-WaR#BGJPUtlt@X2WIaP9K;*(9ao=YZcE&7yP;5D-}+Y(%Ggv`W;mBATJ=2 zTWEE7|4hr+n!st1xQ1YEyha_XZCU?RU_vJAC^;5E`N2MZ7Hi=xzq{Sl*Qm344LSuf zYQW&+Q_|J9F0IFeq{|-|N7KgS{|XbodXn8;D#d(461iAJ-TVghAzT+)l(4ob&fW<__ z{5nrb3iiNm9wZTY;LzCteR)-LIznB>y(rH)W~?E-;o zLM~gbVFY{E7>U;PKDuk+a>$689tF%5aqZf2AZWr$U0!Z9CA0R8!nGW`xq3p$l$zFF z`PiKj>ZZgpn5-7Sw^TQwnhhZBZ8U;Rh!?tQw%{$ztC?(Yae)MsV#wqyGO@yCm)7`| zxn|P({(%WNjtak8lwVI#>Tt0t)d^Zyv@b{`pk)39_HS_9PQJ22U^!v1kBJl}ZuUh(y<#t1#3hb)vpEV2LsaRM*#drUAK7z+sarBOcCShZ_k+A<6F6DUEC1Pkj) zxDaVBRNe$5X3a<2_?N5IXe-@l(6dEd=iN%tz=y?P#3r$)wz;2F7_}CN_~G0GV-!mk zn9LAqqE#2xt$0iE5R}Grw!^N|Un)=U{ zn9YsHbDW%kVOtQZol@5-3#K)T&0Z!WR$OG2!R*fo0esF&)wE8nT?1Lw6W!<&cFcBZC7vJOwi7*zC%Yb9!R$>FSP~47|2e4 zVPIe-UK`)X!<58dS1(S(6R;`m-gItGKg)#J_ zt{ib4%Dy(QZsKF1(;~L|=8l@cwG6i?w+ql-Payv@Z~VAsir(lxTw=yDx5-#@3=rL)wA^wy)AyP-}eD_TEL8yn7rvj#sCN4OIZ(1@qMeR0&8FUigi7a3q}yKFmKYS^Yi1Q zM5lXjcZuejIVb)#$k|1LTuT{{EJ7Lleso}Z2*%gRn=T_}gdwiKFAAH}r?2*7sS&#L zT4H%byPJ$%0^f?e+O=+#v4o-RSZvi!P(3|>=Fr<>No757!Ox55_lyM791!oR?(hGv z88ZK0UxW(u`rM~M%>Rd!qK`zLkCY-N7G~!EP8;O>x2mWADx%NH`8VU|Kb6k=_w1%t z6&X2T+8})cWlAhIcqEEc-?ss4+oZ=O28pU#!PgGfiChKq$jNLb2s$j(S1=u}@KOFVzPV1dWqr~MbI`K*n8Ob33KKU! zF!G$XuUkD@ecyLatOn!7+FVY)%tceGY`MOZl8pmkH2TLV?>5-Rgkb$9`@a*dG1H7DjhwS<9c&M4ofBt2Et?U5gdIydt8?`+VvbxP~bw z7oMixo)CLXa~AaGbS9}ri7`|jIAG8sizZW3>@kIYf>WnV5W<}xT$p{7FCQH*t<}z@ z%J$^4bc3@+_SL>e5K6Z7|R|ZAG|qmg3#V+3B;~B(mV}tLZ4{#qb)7@wb{QJk?`n zxPo0i1z31O=XJgaKdf}^<5v1v*vC;hENOh(-j2yL1ErX#iV)WDMKeSH9gDdEj!O|( z`-x)0GAX$BQ~zYoOmL4au9CZ@nP;|0IJqro3=LN}1>^oExlU_i+5^A!su-w@^x+$S zcua!<4-q=XG)rqI&l&4ukL*OY_zZRfU(pXPEBgzne5F(}N;^5ND6r{jY=*bFief~5 z{)fgo)%&*ciuA;WrYUVDEa83Yb8$8p0ehQbm{N@>dcH~L^eeRQ-`4k%33wz>lPVMX z{v3RsTl>bzgO3O)avsbdSihJe19PgV{*|-2AgXkDb9Mo4cefslLzmzMf`NCl^gr

nRS0ueB5-y*zf5+@rWbTq95Ww|HjE2iQ<`@NE9Ww^yEdp69Z4DKRNCTKtNvb6^XJrC z2Kw@bm|P;%nx440F|&7AJ5ko;K$wAjDO_rXjYxIQN!*S~kxJ}Ov|EXk!X`}xw@tT`7t(U63*w3zx&L3rF;x%k>y0?9`-55N0 zTkUA`hF||D+f3u&O-);)$@5`RW_OH4@BeuR`+Hu?*{Pd!k^?YeOk=d)?IQM2qKRb9H%rPwWm`Vbjf{UJ-7Hp=5j4+p?dB zQumTJ?UlVKeN23tnZ%8txxVJa?vfd0&A2H%$N#d_3zwqIkjAT94m>mIxBI5qDvH+s zusk%~71M2do)ZVzN|>+Z|56Th;`EOs(!R7}w0c3@lwF^kaQknoTddY9+vQfR9#1Ow z`m~dk$ekU+(=%HApG5SqTE(|mZAI+%#O6ge^tX0`^r4X#zWd>yHM)*O!1$aI^&Q_7 zYFXEg7kM|XCRUbMz2lB8*~p<`G@I&9W&1eD_Px-&e*N=i9EWEA7YAn8_oTpmrMgSD zTFLX5)lPIpS=EmF$&I*oW7iDt!{wmdO>tf5?u`w<6ro!VO~16FrlPWQPV9WLC8g#d zE*>04+4XZt_J}&IRDQc%%76CP`&O)E;0(NB+zWXX-2-g}jyV7(`5 zcEh;bjEbGO+-=$CmX&+R-IUa}WXti2f?>6DM4b%syLykN+IRe@UIBbKv&XFJ-`T&? z2Ip5-=_P4Mh!u3cLJYWL$~cKq9B z>dXE#EZdIUv9v>%%p|ohpPqhOkS$AeQ`>FaZNphMSGTuc*lk&fWWJY6Nn+n+_RK4xl-~qn+?PMRmUp5WT5tJ4otU*pMGj{ zE#EBEgx*SIh2iCm4aI+Qea@&l8|$U&JRnOQb)nA;I|BRv><7I76?&`#k|=&X*(P%4 zTRNSb>MZTAL$wn8XB^9GB$+W-37X>Y&ZAombLXPCIyRe~OX6y>6NxA{+PjXl4I^>$#b)G>gUxQz==V2U*Q2m$oVU!_Y~HxvG<$1hGq%;jw6=XAY_!k& zjm^Mpuh~{FHy0((>kV92;zkk~o|g(Qm7=GNnpV@yVlTJSxI45=!;jZK(D(hOHA>T-3m&fZ8mQl?(Z>=$2t&sLt;+t;>& zM!tEH1rIdrFin@Mar{@+9T-uyru4qr(*`CMuHKgRR(sr6?353WQEmeIMR~ueC^*X{MFgGO-=zgkQTk_`SHER56=M@+j_$A zNNrv_om%U(7I^mhR(7O_wg|YE`bJG-G zXt+@-GS|E;VsS3LAr8_FJ7`))xIR=zHb|scm0?X?w-jw$mWf-VROV?eh0dkpeQAvk z+s%fvIO;dNqRiu;FXdm~b+gRz7Q?9NJEmnvQEV0QC~2fUCor?nDNQ$yea8>eKpl~1 zWv=aKZnR)X3r0z2wdU!3t^PZij#2BnR;E|eN0X}q%g5jU38^H#Ub%Dk4wL-Z3)c!U zytz~6LnG^%?WWxr^mcyh#*HVF8`2sKoTFAVybu*v9XWQP)f;TL%;I`a8jiup`G`Ha zE$3fX+;mUPT+oT-$K&Zjj_EZF=%ugPdkTj(YA?)Wq@}{EhY_@llhd1Gqu~!bU8#}# zwp3kLnvJ4qHF`mEF;ka*hJLcSBX-4`p1l-iH~ph1d&WO>vasWK8qIXnmTYBk;Dimg zVX8AXJmtXKvZcW^jI7MEww>7QOVLrbJu^y!>`LDD>~4^4N@aJa{1^5x%(udP#m&r7 z;zw!TiA>8obR$tj)m0CJ*2Eg7op|eN=jACL$f71^3_N-9jQ>n`bKC_cdlsrH^{@4} zjEkck$$jp4Qbu_FUeoHl`1UY8+6=Q#$mnaa<3H|7(QY-zl3$g5McjV8z9BXh9-BdY zuIEZM!m(uH&yPxT?XuL860;w=QIsxkhWQb^x3>AY7=@bkiIkRfM2^; z51xlhI}g$eSJmM>Vn5EGQXAuPCobZF+b|5fy&8_zjm=KfctdT+uiUq96sh5Q*^fuN z%eL=i5V`jG5Tnm%?%Owdt<@Z$>6^q^GR~@_X)81y^vzOB_-(FoBhxXFqAlVW9 zUThEAtGPK?9bc;0du}*>?6$IB5_aDT$EGVaTeIN>*=iiSb`)e?%k;aEHS%4%(z*X_ zrIYSf`t~iUoxMW0L`=v2hPVePZWuEcz)a6yQd{-Fq&%LUztsNthV0mS(@WNRPUxme z`5Jqoa2nRoi!Ww92}{?Fme$ur=|#@LyMy!<$M2?Y(UxZL;V=l?z>A`&yBRi@%jMJK zLFzUA)bX3em1v`IHae233<=Z7J}})i-?u!epIyoe&u+Q#ruTiOmn8jxx;s5K+ld)1 z+Hsz=VsCG|HF8bK0*7P2k*zH5ij8i(+zBUpg#NOkuHA*J(q?{6YH;tW*4_Y8lxJ`| z&~V#laI4W(Cp(DLK8s`8TB45nnl2PNJ-#6kLfZ8!n~H>1Y|q`24fYDfhhe%4P`{UG`1RrU8;Pa;9rOE&#Rd?B$myP=)?N(>*F z?y9fO)AQ3yqimchS3x(E_;R?s{L_n)1DDN)YeaZ8iAbNxb_-{CAvQN8Xrh5sFk&;2X21<0a;p@DEef@u@kgW-ZF2h>E_L>j8&Cgx?^~4I*0<>3pk!QEvcuNZA3z@amQ`c-ackRR&bWFEX_@Q&JqP7Qz#z}QgY{Q!IQ~Yg1v~3{{g6s*yD%S(4 zIHviDal=xFQEy0dN!tB^vvl*gmzjH4)>~HYg=wMAhFbSy?|Tk2-|cq7JkPvAUMw#A zdtGz=j_KZ1XDYi(Kk5~RFdB_O7;(=m(=d#ml&BJ0)oj`SQPGXHFp_fV)|n^p?tR{q zbselH<(|1R+m+XhG8KV;RkWQ}m>s*GHRuQTOKEc6$Py8FrN4Ods8j4-S?S2}kFwIK zdJ#FFGow5TR=0Pgf+OY3hy0+SHkdi@tLDPu*jxIN7d6bPAEw`GMNt?(RXO7)-lP8@ z`|%aok5!4|cc$lRTsNlc?p7?eN_Vy|D|_Z_U*7Xvl)Y?A2xmn@sq~+T^OrJbl)WHP zwQQT7Wgd;CEG!};v?QFbop#IJD{C#U-HeiMH9B_c{sSp6KW(&Dq%IJb;da)DQzLBW zPHHzD>z52Kiv7V4c$w;$~knZI=Pq?3!C%d1_xZAFRF zqudQYa>L!*+e35nPFNk=*swab^-TIMGi+ymxAR`ZNn&sGLqTH54mCp4@0fi*4ALjm zP!Yx{P1TZAv!zaUg4u~`aasflz~r))mA4$JFBmPa;aS%|9cSBa+G{l}zw0h-8|RBo z*I3Px=6#NT-N=*=tZ25nnk0f8*G%pzIhFaFSY2>k*67fe89oxpj zL}A*Z;rrVc<)7*H`++?k^n7O=WEX?%a%2}*&UekhIn(`s^7*;LFS)5{m<`J_yuL`v zAWEK|kei}5#rf13Y3e*b@&nSj7#4VE@AlzJ);JkLgK-YM9`<8TURdhyT@FKkknKn5 z6HAt~Qllr;T>8?v$af!k#$9^;xY)YB+VNVAIO(Xllnt3^Y@|u(K6h~NPIZ8N=oY52 z8x`x){N66CBxsfgiL)+y=az;tTa2(PhdzuRsV0G*mDCwUOBGlDcNJGZkyiKfbQ215 z5m={(t+PW4*H5QIcs8}Ad&wES#^aAlWvaO>hp-=}m$t&}qKFcyk2XxVnU8YOF)tcB zW=pB@V>gmE$pzWvlXCC!YR8o*o3%qr@s>ntlu2L&%`XQH&l{(nqi$~x^Xn2;X(!7J z`J3H$yJhyTn(jG?{H?7Y@iWtsd`3tX)abZj;yqDy|2g5yA@5BzslU=VJXuMke4#Jb z++3+|!9E+^^@({l>FB!Mns|g;dWJh1 z@DHecF7bWI7tAaapxIA?YZ5$nc{uTKXX`H=2^G3oB4l%yHm7J(-bHQ26)NT0?1%Ri_3W zEnHFeCy?C2ZN+yfy@%6XtCM3=I36%s_>d7x^~o018TZHLSa#_7)GF*WSP!!&!oBKp zl}8jad{K%s5lZ`Xbo=4NIDdV)Yj?~rt@J6$=e^%IZ7G|2H>4p@?bW(p;{Py@%p?i( zj@q6+=va|&jjxOxsVc^az4-UM!nPWw1VK0uT`Tg!cWC{}s&=T;n!6pz6Vxu1g|@z` zJ+5X8^pTP4YT@ZdH7_*LMQCNn)X-YF>#7+#J?1AxwChUQlO;*iY!|8H&}1o^ufXFLE@Tm>PYMIbS(*+VM{q)Ki$31RS0pI+U@dZBn7A?y4AXX$tv9T!ea|h zRaWKM{w39CJp(f7F;V_>@Nve7ctv%VFznM&lK~2I#_FG+l7`TZC)KCrMjSO;i=mgd zc3zhTkR!*-%9=^hk*1`VpK~JDh)*LCwnPz&APX)?I9yfdzhAP|4VCAe)84^n&P%Jb zd1Jg$T9MVX;?3Ue-~FwLg&74=ELwd(Rg9o{t8|Le9^7`^hVM7rW)fA`lUi#<`i}n6 z`s!-M3I9z638%UhpkJLQDEm1+zHZpC%1rZsdtJw<&TROTIQ{5-2RBc?G_)f(xnWA{ z&waRb`?=Y>AW|`Y$hf(pCVoC3&7#Mn{I%5^k#n*4C%Nz&rtPigWwO+Aj3mpKdy%!C znyaVJw(RRW*T;=@Y4e0h;J8uoxZe=r@DaL2QO&8B)yb5vF1#^kPW|heQs-xiXtig( z+O&6e;n4Kx?{9pEWXpLv*v!mfcd#keTd6d@f~@p5ggRaMnmYDuPal4@8Kn1%SJfex zLu1WX^U?e5R)5=z{BGP#?IddX$$pZ$!F40?;>K^gQFEA;qG5Pm*$&OTY>iB#pLNQw zc9*N==2_x3^B`Lcvi0zdJFE8j8>@@v{-!z}SSskDlL~9U7lk5=?I6pnu-$vc@RPJ# ztwi`gtD6+fg_{ev7ak-%ukN0u^}BVx+PReJ21s>K=E1j0(%n}V!n|_S-hU#FgZ|R3 zIDc)wIx+L$B+Q<9Bq`J}tsg#AhaB(AzwqFtet);K({|d@CYPF?DS4nU)iSZR6k1zd zJLre`P@OP5lv=KJr}76Z^?GV&uvh8le{`nXsQU!3E<94Lv0(bm>AiY!HkDm>n68At zB*X3F@bKd@gd#dTk=pZPar#eWH$HeI|9R$EtxNcr6@SN@Z@wuLys23Fhpgnwe;B1- zb^Lc%(*cG2>qsHxNG8=$XGTaC-m;|h${ojlXPUONtXvB$&$W_$&vt#~o7Jaisyz_u zc7n$%PtRQ%EG{*WO6keztSd|}R699ydf5`6_2hxXd{{!J6<)^lCYAh>=k-L}NrQB+ z6-qND5!JzWeLHk*U%AH|qm+8^Mv#18A#IHyF9O$h2XgR({Ts2kHqOfC;<7PrhE1uj z`tG=}ZKD+zmeY`=a_qVrK`aGiyzyW32A$YbI|es`Y$=yIrs-}tQ8SSwFo=VuqPBLL zrGwUQgtoM--Ozeeo%7T1arn82OD4eU#b(K zt?Gt~m4$$ZYeky4@t$Pl z@u^a8PVB__naKJHd;1gLZW!)QS2OfK)oW(rV^J6Kq&_Jq$lcXxWXxx$CLS3_+)t0=NM^Xf?cy|*n5#>i9`2zT7iEIH~7 z&NsbCa#yEaF5MU@nR#pEB>ww1Hg>nRJ{_m!DF4kwHt`qY!1_|tw11&m%=NRO+Hj?O zX1!{ivRbdJZV8<32A&xH)l##GJUyK(Je#iHHO`ju1B&+4jcs1!+O0GdN!Cb2KWwzh z&0lq!o*Y(@wVKXxpLLyP)HMHL zs``JWJ6v=l>$d%;-){S>OA@OF`W_&4=9W4wR9S%wr~0O&!|58|*;63t@D+QiBvpIi z)KUQ1ucO~yNy8usAA8YHUU2-uXM^-XCpG$mK$7vUyErn+ZsXmOFRscy4Wx?im1dC^ zYu)(e+1^py86z0b${&X z!i~yTao0v+`wa2e)dZ8xm3nKQb{s-Kg<11Eg=$M8lNv*Y$ znCa=@Pa49xuTna_WG%=Z@cm|FOA)Z?oAG+XOo}jR_uh8+Jr%KD?6k|&3hjm+FZZtg zp7L3AD(mE*sT1!P_A8nGUZvydoev5k+Smkfb^>F1w4rZ;DfQ6KXm}^bZF1|=R{SG8 z(BV0=)rcEkOhj*EP-adX<%aK;R_tWst^Xz(O8n^9c{6Bbk?5zRR?>Xt;D+!bqa7-5 z-mX*F&Q6d#EXCS+E3u72nmq>xzi4DhJp5s0jv0O#C%*4LY`TtbJ&;)9chr4nm)%ak z)9~&4?cT4PT>I9hyEg9iK9#>3dPYwQz>e!Rq~a&R?)ug{GfQ2iXb%sbQku}@9O%ic z&{&^j@eDg{TkmUP3u8*X+MkTxyU&Im^8*L(T)HFjzR_BB3pf57LHgWzKY4i9PaeD& zq(5>Y4@1x2yL9PNQ?#5Xg7iK4g{9c+ZDp1uo$`21tRA@6l_}6}?`{x~rS-zTmc*q7sGs7CYjrhVs@{m`R!c_VZ#TBbD^dawQh*;KYx7GBdNmyvU1UqMwg727X{`-!X5-zWcjL_Sx7AocGv?E#e#R^Xk^t z*7=>Ccee}W19Qw&Cb3_&CpFmqhQ zWGa2Kr=vZYOV9tyq!^2&z3lk+qE}(=?0(L zXD1>|nt6NExLz26KU7|#`@&@Ff|R!HMrDV7G)iw;!M@sq6FqV2r0c83`JQbgTWY;T zaWQQ4!)9T4A61Ib=X>`Loq=DS2(jpe*0nTqKKS~>&dIrfXO^qS8_%|q#ZEhwR#u>I z%v1An4^}sCzP#|t!s`nkT=?w5m+m_Ajh%rp(U&HE#rEml!vI}AyC#y3uOp#piQ3um+pUk{hTwmbNHtiAiQ*r9Ua6E6S2f8bTdc z|AMqj8s6B6FUu6Iu^1HLQqo8lKYZR0pL zpIy%|y}%)!976U}g)`WW8|&#{%jm4EzAZ>39ff9YyRK1~y+&eq7fU5FF3B0%W z{@d$L?0U8oxKinN#XtM}y34 z8^d-YRyU00K`>YjBXey>8QM?1DUtd)sbOF1`Gzdb5{t)9(72TO*3}oy?=-wd>PXn6 zdd;xXlE>9a&+7ahz2=|%o|CIDVOhnTBeD7YqB80Ykp#kXUi(Ur{^QX9VmJ5+g&;pJ zH7Y62hwl?2dAd4;?YZY<^i}okvKu~e^{b}k241U_{rh=SIFG+#53dZB>h!oA$R#O=ZEXg-Wow` z{_5O+k^(fvgaQ!1uj*ZR4_1%%^ zc)cLKkZp$cvS)}tapSZbbtPke^~TWH{Md!Jmrnc1b8TZ+%APgHvj=`!+Ci)LO7#5p zz}&uQx{DI!dqH-qG=;Zu^+UPx3Z=5I|GwJU;4UN!?S(}-mg=;sC$&a2apjR`v)V-4 zhI=^Q!c%lMn3Fu;dHqv)lvvR_3rE^!#rq_=2hHWePEGH9c{_27j)<=xg!xmqlGblY zNW5{Z?j*UQB+3`O_{Xp0S(sY>Ke~0x3vy{}HzKp^8{@yRn%RDui@X&rm8ReBEOoxP zSK96<%=^j}CMEJnGPVnj)DLdm`r)uFRG$zbi4iR@N)dEyO;`rl9NNIh^Z%&uBX5)xJ> z^*FJ&Vl1zy(@1Z2{m@PB-`#cGWJyi-Y;{AS&SMQ75f0j3d|~g=pb?7ZvTsW3c<{Dv zuMvb{qfE`vuon}F^wO@ByxjlxOOmf%5A)NTLBsGnH!t3KTWYqh-!gCcaoJBl8$@;bzy9G{pHsj8{cqg9`L+M_wSOsN|4hCHQb(I4 zQ2+kf*Zyo_RV;k%KUJ@j__M!fuboAF?kw!fkjdc6kjh}{5Xo>VgRj5&I?mDQR9qm# zq70!7v5t#%e4_KHa6yJc9Y^_99!~~ChK7!B%3#Z2>0cG!(dAk?jiCd^Il3Oo)6i+C zES2^^hFsS@)O{~?K1;Vd)@_V*Q2F~Z6uOSChK-~9XzPABx~v51696|==ZOqgbsIc6 z4jmcZF2hqYEX%Md!+T`d1Gps+G96TXflRlm;YGn`X$dX?e9z#WhiaT;hi!Z=;u}ad`rKpzFd^yM8B(Y&dIPP!@7>UPbi@`Df0;#UefS* zP=;q@_@E3A%Wy}Ahh%s}x9bU=M&-RXIrON z{aw@VmvnnibUl}J|F(7fwhn;~DsG_DsW!BA{nWNcbl+58-l5x|zCEDB9o?5}y1kF< z_(yg6$Mv^U-H+Qk&$@2&bzSCT8b&8NjT%cwhF4|yWf{gYd_sm58G15gdOQ_emSk9z zA=P86%24=F$gm-UuY-!qHN09fC|oXOc(?w2mkbv)JT^3(2fFMf9XHm`mvnec!)H&2 z59wdZw+PPzogd#%ZKU&`*LAFG*c4()TZM+(k}k8W(=F=!T^;&5 zzNf=woksO*S@&a0mvKoyKcM?^MZ@G&m!-zxM7MoShdo_~8jBs>W$Io3o_(Ee zTi3m=VSlLGtDZ{@%T--ptmAjY6j#Q&-=|{AOC$YV{ayuls=Nv(-Y3J$x?Kc6m3~u9 z@ouR5dsFv$SJ#v3cC~cafS~GrME7}5x9wPuH~rqPzbjevu&#^b!rOE_jn}?TOSq}V z>YN_qZQbU9ZpV(!cLd|FWcfonzJPDnblI8i^PcW+NB6I*%RbW21n-WnPvv{RhMUUw zb@5oz5p{cbSkb>rVoF~sEbDJ-9N(q;eM`f8MYrvX8urIJ{{ZqSKB0K|Gy3^q4QJx( zcl4M&E~b12=|TPa8t!V02!|=`=x;A+cqv@FD5f|?=`QNu>-zVcn9{o{V^_ETd&PwA z#=6al$Nh?K-~GBTiXN4$eNOjB!Q(XzBQ@TyXc*ko-yhQHUxq+&AaQ;nVC{K=^uek9ncgzyCr^aCxBnh~a`BXNB9}CMG=iV`8Ff9B4SHF;P0H zqSa7L_?UY3^_bXVB0oYMm+16M`dzg_>0ck$&kL-b_tLQ_|F(W6J(*+&!L6s`)bls# zzA8QABQkuWnBbWDReVC}t_nXD-n^vKk)5D$_$mEM?}{#-&_TiMw$6V^Om)UX-5%mE ziU(fTaQzkyL(=OMd=&lsq?qu&RXq-ho)pb2Xd9SpTU*5-3WRFwd z8~T^Nc>tRQXao5eLJf~4U5|=i5K}!a*0@OTp&qMLk6obgJ%;r3JSuC^Aa7Zx)twJG~dzXE8HE6sT~tSI|)x8&~@kf*Vko) zI?ji-EsLprO7LkxKd4NfrBQK;|JR&Vz7` zbsr-Q3+e-XqjVvZ6G49+4ew=LP6qis7%v*D0P3TDd%AoK2or*1EM~`r;t-~VnBoxf zR~O3hSRFouK=+Aoj@r-@Q~V@&xcc1@Q+)Oz-h=$a?8M=HM{V)gFSeg(WJ65x z1oQhfWqD9H<;Q1|BUnD>BU%e;0`I>sX8Wmvt?P)zEIctUZxg;dEFITX12GFvEQ9xd zUfVI=WpRin`0l_nrlG#O5OBJXSxi2m6F|2IB7aaGVWU znFQ-t%=QV(#PN^BY`a4EhJnWs>+qh_8>byUc03=}TbCcl+-LP;*;GHLr#gMW6|?e$=d0^4=UoW%uqKWN z&NKZvY3uqvFWzO(d|c}ISsmZv@R~>0KC7#)&4kCuyBsKsw~51)w*~uwuyf|*hH)my=i3rwyO0i^i>dzD5>wgN5wp58**j?WkROut zXVRw={S9?euK!}YIQ}bpU{k-N-IYQ59Mb1v)|T?2f0U2f;ESn_P4HDV75Vj*&A$fE z)Q1s_?UH^+-;!#>K-Zr^J5n*V5j=nc!iw~IvR^#NlfihA?G!?J1WT$jV88V2iK(w9 zg72u)Ct_BI;A2K@jG-?S7eT;!slC((!tGehjuE$^F(A&6Jf{99@J-oEmxIITY8s~S!&#AAmt{3@949_&))E~~rsN4{K36?3V z8)XjXTU-}c<+NEE8V~Z{b#;7COnC>5Rj9vHUW%)eZ|XOPdburvau8upHlPphjvnWj zl~25a@YaE{3EqS&Sgs4-2(P%!hj_sGF6UiHD=5z*F+2VdD>H?1@1^e&-Vr`y`!PS_ z6ZVgI0R6^dw!iAPt;dUKlJLIHHlVyE%b&4w5$Cy#L;bJLWP#J^8iyZ?5wxcoz_G zF0g9?~NTA1`=Hm(PXgxt9ruqQsW*wbZ;SuSwL<6Mzw#9V* z+1Gg$US?v7f5cBzTBZM$I;b=|IzP1`)$Jg?kYLr)Z7Ow|7L0eM+fB4gIs&!72V>vX zX$baI9@X2{X$z<$(BIqocUw&T^GFY<`xC#QK1Di@5B(?JM$d(Y8O62rZw%#9J20;b zeKExa;%_OuCy9@rIAUPyeE{Nn^S3N+V1V@SM|;Fan34r^;0 z;;8KzD;r@*b@4uV5T8I=8WWN!G&VGMkU<}pb-F}M^)XtLkwV$jUeXV9RtMn~!JvR| z)P^?XA-H0HsQowwgbxG@oEJjdgvO6-z8Lb8ev0-I(F4iD4CMsQ2vo$xmoQyQgjR3GsudZzwRJE@I{nDR0Y(zhWE8q!iMT4SYqCmnA(UWUs{lt@Q!GS@)13e-H$MjS)lKnXCUoS9|;!; zmx(WutR&i{@2@@_Np{NJgpOo|xq?B+F<#TVjg)$X{?AkuM_Ma(vDq zO(|w!kL{)Ta2yw$eLM|?ke?9#lU;{$aRBwBETsG-570&6J#nD>o0l<;)jPwn9 zM;{5r&ToosqOsxW(3U29o6l#|&2MlRqMglsquj@U{i*Y_Vm{smq+POMxz7#j zq~DmuA?$Fhv5(vzO8M(>4fF8w`561+?tXHAA(rXa)JeJv;ULK(^zCGjp5r~zBlkli zejxnkwGYeW_`qoaaTEPCoL_J{Mjs`o30^<{Hjf?==Q%$jJgb8}wg<;F67%6jFroD1 zdnFx}V1e*N-#^DYPDhAioL3RwLw**rIP{U#@gAHzL7be&qv##i1yX)oGe!Y@-@Lp! zI*V8u?hEDo3daJ+8`ll;e%IkPmPhuCUDJOL($IL59@b{x(MLsekU)CE5h{aVO8J#6 z&f%SS0QzuUsE6YA;Jmd_k2!_(#d_NoTH#VaoVlpcbs?gbZ$*L zsxM}F_xY4; zhcW6A90r`v)r}V)&pN#Vc@FFfG|We~j+|0AA>eW}xr>c+gTjq_*+aetnUQ#U5m z9zHK$SALx=r}h^RhjccNPf^IlZ2RldAs!QqdHI;m5wkp}u1{1KwtHTm>Vh*T4)YNH zaenEuxH=j|U9zrz8fTByk9-*4iO*sFfR%&m^7wdjS;E(wBA$~kjPpK@KY^I-E4G)* zelAn6EnH@)IK@ZQo{dd0)tzu`3FncQArHi{p*1_C*F~Zp4wMe{eze6y$b+<>)YMP@ zFXD;RPxSktAAsi2DL?w?X>4fz2yF$NTSZ?mu1`U~DEipRZ|y<3TQ zAUlbCl{mjeupz%X;RET<X?~$*S=$hJ%a|5)F zgZf5uW8_mMA1>i5>F&7Z4c8waUnbfh+QM;0y2JHv&1(HFEKW@(o5NyHJ{vqU(I z>ygn$K-#X8^ECHM{pEDdeThhu1bg%oA%7vA1ZfoYojRGs^>$tk)m0}8QP)}*Qy>4b z9tX6Q2w$lGh&wnxxFn|M&)|Hl8lwTEr~Q>7lt*PJP$sU2UKdlDtMbv=6xa@ILtaxh z%~wz!oGZv#{Uqa4D39V|sFVEoYHne%rd%pJ5VQTqa<`!UA;jZ;Ok7uie(AB8<-OE4 zf*r0`AfA;$`7JREE0ik*q`@@~*gjfAq~2Fqy*a!`tX_f}A17L`Mtq6-OXq@kViqqb zA8!|p9iQv=;Ty#-iP^T&vj=4$36SMS0bCNWl#c2RxNaeMmeD!@Kru0qoX9S}$l!>&1 zv_LWe>2(C}e7zsfkMuyai0~n}(L5ma8}TiGdQj&fJt2bfh;I0L7@9{$xzZC;+UScZ zUUB~G>2cfC?9o4lEZ{k zq`To7Ak+&ZD3jX&5(K$Qli>7i3moW~X z(T2daKBO;@Z9*~v^^iK)k?um@P$vtaTuO&NP}FlmNLT0kr!pgW<~AG7+n~ORx)Pt? z;5Ih)F@m@N;xHgwTvi6IJ*l%xu`kGTe2A}W6R(%*4j^A5X4{VQmjo;H;S-La-<;3$ zdF)$I^Gx{(fAKr@U-355sYrJq-i+&Ysc#9qqYg~6D26%-_fdBsnM`G2`6$EbJJA&4 zu>*CWK2CNGy`$gVh4-GA(o_%1AYMUs9NG@letI84TQQ#3$?@A`<#xnuTRq5+_=oLx zYw8Zf-F@cv5&ohdvo4O?nq*&M8thjHc{ndYdo6^3wh_udDl3F%gb$`CoJG1LTnZo# z^+1wO0;<#s7uf1jL{!6Po`5|>O%y1 zus&`dkbI$f2){^|Lp-&ie$tT$=etlRpA#flA`KxP;Q4FB+bIrp2J(?{J%MZ%f&urX zV?2!)l})rvWm{sZFQR{+@D0x^AzGrgBHk0ca8D?`=Q_)$GR}wMEU?sTl-8L=Y=B z34Xr6=l6S__jBL>yx-3~_niCObMCqKyk3`a!;<*g_#-IJNuhX1OA|WiEcM4G9%=Bb zrM;@Eiv<;77&#olSQrmiyK>NYIvws~NvO#MAh5}wBAY3W)5#&wX=|gG5U(xFww9ELUyb2<>R(dmO!>y|3<-(7F_1k{$I_4J0MXck*b2 zYD6LVxi7pyYbUmQ4)=09EX{>>7$i_pb)RwRA^L+boW4ESJ~V_L;(Ib~_)Dr*F%o`a zU8l=X!bj|WrVxX5oYkTci#EZ;IM92seuVK>*Vg7xd0`{)ol=_q-}l~h@PCo-=L6U| z=>BYRe14`SDG`lR5*leQOvC^FT=G-A2OST}x_%(O!T!f9dkcABolorw&$|sKEaR#p z+HANd!|)Sp@L~hK=J<2STxKe|tfXCGu#=F*1oO3&3Ys%x=ks5u+coBXW5yiQ1j>rT z4ZRvT(fHf{%=Ai|73PF{Qbp;%5ZXBQnb3#oYKFi#Fwfb8(mMoespvefhF(z2LOZhF z^%8bdOUc|Ej5X%eH7eJZ;2uOjp%tr}9RdIome(7l1uIq8^UIDrG zurvL0rJQBc*m{FR3EyaY2GKUbg?c5-T`%6tjkM+@fL%l z!#JT99xH852I^4R6Oe-HczYoOiYJBLmg-Lt1H+y}@Fh=9y-FM$IO@z(TD+Zxvcf8w z{~YcL_@j1N*)J| zU3qm!6Qaaz`X4pn>2J$fWZZ$DwIN)ixtn!NkzMy~fDel$%10&EHg-qjE9V@lny`VJyalIhYn({*Wt(z<}5966*c1o#dz%Ph*-7n zOIgHPw#OIt@|xT~0uA0tE6F=5ku2Gco79Wot5gB&4H{Po+-nrI(!>=}VIF)Qsq>G~ zwe1`08&n(aJRw4bd(`h4%EJnLW(80fp6KbrJHw+s*@9o~#gNQ3?hj@JIsVz7y*1%k zPaoFmpO!b13B$VcN;3J>N3W~RK7e+payMj$>_obUEuG9f`*Udj<}F44MNxxJ*ksPX z8Q}w{#oLv2M^&Nnn}1uqdpn7rS^GXMmk0~3wiufsBUJ7lJWr|-#@&!p(A9ETk>-Oy zvJk=Nbo)RLo*3fS;~`s;aVlyToDR?h7Vo9&Eo}amC^ajg#u4^p-ponJK3mlvs8&V{>oV(p!&$bAClQ6(L>xmrXC@OB)rgO08K0>Z?2PFoaQ+@YS+ zZalG|An^)DWUG$+a-hXB@CLh%Ps_HJj6&QV^aC`I8cUpPA0OdC8X)Fz77c$1oy7H? zK>nO1_Z64_Y80j1IlDmRohYqYdjGhA%(n5xs|0ileB6843aZUqut-t(yzQmXPDbI` z`}8emRVDU5a}?peCZj;th#n>ZUeL7dqq0s*fMBN3o*aLmV(Ht#%N$jM8BR`oi7KVr zmStCt6=v`0&i#V>neUPeI%xe<7l@M_-tv6_;7q&A!XPA}{v@t*l&$7cvmzB-AlUZy z5;QTj{X__zzP|rBDcDQt2Wp+LH)t%FI>v&uWhxqi1j=_U`k$bE7jPfsb-PYBOb_7e zI9vW~G-*i*Xk+ewGA-0Fxk@C7PbTX^MpDBlXd67F)OlZg@(&e`+Ck>jf;hQ<-*p$% z)xuOlOW1W3XYLLYqo0frV+#e{PU7%jGHu6OL}Vf3*E!fdwW9g?{*OWo_I?l%XXfTkPl09rMLVE=c=oNpiJ z9Kt5rzg|Ki1_U1}$H^|Soh~a_yi?kLw{ZG_eKg#v|?BVz(MVp9t_R zxpV*wFvP3tFOB8ug7@rn2CRnmROUCTcH<4rLGts~jarP?*t-ONfm{XL6%tk1+ht+} zkS6egm**SLv+@VP_i}l^?p}e271`+6EEC^tr=)e8W?m4W=>s3QbwlY7FV|JM2S~SS zQT4A}yC3$aL0MdQar8u0TS{7i_|k8MI;mP_jF!6^q6bg2ewNAL*uq4kWuz??&BeB# zm^~tCPgH4urF{CNezr`zD0j2q`HvpAy?Mg4C2vX1@OgU`9zyKL`9Bfw<*HO_{ow(b zR1tXuW}OZ#XD3Ii2o=8~ z)a){8yciQsh44&CxYF_SEqXn;SW>!*rh6yY+*DVZGv|jIgZV^S{5A|a@ z?BY`#QfSiokMBsT=#=YE@MO9NR&j-&($(5r>z2Bnr@B7wEf#(H@+c|cHUocl&kcgG zb8+g138H@53O{XqID6|G-{R|Qw*UM-Uz*M2nafYb+(%N;7zKbt40C@_$MZ}dQm%7S z;;Q}8bOoKQwI4tiU*n+wII%WF-&fK&yDUC=sY<^Xa>65JUSc66dDAc{by!o~*s3{! z=3UQV#@KV7Yqo&8Xv(h_dD*VC$&pX~`c+CMv6JFHh1$Gz;rCUFH|kzQK-l}AXSQQm zvY8kZwzS-e{q#ysDZL5~Tff zm#x-6@yB+rQM^S0(syH;eXRA0mlAznOgx^TL`Ad3+#4}DVTvFglO?dGV{W$$>BwqswK0narxkngGg(*Fz-vuBYc ze-Cam;L_X#@-`{%RBS7pZ&LA+u8K2^?D1_|UwG;G5xkBtYQ{bukA2jUH2~ehS zELSe>WB(`vn)6! zO!{B>->=6J-VggIN}PrJ5qqH2%pSs zUu@&s65Km4=pa)KfXUf_=PmJA<|xKW}MOUOFjpdG4Pt>5N(!G_IkBkx96s&=G!CTxUPNf zn!D=-gdip=fVy}h4@9NWsZxkL#{AbS9H3-^~xT9h;8-6(Yk^39t;*FDA@5)D&9#p>~`qK9ADnWS1mw)F8v=t6QT2jX3 zMy}5Qyp(7NFU{)i^MFosDIjf#mlCIHo(sxrQt4bt7kf!cAZd3-vx;2W(>8;7@~d{R zS-~rDasbJ(vR4MFDHzUKc&JUd%Z}6T-Y#)}xvAC|**c@_#;s@uv2eEk+yA@MB&PH9 zj{UJu$H9&4NUZQjpk(2ZpK$i8rn@9yH`k;Wc@;`C!;Bq)_*pkGW(}*}=jDhNVay}z zj2|J=RE>g1f{Qk920P;WH>#_(+WvZLTrrr*c?KxGdv!#c!At*sx(;6dLPL+Qd$Ez_ zIyCRJ`1UvWt*G-}Sjbatuy;O}o=Urr{gaG$xl!Ej*kZoL^nb;sBk7OKjx+8Ty9r^pbpFrGKNK9%QS9`brbfEOgghGNXl%*q2bPR^8{248edj3 z2l02`)Hc72?-T&nHl|tk$kAus-5WE4U0IM*jv@g#ULBK4SXAhC%tD8em581hO_w3D zv(BCu@#8x5v$6f((OvuOY_f$rT%m(^Z@2MHEnQ{Vo&zs3)ARBE^JcN(ECY$9tA{Wm zoV$(kqlKk`p?5%CDg6@6)UAcytKqpvR4Q&V>4W@mCTv!;L5~kk*rjo78^&%`)$UQg zJ}z$jglGOZf@`^j)f-;Ql3~$3WFDd&KiN{8pXlR?lhFcBNhaC!!YajPGx~`g@zqT$ zi}aK4&=nAy4?^&jR#ajebnfBzO5iUrU7`98VY7pi6rw@W)!^Brrc6|n@! zx_esZZwbaC+6}J5)JeJciV9kkZwl{P7llDvC9hJ{4kefmek(0Zi{i=mT&6oPh8PQu zuw|E|swp+;N{5{8=xB`AzdB+to^Pt5Cbaa#^$ zsm7G_X+m!Jbo^*iJ5}oJ61% z0P-;ubWF1)I*fm(X0@TUSKi3SBpg$cmGXe&bP4LuS*HugoCxR{mJ^M zoYWqB(9tgYL-6Q$vU~;K4cd>)U#mY%GyAeXEDy<*(#&YmIa-iTzh#_ZN}x|afcizv15NNjK<*;6cq5j_WI|O(Mhjt!#0!)iV;R)r$^NOj3jpB5VZof z(`I}t;+VY;jJ`o~C>cFAR5}yiSvvCFyo~7Pt8AeBS|Ll%5x)Mxao}ESvK`72XA?*@ z5gBl0Cwa$5r<*FzN0Q!ABbmzaZ+@E zuJm3yr{WERp81=z_CO}0=VZ;!&7VWlh2l09HVN!w$z6dL)R+>=8JL%=-C&E$nXBsj z)A{{O0%FwLzU|v`^Z1zu;!brAepYSx(W5lpL)1i} z^XyTW*HepqjwAj&irQ)Pf%7VWXwxeIRPO`xtf+p*-wu29OH8NJkJO#C(*2$WXdpsR z%5@T<6=Te_lUUMvHp~w%TID`zt?ilkbvxbi(dhmfXP@S|?T;TVzB;{UlJ9%o&L*Bk zxJYi4EH#?4`4EOSI=6^6&Z(g@QH~NXA<>2kH`WFB~%0 z#^^FgEF@pIByMFU-5H6W$!Khv<%a#jlN9h&fVOW9J3CqEIdxA3(b~=C=^6ua?j(kr zw&0wWehA@O*DP5?t`w50um@Th(2!d^qJ)k-D`SH}xp&um&%!Y;%dI{?U^u?n9CBtn z==2+=AnDi^+HZrQZ;On`pdIx%Gl|Q0VdfI_c~O7V_taAQK=-wEj_^^|lPK8nnaGRb#~ zl46B`%*u5W(PsI0Db@EhM4?HWw?4d*leu?J+%v@v19@U5RA|6tSG|u@m9s$7nOUzu zef_fhQKIc8+DDvt?R+Vn=C@@^a^gV}^Dpz10hBT8gEqB%`YiTeW+ElIa`@wyDAj3f zWsuv${;#F40wfP!fK?fnv9PBW8`KaKL|FAS3hHXsvz-@WE8mrKIoWI^1|Fv>#*r}| z$)f@)E(IM??_wT^;YVaLMA+OoQ!8<3yeiV_2V?(>@B zeHkncVnvd*WbTsk&+a4r{)v*`QU_i_K-g>{zn@16GXl)&9zE1Hqk$7fvoATtrRR&7 zPdn6=!U>f>l|PU5sw#R=>^~O0*$UgW1HeYL0>TvU);HHy2&&pzQNHi*C%0~>JS|#q zpOn#OqdL5(-dbw%IFk#hbxDuYJAb)gG*jlmFk?UPx))*}F$v)GWe_B^$D1n(g$?69 zGui)Qzo#}e-N;X9N%V?Gn5+pb`76#Pq89rgZTrDtP~3(ab&&6cDr)Ysvg0Q!%5#tR z%^G!5VD~@HxLT()AascM=(|sturDfazq9)9tatGAU=xN|j?>l;4~`;P9W6BX^4b7LdR3%HvxE z5I?hR_%Z$25aK94&iV)P0b<;$T5|E^{wvx6eQc8!uQ1&Se5o6uRNybn7igMdlwvyR zZyb%QEftuq;QJ!5qP(4Z)qG0x5#B>|b&QFdLABlU&BKThY}uHK={FlW4U;1SMre#OtHh#r}cOt3A7YEuKD?VXqWil~^?S(_LZR$q$o8 z>zsW@4=hTLo?7JBu;5=eKTAQQ83J6`zHHxttF_o+G-k9i`gDtDJ^If68H-_z*1cCG z$K>xqI*{>^(EP#v!|&d=>P2^x$)YKJo|b&ui86gCJ-(ZLj>7PNt%!5(rppD1RP!?Z z2F^>4x6PJgqxmXODll}zrLOaFe-2Vwf*IAYgCIUVJb$?i_mL`i57?$cew15gcQl#* z(co*u;mo@Q>@A65KVo~u)9=O8d!IibEz&=pbtO8+bU*ZHg34uhTV?1a$3G5f-B(#c)G@ItLnRtV^!|fiU(_$nneRM&Sy#+qfq8 z#%edWNxqSbDS2t8Be_Jp?b$C~L-Oe*2Z+^7frRiH=cfy<(FKzmg;d*ixBE8i4!$x* z;jbvw_EBTuYs8S>z04^7D#m9sklFLnylL1aS}mj!7=a9ba7TBm8wWrk_$GN;Xpnx| z!=};+@~eZm@{Jm|Z)k!qAj+dL07CzusG-!}7I^EA=A(YA=BwzIEuqbc_}ici(8s#; zeDjCx0M$Z~&jzm`i{8uorKZMM`j__z;^ zOjwTjHSD5I@>vR*%6CAzH{jv8WdS$LIVk1mpVE!Ik>^8uj6+}pr1dsq26E1xBg2>C zqNc(Z()!MqO&Sb3fn+^B^f&(thRQH4_}s&#bEZ(WBt{@hKP&|}-0n0}2lzxPvMiOo zQ%uKE1Np8iJ=snlQ%w>1_w9rM*g1&JetxfqZmM*Ox(oH-El$EMF}uaR!n5Ub*049- z-Zii5SY4z=I7vciUwN?67%>G{3WNbVPWhmii01Q88zK(7O9a_}h?m@TMoe*jl@+qf zD4T!*C+`JtvD?lMC8A`YT&gW6X?btL8Aht<%_Vf$@{DSC*ft@g$s2l;e7ThT@+5B; zOO;wtqI7TgEG^S01PZK0e|4eO5!z&Hl)KifKHQ!^`1yS{H&+7OEQjG_VF#)(a2a9A z#J-v>^A}tiIEl-I<5IoaYT4uK?w(Y(-bda)Pj*4thM#_))x19@-DaYbW}(56MI7!3RI|veJbDl%hKE%1;~KQW%!p`FOgc_dUoYf3f`7J6CtxoA0X~skKat zVQp^=?UA=!iUt9^_Q49(DhxKJkpy9r`{p?+`x7At#(Wj(IHsWHluc#F?Sf$cQsw@s z0UCJpTUwNqZ(!e{o`V}FhIRvOm(!eCR27JSVnMVwM@i>IJl*q6AVeThiaYoIQts(N z3zIQ!Vq2YPenN%5H{EN#FG1x5y`%x0jaK3Ez07D8x_Y&TKiImD`Bh0~O##T4 zJHf`imu4Bj&51Hd(9MZ?(#o$oUqn9Lr#a%;+OD3pJryGMDd(f60vS$j>%pG47_&Rd zXa!Jxq)8$?i7yiK5&nBv|KhF>)`@j=EYt15uJuQSIXW9`PKF})ym-gql?`y6(RBT= z94+z9EDkZAUts4*c8%nG6D~R{ba@MylNj5{5`G}IYSL14O{M!d z%zY9~AJ=8x$YFrlnGN+4@LUXC0%57$)DSgKx3=EdThbB_^=EMq#v`Ufr=Kx`e8<7< zmM4QxW?Fwq=Lde`0P3_D7c@|`Z!gO*qr;Aq{h~K7F=Mmz)3udU7`yAFMnA>$w8-RF z6to%iyu1e=Q<19%esi?p`y-~l75N=^3&RkUzJLe0jV?{ukh?*7rb%OnCNHe{AWj z(;z-=tVW0U@2bL8%0rjju5+h)t!Q=i`E?{dv&bx(-Me;L|I1X(MPh9(=IDFs5PrO+ttb*pBqF48SxEW`Ipe|Ay8ZqeM+^nT4F98% zzelH@tb6bKSqTCd9}{e{%cHfMeP3?qPu69?xDC|K9(y8KRmYGpy^EV`GkO;(wud9F zCRyr>-wU&1h>cnUyMMvG2oEwvWMGw^RE{#LG6x*|0XYoPmHqDXeA%LPm$vAre zX%Qwu@rZb(YFz|*KROAA013bM;gA?eJh(A3d=j<~wd8r^_Bow9mEitc zO8`@RV|KelXy7L|`>g~YKEBiaX`R3OeNsPB5;wXT`{=fTjDK>qt6{@ahZYW|OpN^WX1xn^s z%1E><9*~}WM9^FeH#BkQDe?F~o|52toF)W=q-~M@i{j2#sX!!9qMz!lu^F&Zjy9W) zb@S>x;{_``C}%WCN6wwmANhBMWksCT|4axU%ko_0rp5ke0YA&G_3GzCgCE;uZMoce zhGb%j^Y<+}O-s>;NG#vP%ahnP!KsLdj-%y;d zy2v4nZR}YTP@d7YZAZDNZ;(5)HOGUoxTl502Z#Q1q!{+y?i#%*`(A7oEw&PV_n?RO z8I+DkrM+4GU$)~0mQU#?r3&W3LT=?PsS1Lt&i5IQ{(YKXw_Tse#Tx5Hx*W0(L;)x0Y-mKd;3um^E@aeSo+frGpOVTa2f(jx~ zPhrk)9kC(E_(zjeDng&l`8VBN^AMK~5cz;F`zR??Bkp)E=(}6M;p2JJ*M2Nr~fd2cjW$JL%hkttM4>*Nu z>VN{+d2onjRBQW`)BYsBEmK+jTmw|^MmqJs>yy*>+;2ab`P;?r-1Xc)ij8g$pGvMA z;(Wf!pc+wSv>ngjNgo;|M9AUXV~|<;zRqf5a4(!F$bYok=UW}56(Byg(@cH3pE&+1 zmprS%--;pR%{}(z&PGN*4ehzpmxv$&&c*kJsx|=FlXF8yqPjy_tSzL*%QiB#EBf@^ zr1HbD0O;*Ly&UI)_uiJK5~7wV(M@l#?}}$_Rc^(m`pOkJUa(S4WgpM;cF?fczeXPq z3C~v&pbIn5`&W2kTPp4C@A<6V!bdmtwtf`pZhfDxx?Fiza#a3~?f%5ec~d4w+xcC< z?%%+i++Josvn_Mt0Psb|wVrN{?l&pg|tT+!BAUX{&RPLB1ynANZ29K zmgI|FZb#8S*WB5=ISVr}@;Fx5Lg<#a|JR#(@T83~)+@6%=QE$%Rt3^r=@{~2i{Qa` zZA`GOBJ$W{Tb96&HerrJAGy4!rOMf}7=E!VfO%DCvWJ<{Osn7Z|1=^<8P*vTeYn82 zpvb?C$DJUceBy1AP5DnL7VNz*o*x<9>Qo6nqmNf^@yR)AC;7YMWk-9DNFDZL|JnwH z=z{EUGXBpu2r8P~v1xgGK8MfpW(8n!w&~{ZdVmFN@~aw765$4W>YVxtvINm^h#>%^ z*^cn1e`e+XlIic~InyzUv`sXK9#JP;NMi(Qz5BfIDz+^X4HzJKMfI+5jw8Nq#kbdT zluPOIaJ+!y+7`_D-61|~*Os%uH4(Bm;}B6a@U=mI#WssP^QNEXf*~c})X;E}S>BVU zKXvb~F@50cW@ToGG~Y7$?g_V7kIZUrz%D;9Iv*@E{T;N-dw<>^E^Rw)(&E6LAFQ%m z^IpGDfv--$IQ&U@pHz;QL2*{f+U?(|RM|pcgvS=wudr=c|Lwlad|4)|Tt!RH2pB0V zC+Nlq^LMIRrfgd0eF`f}N~D4qHPU7qnSQN*P(+m+@=R!SLGfxzWYW@-qU3StG~qf{ zaeFR2lhgzVatc1e)}c)eprRVVm=xpn?MiTvT&a(twoGn@0jIWBkmH$I4g_OW-O^ z(7)|8*lhNyC`Frbu*QzGL=KOds^B8E+>(3JkHT#!YkxKkIoiqS3`|CysTb~O%j8>e zsFPDcMi!9mr>k@KEp}$;;RzIm|6VMIAbMP`&ZJAMvIctJ^6EKw9427?^5ji3E#*^= z!la%|g5*-o)@%g3mo}%rX$0n;r_O4o*S6L&{oKdyIi=TGmr6M+%3UnQU7u7W>7>5e zu)V1y3UA&v*do$-(3=f^;3snnR^UI{IdI#~2_A8;ESZ)2kYoqGu=*U>I%L9hQfz|m z`jg>~%ZK=FlknqifTs!kd;fR2J8iVL1GG*%r@|NAL~F8fr}DOP{;$h_KFQ7tu{(Zu z!iAe%5u^4{5yrVH?}FV6dU0V@JfbAP{lRHM)kXRB&-YGQaP2W2>6Q*)9myP3yq^F) z(IbvF;E`<_`N=>&cj~G0@|`<;IB%v1frG09R|bSW2L~|f!wJ7);?Fq0`>)1^(>hfJ zxdGauQ*)?jb^zj29*`^dxV}#f-0{4Lv2?cFl^25F<%uotIhPr4&Lz%F1(LlxN5&}@ zto~Wp-iw5W=HYRQwI%0rS?*rU0H4Q`Jb&NwBZrMkPJu!tCdnCTvCe-74(hx`jTiF% z$K~z^jo*)N1yGq@mHws*T-{S){OjMxpxk6=j^T}*cj?e=!g zIpvl7npl1N{zYDKOxz^D(69XSRSu zo@vgm1HCrou9;~42cR?F=<)){{o&x9ficoJIy*Y>;pzG5dfjT=s_F=%hFhcsU#p-W zct3CC=mnPj?3t&LCwRYSb(j;&GsknL7jW2ma3PO=pYo1Pdp)yfgwz!J^v8s!d~VID zN(CzFwEn*eq-?*^LwSD3fb1W&uXun`i$fp4_0mQr0YE85yo5?@_`wHjYewz(oOU<~kDDi_D6h?WQzmN_!x7ix=N(vtiqTo1#?oAGFIf3X}3vj>rF` zCzpjsMJ6WY4+jnPx*Ax30c8W(6O9!=_CA0oey)iYMt*z&_ee=q1uhhX7<aqDyV_uqZ0;FcjLo`;MT9L~zDg=P9jKp)OLcb7{5&-A*{^adF+ z-}h-f8E2cfnb}pN(xBaR&gn3yePL}C?G40GHzDtixt+!AA{c_RMWTvM-CMMP8$LuWM zk9*0#+2dLg!{Oh~3FWr#w3nxKO1i{i<}J*ihnx=@@yMVEq>q+AYaNe~`RxSHpV&#-Z<@m?xM!G@sV^TlhU`qz8D=RjXHXRqFV6hrN^G{ zmzrW!V4PV}a!uY}{N|^sKoGKi?_g^BH1~TJnM?X{Zs-4F6JCd+XFoNaSSVO_7BMzc?<4O6NkKF*^d?@b3aNw_-|uXB0=z_qjQgmkRTuP9+B3jC)9N4sn@CGDJV<%Gj2w`% z>qxJ9=HjC1QaU>n=pi%OA=opQKg9mga_NuDz68Z7CnAw8TatU2#d)9-)k(vc?I6>aPpRllwhZP)Ernp2&$8ld6|w zZC6@u2C`6Z8^{CfHdWm(Z*)I>>Y*T$^g*H`crCFr!d^ID9fNr@W+@(~zH9q)lXHvV zt;1f>zeCEnqqVa4a!*t>cx9Ht*JxqN!PWQCxUyD5;MTXO7!5|WG3 z8^~g5Xouri2dL6BC4`=@aCPCg+Xf&U8M==Tt6 zG^|N-eczYqT{*gD%cGjVI4!no@qB-1)N1)Sb>YqsO!(P=nF->Rkr5{yUh*MM;Gns3_hm` zg6N)zSN4m+ZvN)^nZ2aGX}{OwCYS7ACm-q#Y!Ht&g_HhRJR=^xrN2HIUN(3kD{URy0ni zC>G_Bw}MYU^>MX}nGB-Ej{SAS3Ld-;m`dj=eXkscchxa!gtYOQ%HTxio&>%lJ~Hz` zwB-%wHGOH}B(Ai4DMS9=WAVdQ9JK0aGZ@$e&p_|mw3bh9D)<};h8uFBPc@1p%2acd z`8pcn=Jb?&>Yh*%WfEOitaW2;Th`U=G&om!JhBw{Ho746f*R%EB7T~(^6-G`qMxVg zK3c8#Fr#@ZO>-}Z9OD3tIqcAtN+-hb21Zr(b?@U>~Hl}lQ6tW^PI*1~_N=g06^ zsshSxUF?cevLBank;hd3Fs8cnHaX$$Z5r-EKvyfGc<-B1_S!NP4~*B${J5sZSWO^qTh+&1JR$v+3;i*opCE z1g5@BBPKDmTq%|6i4%2_IG-vtd3qk7o(o;xpA-HDL-`d7l_5c29DXRZ*-Xr*D>zki zR1_A*J|1Is53vw+> z-f@!tvzv&N-AD?Ce43mIl93hIOXAFOBylaaqReB4h1U(^SKL=Q zB;7@J-FIp3t< zYzkLKP2q&5M%-qMgSDr#WxF+UAKf85l(8or1fw+lHCkgYl-7rG6qG_~;{9O~vFdlZ7P2@^{ zxmWC5zqW19wNkaR^dHj)dA}P?YiD^IYa(;2pQNmGEh|V-YD$a?xRf8uM`?e2Y*&`r zT@E9ZuNW9uW@y?C`)dDr!bxTGeE#*@^)FfJr^dP-RvOl|T6WONFeh_gv0L)?X^OF* z4L1IuFOpA`NQsYLC)fuC$CVSPPb;-`&(w!K7PS(X`0jCcoiZ{w8Y+XlJ zT%jiW&wbRx^My>t?` z+3D{SEN}i%wfUl-D@iI|_TfDl`B4COOm-RFs3pk(Cz&^zid^29#ud1%+W{tpZ2W_< z!7G}OUVGsM`e%W4gD?yu>1r0liejjk^4iu{=Pd}+R zNZE`p{fN;p#*hls>QB9tcQ_WIpU^sOXorzo)qf;sS`3HGn4HQ})C+P;J!|SZB{oZw zZ@Jl;o;nBZ$$Y65rJyUHH`%`oRn^$Z&CA(GeFuGFz}fa!%z2`Rr^DZ5+2+t}d9^aX zdf!l2gV&y0|715DkRjw^dFUZ8C7NLVx<=>T{#UGRxZ>^C`}$w}Eo*W%BNQ39xXUZx zjd_Z|jjbu;sa*fA<4(D^y;fkvN+1_^g*v=WIYM{RN{Cpw@+=!h-MroeEBKq@Ke*hm z*stKcY9b#v+PQKuyFs(R2R8}Lx)CTCUq5q4peVUN5)(AuYk_wj7#up?obKI_?bmLp z!sIH*0N@EH+H+goRU}1JW~(fi&EwtJM)}IHmsz55F6`)BX}ZH z9c+72<_aD|J-72+w$#V+HX=UxShBhf-OCHf3hAH`esC;pW#5k3@VRrsI@YzCR+S<0 z5&O}HZTqMf^T(?V!)v%+nf$eM%+FyY_}JFp>6sFy?)YmyyT|vsyBQ4$SCK!HOP((q zeRH0=HSJ9O-Cc5^a)XBDn-b&4ABzEhdB2VKb-(U%ibiPOe!E?(e4X=R=XGcnZRFZ! zUN5lk$rDd$%a%VjZ@Bc@%C$=9&3O!z_>`g;(-lN{bSl!wu(xrr2Iqt;JeY&c2?#EIZA!m3cs1vI?f*Sz%`H6{EVsH}Ml(+OChg1)-^rn0K(gKn zQ>$`nK;gdU(^izdBObiTu58%H%K~_Na`S23YK#@@Zf4)GapzgI!60{RWdThx4+7pRB5!Wqw*f=oH7@>e?^l?n81 zZ&odDfYAAV@o(6biB=%nut~P`EWPYwAc;h~ovuD8_tU&5iLlvrKZuRuSxk_y*i8{` zTG5LYO2rj0z8LvqX}H0TIO#<3p<5|m7Y)cTtUJ1~eV}~a2D;nP?M7+D`$AmEEtEI! z_A>$nqY(JezXs9ALyX2|L1>A^zL!v_1QIU?7ge_OVcWn=*!O_HyN>kNV|M490KCzqfe=geL29|i zaod#9^TlRFxk5w(iCr}i3G-&GXtbjB{k!CoJw?N7*5%~(?nxk8mWuM9wan~(*bu3i zPxWG_iNNwd1DivSGEPBz4L$ZKK1w#GvjM+r{+GSU_v|d{tb@z-;5ggyimv!lEXp+w zOQwMd4T}e2bE(6NSZ{T^E>2d6u&^`ELpS9>sBYMHtDDpD*^x!2?|>}fMh=FD5{zVN zj>I51d-z&h2)<4uA#jnp=xzVG_O4BPpSW#c?$ierFVJEfEh{Y*aEBwznhAKEWL@u0 z5D8mS>8i1!{IenO`j}T;hx)mZU|Z9Q^M`lV7q}#A-a?q4 z>-3jr5S*8v69oM0&hcIDK$6|&OeuX2z9Ns<`FxAimV~hXHKdW+Q(bs~USu{BeKe!0 zHEE1-^EMFd#vZhe*u?4PJb)f|{LN`>j`oksQl?C+W4fp@oiI1UOvz)gP+fS=oNwUq z-kxfQkvL#C6lv*8S-|DNHzm)PWG+pRtG59>9~B8x1HHpdM{^xnR4BOZly+MNIx`*3 zK@)J%1flbO)hKWn83?sntU$ON?SPH8sSzO1LWL@Ck0sF4YN_1`Wl5p$F2~_2lz}99 z%iJkOmHom^oLlP~5|p&3EBrrOlPDy*?e|Xrq+ffCLH^}>W>d%Aw5(CB?bt>?pPe24 z|B1q_K)Hm-oAVJlyqW_|{GV+6fO^HiwMp@Xn*YfA)E8#Bhp22jriEYj*BurebbRY2-}wkn)aC+T&_H`T5Q0BnOsy~!W{GM= z0$WK73sub$W|SK8Awy1}wbSw7qg!FtC{l83Coo6@^pA9m`@ibphEk$Yo8srTEPw6g zkj(HfQUw4Z3*BOh4mz8lW4~CbwU#8EXysm7+j{nbLUO};krs`3r-CTWNYUc@yWwhnIDg{~pe`O4jY}LH{@qg<1|F0?D^RdW)p8vzyTSvw9EDNK! zySr;}cXxLPZowS}w*bKj?(T#DAq01q;O;O4cXye2VRdsdk z>Z+PGz4oq7LD~{SzlT@*@z=#cW3;i43+N`okF^!T`38mq!By|1ZV&Jk5c!{xlbAt^ zJ6U7UK4u4xVfI6K5pjQV37`wfE~(M(_q0Uev>sq*4Clz*@pr# z{@d({x?fGO!tEqsawR^Gn6bOV=&ZAbFG*)m*Ftz2r(C1$_jn6yxG@`phw&T3k7w_O z7}leu}$(Y|RBWbmJdRcAmG)xFSUz&T)A91`)_-5z*;@rEaEmbv=l&>Tchk?dr9m-t;n3;as}eg&Bz*!5ZUU*>~{wGk~`Mq}+(`EU8} zo)EMR%XfzQqos~qFUaVBQvTcM|K}3?-}mnEH2??1+d{<|vJ<9(xSce9=7HDyE^lRY zTWj$HDLy2|#Cczb-Ei}2g5}Peqs9&+o|^$nI)R<${lR}C>TAD}#|OXB2XP95rW=8t zb1dkcFOMhW@$j`l4sRFZRb=t#JT{Tp^|&7Q$A%~M``5|$*U9_Wv+uy)E2sU1?1a_2 z(fcJ}U+0Q%<-NuZc+XRj70Jr^lLx@v%G~KMQ)M`Dw4wsiSq=WN6FKatulI?i_Qcql{R+<{FUutNWsC&{tv|%NmTSd`u&Uh@8i5{sBGnI1F(JPeYuoLXbe|Q zc~lBXJl~h(i?MMFD%#m3C--=(Ts$59yNz5>ZiF@Y!S3?i84g>&r@e)HRK*Wd!kh+U zu9U~M+!MDnJmB$_e_;GkIVsvznZ2c$_s!X!cb(d*j}e{r9>^ zbpHi*>L{V|tJYFMemd^d5lAq;;HpEI=J$0j`bW^89p0gV?U4Pe3n^kY%Z-7`b2Ycd z$#k`@NyWRT5BF!m{eS9?T^&L5*w$m5LiKd;3_Vy-;s~`6v>lMb=FkXVShmvCAvO_c z&7_tgo4$C9L$VVV(h&5-n?YW{c*AF4CT?Y#{cXZM{{{Z-QXDhb|EE#D!+%>5^?cnl z=JCfv1W#tb7ePEhNz&Lr?DL%Pc=cgPxo?*5bhmdp|2v(eNwHN2%~_ETp0q69Y`{a> z?97fObDw<9l|dLioT8UYB5^B=Vp(H~6qf{PN{TY-CJuYRcL;G?cn8KU-z|oJieDzz zCU;xihYU%^&a9G(l3gMRBePH*L0O#!hK>OX3L6J81Tp4I4~&eYS?K>DU%s;}vljDm zk*pwnriL|-^nSQX-UM#H;NCn$C}f{-v*1IYQ?4UDycC}7bXVDv41Ln~1pnxQIOn8E z`|DZ%D`fuhr@ubN!}Z?*^N%zB+wuNiLPJVH`mfvmrEvcLh!7;U_W@eI9snyR1!rrQ zcW2hLvax&D!2R}Q-U((<9 z7th1XO~J|YFWlcdCm;X6@PBFlYQ{*M-2ce<@4kTHU%r2M-^c%z@!pJ)xOffuI5{bJ zdHMb}VUMjKsrX_|Eee{|>*;^_QQQ-;kU8 zUGBRM|MUJ|dcWiUONW2K#z_3{a{i_B-*NAKo`1~nch3L$Tz}{P58q$e-0wEw{5uc- z`xp*Bfqy1`C%ktAc>i{ck^YZV^pCm!w@3WH00193KuJ?sE zca^uYv$1{e^1hS)mG%G>ESwy?|5{Q>Nf#eO79L)H3KlMQzP~E5Q*f}ezZ>$O6<0NP zde6jvQ!X_p`+L@Xzy6bWk=PUf=8krj63#Y`Ruu1PTI=1pbl+|=c0k1q6@+ohtuaa{<355U5}3_y^leQldg#iW3a=NzT1G@XGAmc9+E z;r21J&jVAjLC@uV?tUx&+tntHs&?BK`7^GEzCDlo8X$mcrj4Zd>v6}*oDEA%oInWC zIfUW?CDrrTBkpB4TOr6 zifa(ED-T=t#yYsfW-My-g8tWh;$aTf-QdI8W?RLKCg|&oHO-aB5>PWA=XQlY*7?az zVYpG3Q}$S`_ZExBD6G9cg3j=0m{zRArpzqh{d>S)Q6VopX>%99#dE7u)lx1~O`be7 zqpIHYe~tW>T9%Z1Q)_gj{3^2wLxFy^G^Uq?|HLNiBYb$*A*K*T z4jBNu3$X)<&;DK;P8N#kbKS@qlo9cUvd0%*A>O3k`J5f*MM{7LP?Z8C!ZE%RGEaUf#h;3hv&D4-C zK7>tZwo*!flfEGr~ln4F_H)GXhja2#i`@MnejL;iwNgssg z$BS7^bQ*|2$eDAzEp~Pyc}!n*7vXE6=>#BRK4u5wqts=@BzTKH&t_1Vb!ZuZ47oZh zF959u)*^)Dy&fXPuOvNFHVZPDff?JQxol{#&FD!` z_p9IrMMKmd4_$~BNt~;io(~jjhn9w4Z}i*Esq}mKDn7&hlC_GF4cw#FrW`&xnT&mm z{w8FauLiBIY9VKv$DMSp>8(|FE4Yv8bj5f00a8QE)&rmXZnw4II$2ff)er5%;%5Qr zJDvsL`;YyP=NI7I(x>2Ho6nl9)2Nc7?-e%zDq{Hpdj?Y2FJ{J4r9pm1HVyX- zvO7&<^?cuO(X@+5H1xw60^{Hy<>=9+Hb$Sl)9k;?5YC=p`-;ot z9(I2S6ANc1h+yCUK$9`JUL}@DIHC?xE%ULi{iPJFTgy7!teH&!-+8Keh&yj}snIb0 ze$?sC!k7_^zXUn@NRP86=KMsMblx$4o(+p%78MwM$o816a9yd)7FgBH6E_<7#%P?i z3vPGTlb*9ZQq!hDrKW#X5CqnS#E*t{k-)tn$Ol6U_U0^#e_UD+ybG>9c}Z&%O;EqK z`{k?_#hpfCkGF?7LRQM7@dLnitPj!k3<-*S4cZWWB#*&zq7IUu7RB^}T3Q-oVt63g zi{YVrE<7QgDOl@qk^9XEDE`A5XHJl1ZjB_Qwfr?)oV%|Nus)BP6Xy0Ca&-Nb#oTk2 zPBbLm^7kq2^%TBW;VmzZ;)5CJKG`rE>g|miumf>;iAhjpsm<1j68vGi8FI|*g@rQK zu}FUwenc;-!8+9+(Q`c<5aOV|PdXiGTX-v+$3_|wcp35&bA*KrHV;mF&a5eN|K`H3 z%K#})xCiXt85BYb0v@&?``v_@tx(){MY#SDWs7w3jt3nFo^7hl413(F%~KvPj8P^q zr@-#qqHUp0Pu2xcpTT)uHYa)Gu-H7jpqm>--uC=FH*ft?1982JJ-xpk=-Sr^Flo9Yo?cGtv=Ymk@oo{bK zQ;!#t)y72yV8gq&_MX=tFy%*qk3ze3lTtXpH!r)P1*1D5J3DcH;fV9&|H+*8iP%TH z;yb<)*+=h?v>5jvdO&%|T{1^`mmji~kT0DzsUCBtgYeFOXfG6GI() zcta}hk!@AEMHJ4wn)_)WM+ha18mNM0^>hF8k4uda9r4|Tv790{$T~0B$NuR-aXml8 zPomRKMoNP%ip{XIQa9My8TmJ1AiL|a9-QTRHZ90QpXpJIvo34@Yrs@XBC~*AKAA(w z*1_>>F!oQce0UIy@D2E+i6BG|lBsa*8}v_jwvY({1(F3ohde+Kj~mVplIeo`)_&OG zatb2gQY^HkrqDrx(q7e@Y7bxfrDE=XYN1Vk~>2tmbdK3VA^)dFR z++-d4TkUMY+WU?Ycf%CIOu;E72wQLqSZs5~BY6XM;BLs^hhG?`Tn=tD3=VQjz%Je* z*YfU3zemW?T%PGm$GxxrN#6c#xfylNPWfj(Mk4|#gQJak_XnA0-;!nMr?xYA2T8w@ zc1NoUTVgZVQwc$G->zYk1FI1--71b}$BU3AVN8gSsQ6 z{6t~qC_dn@+rB%T$k}YcGGsRvh)3u_4E3Qw2(;sh-cEya9$irSu-lm^lJZ5w6+vWi zBN(Z!Iq{*O?oBH2ur>butPJ)zj_*fkBO%I%)y!M0Bgo5O5DtXD5g0Y@sm|yBT!xua#bSg)QWvX*&ZCf9=ACRpb zwDJnw6^MCP5{p$;cft$L8^7nCeNDT@?TR~=gEcOhX$>Z*xcCljyz-|7NC6r@_=z=_ zV)(&jMK_lHB=g;PeuW5kG4(EPa9x?K>6(S)=k)kulJ1u%S&}xtw*wRpnHnW85m%aq$$al z;hy&C15-NIqc4D-11CokO>hM{b}zi{`IO14xNNd3dxeVX9ie8#QaxgCGSANig07I8)Riwyl+$lGY@R?DFbS|19=HDd0ucdp}4bW(tO4!hD-5~%?xs=2Wv$Fib+zo*|nzw z!o!1x{hqgYaKY?wq8-$>5$YReBI^SX=9!Pf%h)@l-ZuVrSKm7nb5=}#@dBnBnjI`I z@Lo2oVlLFdAv4G;>R_P&lNWa%K>O^kXdlsm5SNgzm#4OHoYGT8kg|{nM%SpGjkH+M z(5A2L+C&F;Bic;fr6Iu(*n<01Z5_6?+m|Kmc*MpJ-c~&f=or3{9H-Dl#WE@i(W#9fHT1 z_)oB=w-BS_2$IWth@mdKzY{fVEM3(Ks|%k*LVP4TMKI&kju8it4HmGYY6a!Qf>?o5 zcRL5$bN*0QN8s?FSEJ7SeDRJ0{AazjcaAso=PQq$+(qmtwU_XqxO2pTg!2{jk&1-R z!*y(LLJaq_pd2W-pt|O8ucKFtpi{P2kI+9my#WqW;;)av(8|cEH+)p+_Z=WMh*OzY z%c;T}M698#UyGMe0-k^bRa&) z0p^ygw*9G|-)FbRnhxlE8}Lh7sRa-kF{gO_uy#-PxAS zaTWC~kkHXjmF`eQQ&hI2WerI*#g8`aH~hEYC?YU|Km4<-O0na}5z$;fw{jlMIpP@O!6`(j*4uBDhf?T!j!IDh=9g6jkpxw~;|o!rFsCnvEB8a}M1{bw zXR|<-=to45IoSYr9GLRy#$vIQXodyW0Fp zRpM!}7*qMUY{n-!7RSx{r_E84&SqVViQ|AMlO#G^XBEv86AzZCo-22sUT58|B(GMQH zSL(Kn>cj?Py`h2-6}OD}ssth3Uq}#EAH+i3i8yiCQwvb9;(OU7!iR9!+a^&1O$+VCQx&NCEh_i4BR`S=BkFuD{Po@xW2l(h4v`d+g zSC|RJRev@Zr*661H^7$2M_cxua~;`> zM}lBK3w-|u;NiK8%;}H5{jT7z!wV1sDeY#B1gSgWwi&*H%GT1%R;;vevp84&+Zsfv zRS(ZHJP;`W9n3laM&1!Z3p)K;#shJxu?e*f4_2Q}5cj4)ceFJ5{If?V$|KYS$p|eI z+RPNL=OAP;q?qMMYg*Ba8w0bn1bT1TVEU}5*sP1O^zy4X?yIUg`C*tiE@QFiy(~L8 z)!BDaC`B%0f{ZsZ!b}B~;}tGI|43{=w9MTXNyc1tBIHf#U@Rms)>$rEM3(d?>Ysz) zYZLLqd|`RqG@?C+pG#o)`@rqae9})EaN@N=5@Bg1?y(-o^IFJpI2ae$CpK!>zcB<~ z&?IUq_~+&li+=tZcp;`7^@iUpu)&6QYcu`m(hiN6_0fisSnn(SkNXhPtco>N-M3E8 ztIazgU_(&;Fu=Hbak`-pRWCd!`mN+6GTUd5QJtYC_k1BDm0<6Xx0+y>JQliOShLEoM@*h+H0r8X;!yX&-%r+*arwvcX3%TrEL}JuHjFUf^x-$ zngY3f7=YNPvP9#03V8Dy_>-!Xf*C8{gl@OXVDjfRIJ>4G)eqZeZE@H(T~Edb4aPPN zMcoS1OLGg$XGurRC6ewf2mo=yB1e2HHsw3J8<3@+- z*GW>v*4`ctulI%uqeY^HQ25ZS6k0$vAi?M0x@El+ zze)-QdYR50bu`eHNCa+Mxly&m6(4)IHIoFm8>azL(udKO{_u9mvCnxApIp$NoJ81#tor0?t21ayB&205M6?R&?0`dc z3TDbzvVjBQZCq|?CPF^*_jK8FtD_luIx-PnK*b{VI-j)H+C})i+q0;YU&MzrN_j2` zQ)g^%L=a@J2e;+rxI(VyMoD+VmA~Q=&=L-H6&ba#Aj}~2z_Gk1&-j4vQV#Bwlnw5| zb-~!b=nY1we*fgo8A^T z3@#m=;RIOgVyEDyVO|jW+ObYz>ZNNf+r~HXei)$uDdX>Z8bb@o07qp&>H?NsJOVlw zA?kN3rCbfwuQu{2RkWRzGO7NGgS($@FiCw7yzKFePdi3{%tE; zVUR4mz{A3;)_5opyIYebT4go9{G4syzP6E=GuH<&L}%yoDF` zej-D_6?a2#Biq5qv%^{qcgb&jjxjEW=lN$mJHIen>d{EADnl$*kt83wsL>MmrP`x> z`;5gUn*0l_-n)~f76Y%inv48nJZCBBW7|KQlqx5Yg?}74zBKL_+s4*9e{(UfnkiHg zj`S(-nVYzukIF1ZHe#VuOS9R0M$TsEP&hHPU3d3(&`kE44$e)sH#(u8_g`nh^-}A< zJ>CthYY1Au=FQpvm6w;CQ$Iz7ORyN2lwL^Rx4yyE&ZP2iv=UzHWwg}7IYT|=`co>_ zp&VJH@?k6M8PUc|%s{N3{MVm~kRt6d=JDYtmhxVelNzh2=9p`in5AR48gK+VI6uWS zmy}C{pv12Rn0SfAdSt;}8u`j1YO3VbWQNgiHqP+OFOQzFyU*-!z>;rWg$&a#XUX0) zM?8AX@#x%~xrus4G$CGSwXpmwzAVFaT2s3BLS*2RA}@fY7@fnQsT;1@7Q^S;GmL&x z%Q@6YFxMW$viO*p?Q6xt48l*gfuD64_O~Bat@K`~c0r1FI{Q*D8FCwBY$rjEOk>t# z_=!!{z6HLT&sniEs;73Fbh}cZ+m0Ze=y@57tHtEkO~mJfHQLS(mlyLtvSkWICFg1V zb@ISAjl^+gW+D--5e*-6B4F1e*qdK~%=719#njo~Sr)xE_KY`qq-3tTkJaZg)fv;~ z0(ncSnZJSpm-X$WD5@80$+z85^??mRe(k{vC<$Z7`oZeoGme+|mbk;qyElSu!#b@- z8c}_se5+BaCo1g=B5O$7)hr!`)hfkoY29n`)PbXfn402?zOhUUYs*}j&l0?nj@?TNY3~nLb zZTO0tenf3$cR`zlIf~_FT+`Peq|Ym)TjpCULq8G`r(?3HzSM!3xO{;e43_rmt|>29 zcYwd!J8IzJs=6$cw^dPOeGR)61xgX_N)PzgTzQG?Z7hEOURcp!+B27)XZUDhm3hk= zi0FD`d?Yf8l4?IT&h+|yHVz>|s~(7^A{{cst65~$;rWMVc} zWXV4MgLHUsGFg$WyCQt)TW{){Q<6rwb}PYZ-VA2T@aLT#y=_tm1BwkfrVy1L6Y(Lk zCsA!z@;zuzD%n+*Gg&e3;tp76(Vq~@Fe3zg#!&@q?{x!?pcG|R9LjPS&;Vh864eVx z6{t?+E3|1LOgw-;dw|e4`EC}1V5YvSLc_3Iyn!`|W(wxRiQY?2yGj#hg~anjyk^1N zMMd~oV{u4|UT^FGKJ!Co$*3*PqYp=ton?WA;f46H3=t>Sy3KM0C1Yz?j^RS=r5Too zY{MhQ@k#BupJ-|j_Vh7FQkZJQ^KnP&p?r1Cw2?{&ZC&b?v-=vMSjGEV*oInSAMsk` zK3n>|F1YeR3rWkj!R%y)o7AMR+D-sMfA2TlEg>F&RmG23mE0i2jQQIX= zrUjHWXqr@xz%DR9yHzF*^>6?LWgFt|skJ4k(rR5*RSU+izax%ScP23EFLi=Na zMg=Eby~>;g((iWlZ7V%FeL4Ml;Yy3HdOi{@c%pPhZbZxRqYJYrMPyqBA=YMG6qa$s zm>0>D&_o=>UkPGv2FxLiBu9`)!od9BLK9)1m`7|Rs$uK!2C$5hIN@xevcoFv^%TFw zjeyhSAqRjnGE}@+mi{E?dVTP5;v;d1gp^$Fq&pQeQcOnp>~M*TR&V2|^{Z)P&qgeA>N9msH1xlf^+m_WJKu8q5pxlHk0J{rcOY}( zpQfG&dv3Okx8=EqSM(Vs>DOsZKJ5D6aNT&1swT{xj04V7`M$0TU0)^1$&G%c#*Z@+ z&^(T2x>nS1bJw@&ehdYPl)fJLc`ua)dQ$x~E-oiCG)9?S`J-dFQ%^&e?bZeXnN- zHYgVB9SuaVpxsyRICx2^n~MG<OD)WiBMUjw?2JJd>TcxXHE~|T zuZyf7v&+pjcJ#SvZl^fJtg)A3VYlRt{AX})NC4KuPVLE#!iR=*Q_5@L*Xw4=0Q@o{ z8IX`&oQbm#@sDA!=*RQ;#au$$yZgo5J=;6p(HX1W+R5?o5?DUBafFkgq4vZ)U%PiRCZI_KtT(AqMl2o%FfDG;%z2RUo_43GV@r zZ16@!@)DZ2!?XD&EzF|DAWzO?HS6K?jn$O-!JOThqh{5OylOipV!}a#M-#9WJ#0O+ zBbU_md?M>4V7jE@{FVY&tFu#h$+z7m=0=yTuV#|ToeX!QF!pjFC2>8GYee8zK(j|w za~`EWlboeNX8m~m$O`$)+v>_?)5fHz-B~dc>^I8?NE==Q<%dJ#tmOv`HcDv2?|TI4 z1l9y}ghNDd1TiSBa2b??iH=b`vcxq*@|IsX#%;O3%dU)k{S@c)K)w{PeWGKb^Jb{4 zt>5zNyg5e96_w;sQkIi?c-1aC$=>(u@xvmPu~H-UcZ26=`!!UM8_QcjBg+s+cO`Im z*%K9^@v`wJ4gGwWgv`8s`*JWD&Cs=;O9OJ7#obT@obHHqO(V`-K$m0)=}Y=nXrH+RgY3p2IVCFFG}A3iK9=%t+#-;RoPr zmhm0=Cu^rUU>1*7E;+Zay*0E73vv2{lA}IP|K2L!N#0hkq~Baz)(uRaX!{=N)ah}$ ze!l%u*AdrLlCTzy+Jp8*Nfr(1gDgBjf#+Y^&Blg^q|H=SxP=4{raG53rqm3NcGQC582fM|ZhXcx=; zBm+pThzUsj=n>?(bg?}cn}NV=d>q#P$*!2J876j7q^rp)YI!QXId!SQR%HQIS$TMN zYii4H;@RhE#=WaB!0M^|`fSgCEUiq57=!f9=XgeYGqa7t)9Sc<{;bkpODBoF5%gKN zOXO}$+m5j%nx$7wrEzm#Q~fn&E4eIo5WOg5P`pq8s0rM99@Wl9bjaxCoGK&efyRN{ zfeZ&BJm_q>^oT)b{Rs7^cIBIp(@lQo$6c^dZEm8Ci)Bw0w2nNj}?QeQr+&zt`v37L{MK51^u>LUL zvzUweWD=ay#s-RFVW7klitCaKBZBiaU!Dz$9ffO?xSp)P5xQOO6*$1|n z9Wk7h$s0_X^w|2DnXjY!{`s;_E{UjIWO&4qtr&YKB^Yn)!O}Tatf7X=W7)h61T(H1 z7@UYj8JTluk`+sxO7iAk{~;gpV8=39-4&BWOg#$z4W^Uqu0Yq%4kkwpyP%E<_3TRi zG0gi6<+mA3B%)G62p~7?6%BV@a+s1MJ;^06|4$(!C}$cDM>Ut%{&e~CYEkBa)jN7!}s7@$-;(FBq8DjdA2TlaO z68`=i^k~b^Gb`JMU2+AN_n8r6QqkCgf0z4xBVZB2UmY*_=6NH@OK!?eU=o;Nc)KD=iVfmb`p%;);)u*M+c* zItEliyR?et!?-?u`T%&+620m73XkW&>F$rbq8B&7t2EGW(90Y7aYnuBv7DtH5=)^K z0C6K+KQKAJIQS=dE22Qcs2Wqo2h%5)p5<~a+-H->^&O1|xN%LU#+@6ue9oBGOPsf5 zTi*BytDI%L#mP+z7q_(bq7Pyz2Br9=-n*JIR~bd+K6NSZ8Or6pnIS;)2A7_jO*OSS z59au?dL!W(Vg@h^c~8|GpRP4cdf{&<16WPJPN}>S1cwDv(JFD( zflb1vc#l5c;*~Zqzobb9L@*MkzRcsJWJQ=q5Dww16`YQo+aCHfrwFD92G6yY-9%3& zyrsO3%>=XUX_EQ1!F6`v3b$Yv7;g-I+U%@a(@ z*3~^K57=<#giZl?!zTJ|XblH{eA26{T9Ngt2d%`|to2=x-s#Uew)SJKiFR0DW!7Z@E z?H?ocJHU0%Alu#13qJ%MgHmeAtYXtXJ~h9JRL4qJ>g)H@Sqx~4O0=oBsXo_pMh4Qh zF0VOu0=l??!Oh%Xz)A)^bXUXr-*>e~O9cQdPb^qplG5aU>Ctxduu?Cg7S zy8(Zc+EKS_ol*Lq0I;OhtGm}1F&1N5<2^8R8spls|0pjAsdaZpIAXK^yGg%`L{Y}m zQvmeB*x+aaKy>I0a<7@-LLz`=EZrofc_uPB5{%tm#ga(GR)^_MB$n`C(@yColo_V_ zD|~+3neHp8HMoD(92>~CF)VwHFs_TtSKr0nZkoJ)%EK5(Bu%a_m|$u7s25Ot`^``J zLvPao!CMxq6&6z=uJO)-0=^~e`T!TjoIQn#iB)*2e6#pw!Eu1LyyKpV74+k&Dr4l3 z2BgXkn>yj*P~_wM9FMkqWKWnw#vO!rKs4HnYaHBoa zCyNDyAe5bwI$rwKJx4npp62<5(<;fm#t8cNJj_!&WPv_6YLU<(oM-p-hlM&As+EOS zR3i{t9(>*nUqgZP-XRdDO>0u3);Mt-Sz|kX(!BJK2Wn>zzj3@MdY>RUHG0_Y#&$P^ znvW=8UjA_4;4lEp!cE}8HyNB349)X=ajQ0{>e&Gf8Ub2?bU&2wC_)#MKJHqKEFUau zP0Iya5mPir&a5TMnWW1X{JFt+7LylHKH2LQ_tJf=z3WzWBS_GWn?|vn!3JQyk?!y+ z3rfo5l{|@pF)9g(jzx%0x~DEanR8DCQ_@_NG|Ux>-&a#b{=)8Pm&or&o(3|<*gQq$ zeRsUb?zXal_D~Y$f@qDF@5JZ}hEHNdZ7E=2HtfiTd(9eIM-f)T$wFF}{^T1m(iY3` ztXaJL46$v-M3XC*%3nl`VdFrnC@A~MR5~?aiI86-_}G?esvgqEg?g004KC|YZ4on* zu1wmOIvQ!Zq7NO4At>@2Ko{a|>IG;{@oeM|n|D-j&6gbF&b z;}1ky*EnZjBS~kTv&#VNHk3~%>?S7^z9NDV@8dWNs2Gy=N%z}FWa+?|6WXh_ncOwM zmKnM#7#IJrqA5M=?XGLi{!)s$D3O~6df7cvTZesGwCMZ8HZv}5{!1s84Q*~Wc%>Sc z|9+g%hqhpl!gqds^S5tmkcQvDDy<2qmDXg8ouw@e-`GW27by_+?n+|_c-cBlNOKEJ z8BKHb^t0f9wrYAQXbz4fHAZ*M!KSrR2{vm6tlZfD?kAU-nn;!Q7M^wXU%WT|f-`dt z*2@_-te#sBwOBj{m!cY%dx7f>kc6A0`4!%_0`PlLFcC3@nSFW4#+A<;C37ScV_=-V zrXeKU{#+rUlZ&y5RZ~=qLTZ->S@y}`3>NFIrMS48y4n>_J)AYfyFe!p1}w&3BXR7C zHn|#Hj#1-rFqW*%wY7^0E-CK#b_<(=GeLP=&qME(*5aOeb7!)(58g z`(*+KE1yLPm!gCBsAs}K`m;5omb+ynAIx*^X&;eHNZMEk4gIRPt|Kzo?CIf=r@UVW;`Zl#24B4iTq6&2zGpuCp<9tj+oS zzF5xilJxALnYR}?T7-;f@lb?Qt-%KnDNPQzidPHZ=GP8pnm|dg2-l(_neApOQtRE? zGw?Dg7_Y{W`{*N4@G5FF;eqr1neLwzu5W6J=P2&{H}3U)oET}CWD<++4y|h5nNt~o zutBzexOpe=wQ$G_Fw%ZZ`2drPTOMIxM2?OKHLgM@P~GyeU_PdP9~vAQPQ7PgVPRCA zYGJ1j7@N3QN*Dg6M7+edcaCxsfm*@W=`JsH@q^HDsQTy*ebM^9s`^EQxbr+hUevMw zH3{E%k&MgpJC+^6{L}M2F}2WM`ipKgxZZQ&!%7MxZJi#~emq&%^{ur>u}zU~B7oaiWYv4QK-O=!**UeJ- z@#k>I81U+QTe#v&CCct8&1aOJ z3bP~}G~Rggc%&5-+(fnnB{h9V)Vj4J4KK5N^IDa}`2G#U(oQ70k{Q1DON_uN+fTYF z@7B9YQP}9rBI~kW%Z7>PclJkbvmPCLCJt>vB9x*Wn|N%Mn435*8U?dB-<63|`<`F6 zh!bH=m9=vp@qBEMl#n9XR$xpOg3OVs1SL8pMQe9F{n?gCYZ#7zMa zw~2`dwyO3v=kW+i8P6*XJGaTu`94wBqmUwExibUK0Au5gwj$X4^^W<_j)qXEW=?RV z-RQ<}ZSY~;Z~sZLVBl_0x5zJZA5%HN)wf7;Vx4)nt5gw>2hx0Z;Oh-}wzj6=Zi16k z>@%86U_%oEiTX*f=@VwhbR^uY>$tJt0$J_eqCvTA_UWG=kMiwfx|L`XCT6CIt%bG{ z3uRt9te+aUk%1OFv9Js7R3)+X4aISxtG$F=C@Q~I^i{&^;d${{2_Ku7sQON0HQ;e| zt!d>leVe{F)h8Jo7w#@ggeB7o(6cpD0DEGR>la9Xg_(*el~cOghP|d|v&)e$s8{mk zUR|2V&1fP-%V@JY2|G;lLa1X zK~{zWWftRSc8hy5(u3w&ND!NSe^K10XCFg2Os^2icgq<AUWQ|9SK!_=o?L+LK^haQ!?*19 z#OK)s4>*w5({mZFQrS&sM9d`p(iE|&(0bQZb_6nO{<8bgb+8eK%U#cgR)k}&e{cK{ zI@#OJeg}6umZ;g-y!inO+ev*#|NUiW#=^Q#Ajmj;+n;$zl8yE#z3<}$8D8rsM>Dq( z!yH19@>+iZYB1MpimQO+;f$O%BNnzLmZCOtY!tgOO%117i`+*B`XX96!$AnGL53p6 z6)NV^%yUIILpw<~=W_^SOly< z0*;KKK1vv(MVlC>XWZz#P963nHma)7ipGyA3hP?6kS?^?}jUV)u|@7I^#gm`$)*_-$zO?qMTBbhijSPOE(;vTz>5FqI0}MDj%Jm zRE~N_hErA}ggMV`XUQWwa&A{KEKRM%KZz;n&wjp5?kbat86vdQs;*qTULIZkzT7Kt zpPL`O5ff?2j^ty;h}jiFyTG z1X|2*akynfU$#;F23HZI^_VGO0NwfTr+CPtv+!SG>WAJ9)OU|XR|d}J`qJ71bC17k zdpj>Il*Gh1WGw9qlJvt{q3*ib!FttFv$9DNCUULkq~;nVrdhcVaThk+|4Lmqiu4~C zbazy_Lt!)cFy2|es`W7=|Kv8kOnPvQdZ&t$L|~==^!{gxa7vNAjj8hwfvv?nQ)yX7~vIys>;aBlrE|51Ee}Q zd+{D}S6?RO@9zk_zH?bnzZ~%2C#a6KsMh=B#|nAZ*W5DGPW!Nwgz(V_OM!=eJq^&d>NG&&uJCs!~a~p<}f7I(7qCusqsnAo>u*+=KOT z0NGMe(KG2t0aeBIGbL+&`}8OFFa9N#FR2UZx)U4otwQvkbD0G7jEma3^EM;fL#;2; z7en5dMRwadWq<#ZpMm!6(X+#m!?ht8Abjtx&~a$6a?;-bv?dK#KVh z4w(`(P=CjG=>>$>=qh&!&Y$1t2vC`4dQ!WT`1pYJbxygoV{0HNj)SAfnsv}U-dRd7 zLJCT#@neQI1&_k}$2~_arHYX&VXv^Qc`VBh`K;k;(U2^)45lS8HB1HUmXC##EgYvF zdu`-&jO_zt4qw37N0?S*`ix)E8ddq)R7t7=%GZ-r!82D*D$KUQ8 zx%;I?#p8IrDy~Wknfa}DL}u>9@dRr&@5tq$#7HfBQtM~av+#Va%2kM~f^+#LGI;(| z$zHi_kT^fzR_UkP0*l-PMMXAu*r045J~^Z*k;jCY7j>?!!#c{Z@ZKZNrC*+(Y)mn= zNLX;3Bm=aWmt>?&9YTL3E0uhiT+ok7Xz}`;S}vr!-gUn~sAl3eqs8-vpU&#Gi(7g( z^`VNDP`2cD3Ft(NAJk}FuTZGRiD-@vu9gC?hXx*05m#>%p?7d(rWTAp>OsP`` zmSlbA^?@u4PFdGzw7QGLz!p4GLc5t_MI>e>mK+u#=3j&9u2B_r*uA}XX$gJS^35MV zjFt?9zQ2~2D?HJ4^%R=SJ!FW38>^z2+x`iI4HKFoAI!wtN2(>-n8J4#i_Ybf(xG!* zaQ^BxQ5=MJes`TJnsM#zJnAKo8;b668P|?`RZz8Wr$>a<|3Xhght8}zzqS0u(rsYa zhHwvFrguK|#zzxl3)pEN*Z;AG;~3E!yq_o@rj zQVUSdm8FR1Qb_0U()9q$D2`&}E1xV9a^kc3RScK6gfO*TpWz}6q4mkS)|+M`ZX-sn zd35)#E2s*v;OSz^&4-{O>5|*OFqjb%y%*nuUl>9?m$VhI^WHhyaTN4$YWG>Z2h^S& zo(X{~%mzAv=oVb8xxzHjjCo>y`nYS)J-837#y*!>OT1Hg{!5_A+GqWjs0%KV1RYVt z8~qVkvQl>N)nXKg&U`yV?zWbD;R`Batx8%#_D5aaZswYn%17I)tL=+E6z%a3Doi37 z`)7(mnO+PRtsM`-7rx-^2mRxU&8R;^)m~mshSue2t54BQmax@@#veA>7#Dr;fRjxZ zYBK>(gN$d6t~m?2X}*~gB_BDjy6yg0vcm_|i^6CAZc0bMqm0Mu8{Yjfx|ieOy?bpm zaN)vTJy73#gWkwqlX=DR_@bbh{WX1?2&6!qH#Z}yy9JV{R9meH=d?Zd8Ux_zEJV%^Ob z)J?f*Tb~1$wOnz3bw)~#QK?YFw_R9Q+b<=v(h+vv)pc`M-YZt?49xCwWhA+oLmPaL z%%U3{=Q~y^E7g@L*UB4H3?;n8JyDwIn(UsRyi8f1yoRq&TAzHkbiZkb=eWm09%ivd zMaRU&Cm3U)XQXzPtVOxpP%yH~on%jz42iOW*zIjDmpkSV)=i=mqeYm_>;vK-xZSWp z4zWRGXG8a|GVBnu#=gO;rjztzQfJjq7u#I(yTkI+-ge|CqSR-Q+Q$; zgqn4l*n8;{d7^7SrcWPaoLEz9jCno<%8W!Hu&gxmBDRPxa$U);tS_;wu&M@6 zt=Gcm%L}4sIhvK`6lGciObUbBAnVqn+RdT}PL;45nY4U#=e!1Hx_$NJtH&;0e(8cd zPf|vH?Wm>uwr#lRQ6?)RcOA&swzhr#fmIoO#}_91tnT7{D=&M!EZ4v-LZ8-SF7{!i zB~m7x*0PtHmW5psd8O$s@26f>mDuIdrShfms}tmMW2UM|9$ThOm0XQ5+^|v(xUj)Z zx4;zH*p^6&P{nN)GsEC&Vve-v@FdFCvbmPsl)WSSc($Bv3yeFGD8?G&iYbWEVm8O@ zh%v<2vd$>v(=e}21U0h0Fd)7#s@MsOZSI+|d^lXSt6U#Ktd^W)uPHhu*^$iED6cu) zYw}<{SnV^&9SL7rSo#c>9POGxsS$9f>i{J?xIkE3q+|$5F`<#8ilfs?i`y*z!gGS6$SyX#NuW*%?Z zb?vUHZ7k&g8`fFhQN3n-)B9EV_iWw$gA1!*r2mAp6RRMco;XOd|9D%BeUPGm#WXxN zBU7YoQx$3ECi4#S^XxhPCVP_~H)BXyI3u%WmbfCre!5Xhl6b5naal4e+Rzf^1Ezwg zA26_aJG=G3j&K%ki%<^nPe|e)YY`+{WlbI(MT9t#03w^K%;|P0GoH@a0F=V{V|I%6B%x>=7u&`i4QHnA$ z{fEcor|o%7;ey{G0!dCY})~YN+UxA*v6v8!9XqE9JeEm z$34ot;Lbl|C$0DKl&;T$yH+3TW}?P)H&@-k)B2qJ$|^ynmVn)wSJ@F$74TJw{#=J0 zLA$gM{qfLD&F=O-5jD5;8Ou|tJi=^`iL;s_4kSj1e#N#+qcx{PON8xgF*#u{m)fmm z7Pr&A$}PE{wcFB!`K%uzLgnh#YzKOoZ#|)_Rs8A~4&6H}{mg!WB?UWwUeAq#v`-84 zUHjpOpKc)-Wzb>l*KQ(}{`p8>i6u~?%7arEE|bx~G?TfQDU_e@(|cual_5T1u7|5i ztSWI;;eoJ;SS(30G4+syagwT9B^R$Wv1)>4!ruZXy+tmPS+$m@DXcolqQ8YA)0GSv%(=D6b?2V58LM-T0B|QV=8Y{H>rGqx>DupRD-_RJ2ef4 z%E{By#Lq(gh$4U5!jnn1j*g^65bMd4kgAD38Dh1|16BkZPK#l248Iw2@#z`TwjW>Q zPqIHx?!373f+_3`G1p%9S9z-bUrh|OVxMZjgC0=o zFI7xi=)(^7+iCjsm)M?(k5}&d<8uPi;&G?O>HF=&a)X#ZCTn4dl%R5-)L-hFLXwRJ zMNFZ>{(~tbF@>r=g>rBTm98n|m_nb%SOr_4e*>z_6q#FC9pfUriEU@{0>&&XA5o1+ z4b5Ms3Qc|lT`-2{Wm9+3CY4r#IM}&Pd5AH+T;eLx9- z0SJ>m&=QP1*&$hs$?+-VY{pZAZH*Rgr2Gn@XI?t?QhvTrGw9pjzhL=+AM537o_fj( z^eM1wc4MT)Vz!1kOwKWGHO>-aO^Qmg+mjPhRJZtdGTstF+6wB6b?VF0X=_#>+La!N zPI3gI6ZGiTIGt#>SYwLKmT*+w-!j57#5&wj=Weh}woZ(#cU)kZYn|sl-4q6Uc|B!Uh@q*=f>$54(JN{~U)B3sP6YD3A?=AnbexLHa zBgbT^wR0yVJ_dsCHh^R_9=j zcK(GH>Cz&{M1B=1Mc(I{zh37ITN0#YVv;bU^y@Qx;GPmkapmGLTUbwPrbU7+)?g3f zOC;+_WsVKGYtrvC_9X4e_%!LWjF0mp`q8Bs z%ZqL+ytQa&+ODEElitjDGc!ysYv&)gS>~3O39d*^Ef!S!TU<{UV{;T2YMx9u z_LSo4v}*6Vq_@}`X>S*O=rzb`%xf;RN^z<^Db^967M~eckXKlfHllbktG7+b*utZ% zWG$P>rld8MHJ7a_+fimrDo83ELz2~yl;+5^<;yCU90`uPqP1zarM*>TaFuCgW6Eao znNpL|q&69v3YMvhlNQ^X9ZS*{XIz?jwR(;H8po!hRb|iTzn%Yi+7D^A2BXDkH@Q=- zPJ6uDQLFHWMeW5a}*@=2EHMYL#P*8vlW?Rnt5S@pO}^y@rvo`M~nyD)Gj?bWY-0@kb{Drng1 zn|P^pNLi+Dw89hiSy)q=N8MYj=L>)M<1{}5*==Ip!e8xTo(7+_>I!~_}@!glJ-c4 zB~yeHDf5(JQiG)A8?8}kc9^!xer{*%5VcFS2VA!)naL7s=kM*0^0io*wOy**zsbDa z%+2jmzLp{+{k4*|!=64nyJ+Q9xwfEXPzuvs?{@DHZ|8VH^ z3v9ySVev8c0x|MXmGZ-oPDQL#tI$g3CSQ_#d%**VdkPL094|3WvNfyChLy&Zrd8@H z!zSYv!Ik5~Vl?eB8r3}&;*^98RzsaZLR}weWAwU~)_n3Cp_9VJo;S@mUX)t9kVpC@aoH!VVxP17UOl0;o-0Yq`?!)X)MYu z=A$UeqaTmB#c5@cep=hNND3^oGwV#uEPQMa%U+UEta`nXk`hsYy14wd6}%eTRI{_}-FU^?fRT z8up|7W0RUqanWsiqoFpPE|3fA|^|*tC6}S zcR20oZjUb~Gt3wP^Bz-;;ZMjVPda5UeGI*(5BU2qI`b?(iktzC2rfkcb9$bb(9P1N#O1GxP z>lr8Xvt;Yvr*%zU;CfC>p)WXl9U+P-6j$HadPRvT!zAW$k0&bdIz&-iHz^sKKcus} zznfTR_Ig=HP4#!?Fjr1NpN>NX6VemSVNNiG^bfNqsb=;Cn6%Gp7j%}?jqr9(n(MYj zCwjeoT$f502K=3GG&W?4`6`03`djgzNazV|fm>fuZcRL=Tgxo5xu-|XDm6Oo+?|@} zv`4#BZB9FLdrVGyl-m;>g+Vta+PD}un^6prO%_FMsitP*D&ujZ`UO|rtbXpq?wUMqZw&5mo%rwb9+vEW^9k`WR&|fG2}GC#FTKu#&!u1 zAdP!K2y}^CN-5CAwA)g8EheXdQcRmtdZeUX%0gQf+J8!+;BA&o`)5n+{NL|sCP#Ps z`TU(o@9F6n>FIZTf5&@*!#L8D+|=n04#2s%GN*?KGhe0+W2^PeC{v2|M~9-!bo5x1 zzU()+zi}6nF2{-wcec^*q_eYx8DO3GpIznw3)a)zJXBs_~E0yRaZEKS%?!jy+ zabDj*{nY13oye%uzh#`bhho$_de0IWZEj?e z=I6LKxYM}T=tejFQFoJW@{<1ZC*HvN`1Gc$uD z^cpH?$(ay_^0jM4HS}1OA43-JLvWF74#UrM<1sj)c4yDIG!#)z37+r ze`~kPHpDn+3vF({huYo#2=y)WsN?7DoQ;C;q*;hq!}eBeUl*-)>l*8)sTV4*QSVno zt4+0)P?aw03-$gbkD-62zp5Xi5Al-=k5a#(j-i+7SD1IHchL#-Xa0onzFqN_y_HIP zsj?0|NPW)!`S$xO)`smHEs8DS-tdye9?Daq9VLoMDPA?8SQIXk45WHeG>G-tACMJb zuMzShinF}|y=Jkq0SlI7O0=aI38bSfy$j|Add((Nz$}8?i$x-VR5aF8=??T@`@h9u zcRL*R1r!A)&$?2@T`E;5WUnkRFDX$ADyD8bqTvJWEbAC{Jm;VtN!G-&Ua$HsrMIUi zolec`?#^VA-%2SypT%NM(PpmqK9i$VDw@X3XxNO*Q*@Uu==Od)J!VIfc4VKT|69)$ zMIs$VA_zqy9mSSNm`KMkX$%vd4nMMZ$#+mM0j+8iv^K>g7LcP5>>)0Qo6`j28UpCI()?Id0=W>K)3bWrmM>H)g6mt>9y&Yxd z2U^=B=>6RCt5Q<{KUrqI25*|D`_J$*@- z#2&MS2 zP~!=y@T8R|q{2TEy=?HTA~mGd$&(}`22He)a!{Plq1w}8Mr<{eSa}|rR~%5bp{tb}iaV4C&_l(aD8EwPM(-+i zyMke~rL?rfbSNFAWeVdhrIcieu_)$}&&On`4BqOYx_vdJs#Z(&_BHKSQ@2t#DLd2~ zOLtRylsiffQV*2APCZt7w0*Mur@kL4)9t_Uy`~&%pYi=s`J?(~`(LQP`u=Yzu^g@N zEiGP*27DWe*ZOWzUr>HfdPRAq^n2y^C5K_(YLS3H+EO4=gpLHbXw0y9qeO!E6r}~+ zZi-4FRZ&2E%r6z)CB;`ND%j$|_wMtns*kpE97UB%sWeyGjIn`QEVO7^^wH>K6m;>M zQA_j@y&bh98ZKd%L``&pt=dMEG=7(mP~Qj07Ajcl6zA{@Z7{dtMuAAlnE(`VtU;ym z=|^y{VGTcmIhX{V0!7I!)RD20YKr2lDUw{HIHl&BI(_V*ujVVcYewLgECUEDuqffn z^SLsKi3Xu_5WzXS5wmpmWFXOBnoE~9#Jz6EsTVgy zdl9xV*Hrh?NDk2`Mdw{zu3KDRpx>ZR)2yqdMaJ)pkG9}<7KpYm@LpqXc&{;8b|RW? zkz3rd+=4mu%et8Q7UHc|L z_$lgDJ%cp({PCL^I^%M~B|On+sRd29pccZhEfSpB0=eKkoZ6zMFZ*(nYYmhEuX|)P zOJE1CzXby->c+dCnc3ZlGhzr_Bd?f+L?CJq#B9Rn#DZiK=oQ zs*L?Dl@aqQfQFfQKr!f5pr!b!PXv-{Fkt~9^~~!D^_W+mVi`x1R6g8eT72r+GlcWR zlAOy*(Zp|;CAd6?Zh49-;Ma9ucePm9uG|>7F?f4=xNv`v-Ju+gKb`({;Mc)l#Vu+| zDx{OOL@m{mE)_0LU6UFvj1_DzP{<$51Xl(BP5pJi{CFDuIR2XNSMk?UucqIPTY`El zl;#{DE?Q6|z(!+On0TTwDx~FFL+N^KO$?hPtfw{Y^?GQINrO@dcGcI+^h$%P1bitCZSWMu|YI+;nt& z{NvP$br}AJF^k1|Of6xphl*+u3X$OnRF-%_Mn=&nkX-hEks`!jbtZ+nd_mpYTvDo z9dj(>`7e!adSI0czxm~J+sr%6*HIoSNzLk+C1?|SKVmEnv=P^?L^q*3(Kz*a?uX*< zDXU4;sfCEyz%dV)rs!jOk@Kb{h6;a+<3LA4u){=M#Bp{e+tM3x6*I;a2X0bk4osXb2rQ#{qMQEOn>~uKhI6h z9p1NR&zDi}e?2y~18kJB)4w&D@q6f^H|nZ*(t6GXc{UWsADSza?NX+szKDb-LShqwPlfjgFhd;qY)|xHw$e!`*4yZQt$ql(;+l zxask-B-_jOioF^thbo~eI3DvREv!W{nS6OZnorkFCAAbTMM}|mm3h_W_T{bXY#Zzw zr45-4*-!*U=s-DA4Rowi)~W0K1MQcTFR5Hoy`M$j(q_D)$EJji3HYSz^eEYv{uX-fp;Po8i^QSGB^xdq3IPRwLv6}%rr>1BVJ-% z>7*@ZEmIj0goX>R*b?64^Fo=j3At=a!XiX;WLgt479(8IQbr_?>6{@=hWm7o8XW|i z<8s@T_N%1JvtV;zb2>|n7%4P@O%ZFRl2|L_1ysxcA`YL%0GKe{NX0hN)|}-q-Aty5 zC!5Jo;3i_enUDv+b;;FtW#_;1-FsI3={r4@$oKtfh)pE?n+{&T^D~{@skyJ*xBAU* zU%#Wv=a2H{>*lh%AH8(fh4ag+c3yeI=PrEc4Xe2xE~1}*_A^7Dy14zyT=@GpetO+! z|Fx<{iV(Y8ae9W?!#qK?Q}dV=jo77DCtjht4szWSU<(P(W~0|9h-^<#LKzT(O_u5P zWiK3pxBvYBi2E}5HasBm%jDQ{jU^?UC(1*!;!_G`Ds0N+N)7 zyxOuvThg++Td(g4alC`osFoFI6~Dr^qPnVcQTK{@8*NwF?y!D}|CCK!=l!IYj?}l* z=^?I6ReB4VeB~Jwpae=dJ$=Yp6VkStKqPN>RTBCITF0v)fze2DlVB2h6@V)lTWyW9 zMcJk>MP-*lD<6wU2%fmqtM}6Qv4{3-kf zEbGh9psT1jm4IKwR&FAa7)y*NOnTx(f*wntgaijAo}m{}EakzrZ=~j#LRabGK(W@w z>W-Sm_OoLwBeCcNi~3o_E}FmS9}Gqu86C~`VQ$J|>kagE?`*bd$72J4szVmMG&544 z0lPoztidtaY|-$X>|+Fs>;`5U0~iQLF01wgV`f)pS4S6Zv2r{|Td-x*LR+e~8kYVc zSCEojV#FRqEwLVREl7276%AD@HaRE-k)s8#x-Gpyin!N-IStj4ZEc0X6dFa?uE0!* zIdap!x{M%|ElZ7J_B_yrpR@oT0ErZb9JNjjKRX~v0g5+uo2{m3wwezwL7;ZOt;XY5 z*_not$0d(TD=w{P5M=WY!^eR{GgGd!qodOZa#=h+_nG_}@FRGL{~kR09)d5N7Pc{{ ziC*@P@s4?0ZVP9A^4`X^^+b{`CKJWUiCZt|3Cg@rlmt)j@Re=d=z-jt#T&X-f9eLO z`iW~7wJpA7LwwJbEiJk3LVG2@VLTIAn7w1}N1yC*v-aMu&o90Y4fd+Jq1y5-nDbA6 zc=|Z=l=)uDOU2PI49_{f}r>) z6K!=(F~8HDZrzFxb7KUMu-c*nG)rW5{q?LNv<$C$ro181qezUB;qr&YiL^aE0BA6`w$v0$xgrqBD2W z7BP<-sYFkh8S}e>m!g!oAV~Ip`>=i7ZW_a@NjpQ?0a}%Cb=rQ+&e}2eAMUN%hZ5gg z)rjbUPuuW)%?^$l+HAD$^wmf{I$@2psxdY;sWD*<`FSNssftYqa(F9ZifTx;1%gz_ z63`kMghV+Jz&ipoK8QvK2GH3oLQGo*Y)vJ~PN(-w8X;>zJ$F3xkH7fxJ>TsA>IP9$ zf~^kZ%9n4bZT_c!+FGrq>AyVnpFcl&|5$f7bMTAH{ZedrHa+{B_VSC*O+FjI=!{y5 zX=o+Z9#Qn?eVhq3^#|>@5cqE)@ZUmofW?~-tt^afrXj!NAkb4ZgeCp~mz!RXD?dI2 z8YR@mU=feScXqITWQGvRQOI?9QjUQb{b*~xLdD?i`Rp6bbkMcVwAQ@VvX0#p*c4>1 zGT&q#qsF2K0zc4>X>U@$H(NW=GPFTiAKVffQig&zDWk!=<$GP@&T-{2^cDJv*nadp z`XT#6^&Reb@Q>O_q*&;c@<#cd$UWLv>_m)pYUn$s-=s9WM6kf1LKFz#5~hox=vb7d zqEb{N;fvwu_&J`F6H$Bg%Fr7aM*h&7u(BaAKixGb^e(xEAJP{6X+%J4guMbS6eZ$g z8^RcVoSLK{pUDahH1&;-`akKX`~7IbkNi_e(B%^rL|G(@F_6w|S=6%VDf%-8RDuZc z;ONNg$l&o2LRQ&qeP(8ai0J7h6JDWP^aD zB&8O>a_^9L z|4Uo0C+xJvV)M7oyYNeUK5{{41zmFR`^fUftH`mpFIh}_Zi<9ge&kDE`EXHT2Y&j+ zr;lS)elNC*^Yp3)Z6%8&Zk4epgs3=!zzDHXTF6TTme;0%SaJeAX#xl}hPB|5UTuwbsdgoQy>_cMK}~4iV4q@NXtgCdS4vnQhh2+3p_F$)FdSMO!Kc|w zIS=WzNDk$45ylpwY*9f2fFXN^ykp)cyiCM9?xnr&X8JAQ(McC7P##`ZwJa(u+STwi z_07zVLW&~%froe$KQb1N5|NLTbXH9IlUWm&N+h|AMrBQSm1YwfYBlGyX0`*QRd)e} zgOQ0wAt!49YiC0d%UBUt&q_u^9sA6&suQ4kgNErJE*e|;`8PlK{*EpJfm=>&g^r(F)-^I#iKZ_Wv zg|%{m*|Wm3hyJu>w{W*~N9dpFZz=~}FVnAyzmiVU|HZiEA$Eux#!t7$`aJuhc!I^6 zjg@tUycv zQC(2HsxOyo(8req5WErgr<^w(sEJ;*&AZFH*UNZMy4_j61SIogJUF4=kSwo zliZt}h5LrXW1{u|T`@UbE;%48lA#=uLvt`E9LNE8V#R&wSQK8=KrYw>=$jn@*fIj4 zy%~)2NgDhpkW_ZmiC+=M+S@!BYeSwf$)X-5!&0fPE{LZs+H}A|A(|c;AjSc4xQ-G( zFN=T87OM$*zGlY@Os}&qU1N;`WW>IJu@x}(HMV$Ti#N8cWJ`BgYaU6hshYE9*NBT3 zWzR$D0|Nsti|_2@12mBZUQZ&LG;H!;p{-kYUwlVC;`#9xzWVNe9(w47+1==Iv!q_$ zvG$X6&rff>@$y^Tdwz@1YwsfVC*SDa6z|eMj`7eMiehdxf11kDT!Xt4d7`iKI;fpI zv8w}FBsnaIb7T-lyrr@OqWv;3s6!^KZ@5h@pq{K)C-Gc75%y7(m=UK?V4rM(9O0R1 zX}W%NMw&5{(lo&7Bhm}-?+D3}2s9^o5y?3eK2;B8EOC4#E`taoA`5UJBDk8|#;dxG za3g8Q+rJ|C+TqCOnkwjZSmB>Pdej*H5YXq}qdn+(Fv%=t77NSOJD58J^Ft<7%MA^oVmZZ`Y-!XYjz4rx;>pivgnjPD~c!%#JYcIs6xu_ZlkQ% zjWXUwO5GcbR>mC&NPLT9*fH%m=CC-_+|!JOVH?gSgDCrEFzFJ@wig#~4IUpQj_BUr z+0owmEVgWm4KK8uNV&bqgeRHsrh`<<9S@?0)&W%m8Fm;Xo#R)A{H$2D3T8FINuenMsIMr+OB$K;d00Vo;SCY`8CGmJ=6+nfO$awgzW8qAo*YiL*=E- z^i8cdt);0}OTlu{J({WBxn}dW&h5$J&3jFI&7bsrO4(bzd;TYv>|J%|n)`kCD-W)j zGCgHJ;5(rFsPdy#)0>ZNeslAQ%>lpWDN9v%M`W}4Yuw6?dVumW9nqBmN?jyFlC~Yv z609zl+sch4kemSjkc{Sh_uH=g<>Q~29T78!O1!cj^)kpQ>UA%==FGLsSa$?_inG)7_VZ5$UeN0J@NF}tb zw6`=-VoD0=*OCA%QL5Go%-A}#4xY`9xfNG_d_Z!O%D)p|%sL~Gh8byHB9cbL0q^zs zD|^#uO?o&zoj#T}r5$i&x*4^@mH*IX5E$w0+UC+`ee`)u!_W3Dam*lEK$> z;z_yT4{rK-Nvajt>6TcmVMAGr(SAN-{tmV=X@@aQ2N_n@(wHX6m_*5P?iSvzuDkf? zqn{l5Ue>`_%#4`5x$DSR7ca|2qNU*Q&*lwoyY`D8K7Yq5o3qMpsbp)&vvTX=O8@GQ zEGf@@P%L$CefGdN%aw` zdT&EFhPrfMT#0w`L=o~z9JUn_E>ytF13aOCKaN;DjrW>homK>}K(BQLZ|N3urCQEVU{Imb5KRFQ!tt7MIT zF;&4Ag-be9JW;XG3cfrQN6>1<-|`!tjof0fFEpFLvpzRlaKo<7P1-CdazQyG)Z^(_P29n5+f$ zcMWw7LzCWxG)~Ecol}gcJ6rOZaB5`>p9xDVW6?}FImI~iLadr9EU1L5i;u+bHh_c9K(>0 z3w{h|hmrBv$T2c>H3uKer}9Bs8LO^J^yzdO)iw>O*Y_R2xRZ5p^kd~2fHqPwsfZrO%;7^c}F zNS0XbZ~;q;#f8z}D_E!>9h6=lY(oM6`o4@Aitu5)13szznO z0;8g8TI=H$Xwb1aR6s1L5SQk9rU-TfHX|={1}muJaWoVkj*rJ5jh~2{wRnG=)?pO~h1uR-A!S#$ zQRYiV8B35-FR1-$UA4y*JzG;q+^D0wGOnP01q~^~%DD2VazZgH`(pe4*_e_DrpW;wz^Zq~ zfT>vX!We=xlbaYS3X{6w7Cv{V@)^4B=r}Xs38w2zYfn#WYj4lT)V2k4ixw3ERyOPp zrX9#_z87}%wzl@nMQ61QHB89<-u39R`*WHq#)m2T^yPC)(O&ajOtl$wq#@^RX%{hq zToHJGCl7#tCY1(xzS$(tS9O;`p9XF7&}_%n`y4q1SKcG1;L2~vDG@lu3a3OUOC|;M zDx`IA=A|>^?01^lX22 zJo~ug@zA4Li-tGGvW$dVj%67?mriL5QsMMs6&}g5-sQKpssSw{u-+-;(CrdM2`v6) zal(aM;P~ooHD0s6tjZL!KA#`IUxR9i>kpn3uj7%(xQ0Xx!7M8$G)B{icW~9T?q~&Pf0HL>;(E%76W()_b$=Sxl zH%4xFFzgURiJ%w>qOcS)y-w!22K{9H zME$E|tr6##)5tR-Yuo}9;Dt5D25%D@fr_3rKte~V5J3%iK*l6QEd($Wi2g5lfw&4y zoKR@ugaSxXxIh7hERfW=f`ptAQC)=8rQ#^RUF3>%tE45gWUoikL+K4uaZ%Kca z%qyJBBj2-rkN-cs*=O}ikx(SEgkC6GY);W-_X{C0?1)&_GV4ui%>&Xp=Q@`~6+@wL zWG!uKi2Y)RLTDOE44j}XS0cv? zg<98HTti?D;F+UX0i6l>VRfUgmqbK6WtU4*BmS^jz)&UC!qe6;4@e~y>qr$BRKp#M zsUl^=awo1u+!~^_2*!pbM7t59A!$0|LZ%cg@{*+RofPGpLhtFT72&5g8*jmsuc``f zD+yx)eL_ITgf|6xSeS;Gj?XutAjKc4p&EuZRJ>TE3Q}Q`B+Z%o3uvq`UZ4v@U9~B6 z%l_zN|3FxKWE8S0F}l1!8ilM{@SqKj^qw7)43oR`s_@8Q6k)=VdV5KxyrY?4>o8_| zsVNN&U?j{N!;@j(5tb!+^P?jW@)<>BiUVw~P)5F`gxSW8?UhIxTV8kx>6q}+BBV@C z@3YlxP&jL%QCN)v5*(g%lEmC>lVk@V-tGjO1=3ooFyAN3x`<6vZjO3J&H1+4JI|sm z5F~5fSt)SQB)a#a8y39#?nhcmadrOOqGTXF_XoAmH&<91^Vmd(=J&KZkz~I2)X2+= zWkGO3z>xz%L2|ANSb*VVToAM zc+h8>_{xXj1;JNt!GhR^e~!ouL}UgM0A@F0;eR9+hR}pi0st)%3ZFXt?*~bA-25yS z4jhd6r(9S-*j#7g;w%7VPRZ&R5&4+ zg?-+0V2c6Y>%C`z3O1RE|BT?2^RY>jGHz2V%jW9!g5MEQ{Ank0n(zH^!G^97!6A(P z@G=8E6D?vXF;7q%nSX6)5#Io@HV23g*5@Rz*13LlsVVPE@IvABL5nWJJ1%9(QQ5Z6 zrOl(3Hji3L#&N+>OBXC#K#p5L2xS2wlm)BZ@N27^=dEt)m(|S+;L2Zh6^>oa!(~=y z$>~{gdbX3~SV2Rl1gCXEZj=qq>kPt$JBbkthj!9rKN&XG=_J>1l5032p~bj{R%*mQ zzh_)SYbCMKsnfsGZE&bYH};>xGz#%juUc$hvK#=Fwrt&c9gZokM{Cw^Tfb{PvwnkR zS(}o`+1TEk*~siDf<_x0#K3BH8vdFJ?F`~Nui7AW@TVM+vZQ>0SP5sG)_O0#JifY( zHM8s1Z(xGazN{feq!nquq+Vhb|irMRNckF8JjEm#*`znIH(_UW}u znxojnvJD&I#Uye9Z(}VcGrMeQTS@Wp0~mcu&Umz>pGtr&@V?R>|) zxn5UK)W@3wsSVZZR@1bnd+A);>YB|Q&8_LEuFb1$tLA#@?S6uDQzCL_>G!sZ$=0n~ zZdtWzefRBiH*L_o*ueKmF=s!zd$^!im)o*)tBBQ)wbDhntxXT*I_EqWcLd_`K+k$~ z=>xeY9u_Ey`M+3_mFY8*tV$$ViQwTjBhj7{y)h681=xy(;u(&Jq{bLrA`dt(aj$ua zd(BJoBD_uU=Y3+jux5 zLj~!02?U9?jaX)F?RJ<5N5V^s;cwHeaWUS``g2B>Sg}Z)YZA#7tDO&x&ygPz2oT9| zvYizhmlnOmJ5T)IZKO_apv}0x80U!glomn!Fq|dA-#N6(UE5NJEJ1 z43P&V85&J-kpW=>M`SY9%Kxvk4P(>ps@_`Vs=)uHYJYX8I$RyEn)4>4llmCmnygwT ztH-MJWEBnJhUqF3;=Gx#XgJ<7nQ(k%3zrEyR>ne^aLjPLwWV4Yl)`O`gH)`&OrAI% zi;1Fx_j%*&IEN-VByz*t1oskW;=u71$dp6z)<~v5GX!Jh$1>xY$qYkfqzp}l4Oub6 zXND?<^DX=T?|hRLm9dx-D&q?xvqdrcn`~@M7&15l*+#_o_IEvRFfqtEjb}kX8Lj%# zXIEXXc^$U4g>yYFz08{y^xb@u%>j()UfL!`ntV9(y;U1}Z=c(-F`^Qmo45wuymRCe zbD=?R2y@!9t!Uj>m-~t50t@Zq%u|@pL@GoJ4NePU7-%Fn5;$krbP^;(3w{&u4(x=A z?t(@WIn3lsa5gDH8Jf{ZVKclAXOk$0Y4363Cs>$@)Rg?V}Ui_pGUh!nawBn+RH$4lP(3A-;k<-(l0vB!i4&`Us zDHG*p+TW4h37oTuwZp*K*^fI=NKQDCbRwAK6PBb?bZb-y`8DLV;+n$Zn#(R}D8S&A z$L7;0)r?mSo4NT%GJ!FzVX8xmHtEjo^mfaw{H=~#qQ>(*-T3YZ z-MWa3bZLeTAl|G0&;talY51tL#gU?r>cBT+gS&wnAUV2{AsNw`?4M(e^q(wH^~d-g zpw^nSLfS0tahmSTp`Kj5XH{-{X)d9|R1?6Iz30Rv%!a^h zd_IND-i`dpWqa4$eaXmO!{1ohk#6_ZR?TT@XUgS~VqqnLDpto0Yq!q7@DhDfsTgN! zqp$3^?D|i=JoE4_kC>l(=hAW*29?{|wlW_XC@GFzbKlq&>)v$1l~4U*Rl%-|VKAJDg9kS(^;?V?WW z;wMb3-io%gq6p9;LG^dWQYmdgGMrjW@wQf{Ta%DUfyugSl7NH(hM`!D+qYPdZb6np zq!qPN&Uhpe(a@MSuF;exVH`iL9n;L(Q0B2S<1`E;K6-p)v@w8kbY{?LAnqDL4!Fl4aCUAS&?-KQ5Y3 zwV3|#NA{?wA_%DnCET0@bwLUpD9m6KqTFev1^93Y}Rw+2xi-L4!V322Tgn|90xd;Bg@fh%9Gu zWcQOb;lw+4Hji8KADrg#^f_-M^{b0|ME6f;Gv-(Ev2@Gs%WGYSx5%3dGa9Zkb=`c- zk2^k#RpV|&tih%!ejbNGh8wZTJ}(>aRzDaBEL(T6%03dwSXy3_WiZ4XP#Avo`GJ&iT=Ahc!xYeQhq#>D+Y)<+&ax3t z%{JgB6yGLgW%XHS0?kqk_jHD{P~Q$M2Mu+r6tD0TQcSEC0kMck@;@YtNP{MZS$!)5 zN~m~&0Z<|~@GBy-_tQa>F!P6eQywLA&1$aSe?2_S;2K8zMAi(v1E5wT3CW9?^keg1 za3E>jwv_taWun_nW!R$WL@;buUVk4VPUY}L9m2V7l_VnOvvMQSPKhwH;pB^=i%=wR zUtGB5F=GXSUnf*qEHW=kY-}5xRWXi8)2pA z^wWbNjh;P>-bV3L=DzM*VD`CXB$6zA2_2r$MJoNalCZCOqkBoGFWpZIS#E&}JtQXO zk3AS@-55-Sxq8luKYHWdwht+2CgY=z zgP2I(o(TLcx%~d9^~kaJZWYDL@I1$m{c23nlRt6ij}8%W`Ar&?eV0Ydnq7Cq``{-) z7t(Md>#UcZ8jMEKihQTf<|^)1{`J~A`A>Wq87)U^43YIo-8Rc237F%XxMk+{Sq01W zAJM<2cTOeASstXDN3EXaM$M9xM$j4OF{edRJpS0upT}zHJe8^>>_!w@zS~+wUnU!R z7TbQv*ao{WcZq4`S6vzO`NVrozbL$ienF!*C?PxqTE5l2n11XATsp#3hCYdo8s1xc zum&6_8_@V|f8=`~O(|$9!H+=V{)uB*HA~!%Qex0CODs!N8Z?qPcK)rrIR8`Jx-~1% z&K@DdZc)G%*w@-Turi_c6rl{?@iQ$3cmlZGd8BGrD^}63nr0Xva8&}QmmTR~jcVPJ z=Lg)Er|176r@d^_e)iuaYIbZHti`M!={J-*_|0scPv<*dEK5?6-E<|jvkQXs3-e3k zyydL~CpMC9Mkm_O-ti4-bt_R@zX;tv#q~aO@1oMEh*mX3J+`S9anLMPUa+uckjpzn zlBPGW7&t=oMo$bC(;$ZnR7|!Lhg~RZV0uz zMSp2LGc+W{BRKK4BT5jCS@cbJ=(Qq(u(t1Lc=p#~mTUvy&aO2*Mq*`)aZf9N#Yw?q z76rh2+bX6UnRGzc&`G;j6VW8ryNAHYe@_(ET0o`6T@$T9#7We>_S-myqlFv?k(a<3 zNtYREG+mWTg$JcJ43qfQvs3eKzHr(2$?gR|+ap58=)5&`uLdeyT6h6Y8sadj@pnUd zDO>AP4F0InDjJpHYPN($hQqxs15q^Q4dNvUNZqFf3D#?GD5?~+{x={pgKewbJOBY6|Ser;;M*#SKFzujj1aD2&iF}BCB5XoNQH}wlGGRalR_<9 zcu^TPLNxZ+NPCDAFSf*Hh58zfa8*@-Ftxa9xc{ehkR&qttJ+;@L^ipBwdvf{U}S11 zL?}CR02$~n9`{=~AWn>&8u`Fp_%KrEx0FH*`7s%^6ifxLiui@a_yK;1N{2+zjOGjE zcz&ddpaAuj1a4hL(ZU{Jal@kPlCz%16d08rSo+AD8kyj@{#; zjpqrIF2#wP=nn@;r~X7jK*Ltw`NEA7_`E-fZc8P9b~}%%=svuVkiCMFQb)o~7qaCa z^_ob+kserB8*LOtB6((xP`*tnSV~20$H-H|cXklNe-i99Vbs5&8_nCb^&1A_ClYEBy;Jb1^ z&WPz#RQ1xtkM+|^EVny1PS(MlC!m{i9)o*aMntKmlsfu`7{(S(Z?IyiI+gQc`j<-0 zip7y37NLAOJS+ENMoMP8myQk9BISLXKt=VRy{hb5W$L5zzd9CmoWBDw`jo4`t&}~4 zDz&68VXk5n7<(C06;r?kYjc12QY6qk?rv;oiL2G8$(7RKRa30ZTZ&v2D;L#mF{D6& z4lv5MXiHH4RF_$5hrNJS!P31tGlgsTH&x1K(x} zOll^H$KeDs2*zXBfB@KJIhEDk8q4sl3E`UnGbU?X6{;S}O6oZzx10rN_pj9oLcWdioFr@|i$^D+t4Xa_q}1WlHd8e4)wgwx5p}rZXz4KYsv>?$+G8w z@4DDoW;!@0$>=h{H3LGR4%=_C^2#nENfc_hf6fb61m@W&>R6W_uev<1kAA9{u#<@| zr5B0VQu0FW7h{!)3nvD=XyW zf;6XZl}VKjGt{x0_lGxf^7N7d-K>TvC)_m6%jfBwXbzjr*53=+8HV0Qtg%kw$^%Sq z<`>GJt>$RoQedhk*L%;m+)6oL?GV;}ul8LkbqV?Oy!e0IE(1!9lQwmXuoFz`TNl&` zQ3|WRbWAmJZu$J4Rqg7U8*RS^Z?#Sad$pz&D4Mrpzt%-cp^?DVtpLAPF_~ipuBtMt z!-r$!OjUo;{(81Qg9n2BYJqs`w}azLDI3)}FskY17*|C&3j8jz^88D}%By)#ky&GE zhPzSsnTPB#ZmKKxfq)2~Tfocl+o0o={xnGF`MQEcesa8fFsq;yZ`1dqJ>sRpQG%2h zx6Tha0*%g=n{pqYZdqOosFsB~&B_1leHUFIpKLXfzaXYv`^V|gna{y+>SR*6A^D(h zYzO`TDN;%8CW#rbM5K(|>HMfVt--o+nORdKKjx*(uVuGtpmm{6BU6w`_%asVD3A>M zmXdYR_~bsf`cyoPmAV*q_LooZwaaW`c-9q3yIp%{WkJpg*@IWA|Hz?zcuG10>LwEb z+A_3SG0N^O%lC68`10|iroocoaq^xzO)<_0^T-yt!U5C?m3u8ZvXPTBEJ(|xhNAcH z((rtS_ErP;uJzAgL``Y<`Pcwi+Yn}Ejuo=+J=^tt)u}HN!@70wm==*?{iyjK*EpU6 z`?<1r-uy$ky^!98otrT-uImEUfV21f>Iq|w7ZL&wH#7(YCUe`%0S zbjQ%s9sk1Kt9T^-> z_BG|(;f!g}yMJ2+KkFmp2NuE5x%jyW`!hlhsq}uNwdfA7Fm&iHiF1sq$F$;( z$E@BP$_L5`tuyPHxIg5%oE?wHOq3%7&#ey4jpM?6?Yx=yLsFytda8Xrn>ukR=Vjm& z(BhyYc zi@sQI?RZ(cY4<&h3>H{eyanacP95t<6k{@LFk4M5D(Tb2meE90g-TzOe~EX7KWr$v zPZkv@vE>ze#*U!gslpr?_hDiJs$oH_#~RIl_{^X3qL5FQbwV!z@mR^;aB#u@Z{oiv1CdbYnBPLipObCHR$G|G=hw?OPCqwUjy zvYp5Qw}@mNf>j?k-ixa=boSKzIO!7Cvi4?oDU5}oNdu|6wS32N(Oe<++V~wn0fR2HWHCbRs9%E!m3>S1rnC}C&o49BSAV&weK(?298I7SgeC)0lzjG7`6 zVsgr~!j2Y()^e)!(k7<1&KAxd^ipt);f1Vlpl%x#D zC}!$p{Hdy;t@A%4I_Z3JIRC}bgA)+=Z?*o_`kzUB>LqJx3vf37Y{&KQ5hN_EolPAX zC9DmdO~p+A8U3H0|FMF5rryjC+aRWI&|CZ^S9Mxq&_i*mB2uu0sF4DyD-hqjr06k> zaR5E%R%U-0B1{Su9-dMF?&};YZR(k|bYepWm%DR5T%>up(D|AsA&h9i3^uXTg1 z&hdKu8`~Z0+1A}O|1$`7?4kZ5HH3uFJi|5(BC3+0xNh&V))mRuqdO4M?;f|Kb}>}5 zc6)uI)ed`?oUW3ljhMol4k@)8zTWGl(9pH>%jPOAnJI49AK46$BKgnzu5)VzwG?5W zdtd%6(`#p%Neq#yCDUK{BwwM7B$r6Zw*7};W5WXN#m-jai!Vwh4E2% z*G4QPjm(AfP(|V4k^ueTSHDWH?;inGKANF-yrjjp8$$~_WVO{+lF`9khV3J@nvGR^ zFpr)owf~s=f1~FgBmWCMO#cHr|E$n|R^$JI5>aWff7bFJ4$J>1Sl}2xo2hs>Ih)!@ z+nU*ZuB);sz~WQ72bJ(Aa80Qh6&y`W9X}C7^2gkxh%)&{`&dveH%Av!-!Knww#;n7^ z%>4PwuEWm6qzA{&`ENQWv(9I;f12vSadQ2`BW7V?(S!Sc_hPQueh3D{MPY_2)wxQ1ZXm3!!+jLJ&jaKiE^rQGD+G{r-7v|Ur|IG z1Qn^(X)G(v8yV(y^b@rU;mD?5`5!$feue#gSo}Er@Yqx^yXicvnp&UaKV0+5AtMVL z7G*C~vLD?4@jb(F1C(h#XDoKmzYKqZrYmmJlumt+bhj#hA*h$LH!xFoR9@ znB_HcM~fA50%HH_4Izf9&tqlSgIC~&n)Dt`we1(SYGz_f)wNd%D29^has{-93qF1{ z8e4Z=_E%Xc&U&!1NeyLlxw^zW6TTOD0zmMrR=*3zWt>iloNiTAh!nVxmw9)|6J+!_*#~UQtr@i((Mpp#O>t zZL?K3EC}mPLtsOTl%Xt@co z52lseC!?hpMO%@+GKLDmpEC(V2oX+83rLW&1ZRVv2fXLlJ>t0s9607=y#9LNwRvGU z<4A6XJOLrbG5)AP7Urj4S->6B*|x4swG=vpSnlR(jb)gbA`iH3TYZXSgXT&YI_BS! ze39irHVkl;XF4&52o0Eli>#D*BU~vZ$OD&ZU(t>A9q)I4{1T&#{LFb`hB?i0waCQx z!Ly~$cZ)WF%xdxd#pm<91R>t9>b{N`Nj*yUfm{&ePqKp|=$6#g zpck*J)Z3285jpZ_v!Np14t)FwyHY zh{E$8Cl+uta~NkB0muuO0|)^4z7XY{T&ac%2rk=jLlp$R!KB}foo^EXI2TweKbX~L z&Bz2e@OvB>vuz3!3MMSV-_otrvA(Z7gL5Tyy?=9 zbv?nRDmWN_$u3J3+>x`%v_~_{AlGPa)!%-8NG=OhgaVRWp@psL|3)-LK%j7LV*JBkV_eX z0;~hSCHgFR44;?KNJjM$KqgjR{romnVVY%^>wthovC*xd9-2AZ{L#A;*BBVEkWZAh zB2yvR524~AJE!~2v_}7DR_S`UgPEQ)EB)d-f>*#8rpoWXSfe29A>qe5h3Ht`r=7Cfq#rR}W(LOk|0|joS8} ze>7~PH`cg$nQ2laxefE6++$irbNh?i85WXpdu-OfZY?CAzmf6H4PwDgp<;cWipX`3 zBky89?6A(%daNe8`*Z-K$fRCk8N6_%+ZFC)QJ_d}g}VXHn{Ua7a&TMX1UlKQfEnw^ zR5ZBSrDh={0)9nx&qBB@WY?6rGXxhkjBX*1_wMxQyAR9#f~Vm*g%al*vS#__U&aP7 zx!K3RjCY>eV}|@`JQmD%T}VyjLtP@q-1@+fD?K)GM)7C0J2W3$`h}*zM}5j_0X(N@ z$3gn71SF~tjOK)Q*$d@qbY8x!u46o1YZJKUIgqAyWx7U!*)x5-#H;WAZ` zY8$GK)of_cLA9Ow98+)cJhcbDBVYX@2svL!c}G|a0|>&mML$9zaQ}K?7GLY^*6Y69 zwV?00&tw3SM_HF|Ctf8?v-@)yu`_8O4skt-`n=r{X(1w9?QkPF;S{wnl%Q9V?l>t` zsVxl;dV2omF*kX;m9=>x*_y2%;F8dJU=|?CwfOk5VIfQ^OR#=L)O;=ob>N1hSTy(u zFVpA|hB|<^z?+*efQLTE0D=7FtD7hnw?Ou_hX{y(1~?B2&-X9C&nq2?hNZF03@I?Qvn=znkrWXqf}(tW*dvBW#Odjjy8Up`3+yfz`o^>w zK%flG>nM1ZKs4)^FERWZjo&|%IFup~7|vyWSv=`a^iI}40scllb6vgNg?{~Nqp3Vq z937*V7T$6$uho3p0qgp_+eOOb3hCa<;O7m`g#6wGF?~3+3FU)%$FTQ-ux|_IGbwME z8FIDX&HB|Z=4wC)3Un%@BHaFEMCj}OBI^?2^Q6FCm%KvP$CC!Nbux}QAa$xkD5Ysb z6g0*rkORoAe8jze7DTggFq@88+4bk2g{gTpqJI<+Y#0hx83(BRI7+CHSV|OVDp*c7 zebriyMN)g^G#$#<&rjqsT}?aJXv$jlg<3D1L_nC&mhPC+ICs30__0hAGunrE)XA7BFq` zaemcRPWl0nwX)jNp#B7W;)3Sl%9zo{`e^4$xOk6sd44=PO7?v5!;3k6=Hd$~~ z#Ko>W|9iOv4)~fgGWq7#S9u=e$GbMl5FVUcD556VQ^Ru#Z=6o2aI|Km1!_-CRz z7iKC{;I8IEs54DKTVl*-u6`H|lnI%#c((DoPZp~Ad()`XMUQ*8(Y-^T*dIxq(>-yj_Ho8_Jn z{Pj#}ed>Cd$sXtp|9m-s`v$uJ=dRc#y1u=>1?Ev1s@fWP?#1f#MKQ#6LZX%4+YKFN z-5qs7*ax*zN%7Ef;f-Zr1>Ca?)Y)c?z~vT@_M)9$I?1N60c&=YHP`Aw>GR$H8Kz34 zCFrDGbY4?jiNW88;4yycpbd>=q7u=0QYx#Xs>J*kEu61hF3KRsNiMzrTOE8I1B{a! z(gC~BSR*o(29XeJ16lgWH`0offy3OIIh-5T{w^p_yk76eR|>svT^jXMCC4qj`b%*B zPAOxl193WH*-i?Y$4{%vzWqtluE9^NP1BjA*}ns$Dq!JL}l{!A-IUyta})>r>QJHw|Wq z%|_;OKX~C@dsguJGjyA@Gcc^)HRv@5ssLs=+P!yobGxiCQ^_Mhekkh+^$4wtxM9HxJ-omeYt4z`Nz_D3%9vl&`-K|P$hb2Q!ax+uOM{6 z(?Z=`;}_`RD9i6hC+Lfq87G28PD$fF`{;U&iX(kpf6d4*>4t&ICpIsJp`hO(HS66h zYpF#yni7=d}TD+8S}CZ z3>#boCqjU58MCGRE!h$Sk4*>Ud(7T7@c1XdNVAr<@4oQjcd=AM^{B4vl^m_qz6x5ezmfAg~A-pGNn*wPc8149SJ zP&dmxp>6hx*VT|y{Wp2#8Lt+J9vGBBKWVwqocBNypX~bQmSH~Payr57aa@peQtD%X zl}*17BtLI)@uV}$D|9&6S97~Dg!`G~m((Apxj;u{-B)m5JA-C5?l=hP4dWJf)w9y# z^iczN>61)UV|?y{SyBRgL@mMMYN&bc%w(NTz8uLcOy2*6_Ay`2dpJ%{E3a_-9^gaa z_B(xZS+KFtQeMjGOtWcoO6d^j&56(ExQL6ZhOLuL+UT=HWhhhZEvkpJw^qMnWoClsQH$Q8|{o}zQ_OSy&4N0%eIWOL(^Lvt)ZMTiu ztn*^5)c3IR{*Nl8_U)e$Wh`Adh+a4Q!gKIi$hC4+z;Bd8ibcxBL*j;{} zT?H@jkR+#HUOGrTuZYJN?U*xvP2THc@(?c5plIOMUH*U>Y3A#tHLf8AM0q8&am_rj zK#{T!9J!(Vn~=fy{n+GtzEoB3$2}x=!ji}+A|=iOl!qSPtsn?h zOZ*}OY}2_Be0cr`Y-UD8NOwEVj7>`QOVrC|{g@n|AwzY=KZnh1s7_L#cZtkVNpN3s zzu$i)PxL%{7FG+I#EUm_nikXRob>w4xfia#{!F~xc(i`?Mc|-X$hPe&S&{aOTUmC+ zzsyT#(#EU~g^fd;094C9XK@_R4`GTaoiab;URqVYduxyk-Mni5Ig35_DH}MP7le2k zzsbgYR5-=?@Y}Q-QGZn2bTe=+c4M8qs~U9c{W!>yId$Wlyki)`>8rfZopd&U);GoT zfV>-(cZzHz+Ai^5B{m@Xd1Rt>;yu^2sBQ(Tx%#69hChRc3y|+EpAP?=R9>wHc%eNz zRAMoi{q}0T!Ltp@4&qVRujg+1(TH&G^-XK?lKtv-%3;!O{!)%DXZ9gan$AkAm?o>| z($zQz+JiO0G}1dhVjorqI{Xc;zfLOa2a+&eDAgZOT_mYWNSq{Q0i)Z}^p}N584B_h zmLH^i)!F+vch&UOD;8Q!px@Q2EKu)lIc*4$+#BsrqJ z)xJan8-kxLz zwVs?Wq>TnR!Ol&>@+;5Rx3268sid#A1`QinS3XEnHr@($IT`Go8-eomd3S8FyF)c& z9n+r&-gt<2=X|D&PQzy9H*!n@73?fGUI!i>uREbT$g)$&?N-4V%3h&-n=^vVr zndW%O82<*H=$R?@pNj&Onl==9_L{rPyNUc6{W;H^{tSMWYYKJOO@9=9yncm{llsSK)h``y-nMy1Z;ZerEnT}CfM1sRc{zW1?yZu|F(xtO?H z_wPF`B2*KG4B5zCAx#kRjICDE+T2rgXMh>d@%wG7#|ubN#mW--E~rb}3gTjnjS_M2 zC8r5S>3o}o3sK0y!Q-tmda60==oq!tGd4Pk0>K+63X)+`DQlUR{-H2Kif&b9kB@BO zbXNI{qSmuoSb#b1J^8*l4oaIXQSWa!PSTZBMmZtC{L%IDb8v*1kPuh9w$9YDsmaWJ zP#guRN~@j)b(O0s-_(+@Jt@f@dUP}pW!_36_oNZK{{|Lij>4{@A}kzF$@G_3Z)e>r zC#@gXkm^)qKi{+H&0$rVMs(STHd?{8#{RsSskoaa43(9;$wYqys6$iB)p!5SkXuK^ zeB!CR$y9>C-+%Y|S;y0iy@WbvbP{-TJwZy_BK`~rA~A}`L1`%b zPUM5zJy`+XU^WP5(pRFKt7=VQwwIp@FuzaV(!!tGH})_?bJ!Y z_SD!eEU-*K2J8E`avTlAP*P_@S9)e&5+jDM*yksgk`?&KO-hi><(<51_wq2bo1DE_ zbX3ZIj+RZNaVu^|9z#7bW2j73xV82q(MX6%^asutSU3O8NO@$${L_r&L@h3l>6DJc zTYfZm)veN7!Zy_`AsTcqq9x!kn$EtV^7r$=a4>)u5?=w0_U5~&F&ivxJ(%GALKS$_ z=$R(EC`Tfz|Hl~>rMt|a^^ie0vJ;@Wij??hp#mqK2&cB#bK=gk{Gb;k#pE}E^qlE! zJJGv6?=V=_jQg3VDOyt}yf&S7vNu1D&BO|*$!=?`E~qeZ(;P~vGG&@kx?7N>oWDRWMMy@vS8ARiC-cz-theVDZ(d zmFPPk9*i3JJ0B>-na$D%r}ING7Xtmo1nnzq zs?+TuWs93$i(72;h*`3teOSF1j+{`xa+z%Up*f(Xuz$PCs>0ixDlWYOvzWAkDcFTs zKf?q5id71hJ^rk2*h@k09sGdz7?zI@Z6c?*2ije6-#~bN&d&S>!u#X(&3Qj+WRE^4xua<@hfLM-gR{Dn0Q%oUy{l;Yg0U*-ALyRb#pie`)zvidp2L9khW9dMP4 z#B(b^h^YJUY{R15u-1r~vw z&aY}!-21ejmZ&>kc>cLQIeuOp8 zDF5J#k_wrnqnKP(X?R3>$p9FFcy!o87Ajr1Sy*~!{(*y1z2+QxMEVz{S>ZV`u@Cdk z@pArl&G_vIYrOGDq&vwVVxKQ8$fW5o*;7HXOR$d^-O71XEaEGxX{=6ZO`8iv<4A6T z<&S|q1$(ztPkhDO9-c(g%)(Pa(b+?y*+URt?8p2qx*0xE@*egP3kC^d)PwlP}Zt|sJ*axMA!g8C(6d)kXr16be=W3Bp zRLr9SBM^#IYhm1PNqfk{f2K0OT*(}v!Yrjf!?pbuu50P_w3->Tt>AP*-jTD%N8rn< zTphV`<=fBK#t-tGJh_VGKUoj+D6Y}>XA@IhAmE1RjlERUsut}HS-EPIZ3?X(!NJMS zDL^$_hQ*?jpu+tc%b1Bu7<`C0*2;vh7X`3IPTAM}#?a_=zt6JT$3OGy`|2uG^m%3h z&BT-%a^_r@{&=*jOQoAy%V5h(t7m&#+l{WF2CqgF?Erxz%zC(O&g!XayIk`{vaMsu zoDNLBI4FfdS@s@=(fyQ0O5;7(L?f%uP#nIw(1|L!B5CXfR#eQxdJ%!9iwTvNhonp2 z-LMk-SyYTpRem2Wv>zHhX|cWHy7paspLubKNW&UqCt0R{aF53PI2_}?cc&Du-ov0K z=$BO6^?`M9mTgCC-EWq^42)w`@Q5-rILv2k@{PNgSs;_l9Xl)?1A)BnO+|X96`Wob zu1Wi~yzaX-qTb|4$V5$|pe$+A0VJEaC(9@Im_=yB@(Q|YtOPZIh_EhFBCdl=njE}{ zv|FYAEUUxSpNF(U9($+Vke`1Y(kMQ|@ew8|U}f5!?jZGr+MG`d?@Px{8zli8JPS>; zJE4YE6mxG1=co0P0Cw7lhvA6IT8@r!g*Of zCfWGBu^&Qwu^(VJ=x^mYTA~pV{Y?Q$ti?CQ32I%}T&bK;LZ%a`_sE zqjk=E$y{SwYn6bp!fj10s763{arnJN`eJH?w6*G8SHrK$%EYq0sYsqOC&PpmxLqvI=T>nZ2yW zghYq)?v69)+GT0D?>y9gUq9bm*4L(q%%FV3hbbfPZ;g8_g!ZfS+8`gILc}2okEI=Q zvHe;Gv{7Tz8nfm$-LT37hJne5A#yT=P#Zf*SIIps{4$FP9PX;mZ5KkyUAMyzmQbX9 zHAT{#4T}6$BUt*XIit_2qU2&dVgUGAFrA3`z8x87`&ug0$lWRXbUjru*VL9N2fEhr zYi{G({ybM{bS-?M-v`LZ>Poj^^gR$G=DD)OWA3Re_aXuY^-tNSXvK0^S~>PSD$h8O zdYusY6L&2wVT2`FcbQyK$_hV86(y~|AQpI;Q@=&2i&V-9k6Yr#RtG%ggv*x4curGL zHOVy~drXyBl?D723%UOcPsVE$%%6+iD|ui`9sK*HO)Jw%PeSF`$A|mA#Oau_L2*tK zV}X9*d;#K)_X8Cknvb~C0FgZ zH{F;(6#nK2u0gnDra|HCx48QY-HWD4Ska0i-@bbA&vRHd1#*Y6jWCK%Rx8~m8C6%R&HY@slZf49VWns=;PX)(d^k*WE3am2b z^v5*(-JfBssZpqqsUsV)=<>=%>R`_qvW5r4M359nML+VA#U@su7gr=4J7v|q;kfw@ zE1ujljM=E}j1FtqtMSK3G_Kb+ZvsQF`(|)S7YxS=gU=1dxyDPui$lHMEI2I+tFlyA zuEF2>-q>u3ej|rYcTtcUQqt@m;~vb))ScwrqVDX>Df|&?TN+`JUql=>@nASnWUEn` zCbcb^Tcpp9+I2WS9p&ir%@3QZCaKqG0mx0*7bBn6+TNBCP+?iVh1)Dq`n$z_llnB* zRiBIbTb47UXH$@VUh|btj=;ANsc+#=5@Ok@WWEuXjXP!zT}yO?C@!V@YS#fz=rqSx z4!R<XxE5cqjP1adg1-NOrwkqxOStmPUvOAioqS- z8^(Gs*CWRQ-Xl&%v}E*QRA2Fw3%@hy2c_@)QqgrMg{Ag<1F)>AZ1M%Z$OtKqzh;|8 z{Iq137x}i65dRVKP3eirRVzOdn{0AmSQhQJ|GNXZ#0_+rsnO)l}66}VJ(|`P!{-XV7K^(74&7!`6d_`@=lfJmKu(RAJ zmtbCg%j#I4zoK~&{{Fj9p@3YMY?sD|=EqX1C1~Sm`Yt^sm3G|o_Oiwjx^=K^u=S5_ zb6ZopzMDo6Z%zs2F}3A23QWUk!}dyob0pmYYi+Ox4u~(>%(8%-OU)Lue2+a}-j%6} z4Q4c8hmWTM%c$aU_;pW-TM>_bbO17a$WJd^&@2K7RjBHhjdVPfYsa`dqLD7(Pzm~b zr&L*UN$FR9KNs1sIlH>@+FN9oFL0dqm?acFHHhz8kT?yE zzBXb|?`CfZ#MD}xQIiOMPfrz1PnCsw0hekn(zfGAcE%|jRUK|P*IY-8g9vJz2^#@B z%!CJ~Lc-R9Z?+RtI@?c+YwjM3Ms&)$9EfQtI7Nwpmx&gB04@0BY6N_Jm#d1m8-aCvE4z)HVfcn~hXd@A z_!)Q2Ryng-2&-yV+4vCD&MVxzA&1rblRr$~Hvm_{qP?``n6GsnH;-P-Z?nWH&XQe0i(!Bl7V2{h$}`b-nLpgDzmdJMsAIDt<}^O|3;phMNZ`+>MtJypm-@w% zoAMBC%ti55rb9Ry{{P=Tb>S^rFX@`iT_9q2& zw$hKvI%ID6e4M((jyh*t`drgH$YZ>CpwgzZhJ@0Ux!ODl4ewkzz+rP|F2v{c!D0C^ ze9yrc%j5X`h^cz&5RWB1@l;M$%HN<>zj+h6%dxRkux!fPC#^$m<9vZ=HaJvP)RIv2 zsc`AE$kR$=E+b{LgcA1MO6YzepzWukeBfcdxiT@BLCh2k=gg%>ZD>JSfIoM5pa4<3 zuizS84$3{u3pS`i%r}|jyTn|agqcVX6$YoW#EeHzkXmHKH1*+Q4hj{)x~7b&=`s2D zvO*07C2=+JUitAc1r|AtP2aZ{tg&5}pbxI=L!vv){14~pzgPbcZEqD^N024jS{7Nd z*kWd8X67SimPHmbvn*z2W@ct4i}{H8h?!Z}-Tmgio{4!c=6>AYU0G385t+LxvvRFn zBXN-xtd@*&Odly+J+UlAf11FJ59Zo+FN^28r5c0zP>P8UF^Ib|oLxo!`CM2^L3K1O zDaTIn9H)?ySjej{%GXEe)kY;wpN%AY2-nt@nVyjlny5qY$ljvIk9RL+>)}C)D-+L* zA_tpv=jKh&@me}UbRme<(eZaBDXmMDUH#~LP+>Xa2@L5UH$Lcmm^@=HDvqGNrLJ-M-l_Kn4?DD5Q-LM%CN7K=%k-%@te+#?k&3*6HVTNpIB9@5P8kQAnU#r|nY6BQ z$>fDRG!nOmP^Td9wPDh*LXWs>(nr76KWl+^?(U>$Bum=*WwRlN8D_B(8y%eQF+Vpc zHAHynY6`wV4*8Wb9RFeFlBJIwFQmsYhK7yMZrLov{2V1>ls3QmhDef>#GagtXHm(W zifX9$yMejRQ*oEAZ{wSvquCdJf4p)O)f8@D%zQ$&npy$QQvIH8Z`j+ljOyt37qmw? zfGn8lj)ZI05ySje-~LuPYPvSZfIcjmkm;E4RUL!< zOix@~L44^F7gd%?*}kPXq5tgxIZgEmUta?Gsn@B0CuXX29|`sR#bm5a5mC!LH|;~g zD_rE73gH^H#4bi8qC^Z`)R;6NMY3=hZTckK)S1oHnU3CA?nslelu?6ncAiAuzO3JQ zgZX60Z*@W-9E!>B8+fvx7xHdTP;<3qSL?=3biPQq0HJWIKs2iAij~7R8HZTlP+qPf zdWJfoW-lHRy`dsmM9Th$x%p7nUy;|n?ZfbM0M_6TJ*b5tQwO8l@uQyNqNn(2JyD^5 z_XX{iUMoW><#DB^W}V0PVd50X=6y5VkgSny|2VdM58xW`uHGI#pj^ut5#MQ)7 zvN6y1$kB3U@$y3Q@ncpqRzs)&%wdfodsT@4RO5~bpP;7YTnza^v27z4aB}uIH?2W<}QNXIgboZRa0T z3;KbK+_qf#w~wy$?0oa~1B&C@Q~GoBXV$hn5W9F+;pwQbXO~A;=6j-nTfuq1cc=Xj z*tCT6#NCyb;>hhu6~=pkk9woG4}rSuU+kjYZ+|FuTmD#tnUX{)K__***U^qAB~G4v zDWb`u`xD9}?`{}bZ>gv+#$VbRA|?@^PL_H{ik?byrAsn|_Z!65RGWKK{ZxidMRgFFXpA}` zP?2FO*tjpLo-Z|O=e*h7+;($#`A)fb#@|Ps?-!XTS{Fgc!|!zg3`0OtcT z6RX9U_@c&(OY8yjrzRoRVhohs%Ir+H+JGe4`GS&fP_vk2$BSlnMd&w~gTs zBO9*EIp~Ye<3;T|n6O)q=nJl+2-jaK9PhVHY$4TXAj2}Z{guh-EVmX5fXRE8E8jJQ zxD&UNcKS8rv$+AQeT)9lQ=H8PIH>P$eKpkfkwg7T^LS{7$ogm2G&DPg2QN9JUYYqq zHK=+)+8^zsGEV4!2_+KM2x$odE+@S!%1d;AOoV1gK)kfo0OEK0He0=Nsr2KqOAOfi zoNB(d=3@o-o?e9W7vIXI7{2a@sbYos>lXn5dj1~_>jRz*FdA%W8f?dj@_bFmJe1G? zqA&_ZK$wAgaDz+c@&om{NZZD)i{9a{6wY7>s;Me?PZ7cH6hZJ zD%HA~SS4NY`&^Sdv}hN&DQD%a^`)Mrf&GyQhTT&VD{UK^b%tBS6wa6UW8-X5a&1Ju z^xCNLi~Vuuo!|Jxz><+W*}<=)pgdI!2}Slqm-e-IQ+?#SxyWv}9IeXh&$kx+wI&>*As#+AP_jksh zyGeQUvjgUWxU_07kFr0q=BMO%I<82zO8DUAk%mQ_qqo?-0$RCBughW z>p-&gZS9>NuJ@uszCU>nNCZt>akS)dl8EFR#6h&m%sdiai5WuttVTwzvxbFxhNK+y z;traTltk$rZY2_K*!#HD4$NpnENE=w>G?yy%^>Q@2`Q1_Mg_ZC>C#ttL|{_k$bK>> z`u)+soexxFI22XpDIG+ht_zSptL0?tMvf^IS2j6m*JBuT&QLNtyIHkBb{ejgh9}lm ziLO-is5+SwYb?EK6pwpH4hr&q}0zt~=dT zwWLRHy6l8MJKE~-c}|r#OzCm#v`@z9_naBfb=>OSvUk6K46g`Wt~)=Lo6@2f0;{IO z!yIij_^pS>aO5-!fKl$H2QPFPd5F)xwm3<(wYrIvYjv}?vR*tM`BO}--C=*XNW@I- zW&6ynfL5E;TpRqEkC5` z0X&Nt5|*BA)STc>b4X`M`PdX`k%xoeK@Z;^;HK=S?^U4H%}@OqustVq`v&?TS`?|s z$LspqVCuF7ygsi!NoEUPoqDoM$=vvYtBoQK*>&j&-fZR%QAF4G{J5vXTD_h<+s}B*MNCGfM{G2&nmr`RW6gj)iM5 zh|?LDTE_3Y<+Trg@FPHjSF7tdq*V-)>AFH}y0M5{>yZ29z*!kKFb9MnD2i*EFXhz- z6!nQbX_`ARQ2VPk)q4i#hoc$n*T7wDX0>`gN)HtOoVwBI54_R1Xr)4P_7HUJz^)u% zJn1&@sX|SuCwq9$@lvF{Y2u~hlBYQ{YiHGbgVwyhD2VD=zDs_3-5^;#M8$z05f3#@ z5B@`C_#RDT(;as1&Y0#y@-!~;DB)|2-DdMxMZe<^SA1ybG`b!5RKUK;0rh7&={(7EVLm-r(g6Ed`Me`Ie#{1LQ=nBCn<_>EM0ev*Hmiqa=3!Pa>@EoQi+5Sa&?N+Kl!m%S(XHn!z256 zrZQt9YczO!#|pI;ao){tlIJLRc`l`;mFn{44v|+r-U;CeiJB^$S1`bSEYs=JI~`C1 zP@qHJYGS$5>1fxosYcFt=`!mPKPC?P#u(yqc_`1UepZ#vxv2Uct|VeIi@%t6aBgcQ zi4}1`y}2ET$}lQwdc|MiD04n-#doPXM}YW5nLfoo`&+k6$2R%)DkfiujElRLCh2r} zpGYKDn<*~^E@lmB?&$iWd%NQ`K0Kli%VbPzEZXJBAe4SVistI!>i$mkRjotabLrXT z8Q~R$xVw(uO83Y5-0UL$4uFsYUsJ6Zok-e~FFSNi(!K*e^miz$QXwmE>B)R(%6hh@ z4HKY!akyLgk48(WeT^dtoX~d4tQ^^VVXNvUMbUn0VJOp5EMJs`QC8hJrxm7nmrfI1 zo`<8jBKdb1e(GPwSDF|?g2wp%sXuqNN1@Q+n;|5BLzEVMDSX?q(Ye z9ar_E{$8XRuk#c~LGzF4WQ5krG{xjmKCfg*x?Y(pKl@jnTI3~))mIC&rXM^7fG(v+jHlP9kmsmwDsRWBlpkGt%H;bKaV$}q z*;rJITpj&2IIjs?lW&aA1Bndt0@H7`8^3GOh=BMJl z-$4$ozM^9*T!)?%uZK?ptO9a8j)Dtz|>Gx)6mq z!etE=kC|pCX)GzShhuxRX5>c78U_GlDicw&(Hs$lC}7W|w{m0rL7)Bd9<+GsX=NG7 z>#_nuwD~wr%1~SHczCM0__3O9A7HXuhs=C8w`uy=`95c}nUkWObJX@i__y-uI*u%& zvK+OFRgxkoEs+tBVM}{8A#GPu4|WE(*mvai&@`^NwlTwibdCaAru3n6vB*EgJNdEQX?yk;!(fs>-V`FA-`?k21a)5_RG*ArQ z;d{;}B~0a1?xG505RxnT;StR7^}PRG>xBPd#yKz1v7!KJRs8bDlVY|2ec<~WrThrJ zx1rC@G%QJDRcwF;&!vAh?%yWT50u)aJLtHnI4@hGk3}+_Gb=jB0D{`>Mkv6 zkG=nO`;k)Ig;i2nD+gIo>rG-alDmb7wFL&N)4SnFiL8EjqU%G1sQvB_+w&-vCH8K* zk!L6wy{v4h=&HZU zS%h+rbiYxr0?cF3P5Z7L4HnHE>$!#sAm2V2qtnwzMp!IXD>U(%SMlZ73%ESR9Op>S zVQb*h;`TfFOE0dP6-M~+vt4Fl^N4R$MDL&mQ`>Ehe)&#IZcs9IxYKm1o29sKEqz$D z<1>qOYaUVCrKE!MntU*QNO#Ru+NpX}&(<(sJe)D<&u=jzrsuSOnERZ{!Mk?_7az~D z>f*=XfW@roZZjJ=q^DP7U~ZxgTzT+)tBs$9pC6)F-VdU}3hmcZZMOB~2nm+xC7Uav z$JNz5?X`okK>C47LAGpK4rxrIUPfFLo(L&iNunrjRExVZcr?@>aYeGcusOO?D?H8PpcEb13<<-t-B2K=T7Za0WeABjh@n)(O4SwEgqkB3 z@T(L+tv;iBLcf@Jsw40+&PROKCX|p3=c`1}UqEy2cv=daZ_*7L+4wtmEq>@)bs;0H z0g9upq#tI=iRcKK#oa%kOzs#{|xJX!>h70arOmW(bzbkuq`PpSrOE(YNYt-I|imK~1_ z{xKb@F%ODjF?l*ECJ1;;F#@}TzO*!Cg#&9+o$H)}ItkqMG8D>-BsC`pp~3hQWnWP~ z%OC#R1N3SIDxZsJGXDVifafe^;gREq%znYL z$#KVIhk&ECtYNl*nVe^ovY(^{Rv~{+>MWA2wcaxAP3kgh@WBC&?2&{JJJnJ4Wb#~R z#xtbj@x57E(F*kIcLlA-kmh|d8i$d*XxEr)Hot?_@Sh6%UrhjmfpSX(lS-YJ4L&&e zz8mh%l9^Aij`vu!{{~0@C!PEcVws7Jm6h#(>jD1BBmX1J%FN8f{J+Ehp*Z-T$qZCQ zlmMEdG|J}Ie*xf_R^(!r8Mu zCcF~+4PR}W_p<6qlg!|vON7Bir@$>6zD;OaHVR$Hw;l zo^!z*4>)oZuRjE9i%#rWwu)sjRqR-j=k*du4!TX$;SbE65)Cu9kA8nJ^L{xdCVC4ZCtz2^4o zCUdEXHmoD`SJ`cte4%YTeGWHNA&uwr{m8hVT$M{AoRBy*_O_oImjkujDN{LOEExpZ41R+m|s!~ou1F=Lz z`8yYmd~yzX5TTC-LYB!SVwRfFF6w7tP%;Z8axc|U2686yQ{Ycp;h!^>1%~8DGqTza z-@!H-*VY|`zAba$^GI4m<`w=S)TjfS8qtXPD%{Vg^&^42)-AK}inBjA-K3CQCxnpp z_jE3lwFnzBYoc{iZ#~%n*jh9c+!$P+;T9>7kpL%<=o?fYlYc4_Z!)3k@9q`cI$Nay z@2`^af_Z{Fq}vdZJlRsKrbu?ov4SpNdw;X|Pk#%ZQtQog-ZZcew~vWc)Pf04jvXZ? z)gmVOPWh!!F!#GmUy`4)0#r4gpdkd~-4aXi;};tT@zGQju5~KJfn~uTKetp+70M)J z{q}RaHZ9~U9@m+4guRG9_>p8k>u76%cabwnd)zU~?~ij6Ep5Lk-&}mZ5Ty_`{C8vT zKWqW)|K(QlCv(DM(aPSm}-XzhneLph>z>9!=IbJpFNL<7a#Md zYSUF0OUvmLftZ0%#>7OwktDUi`j`=z63`6yn*1;a#~d{J3}BEn!}}oMh&ajp(8$J( zshFe$k)YNv!SyuR1yq|Jt91{%feznEVzut4w=T2u7j4~vZ$B8`$#ge+7J&lfG8+d8 zJxuQ7yAs`n9@{aZzihr;E6aDk#Y|jIcOUcF_S{}xyeZpDcnRwt7!niyetr$($w4|K zwm;lDzU6V-de7)yT6aHm6|+TpY~{E>!MOi}H0TC+?wmt>>8R@bx|@D6yE5%GVlCMkUK2{)O%c0cG+wixX?#Yttqy`Y)s=+*!9 z2LAz%ycBfeh&O+z5!~zY`mD5x^^xwxQ5TgItevVKW`5iy3(vL`lr8p@vD@`}<((j3 z_G*x~6NHVwR9mwnIk3jI9(7r$=Ly${O>dOeCe`!6TV%Q{utGIm_I^6;rNDieV(AtY z%wg_U5L~lh2fjlE|7g}KKR_4Q!RAGmE#POJ=XiU@)aiqI>lPh+`g8{M!H;q4gK~WX z>1KT)uocU;blUdxs^sUi$^=f0J zvopKD9>4E3FeK#qSqvWIYSUT9_Ttj8LoXcE3AOg2;DP zS`RrV^uoj?*yB^)#9{5kzU;x>0Wisvd}n9%iVN0{^urej?LjoxFgL4Xz=5}2stxlt zxj=N=TJI>|Iju#6o1!;rkI>`CK>CCN27hdYoHMn0^$r|z1wz*{Ozpm7``T9irt-u& zHJ=ketl@!Izj}VXdIn4OjdXTr>Su!xF>*D?a6yXZT^{C*uC-oEJ;2RYb}#N(WC^^R z#?p&=w|#kfUhc*)*vi-{L4uZ;{sM;p>A!R7$`^22b3gTd=+iEL_{?2d@@WMjB5^sg zU1ga!)|ZqrU7|e8+$|wgA1q5iYMk7%Y*Z7 zQ`fCF%7o#%Gu#jTqQB8I^-;C6CE}j32hlJ?1}CTYR(Fu1D9@*uJ+1Cs1Ub@kS_D^T z;1=h}b8X-mT;tJlxmYC7v)0B_!*x#fp%#zh`o&_;GWd=9T)=qL8X2k&fO_J%g^b)Q}_4?tTzk#;cG z&fjYo40Ua`pLI2+A9tI|w%)M%QTwjy;#kCsE0H_~57~F^!$5C=!HAYmKJWr%I|bPM z`8yrgc;J_e?M)waL(;h66?HwIT$atNGyPIIor-SYm0eVarMks|-RW?%19GizUa0l(>q^rFV#Z zeAjzdF{%sDKGngU^7!|0X`nVPE-`%7*UaoHuQZN~)c4%;#xI8?XvIIZubp=Xb}5D= zhFsUWXO*OT6vKcKz9uy^S;M$R%?EG~lQqYD2_hz*P4#@KEBGwa((i+JHYJD&0aWrU z-R~B`wk%PN3AviGKwn>(g3kG@0yvO z4s4vRZqqqdc4*~sy~k*(f*DRq=W=TRT~bv}o`6Ue&P&!rGnAIWs?kx^65F4_2xq;v z?zLT;Q<0r72h@y^1R|W*FXNebE|yboqfpY9dIj#o^iVxC43-oxR(b13IVkB( zmC;bl&bR@~iZYzeM>|m+?*6_IiZz%^(%hZ>Zdk0qx00IQ7L?LHn;qKF+)~DsAL!ba z@O$*23|)KpXVX!I>XQUtAos;7Cx$RfaPcS>@$B5+>T*V|h0krqZRO6aPGMQ&3 z^A}|3BhrHMWlg51&s9dwGT@OV3DZg7ppBi)yVKOFofX?HOqRJD)Wx%7?XyR;h1}kF zIY5`5+J%FG95;NyAC~^7KMM=H373GBIfbT}_v88fv2z`U&tuD*yJ`7No-Wt2XcjQk zB)3M9rjiG&W6MJ0C={-idg&7;p;crsQk5_r9nEJbFWB=j-B*;?C8aGa1+C_nji5!& zpN-VkSV`yBLBOsgvkVVo%DXM=ch_<09r(5HG~TA_F>gF-AHZhvw5hEHO#>gUXwtT% z>#*cc!6B|O(lA!QLYik+J})&fY+^Kx+Y5tTsqi{=VahtF@7?f&J=D_C#wkz_ z=%d`CxPn|iOQ-ZQ#j%o1$-^xzl`YD=_G1- zS3Ioh#b||_x_)GZ-q{v59Zgt>NGs0>E6?{^_+D93TINyxxzaA7s%UsQl-@G0LSI?w z30RDFVRcPwJc#CE=~U$^H@^bfIR>MaoXv}uSjT!QSCs+Ae?vDTvR58doL!1&38V@h@}aq2g;`i8Yu%W9N;q|lfop-)4H+8`|s_QS5enbu^u|PuxV5a6TlB9 zaV^!f+!b^ADbKmg&dY3+t(6Pq_BwuU;nC7-dn4{^nx5Aplo^klEJU4mOzn0y`F^)d zF;UZUVV5AmO6Htnd+A!5yQ@AQAAZP`vOH;OG{K}ksY36pMXjx!#-Jpl8NY4v=GVa5 zSo1MI?N#?iCK!-r1JrCvEn7u1!akcwN7`g-z!-0TG8meteF<4Jq{(F3P;CaB7s*MO08WmY*^7; zMQiVL%}|)SRLCQJJ4L_hc@TXkWPBQ-)^kCr`Ca|Ui(Cw16z&>Iwh;k zq#3k5W`w^?`YVX)v}3)!TtL-$fOq$3mycoWan*=+jRBLeDD z|7m_F>VB9gM}+4<$5QhiTi#cJ%k?|&?n3bVnAi0g%}vY}H97v6ze&@eiVBZ+@+xXw zx*5ei-or9dJzin~D*kmZzHg+#V5AM6uB4^%!lIfz3)&3)QkXlz0>sDhn1a6Dh4!I) z+;SB8E(Pc`{G8k7l{?6;caYM8tDXGzLM<606MI268^g3D<>gHx(v@sQ8 zQ?%$WXnV}d7viWaXSrvO#d^4WyZ}40vGAZm_NLf@VqD~ho@02N%i|X$-t9k{R2jZvAW!TgqFX9a)Wh%0sl+22ozZgNkSx(M6QrAW3(o&aK-KRv2p(v0+IW7Zzu z9fG?{uaJ7|YI<6Y_o)$5OXauxMwC{`q1Q=2TS>pGzMVvVtOkGR!aexll}F z6^@&uTFr`?#mw*scUq*-E+Ut-2=0J;$b3c-^!_zle2XTY#07Sc;Yo8LG+~f&qGQyxV;hamyevPX-c}&h{LCB^-)mECw;zX377t4PUjHO{fYhBUj(1Rj zn-iBMmh8y3OLL$UrUH|hN|KV$q~x}6pbXX&T-Isl)w@}SYi@d`j$4w~7QcS${ws94 zj+M|V-ozpOGwwvnE#D=!OLRf5+bF0$GUn^GJW8_1s4dQ8ntsSc1xsQ!(fyC%(y26> z97d&Zr^JlLRdhgof_0E9jnHP-ue-O#mN$L{gm7eCq{XKuyhrTEZ=R0dNN8yQKmyhu z$wUY^6J>TPaU?Cj5Ju+i)xxp2J$OpVA$Qv_`F37njrVX9Db_+Clz`~RH>I8HH;Ue^ zcN0n)M58HUZO=Cv+*F+W(vRBaIKH*3^|LjOg^O_7(Clmz2u+k(j(tvW_;lC=f+XPF z&}~{8+NJ|1t6W-=3pBYYR27r1ONM4u7$kjIIl~VClAi}M`D&(xlTgwIsq>0UJ~}^h zzE1Bi@sC0*i@|FVb2V4hv!@ALt7r;*f6{CHMP<=UYHtutJ)}q{a{LvAQ{t3ZA|q@Q zFgv&Xj2>G^<{)ItB-)@5TQZ9hg=n!uSuR$2!%5eaEp|w&_p}M{@@D<#q5to zeu-lR&f}qv__~~U2_a7H&^fIIv}AB@*MLZ!AN+;P@27i%08?bo6zeKHN;mm0=tWi`q= z8Tb|jm!xPM$Fm){&tDgr!@+ZkNam<0*OKYqV`Nx^jrE=jgN*$<>#(HjA*B(n-oH#~ zE&PzQjelr!xZzrS*XekJ6(S+KlQwn7<%~WET&X|Hgp_hGmV{kI0`6>;5&nI8^)U z7`Xf~Y)`Wfd&7bjbStA;J$1$5sMeYG?8jdoSOYHwoNg;%O=j%{8QUtH)R7X?gC_+;pjiV?DvkXP*1{&;t`=(>#>)$z4B?}4lc~a)4NB#aEY#JOPpuoP?QR05q_z5S zQ(g&?dGWE1%dE+4U@@L?r5 zZ!%H+^Eo6V#>LsTS3#TzlncIo!FddlbE+B`9wZI**>qZ?MNUQgob+*7GB2WG=75?F z<2=EtGL__6BP+j;uk~$C??O!h71)-4SE&cQmbOxFp4qaCU$hsb$8#MRf_jrsgm8 I)3{_X|vOpW}69n+Y{# zRl*%?U+}9@&f%w$oE7yQ;^{>1^Ieq(ziZplK6~e2fGX^0e@=1@Ff}+i7d~LWK`DB^$vzeCR(X)A%`{z`YcbX|GWee58F41kI(2oB3+$~ zK=OdGuQ7v*dJN*3)k_{W>$fYpBNd#qLf8Wh0>+%!O7(#*fX`4)Tq<2Qh1fm)+uALK|Bz0DS(*Ki<%-U zLVM7(P5rTscK0pq!<+V%8_#{rVrS3Ki#W1Fm=XwWRaZ~D9hc)Qa|2r(o$jV%pgVQR zauG0*l;d$vy86@n`16HNk}QVrxjQjG{YTg9@0bxc|z zU!A_ByM_iZHI1VU^(GNaqBcotF02S?oQT`6qTfD?Z{DfrL`b zdlV~!nA;e{A55~hryA$2Y<&*j!Tb5z$FyBvsu<%cZ+?>~BLQ!n=WYxiaW~D!g7gA3 z)@DK5IG}XfUDGY*e*Xw;%F(HGpkhGE*fX4D^$+;CP2p=zGU>zNXEBW*hy2W!GgdXi z)ena-Kjpshep7HX<1$BtuIo~v*8p#QV(*4Pl z^VNS_v0as3>+nnn!&91jG^)_-XG}@u;=j6vvC|In2)7UKqG^FIeI5K1%mhRzk~YaI z8|fdWb_<A3*k`C_6s~V}^%(pM<^;$sal3=9Cm;K; zUh9!JoBp_lWC>5|?lG_g#YmPdzNgM>WP`mkupJ6@(wjF428erxK|H@n5fHzpaeDB| zgS{zadk745x=m#qZ}j-5J+EfX$F;Vq*JH~c;IAD9c?LXrb%}l|&|7eW5a zoWlw8NuNvPQ0URHR2n&3h_?Ag)=Az{ETo?-l>FVHucdcRC}B!^>rg4Sd$aFUQkAU{ zV|2J6-MgA#bysNLZhQ_JU)}!G`Hl-e<9W(D)Q;4t*}7@Aq3ls2P}F@2eaz)0tK+71 zqH%>e;7x?*BW-IEMeqmAyzLycni?Bfn*9>Dspl)s7OQqs()wWPy)p17j+cu~K0V;( zmS7K14U(|lbMd9tvy|I6Mu;s}A<6l=ujP|vDm2p(mQwF@oG|NYtVLOF8+>YV&M;a>=Dr5Es8HLF+*VfqGgBx&s#UREMOn8W$ZEYsm;V?B7alK zACyxw|MVoIiZ{<<7WwXM44UD^p6q$f*0_mvuqu!Fj$Ow|pK1EZ z%w&V*Y3|;5+qJ+CDz-ch8~Gl$vmS`M_}dZ; z3S?wIfb>X*$!RH-xyiqM!?XV}{cYw&-zG!eJeDI-^o6RB4zlXu9#p4e`O3}a*e;Aj zVCPYMasvS$`8<0_>dVGvxp;xm$L6j?1lT} zFbkAenIb%&J!EO_N*pohp5lIRi@hh3urAD$llH~eb-VceYQk&544id?xhLlviZR>P z?6K!5RBy0S!NLDAf)Wm5Tl7C%_MV6V7#_39K|4!#rtjnT)D~)_?CsTA@s{`UK+frN z&-tj8Ud91q&1Nq%P@WSg8E}%mnj%>rPY=yuGjf$jFzDxswE@w!ocChn_*?25jXrkJ zWK{5_zLxv$78au%Lf&!#SfdHAxcWwZoowEk?rMbGc=kW)M7`z|K$ZHsQ{cwa?JZRu z#;?j^4#UADKHLDexVS_^?QDA-BIWlI^0oTIYOmbCQ2H#t z9%mC=e{qRFHqa1|;hchKiLyUf6QUr)y6sh(V{+VkTdqr>CkB`M!=wDw$~-bVyFq&ydVSn#_Pu$MG~vc4ra+M`Q;a1ji6eP zZw5PKa0=34I$eeJ6|l)z5W;SgNbJpNpQ*6h@U^O4=Bb-x*WyZCeM=#i8k4^{kEO1~^sIT|~yd$BdLm-L3jemh` ze4NToTsTQ`s;5#Rss0Oj^vh8d)8*WzURVKdP`8f00N^?b;+z>I9p_(`(UyMB0!k9g zPI3x_FSfS88M(UBC=q8PS~ZHwF}Z&EmRza(`d-k<+DjDT66~$+*JERHcxA5Y6Yls~ zy9QPDhZ0hvLC{G!jCY{pN$PI$!2hxAmha11lxYm|bJJ)BV3okF0VZV~KZiY+9QXcI zLRY_Em-AUncE@!@&@t)K7;`Npse8O=w%s%nEK(e-jx7?|58Dse}0>^j(~bw zeCWKiHhN{}=$QrQ)nbZX6{R7~2#VQd-0F~r@z;E5O0`T^kv~lERS9waE;9Q^9-&>i zd@O!1qXb*6iEZ2Otbz=|Ee2>XRq45yb6-K06qMCj4?d&o`lG)sL=xvhQGxt|e9VW7 zMUtx%P2q+n-maia&$%a;#SYdLo=a!%0g$PtZrU% zw+K(oOignHro$w78~4ukj+>t6*Pbz3p+R@mNg=k_6kNj@Zzs9Oig3(-Ri;*vucYMF zsE#JB3@#>hKndcgDx;iLkSB>3kC_Y~zt7G&e22Vs2s+fH+KUl|BD2A83<%G2V$|p1 zy^wYIg&pf2{uRh90bX)Cvo}kQ2byfgn0=MnSZ!g_Ou-zPsW0wFue$N^=36}^JiuEb z32iZ~ank3AFU4~a-r>xKUH$#-?r@4w37Ll zm*u(}a?4eNwr`fWqG|D#`nb@t8U)gTC5q>5NxqNaXOy{`DQ#V*|`As+i29@JW zt6a&oyn1#xuscRUUNxv}f}nXB{z$sqinNwr{>EI(!ejg<=j8SvQUpSqMBoPUqFtfV zfjBBvuX;ao7#ZeX=m_iyi<@t?@8PnRK>TP+iH6HaYYcnT^cB>Z+8-Z?<*Ki0=kvHV z0`)U8s4Kl?gaqtTL^lXu=u+PVwXk5?2Q$s03@%^z#I%Y&$Y^MtAD}y>PDet?k2lt5 z4S&_m!fI{ED@7ZokttzaMtm(|BNpTFzm}tM2Yq`jBoJDDq5r83tx? zYNcyWdE!4g^kv$@U_XLgb`v0IQP0}s(kyqipccNOOmVuVhm2-+PC%T4fm*6u-(Zlb zscsOO7J$&+8!2?!H}Ns{ziu?YEoH7ef*~h0rjXhrHDR2Y^)R?o6i!q?ivfFWAb%B| z3(rJ*G-Ags^(il)%wmYD{QaGJ?f`n-!cu3w9B#m!$!Vh)0gi}X;rosFwS{N<(SqYD zsU)IyLZz6O^d?cHaUlNTc{A<@4twRmo_>`{I#rC3;n+URJ;KTH6@n7Vi7eRQ3aO(X z#f6RYb{s^zhdk@s1Hdd3e5r=&_&gx|DImM5o!fNNT)!3Wr_m;>xJRkFhDIC!3RRo& z!G(RK4(3b7p{b+HrjhW(eMrguRudGP%5khx%K&Ew|3n8eWBYN2@|_N9d}B)&{sKMv zcK8k62!7~!zWY~h9$;sB;I(d79Cn6sNHGe_LmjpfOX4y?zs%uXv|Wqd42Th41l|Ma z5oamEqv(R!I4+eafYOvo`YAj1ru%KxH17I)>fJN?fuox^tV8~# zpK&VGmFbJ&S`s&B8TeC&nWI2YFk-Vb>J6j-6=Tjl-mpPXU-h`Dex?vS)(3g&ci)WN z#`n_q_@WKPmdlth$f4FW&#D26$bP$&hdUC!oZ+4I`_R9}>?X-T0eK;|jPF_@ zM$u_}8Aj!p>gl8pv~}rI+^2owUYx^c0JmDb#Cqek8A!eO&)l;(l~{ye)E-4#@TM2&c`$y4#l1U!&i zeM%-Zm#EaOLm05v`0$CwvRD}dbvQT)-)|=#Kxo+9|fUo{NPB$SkpjQ5`ktlDdtUqsR3a^zK3bU2-ydGPpMm`-@_r zH%eq+-?LR77jA3}d+?i?6dtN=;uKonw(CS^2W8V|v4OF3Dw3U^wVeZnukiI6ZzBkL zz*kMqkLFq;mHQlGt;KH`1p~RJGrnZ?W8HDyvnzbR6o%izE!EjDKfDm0Y40AsuP4aa z_@)J)9_FCJu__y^t+1S?Qh(EX!{)RO6Mw+UFt#O^@AKs{>9UU`0&cYcLacQDL!H%C zX9aKdkHnWdC>~1TblefmJ>1VhNVa1NZerC_i1zev}6Lj)=5))R~7fHYuA||3n0-UKA9D1x`lIPbm5@lZ^d<^b6p)B$R#aw z%JJ<~hE0?vvF7fE0pj>(50iFN5%{R_+WdVUPr`E^8}8<*u36U6c3+_I#!S9>ZZU&z zP?W&xaRv@U*=xtFaq!JC1$@4t#weC9&V4$JT3H=>_y^z>j4MN#nqlY#lN>L4=`v1! zw54riQ@XiRQ(fVL>#$0k>YD&8*$ubv&C!Xca^W@u6nnHy(N%%;JNgs8O^@x$Wp-P# zGC=V2+61L(E5@5_Y8#sdac>=J?t5!V8G(cPvzbqAYGddVJ5G%JJf4PR--FVY%bE&k8kR=5yW1 zSpX^5;kY!mt1tSwE{HxcQKdc0IZoZ;5Lt_x=(8G-sqWGv$`Pw!jCT zHU!Mnw*mJs7VS#K+L26kqs(<4!S?hFihZeTPs51~-s!)|^&t_=`j=#0Sw)V%sWkKp zUDX1hQ)i;9%@Ryop!YD+JAC)yTy%^MITvv*0csrIF-cz}+~sEtg3-8bgMPYG8DcH+#{;RN2SLD%sP%g*T=Xp&?Eub)513Dcmi!W!b^m7LFE0uyF%=`X zLUh&;X>=88w_lOQ`1a@;Yh-gjsg7tM8Hd%Qq4U_#x))cyVI3k^V$pe}g(j)!p-*tX z-=Nv&JavQL><${@g2S6{^GN6R`FoYy-;s92HL}+$({!MxS)hz%TKC<{`sNO;nqMs1o2}@lxq6yJ8Sp9rqt|k_c4*Y`wNgGhA~(B45_%YC{(mv| zmQ8U)QMYh_KoSTL9D)UR2<{f#0|Xn~-Q7b1!6CQ~?yiFmA-K!H5ZnR-41?=nZ|-CF zx&Pq((sk-|_g-tSQ`KE{YM-^y6nZt+_C+ntoWIAJQHvP-zU)!U>*rhzbjq4}ZSXpK zVVhD=MVi-z5Ta!IOydL*u>lz7b@^z?{_}f7IG5MdDyAwSDdOJz<^K@+27&tPRjo*| z>+5&&6%jRp1eEtktb5}T`!Kf~)pbftFt# z#8d`FtAnsbp3jqT>gjnt9?wdtq)tbCY9#3-((hY0{tQ_D^mt+qrv~-A|K!-tm3qT7 zfgA1y&ZW2OzOkM8!J%q%Nog~9>3e33^6k2}sqI-{s{Iu`NCP6WyWa!3xPqKd$vm|+ z`l&J@zUQ5G-a$MpU0=XUCW57CoVscklaW7L7aAWGsHixjl~28y>i9iJM-4t~bnfL* zl$+&z*QWpG9Of+)PZu1|XE+X=J6+wpt*qC>2YU8e6`x13b%Z{Uc|mzpmxzaK8 zdRL|?>z*Gw&h1WA{fe*jVI0CDn;I+DO)=*5_k(nlNKLGDCe?RDg zA!1ZPmA`IJ@RK^YMM(JW%YHVEye|w!XPEMuBi&rN3FkVXC!J&uOIP70ndg~%77F=k zYnmCuEP2SFFL$UNNb8s3%b@6kI-I1Au`XMId&H|#ye?05OW|MfjxSVR^OLMxM^5Yx zVOBw_n$;+?bUIq_nFRS(1cOyHpV5bqOjf({N{Xv&<%W~bB6Sllr_&UrP5rlB)f;xP zoC|{)Htn3&Fs1Q5gD0*Z-|>OZ0vxFD(tKMbz9l>Y#D;3&MytI$cv1=Lbd)~9;rhKW zSU;6=C}rGb5w-gATR6w%aF2U%@R-r!Yazj*+7oj|e-Y^A-~EhI_QhW?^!Mc><>?<1 z9=KW(;Qk&pq6Vg>f)^V`^>DvwY*U%{mNSccK>)>W(~U%o-m={%WTtHH55thdwhSwi zy`Fx@@COr3Qpw`(pypX}@W}~1MzAxN_lf5601%BT6H{ozT?E84OiJa6(L3&2!C1ou z93Wa7hn|z~T<~#Tda6V7MrN-dvxJ622hnZnTJh8TPs{wj7yK4hM2=Y&SNmy$pMS&T ze&=e)O&Nmy6y=Iwp>n+gCrXvvOVkcCW{v@HXTe<##F1JeM;P?8u=us!$@zOHCoAW1V}Nr_n@Js&q0x#7 z93kSfe;46QF!RZB=VKuGaBE8vMhg??EbqRn)l%sv5$ufi3X@{1r!leGzhC_gCAUd! z5;4fnz5tA7_0~SDU&y(OlfNaM4^^0*|G!{mqV z6-L!y&^=(1yuv^V`YVo@_Jdy}|9RJlE)@RdLC*g@i8=8!wKI!e-p1GRFRat4DbvY% zB%h7?4=e-N_PZETfsWu6t%ip3TpPq=1=2wat42m8HG#Sil+1~|rZ^v5v zQj=)N@r&4yW8~tWT>jm>I@cYYMm2yOW4Wf33QBX`A*71X?iMj)!z)RYq9;T27`KDz zr|Ij%U}7v5!F4uIYjXPFtv^y2dl{Hb*DwU41UWmRafycV%v#%~4oXh6j<^%{=mHYoTshximuf)FhISD}I5L6yiQT1%y z<4dNkbE3~)>HFmW5VuzlVo4Q5ttsCM9`jEho#v(1e6xf}>G7HIf&WX$3Uk7XAW>ir zeA=ChYO_Z7XL%*wSgm+4+kajJ{1~X*E!l{@>n&du^apj3TKKRMa4GnNQ-l0 z_7w4xqR-l#hUO*rzT2{PF%Q~96GOR7Rk*KpZcj#9Vcp;}K8^SFxIbEUD5;rQg|3F! zgqWKq+n!ir`fm!|33rvKY>q8WS4WZJK;x0=iaIe-8cojoj`|DE`-uv#Q0E~L4dkt* zlEn2GP_^S2#`kL@%l5AM_@5aS**xG;7=4&Jw)W*#sm%WZ$><9)*mI+FY0 z(ZQw2d+{hIM)&1S9?6w$miknrN(pgtDELG(`U*_l+b4`-t<~iR`VrzU_QGWH-FJ^K zC-dVOHE8WcPp5i6m2cwq-Z7|npq|X$goaULF2<$?GL9S; zv^utU#3EDn=HqOONm@EnYiaW$b)&jePbuh61XXph4g#|RBYn?HPTrPqV%MvMr}OCT za^iyZiU$n~8RYT82%mp{>Bl<(!*V-D^B*lMRhEbRRt+-lI?S?QZE3}el|Ag0!GOXg z@859MnVR+mB=Q(2!8&4iCtyL z=~s@?vqLai*-lR^rmBQel=misPE;C#`rL?5;Vj?iF($qRonaT5=KMWD zfe3x=tPDKT-jDrCRiNWlon)dENR(~-8N@DVWublaC#`9Pwt1Zg(v;SNg?#UqlDby? z`*tsNZBe%H`6cZOL+N`Gx7@$^kGEbPA94pZ{%t%6Gv|By4Gm<}YnYNhFHtav1Z$_+ zs0-G17e4Z|)!0tua@F(-v}Ne@v&(tlX`DzZN=_aI06 zle)EFt-(y7gMiRgQS3J!eFJ%fy{4sOw!t)J&AxLIlc96~)zJ^b6;M|28_;Fr0i~gU z*4oUNu)9)Qum)lKvel#6!*3lfGh6NEP)@(6f`tz{hK{?JQz_W*AG4Bty9Sd*AHe%_ z+vWW&;z(>7W6NL#4y5WGNl>(C_BnaCi!~ktm~3FYnAyt!F4%{;lOx^BgBuhK*##(t|)lCR-cxoK7Q>HClcPv9f1cnR#FXOLAJ_jtgL%1V!x_A{P3tE|JRtd%o(36tmU|sm zK~X-tE?dwGg{D{hJU5>J6eMC@n&y?Oy&UHI3)(nAG?~ZDr zz{dOhfdsq!62wGMUY{=kU{*jWr@ULPZ}L815B3!RS2neNY`W{_yrA&VjlAH-KHi<# ze!#Hz!+zpWz%L)(daG!7TjXi)pW_kcx~Zw2g9qtVFnB<PSZw%$eNK^e$8v6a$&Hbs3?^2kPaU8K0~s3O|S ze5{)4%{F11bW7jSJ({sgar0dAZk-*-g#1d`t=yl9G5n}vRBEB6#?pfQr&V`D24NnWwG);#EfSdSK5wMhCh9w$^n}iBX;~->dXbAS! z&Abb?wynWF@%G6zvOq?pY4uR1nB|3r0o)vi7ly`A=QKo9s|XR&0lea2?v4?F&`5mn z%}|Z{jF{L%Y1(HAg)9LVKY%8y-8F;gZj9EQi5GGz`nj9(jJ8{c+nYu)c6HKl)PMxPcl40%>6pFW7z^9Q+I)z0{oMLEiFM+7$$@<9~tT_AOh9O-H1 z%F#W8V8=s(8A3K&n)#wny{hq+8u1DTH6=(gimb0h9bUjLF`oG%hZBPcf0_5sm|_B6 zz=ZR9xp=X^<>emjdGEIByKm+JoD_xH%Smj*<-$;mydhL}_UB2D+O^74N15Hf74>c$ zycw9+>$E^rKEkJUbOe@3f#6RcR+zthksX~QMjw^2Nv+`<6JL`ze}GYDNJ{?Q&Y3@j4OJ2PI`e`O4 z>q8}MUGZ&~2ate2;b*P0@qu0bC0X&fQ@{>4b!CALPm69D5n(*`tD4;mPPuP^O8u+c zOF&=&u!i7oJ4AU0U&;6Mgejj#+|9{bS%Oc}ZBBt`Co|c+r;nA}lFAPrHnz#8Ua-|> zH%(}_2=%GanR=?41+&&-C{Pg;Ok*^v!)NR%32Sb@`a3?+VufNwLZ6tvUAv8-51?BS zKX0ZeB~dH7TE*s-7)CcGf(>LgCb>J`$daM#mOyEzY9;6Ag{3E}hJez55<%os8krh? z4oH4UIr#&@F7d0U4>+6QR;3+?;0=vCa5DCdU<*x8x3$YW#`ooj zII%9YTtOaVj01ZX;5^lht;D@=JQya5kZkxPxY{(eeI^4$T2>KFV{wr=(4tbmMQGlY zz$a8%E0#ka^AmUF8Cn+kw4GoY-19ykT}Ph9l{{SNA9TVbZi#@P?+NJVM6gBwyS#Ln zK-yggufYJVg!_VXhjP`L>B+DouO;&K8eMC#iKE=k<2qnUjilvOddkud2>AS^e%q3Yk3G^{;Z^J5QNdoP` zHFs|IaRv=!yy`tTXx_-2zf2$uE$xDp{}B9u^Cy)0`cH-WYW=FXWX4-=#YJI@*L-w} z(p@cXTzaVBAALL>gsS5$%Nf3Ig=l+Lo4GfXMLp%w*+D1n$4Z0+PRInHl40P15J&CX zs~*oM;d`Qe?Da~77p~9oDCkqoJ@G{=kKJHM!i`pj@|#|MvEfPKIizbIo&|*#+dV%n zO6$Y70wUZFA*lpjsw)|l*3Cxd8Ewtwj(yubSZVeQXUASQNsD9R@8y)bw+dWBV|eUB z4d$NGo%||-Wr>(8Q#_5Q=a=3u)lr_hf>FRAXtUvK!H-p^E*Y4Rs*r)*F!zP@C1Mdq59&?WFS6mf&1Z3Q|P;Y+1z2z<~{0( zQG_R9bjrE-^5|Y?o0rL!tLs0<<2;_3P>L=B1un8B`xWHLNe zUxU=K!bg5tYbDUImjVWQ-@^P_qcjjoN8}~ck9zX0O{7^r1H=)YGPDb^1lnVNafL6_ zfITuW_5s4#rGKbQ>`^63=7!$jCfJgEgYL>aperUUljAXaWbfGlRE4Q{Z}&ClW=NX| z`2fGj%W73LV;h0WcQa0-ow+RNh)Hy+2IZezMMG`OJ|lnrmNJcx{3tj}paVZ@YQ5PN z!n9FPuCCnsW;5=|wdPkYu?tKxqV*rCv{wk9=r;sihe&k(=txtaAJNQmq^ptmo zgCu4$_ zvO&9`A`b)@l*(jmy&lL1m!G4y$t<8am9{d8=gBZ-e8}GWgRv}_M9UU7<`*h6a}{{w zE5-TkgbaV}LeEw|v5@l6@0kd?8Lf3a*!eWSlKvKT?;t8fExWH(P)0+B!wyW~)?Z9v zKsNnXh%EBVO9AoEk&n5=^=`M(_>*R6_XQr1cQ>Uzh0bjV@b&yuW~V_?P+R2vD7p$B zD0`&8E|>7;isZce0!P?VJgx}!&Q5HZ{_s7uSKDCcEJ>U&`gDN}r#QlCq?2hJ(^T(= z$mf^?Np1zhViiPU1}9$L z%8kyIvqcCnEZtD#NEb67S!?^3evU4&)na9_JKEH@)Fwbqu}#8WP^pTobyZ&Z1&A`*wO;}TpkPWj@+YuC6|JER7d<|2eUApPWF`$_ifgbtHf2Fus9_cA!Nn6<>D836RZ&h%r2!0ihNtja=+s z=Tp-@80MxZ_)__qq40AGUe5%XEa^|g&)kBc^2@Lpu(7Mw}Eby5fLe?N|*|0DlB)!PF@7WlIbu#+YW&fq} zPRohf1iNIDDIqdW>>Ux308_)I`7P}1HJD3=jnppRzM=MgBZJgU-Hhd;nGqo(bY@c@ zmM`;?ccS|9fLVm4s4>gp0sTCU2rM4HnKw9*p{etAMQeJc~Nj%J~e zdX(A%elbg~V|*4mk^-4el$puGE>RAu>qMsG6f3G)ah3l{6(33?-|n)^qThwvZP=Z2 zu**hUaw4Xom;kjV`;7zRWBlP?tMXU#y0qG<eYly{Vhd>$`oMdD+%>PEJ|xFELoiw%;3KtOF^? zwqI}_9{1Mkr$kBrE$dl@V$o29=)hEDJ^ZcYL%gWz=`3@|wli|NZ-+YGX3xoHYVZ0e znrp#!&tsdDY0tyqKQ9r1tP|E&oDm&yI8=$8{gO1t;i379&jYxz(S8{}mVlBy7t@#C zaY)Po(|%6X9G`OcV3>VAoN&6J#yI$aj>gMn#li#TJ5W7{?OuOt0NkM4))%V~hM}BA zi#|vC)%q!WE|Fnvl;(GvE}kN`ffgFnIi0e|A{&PNjinbTs4ri?`ylo8KL?wh?@Wre z9z$_XgHa^7HhsgRs6O3}z!+7jZh3zvzI^>*;8OX&9{taEf?WEJmU+K!{CA9?&X3Rb zUvG=2B@|8YH$BPbdj1ze-r5I2wEqM53lbI(`1M6T*~l^W|A3$RqldoVT*A0p$rt~; z`u4x(#S9B-&>kXwbD`e`g&lq`m#O_n__PCc^8Zq|#c?LzOa!A*jBn|9$J0w}0cmwGty}pZ`zD|2KhC``^P) z7F*}_82?9OTb#Ne_FuMWMuG1Cv%2WV0V~aD$04ErIdj_a>Zas>Q1Vfq+9avlpOS1} zt=>-k3mpXNn{>tb7s^y%6&au6cAxQI$^S3dr|qrV$Nvbi*^GXy{XfC}e>M08<5wNH z21*k4a)#)t%Fzy-Z$jh$nz`>RhCaa{`M1&A87cXANw@2wq4Xg#56M42QE=_-HmKqx zx;_DXHt0Lk#D(rkx984ki?_!-6C>2SiXmX4e`T3(M`sZ`D+zHbejR>vsX;-Oh2zE)t3CD4J?_sF;L+Odi~2$8m{4|*s!J^SVj@Z8 zt5=tS0v-IT?e6fs*?8B^de{4>+ftp4h0yZju07$|^Ao*{BANh|mZ-~T>KcHtNiZxw zHk9TmY=5AP6PFjB@v6;r?c2nUzo+|t`cD>%xRV64JR-+kbwm*ZTo9=npKP`l$YKT;JB`{+e6&0_k4k&i!^==#IqMJ+#Pc zcWPBR<*5-d+&Sp-h>-K$%JTw?7*<{m7d71>Lo8cwo5o@19o^41^9r+q#u~}yz3QcX zxqaik_`R&0F@fz2j{A)xH1vHZ&f4h4-}CGAs*%u$0MbJ&+_q|+`y0KrM}?~f*2Fq4 zWMOZY_8D#B#YuGMe&-JbKCcvo(<)Q?v#P8z5LGY3lhi|uezf?Sj0(hRE2~Ot4r}ZZuLV2|^15!9FNq>E zOp{cz_44k1T|0q1ZQeU7vm6I$JvgqN18%koIeE$2zp$$3QyxqinJL(z-ldOTy}CH) zxiRWYvdTy(w+?As(TMFCEcs`>`AESL>Vk|Ki$zA?}II zbr%_F9Z~%C59xd1rtf;%8IOtnxl+XHk&p4q4S?DWJEa2MyQ8ofGMA0oyxmF~TKENS zql{}^Js)u!6OW(5M&HRjD!jdWT3lGW`mC|%Eua}WXh`C{(ak@ZBD>(tBWaQj;S)|# zv#CFfxf5fvP6jt~CL4f~CLkGlYdnF@n$V?MLmk~913^mn z;)s{+L7u7$CHoJ{$KhXkeE!)s)=7>M#>UJLn#tHL@7SW`l$QDvppJK^PPJ8|Lxy{Z zecl33Cvv}a!40w8njf3JZ&=oBBO>ll-S3~pZo5(J`Z&?F5i(^?uy!$`+mRg2dgF`a zyGzH)xxZNm%-{^Z3p$ze#K8&Agr`52-DYfu&aR#olns-_=pFiWyoa;WnNw1Qz0hAL zjqORg?K1CD@V+x6-?`UY5V;)ms=J)@wY{Q_^zQ8x8uSY(aD8yhpkkAo50bBwYyy`*V%`w1c_xX8;^@E8@psH4d+YK_LYG|mP5*+tkJ2`zzeU>pgRJZ(VlLq0H?nIToG~WtUH-=(l!ya! zoOxD@?~6oxd!k5YgYbLkt4LDX6N|Boz0OwkoF}NVd`;(%eeKsXCZ%&E`S%@2>{qTG z;6@w}KYeE+)|ib&l_jH*YUT`2|J~|)^NZzHLB8yF{CN=3-ddI;>e!WSooo;>@i-GH zrJ=hNOdW(`&G6wv^i}{NWW?!NucP-7$(${>=8(*OZU>zlcUhRSZeANxl4Q|cV>fhG zYx<&IKDlu1w!gfkCjWZgISE_S(^p(&#hf|&ngo7utlM-}6d#lfOVJPBI*tI!mEm_R zi|T!`^%x>v-ZB_?P{Ejy)f{tv$0_7MDkrx zT@?~*rOM$iyE;SxxB2c1aU-jWb$bmvgf)@z#2?{kO0H)wvXqTOBH){*brByh+v2?7 z9oxf$EVK|94^Sh_iYc=$)3EV0kVRO{$p^G&g{q_x@?@)-l-5sIHYLL@n=>Y0(Vbrw zyS!`d?JK#F_uKh)&Rga)BrYGW-X^GV;L%|Fufm(Ty3-$|7$JIl3cixCIrcR_i>^sX zyHl(+GF+rhP~+xgdV{TNMaQJ{g0o)toIe(!?_#W^B|N?@2br@&>@e6K+^X;z+D z6+%m)(!x2_Y3b~-a#v-pp{^T*{dC?dP->R*>oKhQp^Lq9&+F;DJKgwVN)GZbc?0A* z?%5@pV&e;5D{-o(?Ol%Y^pB?cHNjet#@XKdIpVi17F*#rZuhU{p*h2Kk(%6Ms#2cj=nqaH$Z87q^)5ScONGTvA z1Z=#o{^udaH^X|}TF#VhvLvO00w?}Bz^~_s#6XW&&i~{ys{sN5GcXajW1l2f(^6~h zIZD1WjO@GAtn*&oSbRUIWi-1yhxS7wsVorxQXj!@fFTQ}PeTcLwh3}+G~#?ZWM@mM zVP=|zmq(A8=wq)~LsFkLn*pF3y>b7fo-V~N8)JXZl&iWmAo zpXB4yV5q|=3G zmmKm&%0NwWTZWJ7t5LGZX+e#m_Wb?XFDn@&Wd>aY3Qg)DkZxLbw^=y?**x;j1S%JN}wp?izu6T!C6>@`Y$`y>oSD7Vf)!r+7z6wi&eEC(hlU=Wt?OVEy%iimBAnO5Xx#+8=;VV?Nb7|nPKsR1>c^KRPEKuxB(KCl)GqR|qiqgjiOHNm$6+5---UT=ppvJqf>QZs|W)iV|9aWO9-q8PMJn(zbpuRS( zaQR*2>!TG~4z6P0;E+u(r9E_E^2o76(N@_ozgn(IL{jh3-x)==-royft=NT(f!v<2 zBY824eO@eP{klY8`dlLVIV}Cnput*sq@FJhMBfAeiaiJs>0;iMZ|z8pUghZAsr?S> znkgj^cPn3uD9(`)sr8+b@UrPuPwtE+fSCBho}mKkl6bz+i_4mTwy`*{rGYA-74ev} zybhgXqq?l{-4StEA5qb5@Xt225JK$Do>(tgIO032*)+}mu+ehSy$BE@fO*$A&nlIq zWeCi*c8f$4H-~1@2MZ|0aMX^z&Hk=cyEx!X=&RY7-FBw#4zI*gAphr_#47ArrfwZ# zUP+YCr{2jcU&U37*p1LL67M3DEJ|+4s;IURTP4N0oW4_H-TeaAS3%N3j6T#tZ@Ar6 zy5+ky9yrc=w(t9zcYgT1J6CX=N+WNpDCdyNoUf_>BWSr{;O-O>|HBiDFdN;LeCLBX zM$}&rC5agQawA&p7s~~Do9Mu+(k@vEbPQr?^d7a}{7{D9>POTPLM@^ip!tBfk6Yh)7e@PIor@?4gi( z?z=`KwRcJy@wm5+z?Cxf?%pDaLx9gyHsX6lR8<9ZCz#VVYaX`O8B-Lk+9HxYX2(-m zyf|HTvFpTBUo0Zh%xvSH#EZ zcOm=n+OqoYFVMr1F3%Ah9m$wu4Pg0+^_V_k0}*^1iyhploe}Frm(+al6F$DkmEnth zc;&fJc_hiU;G}hVF?tc>KUd)&RGeDM$O~`1S38T$E}x3>1XRCkn`LnD`wb_;XVTA`@9^88cRe!#udCvGCUf~M-8sJuKPn=Sc zdsW@M{FKGs={YSfA=DXivPZw>#8{c+K165rz^DKii?*zqln$M1m&6CH*_tt}6*~glw-$Zs0aj~4-lfZvI{b@*JDn?l0lAK5Cs@Xzc?Gy9b$|X|$MzyVN$sQtU zc#K#yHMk1C(yQv3tOYcWIN0{iy*aML60Tnnf_UW~iaa;gH8+~(+gf8mGau|qXj49) z*`lg*pCk=DFU|IOVmaG6F82`HI&0y+kL~hJi+(@}R^cF6a+z4JgBP1?`5FhiaOxP1 zT{3Mit!2OIt*AY`#(vzrFtlG4f9Nz%9`Z-hiOM_=422Niw6OwhikUCQKAtX$Ra{Cn zaOb4dJx4Ah0~0`IrcMu{{cjw{v0cpy;%bd}XOdXmMvi@*h##L4RQCe8Sj#Rf1UH~N z0lqFvY2MC8Ke}{`NdjSj$CDp1f^fEsi%(ePJ*F!xFfA9Dy}t`Xic8kOE17JsSbCQ^ z+AOh!-zly&B?`vsG= z`g4UjKa)imCu^zA3UHpsZ-zN`0W{$?-L_(1vrVp5s+m0;am053wgNaa*l(!OI?o#R z@5kx^E;Ms^bXH>dT=Y5t^0kKa7x}!i8?~z@c&4Fwp@4=;$=s^(=aJsj- zU4e7wKz>!6Y5F2IvNmovtJr}DUPowbXktJ2NN&wDUGGeP_6Ydms_q8eDfy zJ+;HLu>35eQ$(B{N?lu0W0QrX#wb4Fz^HwGD-&8gCFysw##OUz;Q2AX25G;HnAqvB zH3(9lge@)E2MO@zew3>{(c@?O_xT(jDb|p_8tD;IUIjqVTBAdMM+)W<^5yk=_m1J} zdgt+{aT``?d5`y3ji?A)-O*NMq_j_-*vHg#!2%4^Cv$ASgRI8HAofK{P*OVHe1^e} z$)zu)o@Bd+Y%N2nCCVZF!fPizT0KwPUyw? zFH^~Z@H*$?+3PRFX@c1n0xVp5kWpX0l)Iwbskh+NW4I)qWVu4saJ-I}s!Pu7-*U^0 z?|9p}XtyGFVzWO=q8m*sTUwV#D;W<5U2-$XtDV5cg4y_AG%HG+mOsH6x@?yXa*zJi zo*NL#J#8#1>&BN2hT5gxKRgB-bC~$Ji>l9Osld{Q{2}u4M(cu_l~kG9X`_++yE%p@ zt8qIV&R^**>J+v%i1Od{7I>$j)lwP>AW@KIe|mQ&bM<416_)89*4WEt6BVihcurDY zwWfBtf+qfk-fVuyy}nuV-FP@XJzS^-?wT4`D5c3?{6)^r2=qsc>m`qFfP?P9DjCPk zp_4}mg=3PlfEYsGSivZ+g_Z+2s&4i_mUKTQp{osgl%4}EBzHzT# zhjMaBGQcDwduog^Y4?NRIU`4Gh{!$5jsl!V++}aK3IX5hka6Bif@?k3$7A1YUf}T7 z{;tx0ssQbI_j20I-A0$sig&7*2KTM9Oyi+bhSSs_WsN#eogHO)%Yly)C^ zWit{Js19upK2KTl`lR`WnC|TDSg05+dhCr7Q2immHuP*Kv-k2FGdhX{5kFp^ZmO?1 zgc^PxIa(BM zkjOoQJpVKIveLqiz_lITIDe-!X)a^T5g9_vbskw&$2DzlX@NH?kQU3EouBY!c0wg> zf~#WEr#P2HscQ1uq5oN5(TG5m(IiAJYrMv2up>a#!LW8>uFbCR3w$c@j0b3c-{1$1 ze$RZNHxL+j_E?p6aaaTrZZ-(618VF{#8ljQ6*`Y8q~U2n3>G;>_@@(=t**td#Ew@w z(&s8G`177Rs9JeBd|hHQq84MhpmaB;vwyb3eB8yhwLcRn)Y`IB-c_7aF{Wk=@lKu> z8Qk{I!f79$%hDl`C0hTdplZ%MRU4(8g{2@(J5O&a(@b2$C3b)RjS2p(axHJb9cemm zFy0C5j)Yv$ccVNcG7Wl{dveJNBk!_JT^+n(03$fJw{Apm681rB~8 zP^uj)opgfg$|Mc!cChCS@gPnAnLv)28%U%(tu-4_T|d_P0~UCT4`$e*a%UA#&p1N= z<~~wn&Fm5PAH$3Nwf0E~IDgS6S`i><#ay+ zBq`eOy`8#ku*)_tQaCwi$$_zIV==@E4rerhOdPnyfpv5ADgL6(k)-}An-u81P8Wb>b%gAG>_jdjR295=TLfMAG*Qqh9vQE+RA4%iSq*ELmK4zX4(9DtsfBp9p3b*_GW*hBa zUj(}%S%TmNSA=I4#NKJ1~3pUKFyOoW&8Qb$YcSe)L(TLiM3r%9}{_+<)59N z_9h<@*wa?rzr%mfA%Y~|y)%eyu^dR2Hn+0NYJ3m>WL(tb5>_ zrMHPP0U>(OQu8nH>SE0ZJ#y&6%#5`=6vxl}Pk8egm2zU)Yv6qnv2ZcXj`Tvd0G07n za3WfrYu`5oqP++D@Z2|OOBPL0mUTF+H25iwEEB^CoV3j(N8&jLRLQ)I0tRY({^xGx zvOIl-l5M1Zc+xa~NnMrwsbmX`y-8cX6xMEP)|h=HRJzWnW=!-iRS2VQ1|iUA|X_vv(^?Vm>n zL0F`O=k5cau}wxRy8z~{Jym&SD8&phiV_ef0W7XC;~sc7;_uHcMj%-=!e~9*oSX{e zKX7;Y_S3nyC|=s*7gO zPBeNQl;*$wzdyhHV)Dk!T5$N?`S)ndt=}(42JNXni^~N|D$?f?eF&hi!$%`%Y|>$y zWvtv9WL;&;ahFNYM3uqxkc@y{G2V1B8FcBM{ zq{@PyOl(V&i$*j3}a6yW_BTz7$R2v%762ELV$cTiK0n<8hRM{ zhn1Sy@|ir^DNZgTVzw(3G(}0L)A41{E;5X4StA<#{bs)$Hq+&cZtFdcfCnpVPj<)G zB-3BXgQl8@aQa2jMD@?Li}KxP-^&?M<^SN1%aB&^v#G<^pe8YD_k1lMO=Zq2CL*65X^G6lvpYd^+ZKT9pMgep}ZyDDQ0w z4ke}i8gG6UP`&y>+J)U~X|Y!$PRZv;Vc&^R>zT^zrNg(%HBtTc28xp-<;1K~GJ!Rv zf#l@M{l-&_)1a?V`8o~vM)iZR4sFk2TaC#_VjeH-Aax7Lw`RGR`sg;wAIUnuNgd)S zW$JtGs-nWHw|2NRU6}9v%9pO8t#Z+iWDQX;br?4+b_yNFHsz_tVOFi=M7V(XD4#tk z1nQ_-GlFso7M;E5)f*z$!$ACejNu8gBYkjl1!eWXA6umYJnN!pzNM&IM}5Ym7?pdO z%UAgt&sWAqV@(LfX1q~J&EcN!LXm;CzWbf_L^mH6?N+BJ4GB%GMIsxE%jLH(MjvPf ziY*P&S$?hUU6E#(XG{D-(c_UVzd2G8VSK$kC`0-F=g;YMX}<)k%x_yA1Ru*;r<(rw zgsQzew$$A=LWG(vjy7Xl$=|CbuEp+Y_>ZRJVc)Ebvus4d5NOt|%rJBlw zNX03CW3K}X-nv^#r`XHwIL;G15>l@&$q`rc_0vh#XKf<0ii7r&t`ENyfr^!f6j8QV zKHgNrQf(ZPJ!EE?iE6tFDA<*8qor=z`&=l7{CLClUkP-R>8K)pNUz4dqmjFM_o~E& zK|!B_pdZ8Kwdi}wkyyuY3wCF7sfULaL>Rs^bQgs)wTP%i_6E$<7|GhBkT`=XOe$VrM zioW895;NtYO##g0oW9?*zp%1Z8YqaOg|>8HRBG%L-U;v!)*^7Z#_-N?DjC>t|HyO!Lia5;6)TN!$+u_q;~ zw-;1<;iv!WJr{a{ph%4?MRdl^@%Y(XSVYZ7JR5f^c#S8U)*l&1t?TZ}1nLFtry_1q zSNwDr%X6zw(O<%Iqj6Zt6fxVwP`F+>C9S_;^D^pCJOZWtdVI%pNo9!Ltr5}khiFDr z>r{i_&s)x?i#8jV@?X#dDaMx8m|1%qjR&6Kz>j>6#-O-FTU(6p>!hS4GuvhUJKLx> zUj=Pjg#A8Os%f0vk+HuP5H6KIl_^V`#k4lvH@hjma(Y7*c93IZE&*Aq^ACf!N{~`gk>S13FhP1Tx=Y+@Y5$oD+EEd0$NDRo5` zTNqlhyrh$wB$n(>%em(B?vql9{cV+Npb}FRePu^QNe#P04myu(-&disNdVPP#qg}H zf$0$rx6A#9hh_}74+UJLSx#OHzPWP-FG}uNNDZjF>S_de7G7&d`WaFiY*5*D%5+hD zXeeCfpE&5>-n<{HWhJp7P)5h|Wbx1#*?+0Lh?2H}27kvcC#0jF`YDQ>e^^_HmN3(Q z2G68};)3B5^Ks3F@PeiZJ?$D^z|~cax5V^f84j^rTA6_+R6~2Z*;szotTOHTyAaj` zOEr#CL%LwilzmpFoYRTW#ioCGZXobg?zGJMI$**4ja&CmP=rw1BEoqk1k52O@NCX5 z@57NvrVNCyC|}!Rj*x+_BZ-D(=fQksFPq*^abo6+Q7zacbV!LY2I*#yZM`t~yay_A z;$R%54XS68_5F77MlSV@hWR!DffOmt+dw{%J)W4?2_Zy(*c{X=ic%LV-i&^;VhIxz z2;OYqlUCDK@Bhuw9fN2{$N#m>YswAd#81#{XN z&8(;tE{ua7diO5#nEY#$F3KdedqRA})~?75X}KxO(5dUjfIQh~3>6v{+fYd!1|Bh* z*R6mExF%Vji@H_(F$2GJrmdO*haDS@6aRr&?KEbt2nTQwrxA5HIxZ zCeF#@-r&e|8J&P6rigVT=w%WHiZ3{i?5LGFnCdA8BtL6-O5>dL~E+5Fog_ySux) zI|L^JCo!H11I=Z^gRvjB}2&Vpj47yDR zXVfHbA@a)T4der$8@D$l{y^#wVJ7x=#Mw{`6h_?yQ?*fqUlvG<#N)k2cj--8N=#XA z#5>h*7(jteUN99#Iph92{@FjEt$*NKESzjCZ2to?$oUUi>z`(3E-o(4{|O)bhy3@S zsILD8jP_3x6C9(6v9p9{vAH6#G~DKc^{YW@iDgBx2_J5AN9iBaAR%(k_tcoBt)B zc+6@3E+cGJP#ljIIpXu|3V}aGi73v;4!zzr#<~vt5VRu2m~+LLy>?q2*6QobOm!Gq zT!}(_y4y)=Bh1zq3K`ZC$tILpL;0@g-KVw|##g9o>1oWB7Muy5>b19qi?onFL(5D% zU5zvy)(JE0p67Klmo-R=vt7;7zUafJZB|!Dw}yLM3d10Bb#Z9#(AY_c@meF#OM#zS zHZ{F>hj7a24>dH25+gJah3~Fz@+fbB2zqXNggp^3}U~z&_ zMKVbEkoP~|1bu?vS0h4-fVKZ>`YE2&fk3DP3M(G`wO!h24+cuuzaT-3`#ydlv@ny5 z(^sMl845>=<+~;QsE+AZ3kWu<5urR`;V-*kM6eD-gUFJ;xwcroD^Xj6Mkj|^eu{d8 z-V;|*g#kSX4Gm@jv{^uyT*4a3|HkhAU5_-ICWXr|LNoY4;hyKlx5@6VAy0{>1Oe`N$|F>Jg*w&2kU#~#Q!t~)7 zK92wThL7_IDAxMAH9>73BI4 zyy^df74%M8Tov%nJ@POgH)Tm2B}=btAWt-7(Wb` zHQ=_(=d7bqp{THLjq#@SbUH0VZ8!G0lN1?mO!1p*9XAWCDLk0o@@jP)IsdSL2Pa_SDxS}^2&Q^Hom#}oy8 z1`D9)8WripoZ}2OoriukXj0g!Pf2Y0K};oAq*Utpn%lA{pQ{uEecAPyAMn#VuAZ6V zT=vaZoJDL!+EoBr?<#;_Ek2SC&_Tj$4`9#6eyVw?@YZ(*V-ThJli4idn>n-Q( z{%yTSnOGZUw(M<>>^_1NcWM^!rUt!-fnR=)I6Q+_f~v6IXdD7BKhwX!b~8GtE5 zxzb`pmjSCJb7X$Q+Tz={{h|TCxz|~nW^T+cl1FhcofOeX*Bi;cj+{;jpNkvx`1Hg3 zV?ASI<1M~8KpfHc8Agu&x_`{NZHl#M2<^+vM~YiQm>6!g_J=FVg(bK^F;gPATM~O< zvJZmD0!t95f{Ed}6_kTKf>1u|LUH#t^bYna`X~45<-pt>k_sLr9+Wje8*?5ZP2rYB z*7PTRy&MSaxrK@i22WI_4O9E$GySYo6C)r}mvKaL3GuVC^@rb;YOLBU{@P7Z^ zH8woD?2RTr-ziFYB@k8@*dn3Kc*Lr3Ks<)YW_Y&xZOUGm=H9gR-JqMY08k{jlItDo z@cV%(^HO~(k`(PZX?pOhd6*rr>fdax9Uj`jm^zGa2y+~;CGVo|F za(^BHHm48kIE6W>=614udBrM&*b~h;y{c2B#ose&nXKgoXWo(k&L_`9s+}mR%#RHp zo&!x`FUbV(*AM4?D*)7fNPH6fE;{*hg|}w5NiPy8@MF-QRR)<5iUZ=&ldwa(Ckx?3 zVZfK<2~R7(Y~_?joklbcHoWLZPCc?;!Tp!3b*C}m4N3CkoX~fVk#3pvTEW>A5YIkz z?G$J1#u^1u5PzH<}2S>}QYW7Br>(C|Z3ei+6K?r>u9m;H6 z9uu)r8a0%7v+6PibWad0i`ZS6Id@ge2wSxqWCjkfq?Q2vNtGA*Rf`6s`}vQwH(><7 zJG6GOGjk0$TA`Xs%?sM-4I5B?fYx`!G-pXt`-U>9S`Sg%O&@6TNXe!QoaE3U0V}aH z(!z{Zu8J{C66TFhA>bq5Ir>14?YX}@K_5f*FbJ^W z5z-LAV1`j0p+5{r-GEAjcR3ay3RR&$>>DK>^Ommj7I?t(`N{C7-6Nvx)<4d|vETyI z@|HYfGT&teZ9_LMy|aq7Y?w&7`Y?Fr89AFD`w39*&3TrmMbh_Pqg1IUZ8eizg)FBs zHJnu=vAIU-3SAoaEVAVXp$$zitEa#k%-=W9@N451K4T!?rj*W>f8PYb^j$}0+1x0` zJYMqZZoy$KESK&KwYs!5yB9nz7O{Ar$9EqLgk0wAyx?PQN$e{`c)|jv1e=DWkDklf zai&0M1hpNzPg2=L4-|(|J4;`xVk$yT`$E7Q!6;j{&482Dhy=gAK2V#!m~4llWl+%< z&{)B%Q-4`8Z@S8rRKJ<_|H%w<6oN=K8n-hU)`PGT<xRXS?${V+SdD9H}T)|9Jfp3g+==)IL>E3~V0o8#|^e*r7 z%cntK3%|_RJLjUm>SKbJr5dD7$!w~$W>z=KGF6F0*Gd5`+~FY`n`7F_+vb5 z@dKM@jSnDPv1eeZ#-IT(WrQHnZT4-~ZP;zpVmxO@^j8RgP-GxvC2YL4)!%8;ym_f4r$GCQtPQYHq$W(vC&m zh2K1`+zD%|4$sYcxzZ`dMn{e%T#m7t+oSmzX21JE+)vf`WBMPx*EB;4 zSU?#-)A`pbPX|?b!rc$jswn~hkh9B*s6MPv@0@whLM_6QMZW|f(1b8nx!^kUhMXy_0SeY SV*{bHK239y|5BIo8CBI3K1$KWbhZ%=Tw@Lc%D1i75+Pn_BmUMnB@6gjoppsKzGP{HtkfIzQGEMDu|LqN6+<{}+fLUxXfP7O>U57!ZC&^ZS%P1FM*ej#@JT zrAh3v)w^c@dwI-@J#gU|nGQG5(%T-&WImF_!8vMi@SWOM&h5R$Omre0 z_^eSmc%%lk@JYjn(qU*sXYVY$*)W;c7;-m@mJrS-puPJ+K=354(s7(XeJr``)c3BdQ0^;D!_8P3n)j;MWL#>3=^i=C7kHT_KI_YA@=!KIoK?7!Z?&svnb7rfpu0*PDFR~^(V%xG<*F>}-}zA~z? zULdIJGa>;`ze+y;dZZb$N8*Wb4jI(x@^3Sz95k5I*1pnJFPr-9*hi;2j`ltMX~I?^ z+DSu~(FhHLKJ}PBSJ(!5-+TzofGlOnrxj%e=se83gf8IWOO|aPez|wOnj=*<9xPOS(k3_iF$_P^nnXrF?u(iPewlhmD;;Hti^64)niT zk#7VVy8~FI;^UTE{hfZ|^DaI>H{`#z!`% zZsd?JUq+-buTad!ehOnk((p6BC+5ToXhv?Ee8x8OA8F8w3C5dqJ&fR^1WCL!*AhA| z0$PL`W6lBNR78eA6YiLoIg|+F@!1y2*&#F#FxiQpkI<8+x=|n#6aGn*N zQp$Wmoe*E?stIzqB0l`UN8mF?nEgwPueV|M<>@eZ&}$gtdhem5K_f@LH(1?q4td}l(~}|#qYI_3putubn9EG_}UJKge8U&?nlG|{wC`2TH^5E z7Y)N)0@R^Zp9iB>eglKXJNK0sSbdyPR>8T83x}Ricwx-YTSETt1cg*B&sYy#pJRDX zKKVrD7W<roKn?n0JiCiA%p}T?(`)$3qqZT= z(EmE`b?IYr5jjk7LNbe)L;iU{06YWd@g9k8^20wa#sboR1mT0plW&gVx!hc7UPs=M z)rBx9EWh%#ptjp0m@fEc?HPo;03wc<(pjE{*S4q4TGylaIM0$4g&)xJ=LejLtA|@V zqmAQe{Yu?!dG;@9ld3pXbIjXY8<+|Z1K-H^LpQ^6f=%9_GS8?$BJ9}vG9>3>EH@Uq zq)(SBH?;Oj@<})><*IHhyY?ih^?`aJqHNk7F%%|msD-wYw)9q*eK@$?gzhANIJr)J z_aIAd1R_~ETV}0^PPYPI5!R${`Sx!W^A#)dGij+2d_Y8ok@pM}sqyuI+djaNx5 z7#pR@Q#NHQD)$1rZ>nWbpDRhD6NXfu!5vDTvG67zrKgu66u$89KUMx zE+nir%&?`tT*2y+QLR9~=%$|qt_vgja{DnveBYG}WjSYCxaoAMCE~u{P>$3ah#t8S6Enji0mHrdnP?RTr|y zn&b!+o{bQA!Gh@W2-Bc?e;`x|zu@-vBV#kO;xXP+^e}d?qxj)C5Y*CTP0fqo{_*Y3 z4SvzanCcgl^qZO{*|D&n?Z9C`4btPnb%<_OePmU(`v8f>uHIRBzX-~_Px7*Lhd%PE zZ-JxUpDhPsd^lw!S7)~h3bz!6yL%*nh9n@9Ya7hwO)Z}L*W^pD+=FLJKyLKe2YbHD zd&`SyOSGc2d*-?R(00K~f~_;qU(zi?^xsI*RPWpRz#YX6n&ZAy#%c%Xw@ z@RK|Uzgm?T_48y8N!Wno+4Ei=bf_~>;Duv=_lw}C4YhqpPz_$F7@{M?_ph%U-?GS| zMPRs$YEVPXj~u1;mHu4v6Tsk;B80wqH$ns7a6;TmcVO~fp&k`+gbTR*UOoXv%P?oQ z@Py~SqHq`75@ZhpJB@7KQ7+BIp7E>%buQll`0pRwh933{!jor9|I~LVKJ@$-y7&kE z*AP3h^UK+L{$9rIqf0$IOfVAJSg^;@R|hT;!7l<4-MnSJV=5Q8ql{yC!!WjuVnpo} zJ7D|DcGek+#;+jiYd60?{@)&|A9qgcVIPe*tnFb^RJ;c6pAv@YdiNYbJ~OC+>u_nk=wC_bTP%Q ztf4R^ZF;nXzU-1*LcZIu?Tf5fe0o8hAuwkHs19u2i~Z?!V8RoeYcY)NBt0gNHplJV zV$2SAHeh~uMqG*Zf5G|WCCoQWU4VAr6ZC<`i1j@x(aI_>70chbOds3bSiLeR&qsVS zh<0xY=}GLjojWeui)V@+S-tA$-r$Pl`^9oQLbuuT$Z93V+Zkh+_~)GDk!^rq5oa~j z(YX=|5Bcnifj`2l#l-O9RS(>Qam(f>PwFeLPxsAP!JwXBH+mE7!?)rKc@2WPFD6|EkBTah3 zDt2@0JM&D>V320_k@3 zyu7ywA0|KF>crSQ;pcdxWUviD49y=P7SdwKH{ z@68uuUB}greedgG{71+ft|6_%(rP&6BtQDu3k)#>6~6OrN{mu9aM$~B-hx-T48_lS z4|=DIC&{MbU$}$$;#o+|JP5KVMKHwk*^iO{JVR{!?UJ$J%J+!R1~}$axbFD8^eIh}*^o z5_lN-iv7yjCo8r(k~+GTz9azuxJG;-ny5^HU09K!Eo44iy&TEwx302Bv)5-EnZNxotds?Kk<)Shz<84Gfl-N+5$sW z>_9l^z9b#TMN5e_bu$(c#4OJZHbo3R*)au;aaI(Av+| zVSLv`vV)1{xfGvv+|3GLubzJxrR;$uz$;N$(1o1%MC2K2?1;EEPP{ohf_zDC;Sj-P zhidBY`{gBDFfeS}=hHgZi`9xBAeF1%*r33Icm$eoPUEM&*h{XX22PNEqiSbnmnd8~ zv)*U`cUv&&kA5*@(Je(aT*a*7rDgfbs&plNb!`GFT6$^*8W85g&8?Gj=c+bA%lPrg zgp{JJd>thv`CpXEYYgWs>Lo0bnbSrb@f0ldMt2>rtk=yP-@#L>pSQQQ7N1Elq9TCD zZY00&);ylF`AITw*SOX9&+rpf5suU(ome6pQmkL0r!Zn0-jmH^o#Q4mnK9ogGWhkU z-unM+5x?jLZ$b5p2SZF@JkOb12%b7NgIcO}+lz}!u@}$ThCa40rwly6TUtaR8<2Xx zj^-ruYCGYh5>U69{ZR-9Q2>4mF3zvd}ECM*~f7%+1EXR`rF?3e;BK6O-JGb}SUT=OeY)W(@ zopXRGbDc+g>B7dUjQYc(x1t;|`;Yvz= zcZkAJHx0I#E$PKp^axDOH|25rltwpC=n2LZ@ThiY?=r4fg(E4RO>sF7p#H=;*Jeqe z@Fw8}H%;TEYY%ztr+*6px-mEvP&)HtsiM23sLiH`tB-^zVv3f(4Qj=ky@r(J(5Jxi z<)mINo9gmS(Hnl}>UDnDvmd4l8?bEnsDhmYu#MS^Bwt?z4E=lsERgk4m(YAFQFomD z`r3t$6{ck(QK9A~v(wg~^-&|3UKubAw`sqq>x2ot4`Dm^8&lHD!6)eKpPr<8x{oQb zkzb4D)H7$WsB{iPDr}M)n9H3ldr7{f(@VqB%Tj_GiU>avqA1u4_RR~JZ0cEzr{s*R zH%~ulP-PjgCtzIPQL;@Y&liO$$|Q=@q}}YtPw+8uHXDu(304`M3x2g;t&GY*{VjE=QOa@6d&T_Y(U0V-zckEplT_d9qX${JKmO4N9!`;bCaIlSL zvK7Ot?Zp=3zxrl|f(c3o8_Zg=NRMo^SLaamXZvRMG>h(93|51M>C8}jt5Dc4K(*uM zXKJfvloOd9xG<1MW(|j5$SK(G*j&#iME%Njfd#;=|@8_ps=3lKwbiAcrDt0p%MGV+=&<7~5aOcrCbLl2-y&T2&MRVMmJt zBOHmAkC`~N&}eO;$XNQTU=AVc9k45TeqNoixO_KNPs_rlC=DL1WVwSi%@Q%oCm!U9 zvtc}#g=DTP#^qkbb>|Y`s*$s0qPk|&-H$9q{lm7RO_9LsPJhitjmJB)j>52r6;mB) znxk}RIU=$GTh=RM1mgfTQvklMh1|8G{SJYKy!*)UY5CKde;{8?nY^L7os=q_R9#q| z?nwR>r87oUm*kG$?jwf;C3cKej^kKEa%&%MPp0L)TBZA zReoHoD5AP4!o4p91y4~Njt0lUA;E{PZagj2-2)PWe0L`C?My^d!9w&cSP-%^K8`O% zHosv?$KI>E;e^D%)k%y+Ohc7f{1W-XcmhRslIk}1ooE*B5&rKdsgZ6{Ama|gDDRga zyE616@6_A@(f)F4+bqZ$pL5>l1A@p5UJU~e-IXtdYPr7Of73cE&PL#Pv;`fJKep@> zhrTdaQUKxeu=A$J855*Ix0|0EiUFj5J788nID)DK373h}`6@Fo2k=z#rq7~G4C_;b zzuF#)5twfvZ#+oGt=nQD@=C|Y1B(%l2p$clVsC~azmQ&T4GVv83KMT&n2u~mT)ws? z;2%;Ruo`dVQcJLK88Ic>*@h* z-bvPN{Am2hn1arW$?aPm<~OWe_vtBk9aORA+lMbTek#6@Fxi|+r8JjEyiS7zkJijd zRgm{=2HRj^QM!_g^(&pcZEkOboFvXM%!u?AGV{E5YN)>V8}R}MOCpYL0HlT=#xL4! z1IRN-0cbSS%wXn;f=Bi zRF$AocFV7~>hnT!e3#M?KklsAPQO!{mN?GJtLd>dC-}M`>60I?z%^Zc(TW@3yZtH8 zs0wZ-7NSVoj1qrP`HwJek?sPl4Wz9RZV|%G7qdX~Kr_)!1NvzODs{y@1!6a+9kqKb z4IB=f{U3qWK(-N#zfn0wf71hgxj?swtC@Ccm8fxDfzvlX}E!B0pJf_LNAl7EBd#vW~;*Ltgs zBLdOuJUOzdZx7IKzC7Y+JC2Au@w*EjGgq_Ov5gH%EUs83(Myyn|FvE>KjTEi24l0o z{v|#aI2S%gt}0PoE7ULFDBJkGQL@Mb5O4R){;eLv#wKlVtkTkV4ny|$t6RmDw%FMB zbsv&W^&qe9YBXbXyV5Iuq^t;r585D6d{ z_6_~4Vsq&fI*SxB?K3HiwNw`-*^_CUv4jvD3aMvtMsQ*3zsG&w*?Zpwc*1RA`>pSf zn?l{3V%?l^vCrMK)i4(_JE}XbaJevc%wTDIDE{Qb5YuCh1unvi?B35pB_yqPy>Q4* zS9o)_4(i8C0a*}K-p5OMm@Fs*_!Q~J2@E4bGS=zLWCoqF4 z?!z7Y7}j}%y}BHZzAjhd@R#^`$n}8I2(Hp_9x5ob!vsjyk%GK3a8k`Y1;;_&DaSc< z^BdYF{Ac`5qw>eA*NZFTgA__P`4HMfi_%em!BVbRx&@|sMm6$?vESUf7*vv$T9LOHGo~L zo&MG3mgjj-d~jpEYB);E55OfV z+QP4<;LBe=AC7cg&1sug@;PU{5oQvT^D2W*ix~npphe?WUi*eiY!8E)9Ti|JeEj|qs88&&{R_PFJnNY<3?92ua&^AJqn zCt=ChpJt!5a&H~Aj`EH?JJ7S(AKK;>$lfgTGJx!yDxqmBk6YL4cm(_FSioq*R&Aa6 zro;6w=`qDvBjr>LMPRkzUoWS4{09ZYrTaa*T={xY`CXb(mQLAHmZ#armf}cpO4Fz_ zfjC!Y)Dajdmpjf-vc*>0=)>X3ic&LwY)ETm=ok3QJA_)4IAxc=`Xm;PFSP;Ec``SD zw*dD*bx&Ab1~rScp3;h*2{wWGfxlzl#S-LceW|4@95j)A;onK`u0(Nxh$X2GGbf61 zYsL4dIM1{`=Yj1r6K^zzUOnBnMQyy;9qo}#U?lp2`{1cJM#Zad^;lWI*mES7>}^Z$ zwWD24FH#x$9i5ieg)w%?8Kn0#Wjn&_e#usUB$d2NsAzAeveNJB+SI``-1z%~ub?|fhhIb+r@hS{i5(o1rjY(uRUC6>onh`x*#*4Jf+*==fTVotGTpYNyFT9rh~-Tq0> zkg!OoF_I@yP4o^P0Z6v?d4nJkerFM79s35##gu^#h{>iCq)FWTX8@(L9+4;`I z!{&*M4(ICjB5p3_?3G>ZnRRw1TUGU~mcQ-ZMODA$x0F{Y47Abhf9hwl zKKie9IYYjvy@@XfZ(nkP=qD3*9~K?RMty~&Y0n++jmSoS0o@fztLj?leh&34K2&Nq zU=U7RpDgaQHHV@uJ?Mz>uv7-vM?VlzzTj1e+mQJnRPosRzZMVWBcprq=@mBKLF#MYg24+4 z&ZS0LJg?x@qC|2zJLL-Nygc(d+UC89^UHnDGpvpR`ExD~&h(pXHFYok^k|F?pN(+5 zjmew8x>A-bHExp9BBl6}>(EJcA}Zb;zuRq+v$!+RR3OFWddEy40a4>Ssk3#RD@8X; zFuAxtBZn@PT2snaFq7*q+WXwEDzasnl4$YJI3vvcd6$PaYJ@VAAtWCxupy2 z63n-?Q2|2!73(n>2NEaw#+4_Ds%q;3KTYOM&6rpQmW9|A=xXC7EvjMk^X0oK^;`dy zi=973EARj`nB!U>7o`Ktfy_MV24=S_()oZ4Ig2@H0PBXh;vBInHm(X=J^@W>aAU-= z7Q}cm<2QaO^^DgLNs;N``*`TbOOYxt@23==Oay6$N#Du%8E(_T2MFLX zMtiES@bsKySyl36!ZcPjl6+vyLSn4W%-kS=`W25A{H=SMnbo;&wCoY{4F8_;QSOnk z?C~>?oZ+0HHiPaEL)=L*JbSSuXD|UVqeNLj%qUx^LTPAvYWW#iQkOoDZ2Kq41IdGy zWW9rVC3T?o97@bSfY^4p)mOBqkh9jHsxq*PL7xc8k^vz*@(p8FC2IFad#vj`rdSnMb{Z$bMi*-g)z86auwc&@Y&yI?2UZ2n6~(|n17D3xNA;; zTXZTl^MLmt6+QK)hxjWJzf4SEUW&r#hzYoWIvT?fv4Ky;zM;D521aMx_bH!NuQ*8pSKU&RQA-<0m zQ>ok^;xzmnS0Y6ziz!1WyEVdGFKPF&<;Xt4kJhiaaye=DIqnd768W51zxnOJ-j4Ta ziV1-cx7+cdS&7?S8GcH(HwRvc)ZGqF!-M&5rz2u%Ry(cGnQ5;j(AiZf?tUf`#B%7% z*d=Zs`F3*&qZK}VOohM-ds=Sn>D}+cI5fr%r{>aqV0e|~U z^GyGY&)d6cVrV2s_E`MI3PoapI-AU8kJnI#%r>Dq&UNP*_t@o_{#gH*c9mp!9Fben zE?zO2F|lRj^@iaQeDgCx(@N=Y(!m(~SOu0xrg^=2w|V>_+@yYn7u&R@Z!vcftX0kY zW)=QIcTME1I*`XW#i05*-7faRR$nR?()UE@@)o#q#N(QvLS}15Z1sK5S4M~QU8g;V zpN$5_syPoP(lL!7DL2X2b5r9{|IzqZgrr!TOfd+}D?KMKmAyw5h5d3KUWMk}h1|8P zQ^*re(w4U(?q`y0QYx{_QeiGw3Wf~-{ha(BS;Isidqf3jKW6iU_K`lX0owU5O;nsC;{}AZJrN^8J}-Sw-{(ys8-V?d)kOv zE7GwVH3P3=03VF^mw+w4@6)i62>xIULRntv-2O zY06-@&R{m`BF^v~5G{2AfwKLR8SFRWXR_s=$J6P^zyOA5;z0Y5%3UY67*~H(BJqKk zw=e@^yNTV)M*F!c00KV)rAo7RZlf~XC?kzRdkW&1>g>s5oZ>N^f5udv8CEDOD0Rn6 zx!+1nT-}B^Q*u(us&HZk8~FGdCvFYgpP|oSh>rE`FR2&v#F-n9(e6|>!4ind>ZnE9 zD&TO1M)<~9LJUOb9XBuDF6qy3$*9UW&(O~(Wvj821=^hbU8{|_oI9GqQE2|^d`WB zO2xzhH9uGMF?(>FJi%;@Tju}baU|IGgVaZmU#07j^DCjVT$VVS^-`{e=|q;qR;%)e zl~!ZoqgNxRPs}xyo(XZ>{;fT8hN?~IX*|Rgqe7CYaL}Kqv?dy7mYwmO=i@W;EnKC5 zMbeq%>6LR$#JE8x85c@F>NjhMmRrbfUwM$^Vfjm+4$GZ(?o&#+OYR{@e zZE4?M>~R5R4XdVL?e0o`9p1?s58L5co!_BbHwak?KQudO1#l5%HG#OLe4yyx`8A{E z-{K#r^ED5K*~h6e#k41fBhb*Eay0SJplOITEsY0 zlRc{9WUVt{Uiv2~Y2nrDT!3wT!_htCVEiJc&~n46m9e`$V^FuEf?FmceR`O$zJ*Uo zStBDU%(@#L$e?YV$y(=?^Zr|Js%pvF=ynqn2?4(!vo zUc1d=FZQARjX2ep|nEGp>BL>d8Agqt^j2*SyyMYVR`%eIU(Sq=w+joGma1KagLigPQJsfb9sNx{nXZww zl-SH7FI`dju*=$k)UcutXkEH_-D30!4Jgbtxu9R~qrq#&s}_WU$@2GGIw{amQyu7o zB&`s=UfMsIbPVopy``T{zow=xu}c~7Bhapp1tocPabKIIt2lM0GHRQ7X#e;q!PISL z;Z-SQTFS~Ol$B8-xjPp{DB>PntnnW;dMS>#Om}H)K9hpuj$?_b@F<9Di zSxlWxN#7onWb&jKxSak>X|j-s;adV6n$dgSkIKhp`Ru?`i;LH7^uu5nY`c+<>bQA$ z?NGCJubSyHwY^PfyvZ_a%Ji5){xa!;L@Q*vI$*$$T&?8mqlgQiGIp)QWP0iM_TTQb zchqsYW?2h&N#sdTo`g=c#y!l^+Yt<3=5=n>Y{$6MDOa7`zyqgg|5me0spjOP+OU|Z zwUCPO8A>vWh!)@ck*PiGXDh#iAj4VqW$_PP1FFXQNane~dbH2TGUWL(AqMK|Bnq=E zk8S5*MpjZ5u-F}5dYOUV>KQl1%A?vkadDI4J2Br)b6PjtUjNl9Tl2;v~&ft zn{6|mze4Tr!8#QDB3w~KezOSF=k>|U_;D68gySV&xGQG_8#u=uTT;Z8mAFzwS?pXU z98;<@mb^!p?@vwy{YCq<`mYa939c!vnl2vq1!HG!&Z^Ql;zGU7MjsYNzh4IFAR1wq zRoF9Hfkd#TRUVm(h{xP)t9g!fj?$)g(n9O=vF|Wz)3hn+#q6&?y-PMJ{3AH(qQwz2 z*~yhRR!dR}G)XsBG_N_y0n#dTh9v|B2Yc^Vb)`Cp&ksGK-2#Ze4Htd54k870t zwPRcf>%Dzjr~g2I06${UWt4N~tXlFd{8TCv)5B zuT0C5QP%Z4d5en1)O0f#sH*F!3Fhay%0yR9oi6 zng-$kl}5q3dTg}H-U;(6Tuj~~bNBIN68<%rzZb%k49t*G$vzfHC(O=+F!*IO4y9w) ziQ#WgfFrjJAwJVtXPomMZzuf|5YeH=3xVBAZXIrTg54-Rp8<{~Tnzc!)< z^;0=1K)H$LBW!@DYl?tL+#pm{2439fs?iKSs^PNLB6APOq9?eWB=z3%bS$Ov#Ae~c z(x)o(mVWv+kEzz5auY=qWMAZ{q4jt~E*~^p@Gk5!DkwS@E3^DFHFpn_TRq5W*DGV+ zXQVM%;1q|A-x`!gzmJV%P56<2d4v{6Rv;8E&)JtMAF;F2sbMgER7mY7x0-ZJ;gZfg z7`>O$F|m=WB-S9TutZI?kxt+k%&}H%7}p6m#!jo81Rp$-*_wBH5qr2e%%LV=*6ShQ zY3dD$9EMM7OWl(_cz+YP?hsucjrHaT*mz2l$E1Yy9qZfI{D^~=G}$p`%EFi<=cX>N zMAOywE&AK_^mi5hGEHmyW-<9(x8Wjrw`3HxaGEA`+j5%LPd)d@q_1u(7_b7L@jV@1^#vzJWUK#d8?x>HjcARb%wncY{ZrNX zK#QDMU@2t=rDLIEvtryld9*=xT7@BjL7{CCpu6Nce_tv5TG+gjt@G9}X1(~}Pn7uy7vw#K44wT#Pq=|G$^ln%#t40UFJ!BF6ZsMq zFAa>rjPq4;I~lq0=HY~brj+_hy6Qbt`C<-JW9|Bm1+^a>W_2p!yuZ`)mdJe3Ml9iq z5XOqk$uLqotH!c#IJ8q#A&oJ-*7UM!aO48Kb4V|6AXyy@Sc{Sz4fZNsSHRquAfHe{ z!4uRjL^qyFg*J)}JhwMzH0+45Mvc>5x~9xMx@rF5U^`7QRx*vLqpHQ#cqp+hTk`(Y}xe_mrSc!gZko$n6mxj z1lnF7iRcWShq@+@HhoXbya?A;oU^+@oz$4kNsN}6FY`N?^NIYa3Ib-Li#Hb*M36UO z09kg`@?Y|rnr1ptOP(eatfo1c)aaT`Y5)y>A;upk^Rz440F5VY?9{nK&?I(?DPP*P z@sK8V9c=}ohBRUYX`gX}sdhcPc?EZ$4P>DyE=?oz)VI-d1s8Lzjf+39*aj_Yz=<$7 zFFaJ1`i&eC3yHQhR;+e8+kz+nCFz*FVncChsj@h9b< zPF~WDvG@1RvEOX6G_qBZt3UaSlA_Ah^$4-%SRo5UzuKxHgi8nfmXowoCRU6*vI>TQ zFV6qt6Rct^W41_?$-bS@Iwddy$8h~v;X;Yhjq?mB&oBdn+4ir3>jJ1?=ZV!#zdzp8NMnUmW z5qOUF7IyuSHQv3S37`7KseZ?8&Fi4w@Hr{9$4Wa(PbW8f5-)&cPx_`)b=;}GZC#Ka zpyVk0QRNpU9if}a zILr9XG*am6AKZ8ag;lD|;xoRQOSV0hTkfH|L)lw~>kT8{mDb@0+P#_yijxae-ex3S zj?XDID``xJ1DvxZU;Cn@vMIai>J{_Bq#VEE#=nnGW8JcBcC>;k2fj5~K2uUo;Jpox zpZs1Oj9GwPIy?JS<5z!^P+yFB+3ht&8OyeWZmS?W$!3*;v~0KKHAL1^S~kP}^L(y2 z)$W~yeLN+Ta`p{8m9ETZFVQcJdzt13CYH4cbS7EurVM>!=phx9C0C=BS4M5ZkZsy+ zR7?HcG#NlURx`h$!LWQ?Rh!rKGHdM`aN+yj-xT|p3%+#g+SxLE!|ch|TQ0~Z+2q92blIEKuv$^vW0Q+R!S!o2gF!Hs%idzI<3 zX*e(=l)SYf&SIqKKh@+*OC$>|RVXyFzaFbN zg>w;s`!0abozvh*J6X4_qd1f%Cc2*ebshVvdvs#_FNa4jy{@2J(G;fl>Ut+i&$Q;w zqLziB#U+)N*eW>Z%W5sJ5Pgzr6{EZ)4S6Z>fR^cCR`!mI8E)6&X6wX*T!ae_OZpnC zaPzpzZ9Rb>3+$;XUw)~Q$03|gDW zAZR@+v-(;W=iR0qJ+tDu{ZNKeKHIot%8E9G$MWONQT98^YR|_BSv%R7$hvfuc^27J zPPeH@ZNtPpnd?-$Hmg<80g}GYm{qa|wuVEi6eIJy_&eNM+aB~p{xA^@v#bTsCNo6*w# z*nL7U1|;@;3q^Fh<*oND`{b)> z_PBe+c@}Ax`wcbv=Yw5cveU+6Nl>MYVslZGr?c~o6Pc}EGjCs}=pzN!>UA||8i9A;TS`}NBoYt{H`j#P^T9H0>OJ6&KN1%n-X4{)=V#|X)$3_xBx9?RN z37ya2BWW+njH0&zkD#Cr3L?#d0zyD~Cn^fk1*8UubSa@2dIzH`ra@4A27wK8iad(Yl`GMPPlubs(n zD$^%lOGZ`!&SY9n;(W_Skobxr;7nAkJ{8 zeHu;H?{?%Zd(UYk>$v?;Jw0jWj%5V9l(ume1R+FEjEADMCN;>Xn_^Bvg}>1aFwf;W zhU}K@N$92Pf*wA)%D(60>hL-l=XOVwr%PATQ`UjyIjisu>rJ$2S?%%+1Y`k9Yv^fP zDHx9SdFE^G%YUxFt?$!rANU)&8Aw$+c|@h8GVO+259tZUhqQ!Ohxu;YX)D^|8yF>3 z4VR}xX?AH`RU2qs>}_wC71OHyp)z!jY&R^y4D_y=N1mzvaFNOV+hk zY^-0(D8>^%r~y9AHlvz>9swmG#2xxY%$(VU_FZX_TOA<{jL&=gJ-7FTQNcw{> z0h4>;Gyhy|aF}C_$%eg{%*b1mge)E(hY22pG-G8KEdZFN(GX^ne%F#S7K6f+#}jYy zRk9BkEyZV9tz9Hdx7vlUH*MKU+gxyfL_yP(nG6QRHe`x9AkhN2 z=C`{2J^?zcCgZ zK%?7f%=u>FOf;sXO{q4l(Ohxq`q-BSm1a^~Q7wn8;E#Z{%d4nbm4M%#xTmulH%FcH zILkl6F%{abiNYMs>FB3BP8!_EWwz)Gc+ZVL(jPu;g!MVNz5K+5KM;FKluUv~Ni!$u zCw+$7HC8ZXbE$CN_gIt9|9yi$Z2B3`Mj~v;fmZGB9f7Q@A7-1`M#cnid*(|0agQdG zd4-GlFh6Tn-RVR%6W&WZ^_NWRkGe^$=_01s+zWBrD{XS@n+vd-r~ZfSfd}tbA57OMU(`$K{&@ z_l$%-zWc*BrTAL&`FaX?S zXJmOeGTf%03q#lAf4=cKqrG!BGRZ#293H2O|FH(Vpc{+_kW{rzERzM7LA zw3U^eEax@{O0%pzo%m45lEUopxmAMyK*l4jNClZbUgLdKw}wed##B|gmrp&~DVLBa zA4;-V|9z`oQ9vD|VIlLQ^ zZc_2i7vs4}mbW$f$m7{Ag|5 z6PMOLS!;#&(?&k$anC+Zs*pp2i1tb;`fY%ok>~|R+}RZK){^4WGRQS6##@rtF=$ap zXw=h$89?#dswy6@!v`n5p^0CqR=rxD8AI9P6HPtNcplqY*{PHfwN{xRhob4&kGnI) zHn$V`jTt$_SLQbHb}}EKG zNQ^moraXee9Oyrp(I-237072+Nxd@AD_4!xRdj7UVj#ahjNqY6p0~n`ap3jjGwyGV z6G2K4m{rId65HN0?e0!mdyrCK_nQ#YD|XMW%{{ias9B}V9(5meCyRHxufItzP)jS%+tQpn>lynvNup2$$SJF8C z_^6WWa5ek#jhNsU@B0`(M@Rnh)|2LX`)Rb#b&f-|{^d~6;l7rY1Sj*9pc97oGlay= zn(LvlfL*kxo`xi{|Op z>&ijbX@jEJuSnLmTw*+Kh{9fHm%8q>^rXVAq@%CwQbg2z1OGcuB8jXU8~CX1`p$*G z9>yED5{VD-AAvt8ZA~2CI8=Ha8dRnr1xqD%^|{_drf}eD0pyA^v(PB&lB8 z{%z=kWvWNC;+Uglo;n_V&DZL4@k=b%t!WHH%I$(Y-!3nvU=8|s-{-W&ytLB#)gJ~C zWkBk_ER{UZhy!}uW_ZgEJDnRbO>*L2jNKkdsHywZb49fTSCC&=P?$GtQazX!qGM}a z#S0UzCm2P{hPhbjtOn#Dc`F>v%_Zc1`xt**RQkIyijYy{o~!YP*Z|&5U)u zff45#UF>atg;pnG1~RL1BSQ-2^(Um&CO&>W5)yho!Vy23p7{jm*GU4IHyk@Bk#^Ka zq>9#C!2oQQ_@iD0H$^A}`7L%pVkh#~{cVC#r>=doZ&juAxGvmC!$(qGIqOI5qxmcZ zY=uNO9F`Pl?Cz7abM{32)w%8Z^(^)(m~BhH*!!hT5eG|&#{uS+J3Ll?r3Mnkm2tIG z&+Ang`=nLGV9qV^5m#q^JpKildGPYe*YmwC?ak*}6H&ud93WUUhzLVY_mRUZ{*+X*)T$%!o<#v^>>-fK`KhO}%@!X&cLKCBD4* zr64f>?|XNPif;g3+@ROZ-+$Fhmwy#9%35~ihrdqHjX?cRr}}?6+`sj$qI<_IL$?z# zQZ8S=vYeWknOx&ux5|K-f6DH8omEZ{M4W$WG_0OY%VV1%zH|Lz_+1ry5ltBb@nQO; zMMvJ5YK1Ftu^S?AdKSqt@{9%M73MZ3uGh#S@awe~_aydgr3ntBqH{_|tukYq`>QXdif61eCtOX<5)JSKM|&tyc_&-?*T2xugGr zt4{K_yqQ7n?IxiF+$lOH;`w0jv*-JlzR7)JT(r z{XRnl&E(3BC+VKgE?jWDZ~bW?{{hA6mW6|>kPD#cP7=M^C||H|$cmTeg}cf(#j|w4 zt?hTz|Mo|j$m=scPH;b2Yc+eNW$~BMwH_;qVcfvtho$hUVegqECFjS;L8jPmH*j^o z6hAnCP~Q@|<`@3VhsZ^5JISW8EJmxH!l99Qvl*6A1 zZiTtJJC{c6_F7d}gPMGwG#AiirFfsm-T#a21FWi~T?e{CtNY6*9oV16pv(9!3kT;Q8x;@}F-VN)g)o zcYN4HXha{x9ldz*E8%DF70p}T8obXqPdg;k?-`JkgJ99TW!rp z%@0f*0vGdB!Jy#lgD=3lDtMgU@x<0lilGz*R|hqzD+mp&+0wCK6e!C=U6(@ zF(#FexS-}g@8&$vyYfWyWA^LRY|y4x+FrC=)C-RAVWXD4u7%tI{>9Rp(2gVzQR7UF zq`#HIMIT?NnVSd}|8p<(JCN0=Gu%LqH@NTR0BNEs{pr`29F<{;-NdKoMJJo@o*48H zM&J0alXyvALmIr+JDwfc2A`h4Rla^lnXi}~Apz{zawHFO*xb>eIEBcIeta?d)sBtm z^gCQ28qCRq!mod6N#t?Fe^*#?t}Rx~RajOi({j{XwySCK)LTD_Z4v)SJ4IjY8b6Zf zBI>=;@mlxq5GBlWxR;DDMqf5Zlv+2!vkbsm0?!A5_TAgR%>#jlHtugC~7 zHigN3s`w<$WsxDNABqYd;6-UnJx_bLUd-vm;DUb9Z)TSC?CP(`uX7Lc^Cw#43SXuS ztNN5&iB7whF8b)Q-0q#_`P3of#!MOQJ&gOs@%q&%euez7Qd%iTBz=*J;}i$H*wsIC zdQ~?unKOIn`r|&@?vO(AGEMU4+1=rBle`0pH1s!vGc6IOcKT61 zLyTRF1hQ+P859}SVeh-~Vf|nW*{#jFB2qKE5^FPGS1xnRq{lPO|9?U%|KS4vAp}LA zic9<_j`}Yk@SnxvQj#)~|JEx-|6v3Fd%W_0w^6l~m5sDj1T^ifKDa<#Y~775sIuYz zHxu{qOBZDTgA1hfOL&+T<* zaFKE%)4|9y+Tsi6&cV<7}Mf(>oGs@cnEm_}w!}G%Z zgbOM^FJ$f0dOvQtY<}T{w&LN&N5kNY30D|?{&mUwA>V4|szC+&C0$U#SnPtR$2t9* zf2V7oOAb5H&I?wv^;AY$-RKvMetyZ~QplCAD-rwjNgt^(?bT0LuPHy`NbLOiDYxqV z6DNo9w{ugLv|VgQ$2$1RKe;;(-@JM9S22Id)nHlL+mS3^fhN>jbweiSCltKa-z5ZQk% z`5&L?-@T^He=kY@Cn78JA1w9%ipch~UQaMHG(h_3gI8`*=M!qtmp5;3xs~`s_tq;N z>WkkY;p-dmugdE;mEHWxE##dim$%)F zT8$K*iTaC!ukt+IuJ}vo^T@B%PqvT!OBs2*_sV74S18RM*-MWFn9Dre_Y~~11Mu^x z46?^qfIeqQF?zCiaoT(O+9}& z2=I9l12+*KLrHZw3-q?r2V}Go>)?L=`$w!m^ULieT8pfhV1(~+I#LHnd6AhUbl&SI z#dZEwn38z+Ib0U6Crtfm$c5N6A5yPi#7WAJna0A;d*RJ)u9)@g0||*8UUZGCD-J?e zO<6ucltz;+3->7NiIb8NC;fZ=`>d3)S=iPJRH0n`F7NtLe1 z;+m&ND-l1>NtK2r?fQiXu0U;EVtl(Xx!I~+rz{!_U(rXoa8vqbDTKS{C75HAOrh!R zX0*6rVY^--#>2NJ0l2rn;&~_)1CZfD^izy+Qcmrs&GCWFr#pj#=!fx4hB4Z>;usCn zpOY~1IS5Bm#>G~Kl?by@f{Uf*%Z8D<0UK@Ea79tvEc#fgxS#hNk?Vh`J>!8bch+us z6VSQQUgmZl>M)j6T(1FxO=lBB(bbP(^`|Yq9PQ$;a~v3cBn-DFW*)FQD9Y<9NV_$j zrVT#ForfQwA@SX42s7fX3p?q+0kwZ3RZNHpaK=gXpA#|Z%-{gxSsSUX_A}WVcRqM> zx6K*lx?Vj^>IavcPt0KrF&@jvG-8~CCt9Eck(yFQLY636ok^7d%?YHsN}vweHIp*m zL;+_K=h4DbEi#)n>)a)6(6fB>SO5~->UCy``_(gz)4#xwHY$!WIdFF&z&#~DS&4hw) ztrez`U~;BMiYSTpd9OA+RI%+dFM{^8}i{CIDp*mf9~I^4Z~x&Gfz?QK=(5=hP&0J1$KTco9#!= zwE`cV4i6-R15FUi$nv&hS1oi~CTdIyJS(LAxV;vAU;A-e1cn^vo=(O7+?0&aIY-xugVGR%uG1k0T>mDFo-m9#;29E(|b6-`F{=6ispgw4t6 z%0ZxRK;~==CEf%!^E5u&EQa7%%%tV-rEm_1k#U1XvVkf%`}KAc1=ogXids~)VXLTH zsc+ur_NNDS#RzByZozM{M<8+fxYZq5i8fH^1IzD}IIlM*PY1LGerQH(o!M$)4>8<0 z|HDQ!grE(^|MkT1rDIQg9g>t@&GU#5l-t&R2oO=SU=(fQQsx zqm5^zl2tD@8ww-yP@XCM$+93@Ah?RwB@ZZ}gxD+WjL`%T8R5iuUdnT#lcVR2*k z3E*gKiE9s%4eCF+J}t#Dwp-S8)?dOjXZV)866HYLL^Bke`AYRwKvgW}6D8R2ve>FWx$V@0^J z9%R{hqrM*U4M@Nx`)4tnGrfGpd3wWcU-8_ocrt8z5WT6?yqay`lHH`&x{8>rFR70X z@Wq*}gFyk>%X^^ya~J@D5y!2ehk4K6mJv88bR{ln-hQ~y^>+P$&O)*^u6MoN3_IG3 z)}A72Bc%*lo5up3b_z=nXEoe#RDLmPgH%H2&!hz#&x!V%z=1N&OD73N68PjYSxLQa z^0Ul6c((TJN%oWNBeZ*^5UIrD+$i(BRDT_JR0;e!$vV9~j6R<9@KIPnn`S!PjSjHS zJhvgAfv2s=phtn!4b(lFj3%VCTk2&|9|~Z58Ud)a)6*&?zwOq*{j~#Ftr-SGy_Wn& z`)3Aj5}yN_8H;%zo0m`0VQRp`lm$j;v*lqv`bcrN5M6r)!JRhij%xYa;iBTR4H1Mu z5V@9QTf+TwQc2=^pv~d(^h4&Dm?+Kjbt|CA$Sh7NC!3F~{#SlQ4(X9;ZJ<5tv8231@!luof+_ z7vRZ?6>x7| zdPu>CZF@AS-A(p)@>oc6ta8k*USKvcE*xCCBGRbW>USpQNNUz1kV~xCCC<6;!6&_Z z(=v*7^T4GzJi-85vKH6_)vR3(c#4Be&lj^rZP)yEydxf8ib#G7Xpi^B3<(3Eyl0`7$w7#m(GTz=#=y`6tL@7BywLKvxJ zrKeUxpJKZfdhv5zeVbH%Tw>8nz%uWtysAcFmQwweW*kk+KsqN5awJkn9r6`3u{XtLH6Ws z1K%+FmtFp>f5ubka8Sv&Vwa<}i(O`IOFz z0G2Gh@R* zXn}WB@!F~bdDYT;Cy=&2K%E6hV%r>DB+5T?MUNK=y8c-(ub8VJ$eIuKE9($S zKBS4jE!{pBqDN+^P#&z|4h@Ho7-*0uqO^gqwi+a09quumlaZFxcr0B=EJ!K#aWxzzkO!a^;Mo|>XkJUhDz3aWB%2D%5h8f^ig(l zmDb=aJr{nyMS?Jzc3c$jv_C?(jnCuH=gnZhp|lTkCtgdIF8o^7H+H4+Mz-e!T>r2z z8@bb3y#shKuOy0R6~)0{D}wugRylHk`%bqnZh{3r|ObyYbK?ovmp>E3W{*XB! zz?2Vb#t_irbRSzj*n$_t13jPD%X`u9WrarCWg~7c!RL5w26;VbPOorMr$|)jIn!V@ z!XDYFaSJEQ_67UDgi9|s)5Oe@O*@ZDJU5`Q8ZFBAeJ6O0&kwD@+?rPNeRH90%T8$S z>H!&6dmLD_?_K_@(X20=W0@Hy;zezm`-p7bQhVVJKPQ$MCXDTS3+=MOxqwovptx@{ zbaQ@tuKrhYdj6S0D9q{Za^Q~6jF@NS-lJORFTW-gdCvN+HYIMn;D&p9WDYO+J@=n0 z?omVc@He3C?<?YACw=mr zR9E!dI`xLat~&GK=3yIbjg3j@)8LTvxg$`KIcfQ}Q6I2DrOVkmtHLHVEJ$KgM%M26 zyH@}H7tVq_XU63rD%pNBd0{?i(4_MC$#`v90KcMlNAo1CuZtxswr+AFsr55i(lD!% zrrCz8LN14(jP}za7CKI>IdbpLY31~;*VG0E@)ZWOBl#PaJbf!W3QcY&Nl|sHfd>=S@}z4mKbh?A5_0LYM`y$ET917bC%N_mor#b&< z>;`J>qrLs&YOZ5$$*khHz8T`fTK~e7Gapy-6ooedGap_g32yC71*Y*y9$k z8Ga-q1XgmEYbvkc^e{3oY1=~k-TKbJ zQOrWD8HvG@M~R>Y%mUX{Ko4#k%b`muKBuq?NVjSycOr~;bewK|aNjRZ&;d{SIJF+< z!j`>D@wiXsgKE$3>))pda}Zs`*)iI-zITAcLiKPjW#% z+$n6-Wb*u-Sv4${SB+E=nT}ha&QwrQ%_WR&)}^JaRu5g=-%=Qnov3nl7|wVT)AE+w zD~5Ni;|!-MR(wa>l}Jh6S2K*oyNe$fH7$@Z^_(GbeZqj`! zG}f-vHbU*+UnB#K3bhjsWB~B1QIjlfBC?9uh*`no$C$K~*2xNs(b~!SF98#b^B-oq zkipJde)@eO*PGtD#?FQ8yJ|1^kPCWKyFN?!tT+6+YF_84$*t+Z=*L7+qRt@L%QYop zw>Z97Bn+>c*4lRLP9s>kGSgfi+LvcUH(fFxHgxhry?JxWnZMI% zpt{^_>)g5%I`IH@k&-;*ZJf?XaSbvSbggDNq>-D*MqCv(lG~9u<2#n96sp~w68hot z`%Y;y&^9Oe4zsB0?zLKIDnryowEsbyl_td$b?BO zwxm6e8Qz1ZIL+EX?riup(|l~;uH9dVgfsJPPt{Y6rr1MmiRA>lm|b%~xI%HuGvSk_ zHdF7j*Fmqqf+hpT$t5K$Rle|%#aF4q(+6|IM95rc&)qdbX|E6ekrIb~0J}fK*gF}e z7Y4aUk~FNlw$@qj!~+Xb$w>EDBgEo}@d}~10}87$8%kCKR_pE&m%%LzK|XyDc~a-^`0Ik#KU&UtD}H+9yfNH6fq;mMVl z%tA#I#aY|sZN+R>e+Tvwt;brq0Tbq%PsObRIemt*49OWJj9O!K;CPz+JHd{1$7j5| zw#<9cp)R>?R}~>!th@8brRypDMGvtHGsC~2I|BZuL{F8z?zAogzv(XRcVObF-{5cm zegDo~zc1vMzxRf1eUX1>G+t9UBLMa?z1tu;N{pYhwJwva!CMmqjE5uI$vr*`cM;JT zv94g_mh>^&K<)xN_aca(29FH#_{vm>u~_Mf9`zgc@fd&M@;3qa$O`Y%_V%4Ht9&{O zwbugt_AQq)#Ww;E*5VYuO&sZtDexSVg}IlnD)^rVTKnf+RTYbKkGaoW1tXkgvcMK} ziwM#;Pv6%@?lz4O8ZV)$2&xg#!*rT#RJ~f0UL473cIXHvx&>mfgV(Fg+c3x-sj13^ zt`w?~(&HGfIJf11crmE`nAymH8y-hVb)bhOf^DVSOoJSF; zq5%BUvnQ>lza0&ldpBUaS81L7Fd2|%w{s&tM>+(h zR!GzJNg4lrCdP8EZTPn%yK0hFZDs!6Tb!vlYKHRAfhm%b3wp>VrP4=(i)wV?f(>TPSeLBbh-E6-D$} zvQpG}$AQo5T@K-l@KEL^b}g|5xj0mpWNM~qV$-Tx$lW|!UaDEDJr$K=!pUw{%{#Zu zGX!=s1L29M;L3vXuE#QF-rhMQowl7(=Z|JSn&+D0Ruhw|99ilvOLmwPLH&gj59EcV zw}zI?W6!sS<&L}uySU_yvp5<3K0yT*Tn4MrOMnQ8%|VE3eWRCpc8A}2f`kW&IX@;M z)XQ+_s5f|$(DlMc1?w`@ydGHj3yvS^O%NUu^FKN}c+tPETkm|6Vvu>_BKR9K$HL06 zqt7O1g;7g|1HkISpwdLCs`S_Md(<{56~tQzeK<6$K9+T^`FQ|rphg!2stfB-8un;jEt7kM=g6~tZx*XraHF| z;7uE!%^&gF?8+cHmV?W`Kh3Sob=9nCyhdG?({p1+nyQlzcecHYFBfJ7@JA-^G9Yi; z0TVZ+Gt28A1&trU7MPL2w7*SHvKU45#-wo9BF|+gx>FWbe0DardQi;jP+^e%RTt zUy6xZj`#l+r<~{*sk2s5QY%k_o$R>3)0AC2>n^(@#do$`HGaIALP}R9Q#$Z2Q!Fbu zz}_9PY}puDDFw$d_}ERy5b6M3qeSfvCXjALODO(=^^= z>D$_ld%a|u7`I9Xlgkqpztjyps2%iB+{)7gXWb^4GDK%yS&VJ_21!*;v{#5A5usNl z2@Jyg>8iL!g#bkTDo0(7ep`!tsix;?D0*YQnIL5>1BT*%fcqqlwuFB1=dVx zb3o$?Yu9!MO(x+6L?s@d?}@lr~_bR9f#OA`5Kw7{96ysjs}Fl@Qvix=>{ zisDXmsG>596R&Th`cIO6eD@mj^VzIdPI?5sN{AD?bY<3fZ(^E5xyQYHf*Ya@O zG0YX5MHj5JNmD0$e4R$v6I3@%nyE>K>Nby+F4v8~I=-&XoEitQ+hj*~CEAbKQRbdO zj_cSCkI{Y4yTeWOWQpDvsKS$?D-rAxWMHVS*kISMM(b6SmFE<@VoYv3v;v|q5GWp(8&9=F<&HT|ZO zjTJ@lW3qn1%$bE}-#Oj0<3w!zUZXB>{;s(Z zxTpqQ;&;vHB$5hN(Meug@plpz{>m74i|!-cTbrvbpvVeT{84$IQrQ4>FIWvo9)T|e zEJY}P7V{jl7{`c(G9nNr&}B{Ixm5?hKdHw~Iab9TncsZ<6&JLP#gvDn3{Eth_*^0O z&G}AiT>DL)5~ERkv!@(#*^c&blCAPkP5Je}bj?nG0P^ydIsVIU9a3>w%vq^lWCQzw z)^yH}Cf-u(^!n<(&P^BN52l({!f40uz^Xkv=Xog*z~jjz9gC!jP`@qpcEuP=VBT9w zKCSP*qOud}KkCetPKsc6XdHAV}mHZM+O zxmO=UCtjS(Tex9stlXHUhJi-=3Wh6KqXD9fBMjALauNCuz`BfbsC5&|HP3-AJxOkV}Hfj~Fav7BGV}#YxY8ECupaYGyMEPEe(uv8C(#{xDZYQUwm^Qj0A){o5aqwLnO@f*H(yP3 z_SZrmkfxP05;-^+E-9rJhF_5acpuB~{OS5Q7=*crL_dsy6|D{qi@(vl4M|Wq35j+A3d2bC9iwu9&^~y z41rhYfvs=b?;%Q!t>t9nh|4uRJKUCkXTH^z+p=3otK)HOf&^-)gWM73YciFH$@~}v zM;M{XZf!A3@W`WzXU7!G%+Asd{)wt|CY0SyQBg~WvLG+{Hizx?4n|^-WD7GLlOUr4%_y-3mmxJZUNF7I~I$ol&fO(w&WIoP`^fi&c9hkQ{sUj|urDRd?+uz-i)|2v zy0S-Rlt!)1iFBMCOBw&LZ&9|+x!;O5#C_CwNl(RVLr$jDZs+PWa){aAZHTL4Yh|M) zx$*{d+Aj`5GG34N)e6}GLMpLeLaaHS zsk4qjUY+JF=LV}FHojaf`-_V#R7dST?$xr^x@3Zm4WbdpkHCosmC6}0ax*PQ&rQB% zR0|@&wZBAqmNMIzKt0HL>>k*^pc_(zn?doB~H9H>y}c@Ahc0*qF$Vq_-2-BarRKt@~rT=cReLgcewHq`K9w zQxYg^VegUSNxG^F&%+N2c5;g>Fur3;x+BeCqy&__DR~z^V;_MntQ(R$be{bUIs-&t z8D}?_vk9d>?SAjwR04CRc1?Qna{|p>W#J8|BC_YFq_Q}<6MC{!L#B3hD8R> z;A@Mu--_6Sn}wx+#Se!9aTcwot9W-Se1$|HQy(XxATpQ*L1~W|;pD1y7BBHF z!LhIP`TpI%>j&xtVJg1EX{b-b`PYw`b5V=7dpXV~{_*t=Im30s(g)aMb4+lDbE3~Y zD{-9#d3hAH#Jp5N{CrFLtlh(L1wQFWF7V zh^Sb4`}l&Rdv_Wk-l*S?#cMG2G9H!H;S8F4@(aQClw@6dOebthFTXAW@xtiL3_}=F zqoRF``hpo(v$BkPTJfKd>ijaAc6zDxz=v33J<4Q(kY{dj8t1~m1SLh5l%2K6W3WIA zaF({sUFZ+Z`VRZj0$v1~`v*ho&iUK6gcYdCdww~{TFlz;?@7L>)bgAJ3vkTDqq&E3 zX?In+?HsDjROah0Gh`2E$Lb8R*B;%*{)D|p&Ugt=Gbz`wN#NzP^)BVwNyxM*%OKE` zn<*`d%*p!-TNqMluZ$q$Xs||{24IkWITVKSM07ZHhw4T;coNar(X_*MG`54K1;&y?#aBvE>c!j^wlSXb^`^ zoq)~VMN^J(?daS5kuy2!$I)GUCZ|_bv?68_j-FtTHfD9qc$8U_bG;@_TV)+%lNf+JOIzzbfG;w!)4u6%h_bkW+4CeAa3aj z#e&a+zWuRy{q(l1Tce1@8nXy$>EgC3OgMep{2|3!&vXJF1EyPkoTi&&5fQTBP{86_E zsLq)3;)g7S-GS=I{Q(aWZ$`+B$9XFAz!&R@yQw<%+F9Cz)v)*&MHS~Cn70L%SO*`e z+G$*_o3aAkuPP@w0S*~SfV*ctBs$P@!`MnNGpU6am*DYtmLvy+c=`~1oLVc9d}>I_ z-OZHV|JgtkqgI8RlKWi_Nul*6i zP;WV&h0y4%p6#VvbAaOg#NrZIG{PoME5K+-hod`z}z&7=`&_ z6g$itbCM;9Tj@8BXm@M|FWe|utlwY%vJJk~5W#X7F(;rHgf?^{El(uZ%R%n1CzbKD zq8K!gzw4p?riE8{PY>4o@<`a&1^nwp7o$iuNCmN`C#AAeeC1PSEVJuyk^jl{i5F@R z8=0cfps459Vp7n>p)vIjHhU-)okqD4!R&tDq?;C^E;CS~8hCMkX!{!--B|W2k_FE| zNTu>@rqUwjfpD*5LF)%@bnYi_gB}{7#WMs>^cfacbt16K<6ggtD@EIAY~d-dMp&es zBsa)Op$cLG9s-^Om*)HHK6^C@Ind<%4C||gDhvCH>H9S&n({zfP0qG&D{H{hkqxrB z{Y)A(d06!q(v#wnsDZS(&F&2V=LQa1Y zv!LwfhUIPDXzev|ENcVZ?w@A1F%vF6?3`sxl1PStxS0@Q1b&KDh`=Joz5Gf2cNZ)7 zFK1{IT?k)Ee^P(+Elj}>2e@nfT#ga*n{&$#pvfFF{Nd(TT-Z)BtR^_WgdP)EgaLcp@Dp420(N+Eoz8c~O zoVzv!fd#aU&J1pzPEpbRHamJPC!;ZQF6LA2sxP_KGcuIpb4J0vV(6Za@hS_z zshF7ttJdjfuvKp7HdCFk&+kGg+lGqGz=fawk!ZX6PSI8n&$`=KUuWZ1pPr(#H#mv(iguFt`GeC2*z^ zm9TE~Coia%U|hN`69G*P1GuA?zGS%lmim+0*zE-BArG{|C70cY(g^r)v%hcYnCD*dfvKkG3MGl0?2 zS(lr((SV0L5Ia9+P5xtCofRZ_)wDeyqG*?rD9^iN=Wq0Gx^5cl<%M^;73)9&d%hpmqrA zsS^LJ_jO`tw>RBYZ9}xaO7Y(8NtkdGJYhEEN?2RQ&4v+hUm@y?>^X7BEY z&PnPWjLT47|5t^4g{j^Q z8Me18+av`!#RkiJ#41W4-kXscOS+pBA<43PXKKr&#Hvf%w}!Ti;@CE>xiA1R`qe9r zw*g2_IxvUF3Wz}~AVzSPS4QKXAkv%jQlj1&yMIGz{<2s+(Ad(qB7b^Qz`+iQPJe7SAzCF1eWQ}G+ii+*r;f^R;;CK@Nvm@uBXcLJewA|0+;L*1 zANh7K0LykGyEH5-_F1xvVp@@X__Lj#sx&3knO}o;O_Yw+p&ODThbwsw_-k=;;L%fm ztUmGW2Z11&adj5>NypjS?`S$CO>Uqk@0;>ChZ-^G@f_(=VVi5kFhUt)7#~ehR4AnDEp8(yS|WVwL8lOA$6~} z;M@Hd8?`C+WZPsFtT^T}xj2E*4Zz|2o~`^PgUA(w26UGemW*DgjpS5JHM8By%!~l}X)KqbNhC7rk0Jk0C_|{zi%Z7=q=| z8O$1DaZwJ5Hs)514N0@8(x6|w{q4JuGyBqJ= z+3UyyESZ0h)9D(0F~xr;>}yJ47`abaL0-q*U>y(bTRTJibEx{w{m=x>IM{jP0FUlj zRTA;dRPb7NYZ&|A;XWkUJ+6IsE>NAJf7On+*50sjq8o=G^wf5{axg;7Dn4HS-{O_As_VX$ zPBHIIJjEVUHb3s3HK|;S*!kP2<+#Og64^jUi-VndTLA;g7fXu4oz=)04rVM|_0I!2 z%R(HK6JYg&ORUgpffQ~K_I){Cac54f zYpdRe*qwqM4w4E{S6B;UqxyE) zY>Uq#d}>IM1O`yWRsmaAJC-4WOI#bAAP0X6*VX~f{jxX^Qnt7={_OS4v_&9UcHP)= zR7nRqHM5%Yr%8?dHls_C4pNhmEiG12*otH_R%t*pE6766W$19DVo;y*h{2&FVn&+% z*(gCFJZ9+$saDEabprN_6`sC*6>VrrU1$ItRza^9D57@Kgu6aEdC3Fd?Id)DW8sq& z%&kJpV15psKN%32H33$VaL_V$U2gqo2EiJA>g`1m^~*k}Y${e)<~uzlvz_+HU#9PI zOIx5IMAV-_c`|(! z^^YL<)E%o3El6)ugxf<+q7wDZZCW&Jau%Fv<-%cuPel>@^_J*_yECl6i?_dRgGPGEP?0agtWLas$tfqnG=U{IU^bzw#klRuoX ztrz%#rO!&3_R}u1E>wMEz` zvA%ksfxYalrwLQK?cL~PbaGbU?V15>FA6(Vbom2_F@?9ZHvwOUimKlzjDz^+X z#(!uR61AT2Z`aZdeZ4EshWD4w91En>I4rVOHywM|)wQj5~xi!M=_%!{&yY^#3Rm z5+2qB0J&OrBNmjpxD=GxKHig{eL$?2L|pU!lvWyBo7MErNcznM?&?>QB|ya@;$!hn9yWH$;M((JF3rye>b&xnOI2f!NOJ%M z>%lBy^dz#@81znmbzLDO@Wm@_>+PHusLUP4ulBlyAHb7e2?w>Poc^N9M(ZvSUdr}b zQeB-TSv!IJMNB?G)LnW2{*Zb0I3R>&r>S9*hYS1QO`NUC1zoEQWIojV%-LYKYDquL ziupkO7B0F}3VNN%5t7-&D6_>hxi%%7Y75v@_$WU>=>|Z0IHD?a`~ItWQ>Nmp z-W%k7Jzg2;;>(sYn=a(=tjI}uvr;uica0U=!>PtUe^A?=-e_O?@t`fNdyrOKNoP;~ z0i|~lg^vYGR}3FWJBnI&z9t7f9d&Gnyosg*Ruh^K*aV*JqF56=kI9CK@-O)qygaXN~XbvyB1DZs?0n(IsIzw&|tNs+GrZBY-Q96 zdaS}pDEW7)>1}WAnY{sAbt%Ite#wIt)zkp#JyctBGby<$f^I7M*Jf?m5C9!mSULP4 zj>OVGL_7VRorv`*>ZdB0B&L!0(7D2ZRSQZc_i?((dc?A*md5W4Lnq|u3BP{xzWTbE zqVc5g(kDX+a>dy?i3RwAdV}WK7@23{?aPF%N3J0*QbYdK-RPUxhi!6cfQjZ;(*Hod zOYMAd^#AAF0DNVCE;g>AdtcVMxAtkQ{iQ2d>~wQPnqlcAz_KG3@~`)xx!X{+x3uf= zQA{|dsmS`z<#Pbsf=dq51wT$b@p zW1dmCqM4mK_o=ddHw{9D%;so+T0U;~8}7}P7F(vWXE$htw--kOJ;qZ#<#!8$PB+K| zgEd1zep9XZ>v(6ovPGH=^$%Jud;7WPv4}r`2D|Nq4%;b)WCFdh258;!^<*kSii)Pz zhT9X=FRueQllK23%&KznXTyTuEIPEL`cTz^k@k-RjqB|XO{bFm+}sXLYhL@)M>71g zd3QfG8>Sr=1;*;2b=gsz&^QKWr!9~dSa`8d(w38S!Q;BybVUCVavZd!8- z)?DzDIInyuW5$t`8hj(72x1w;NLNlF#4BTZ>mCzkH38|Mqjy!lbf6=6MLoGsR54k> z%RC!0vyU5nRM{^1o+jV5uX+UteAfUwg?s}9j(y!yLhT<%`?j}=OKSfJepOu0HeVCA z(Fzqw<z(0*Chz5T~)zEi1<1OuG?j~})D*9DhPvXvP(=Qf#5_e=f+^P8q%@?h5Jb6$Bk3y%#h+scKb zZwxo?v;ygXi0wu+=8#L0k-{X}v?XMe_XjpZMZ{UZAT=B_Q5ey;sMPPP=`XNv{fF{` zUgFEH(RI*X3p~tzCbr$GH7Pup3Kt8uXE1C|&^4CS2@VhOb{{hY?C<$z_qugaBcn(V93lKE!pk}z?lkABy1!Zvdh0Hb1W9qm)uuVJdl&nmW5`@ zvPY+hNg6R`@;OMQOImRPol5G#92C8G90(36@vg)2=;wIF$6S{46_}y;lAAW2K~)g) zUIO{YM0jRNY%}}X*nY5%d^`5_KzE~h>tetfr0*uFsE$Eq+8e0PbzmNC(5wqNhybRY zZ#LP#V32fg#0jTHP%aHbmKPC%h5`oe?UrcYUH@{OJ0H!iLkbFw4@z{Ke$Sun(5+c7 z+3m5YLp@!e5~oCrPX5(rTc&pIMcQff8VOT5C--g&8g_=;#0s^9oF>X5{Q=rV-0n+r zu`)rHq>TvWld10Cn-2PmXVjSg5$>XvQ3jh!$05f&QjUZxsF#x$O-g-dqNcv~D8`m8 zO@v9afddk|oa_-U4pGxz4(VgA@Ujw)&UK|0 z$h;8R!SL60C6vHg;p>*tl>qwV4THR(>LZw3e}Ba zI#7^eyc;*1d;=LG-z~J$^!^wq#qx}p-D}3^MyCW~X)3pQsDviApR@|* z#=cO;0hBClw=K6LK`2rusCXVJhssHSkArjA(Sc*<(jkwcXu4GtEx!aDY{>F)skuWS z+_WKBo2j^89hHQ80x}{?XMO*Rk1}yCoNZ)R=Ps#??zt(%3pt zxc0WkWT<1ousZA`h*?YhfQ@4R&a^`vQIarucdo&Ci?tCGdcu`?nbqXBr8T1)1;1n= zgT$R`STGG-JlYD3g5l*aT!5YjTH#_FIR`u6jpR4ms5HDCcIw{|ubQi~C zZ_?%v#OPHUIiDb7nHPlE#ln2>mzqCq!7*!=6E5fW3M*%N-nQR3`^o*8CKvqWdeEEO zkk590eH+B&6{TgtN7d>~z;8;5F{oPEtlBZ6r{s6q?R%IDbR~FAzT>_72K!xtpTV3` zxwsiC3@?^YS2=rh4g)9K;jy#mp)+FjyBpbL0U`TTgL~;o*^s+Imwp|>-uhEU(3i2z z+>r~#ja(;I5*o=}Bp2v0cHZU*BE%tDh#->fS20LHw>SCifp(-zyfP}+yEN$!)hTaX zttUV)g}y`f)u8@yhI5r@@EhxD>kd85aPjleeHI;gj&IWY^*l>&TXGaQVO78#?^znj zWNF(c@=#m+{V&<&9qX60P%Gy}R;_)%cd3lnroSk;-wXDR`vrX9cMZDTxDr^ip}b2E z8Z?owasU<@jpW~jd1<~;XSry>-`H)w1Mhri!v17k1u4X2DleGi4ix?sUYDjvRrj1? z5t5JVWdo}ETvAGNv8VG^9*=PT1~(Isj&aYl6RJnA+%4Gcv0RWo{5awA2ZnJE$w=~J zeE|FF>93f}5Dn9S`f<~sR@(A#EFy1Jv*f^PSv4MQLLp*(_cbYqr9A<4T8%W_EvPhhb9g(5 z^?Lf?Q1AA=NJM^yTTA|&Gwio*d+o9kpw9h( zMf5{f5_@>_VDGv2IM*^Qx80iVX-5~pL^;qYV0zNTnp09|I zZgGpdcHF>3mMreOTTR@0mi`eIV>4d)!hHh7R;06-4q zhKsgYX&>n&1zlV~0Q?0k@#6r<fCGx zG>&rpv1W!EF33T+nNM-`a#Cj4WowI!Vz*!pXvfQ)r-D{#v?pa50>ZV2rWBLfoeco| zqi5BDdDyU@-x>7OvQRd5SgZ|`lK4Sn5N_R%o~d-pjjZ_5xR`LF?c6s+59(L$uSU|< zsxC2q7?Lj5yF8p~sky<{`0(OqO6BZ_$)g&Fneux<@yis>ziZLJaCv?8Q5M!DW15 zJY30hGz#+B_{8-_y#%UAw{#gfJbNLF5dt`)Z?bvG3`Dj7wah?d&tF{WeXNq(cJN2yQ3ip^r|`>ca0S*v1NzmH?d;=kda zT&R{sE8Ds)`DJ%TR@VZne95zU{aKz@r#}g2CYQUKPNw4vy?-AKU`s#ct#81%E8M;~ zp2mC(`&E3-oS^viphwkDWh+~k`|vz6l=eA%!lLfckvZXiJ3Vn_&RSVz`F!jNne6PD zI$*cK=1$Qv?p4VFR@e*2E!_6NjBaF@a=9}vVTJ#2I z2M{DM%X1r}j0^jzMX8mxU$PJEYp6kuZN%C-p7;;3H(Q*Jv66$2<%Sm(LBq?o(dnU2 z&@7d-!_Y5rC!SD@)*Xh)Vccey^(hPL5r3c6cU zWz2}}7yvemJk2jpcSLDGh;3~L4gO&iQ;D!+|POflO<;!sz)7Djf=RE$}g zE#e2#Pzy-mQ?r#=Ih3`gUp%i~VMbm{@R>;6${aVPvRwZ*ZY0k8-SorY95Whdr8hQ% zy)Q;1+qzU`kgD_OcK_DCwr+r5)X<9GY+DZRQQ(?6A>*=LexFm!MHwW1Gx9=@ULb=O zjLf}B5>}NxzMVd|OG`C33@T5+JzGKz&eD9v9@krYpsN;HHzQeFr{|nV7h>4Ll%MW8 zLyAjgnhowbhuF*cYH_1hn`dG12cxyg<#wC4Rr_fld$!g?8G4x^0p070Q{%TL?;Kilp?PG#zfd3NyIt@PeU@2L0JEQ8_ejg3zJCoHFxX>s2reiY7ai&4q~&uQlL zd4lBGzLc;Dn;GIh{1H7lj;UdbcY^rc0Me5&I_Kh(5Ry+t^A`|gtHuNNB&aoZZ!-ST zT>3Y9Q+3T`WxwvtSu;Z7QA9gs2`k~4a}?1$c?ThvOr?H+fH^^qb#iW)D--?~CIA&FX!}bI_7}JuuqkOFi=D}(tPY-7339jz^3svgy?@#CX%*$pj<%CgD&`B* zyR|~n>^PyF@+SQ3{!b|EUs;Pc$cZTra5v++*K~PvWls0E*gTc_&47}eODX>3RL1*Y zO8xq;V?uQJiITjNkbW06PD{!{v#}QfHJrY6Da8?Gwp~m>{LSOEu-`PY9Lf`{_^5P4 z36hOr7`GZsBz)lEueu^#|t|sU+KBw&sHW&zc+PZ4@RfgoR?XU?56)txJ+2LTsthDw{0nc>=r6$Nj$e~Qfdvv zto$!VA_R85{Lu&!^cdu*kJ3ES@!Ro-?8x!2hC{SC zEK75rGTUx@NJuz?x2MEU%Wv6xFxK4FH8g)db-H(WWwZ`HEa7-{u{YYGe!-Tc=y)w7 zDVUvL@o~M|wqsY#G~8Kcx!yYN$EIO?*Quk~x0wzZ(i!)ijy4=?i)cs+d-^-LK8u$@ z9%j&@Cy?;U`GTBDPR;FnU@TSPKcs*Ej6XNU?*i*j>x6}t-F~?>QyuD{>1FPR3IW+; z;=t&9*@Rc&1UW8Xdyv3 zejT)bTPh0ojNtr5oK8FvCQB2K5W*f3-CtA-uaCr}7Kn6G?{1#80zLIz8dee3Z2N6Y zs{FQ+a&8hDHmpbRRE_|>3vQMYdo7S++^0t$(?L3GM$gN zq;{2>#-<5Bdo>CFs8?l#PwK77lN|i3+S~6hJ{F#pBiqe6*}7zT+OM;5=(!Qk-R!3X zJ^_5hW|KVOl7H)szIj^OqYCm%H1A%v4Jn5i&>!Urz5-+x2Mb-5?(=-JAinxWr$hUx zn^&P4J|z*wM!m~&j;LUj*{@xEYYieb`f@r-P=CUsBsa@7@s6Lk5}H%-Ib$#ydT>|K=wA4u`4_^eDrdB^xqyI8k4`ZO5R-=b-6)1d?pECx+EKA00$b>+=7O6&#&heaW&yci*SaUPDqJ$xoU@>MQe%=O~_7!n9pqD%}`k=$R>$PZ_GNZouT-7Qw zG zEOL?9vgN7f-itlH)SVO!_T}C~lYQUi{kDHphfMnqrxS8(8NCyZ(u%4eyYdx?t>r1M z8&U%@IrR`K3o*y;-S?`(&b1AjJE6q5x1Sv7KRrbz%FHV|eeR~hBokf8-zp^*r8`)5Mjj4asB9M$@qt|xsxds4i^3gNI?WI=7+wzy=m2vILY;W@1yaE9^m0?StztpE$4w*OO$kJ|XE+jU9FiZPc zw(-%4O`M65;Mvn@N{7+jQt)o)sMy{We&S^ddIzm1gE@J%Drx9&{OD$K0l(d=kWkuS%!X?Ayv=^7youLTWgc z&32{g;o39UyKW%Y-loHn)g8~4OveGGug|HOP+1=aCyKb;k+B#T81shx!* z$5Mgy_5qJFmbexu<;sDdMc28~Ldt&N{}tDGaZv2@i-H%@5o>N3`yem5#Y>BZjXwcBiB))hRFoFzOIG z@?Fm3VB9Juf4)}1pTjCrC~B|LearT*Q525K-h%Z+oN&IxywHX4wvik0yR45dC!K`I zXs7t#JH}1&VOupMk;v{z<;-|UlW>+N(XOS4a>`Lt#`-9RGZ8$hJNkzy9I|5QAX#L6 zG=)!XbOh}!1U59UTaON}5`l)^vbVMSr}!W?!l8~99S7P$y$?bl-36tcL^Gch^m@qImE3cdL2myw1^Dy2Dclx#_IRc ztp7O9NHy+li*kef@Eb{=I5jx-uU$ul9hd$Lp0zgTeXK9z1oeHvHJC%JSTw zi<^cn|3A}vKzn2RjWM?TND|nQ`$**{_@iSFcvM)JIAOnJLQL3j(4we2Y=Or$6)BsD z$w!b9t3SG%lLF`9?`toXj@s=(S^bwtrOwCDxY?PHk2K3+Q zuh7o6c+V0(M-A6}p-%K>T@v{-LX>qq>w`BB34`T**suyD?*H$&CbmR=bA>xA*(z>S zXxbEUN`6{P5l%{lD2#g10rldOUiCXV!Hz~ud|pcjgh$4z6E-j7rwqLy=Gv+D)d~@E zE8mD$oS41-S(zGlXwg^^ku0;}6o_@%s*yI}z6B|&Fc(-&ZT?O3vI%$uKDr^j8+Cfs zyI!)}h`zVK57}}%zZ%Q>%}lktQRe(ck@XWA3tp3rIW;I*e!0$%Jj-BywJ+o(Zu+F; z%?*P52;X+%Y|O#m4sz^vfk!$S4}`aU2(3xeB2F!@dwpd$j*1f4O#n;c$E#TD3QNPK z$*VIQP%+yuRybwRjh$6j`atO1(~5%sv&7#QFVOwz+ZXs|x^i34nR9#p1nW@vNFI$x z&R@Fj1xN8Lr?EPNA7*UVXLvZYp#hlJ4L9Pa$39o$BgV@5q3NNL>|U2Nuj(I+&%=^0 z8~#a)YI=|OjLbFm?Usr{;K{8j^_17?Sx`&~6R>uypyegw-SqYJZK zT2Z+6A7vac`mc7;pY1zOU7hU6Et0|9K9gu=$HY$2HJ{fJ-T*%U@^bZ4eKId6Ks0Oh zF}|05g&ktTW%213_D;{?kq$Bm9Npwx43ILy#sd-+H*UUxdmtsFEkf^cg?pajS+aGS9wRCO9q;g@vTp)@kUbN>RFK>YDoqL zema9{bSRYTcbMZkPF@x|Zz5$ebukEwLZo(Z?N;b_rPDUPUn<6|CxU-S$o+PzjrM>Ia|J1eS{v8@SoHF2!CqXgQd52t2*2n4@lVCvwNF9wky-NB{}IE<2LwO zOMKRzgzva)t*@o{CUHMvh;S^*{N_B0>o%kP)*=UQ*dSjUS6yM{k! z0?aakIt)-k^1~I~bxFtPP8S>tK|UyjhSi3|epO$)g(gRK+_@wCBtzOyb^VEIb^ese zNZ^o?N!M<)_sdOx*;&2K$ROrAdD3wOOxLb$?X?=+n*`JzB>4$OVoPFwS&WUL#p@PO{_-i!(Pj4dx*J;gC|1c{TC^K}o$Aq;!=i*O*T*O! z!f?Q8|CI#DpHqIaefNhMLtZG5!)5TPvuR%{+yi>lN)7$F*XoD!Sz$aM)9Y{F3sPxJ zTWVIhVO(+7ZD#DXi%t(G=0Oc6Sij2T^UXY@h!S+Awk=o&A-yq9fcYct2v{T*3yS1wK={1ck=0=mGS^vTJs&$QLxH`I*-Zt zq9U11dkJN7aS!7AQ&+>PVy51R>@?hT8-bWjEd)+|EgLxgpiC(G4j08Gum2Tseaym6 zPyK_IEEG(e-nhK32A>2lt?V04N4c+AcK*jC2l9RLoITWupTZA%1FLTBihMiB#mYNc zUlo~N2-NNBN3iL9m|;GS|I%*M*ObnGHg9mNWQ%%T#@P_+@B1nIv;S1z-eZ32E$TBw zp#^41rxAk$RfJshO!MZolZS|vbuD=8^@yL^N9s@}u7__=kjj^JWE^8#0Q%JXq3Q9) zaxL3#hYUiA?&7;=^eAO`&B5gA=NZXOs+3S+iU@`t+q(|mJ_W@TP+%xZgcU_su)vpTs(MsFeZe{NGr&?3L0|eGWe7UWn zV!tQ)uY9uDr^gQS3GXoxGZo}Tp~y3OG{Mp&nC1T2aYd{zx8_lHyO@K$AE){HXLgpV zfg!u5yxYDtv)2V=j*8VRvR*~1n_=R(-TH790O)$~GKVEm2ffB93OnP4R2|B-nI2$ z{zHf3j>xINA}HTM)>he5C}^g?_A<0WGT@~hJ3PmCS-x|tQv|+h?fq1!d!aGfZ}UoB zkpg$j>btRPGZq5#iW=`(2xL)Xsv zuv}0}FIV_;K=Y!A)!JMqnLi;`q`Q2F`Sv+Z+B46N$bJKy_=kkA$((msd;`CR`_WCNUY@L> z=<&$2uC1oM!a8Dzg_!(bdg@L=_3G!!-4F6--MRrPLfXm98N*P}A*fr{Db;VG4&teL zDMQHqeEo>{HNQ{BYlbNp-QVa38Eq2t{TOB6#94{UjKPX$ut@A!)yIeZyw#BydnUbZ z&~M;IN7M$nvQ)6DWR39k#W7W5q=i{*zyPZzX2Etu0-taG!VnoMqW3RgCgyI_W!26S zvuiRoE5|Oll?G}n@u=tX4tLZ%?55vAvo0eh3J51(s$sb?Q~HmHjCu9d0+fTkUww_Q zFw04kc_~dmvwV=GiM;A{weiwwx$ju3CM$g+W8wLU;5J89W#0idy&~tDDQfF$97O>9U zArAdj9bHyx^(8-9##Q8POUEDlh$d?EZ!@~8)L$js{B<$D`^dxio7)W{nxu7efi1o_ z`vQB|9!}zo+=lD5oebGy6*MuuRIU3ukL(7J_LyaCNd8t|bjCk7$5ns7R08qVK96rr zB40}(B{9j1K;VW|o7(nK?nqtQ({ySe?l_e?_-gUyU|7W5hMjbQk5p`IaC+|(QJ4Ph z=ptggc7^>EbXjvK*QdY;BvA@1h~vGqmQ{@$<|V9OjnF`lMqTVzhV9MkerkVY#$tL> z>r(>wxyJq{Hea__q`?gwb$-I0-AG<*C_NlL_fY(MyE@?LMcP?%X$AaS#icT8qROJH zfyeLT7T7akFzf!&`NgZ)-}1}5?09>y5$tlJJJvPo!GdJ8Yu!6roZVS_I}DVpd*?RH8tdYikXtEIQwxrMb5YKCH!?? z>hVG44u_m5ujdyl;dRpLRQDD#24NO)OwybcJo#2>fgTg<*ZNgEGxBH|eq)Blg(QxK z>H?D(vC4t?#uiOOI;m>k4ebIQ?6i40KIzFL8)iHrU4Q#)`(~1j8+4L){{u!IOjlJC z91jI5MX=*?i1Qt7L7kO^$CIJW_bfW!k#}-L5jE|L0>Z4f7i0G7VKWAHj$PxrA-cu{ zF681j%cAX6^_wC3j4MztJ&%H>UptpLOFD!yQw}l`u_5WkpL~^UR}P4OZng-!Idb_+ zRyiQRPnuV>(Mdt+nr~OECLVSl{*+jMVQ!R%Jm)e|cq%x3`2#z|{s1-}@B6@WG~d?zR+Sh#PcK& zRX!jW)XU@&d2ActppdwFY_oeO+@ATOG1}0r!J(B0jd}j7>3^*LJ|(wE z;=I0C+mjVg2E2z$&Z8poyG}ZIK6^#NHOoaXV=q5=gKNqKx@{lGX*{CauaLncX8XO< z_sXeRzj#qw(XV~%tM-WhBe?=3dpG?BN20e)mC;N622iI~@xyo2We&f8Yl6z=le@ja zLAlKV$H9NSg986MXc{zJX0>1TC?P+$SzS)5uP(3qhBG7&*>BLzT1a}Uzjjvg&NKA5 z?10KQ)NwFMmKf4yOevJUe1@^}QH5;o8Q?`ZmLML1{{UAT@-(-PUi^lWH;&Uf&TSw2 zgc6pWxp`{99#hhRt{bU4h$ha(cU}6oRx*tDX(K9gNX0~3Bj9kE=A^xRY1Uf{kz|=; zIpqb?8T&;mdymz+JKL1q@7kj49e3&DI|`6*LXvtNo+Zff8)7qMD@M?c zk4J&{8y&UpdTBL%3I2KAv>mhz!cOiWG4v#$WxW#gMt@7CWNssYbYQ0jzM`PhcN!RI z<3pKE8)2QUuN;3XjdFwF^BJKkV1 z_^Fi;l_Tn1242}SM6wMmza8!qbxH7@g+n3`ZhaZUQy6%H>b|kZD%G7}oQgV(|MTgi zG|X~hd_K4i=4scQPE%e+z4C+vK3v&#Rm(6b5UsgI=pHn|-C9n`Vw!iGV$o#RPWtEO zm5RMybuC7fT3!UGZ5UN;plne);oLGCo3bGvgO|+>+Owf-U@MAUGH-dxpFWnerjLJf zOCeQPyd$1fk@LU&RZvxSZDP3_y@)x*V~U$UNMX3Yi?n;05ki%McD$IW==iKi(;(WA=m1nj zl5#7CT%AkER`^mpJrnz(BYV^$>`b0#k=3%9qkBbuH0#GvtGi{Q=i5Tgj6z2vu>xGX zUAVm{w3si3vT}TiS7C&m4^IY4xaESGYqxkw@fm)$zxBO(f4r*ReFmkP*!vjn`e^?i%m`CjqwJpFm+p5y!j5JNHH)!Q5PoAVVwAF1%zf zYP-;&+e^3z{B&11pB>m}%5j&d63zBguU7gUr}Eu53bqae$r3nRc#ry1@9z$x+wA8x z9PN`Q9Qm;eiAOB%X{yW`nKQpF7Sbfg4cy~{=gMM#ZH-}p@;5EJ7SAwr z10kr(=h|<>G==*-9p=XaTY773-c|I~OkblURYJEko3eSJ

4RUT#uGc`f|wIHtUm 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 Date: Thu, 29 Aug 2024 14:13:37 +0800 Subject: [PATCH 073/136] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91AI?= =?UTF-8?q?=20=E7=9F=A5=E8=AF=86=E5=BA=93:=20AiVectorFactory=20=E8=B4=9F?= =?UTF-8?q?=E8=B4=A3=E7=AE=A1=E7=90=86=E4=B8=8D=E5=90=8C=20EmbeddingModel?= =?UTF-8?q?=20=E5=AF=B9=E5=BA=94=E7=9A=84=20VectorStore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataobject/knowledge/AiKnowledgeDO.java | 2 +- .../knowledge/AiKnowledgeSegmentDO.java | 8 ++- .../AiKnowledgeDocumentServiceImpl.java | 37 ++++++++---- .../service/knowledge/AiKnowledgeService.java | 14 ++++- .../knowledge/AiKnowledgeServiceImpl.java | 9 +-- .../ai/service/model/AiApiKeyService.java | 18 ++++++ .../ai/service/model/AiApiKeyServiceImpl.java | 19 ++++++ .../ai/config/YudaoAiAutoConfiguration.java | 58 ++++++++++--------- .../ai/core/factory/AiModelFactory.java | 13 +++++ .../ai/core/factory/AiModelFactoryImpl.java | 32 +++++++++- .../ai/core/factory/AiVectorFactory.java | 27 +++++++++ .../ai/core/factory/AiVectorFactoryImpl.java | 51 ++++++++++++++++ .../RedisVectorStoreAutoConfiguration.java | 3 +- 13 files changed, 239 insertions(+), 52 deletions(-) create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactory.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactoryImpl.java diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java index 89e7486dc..756d8cdb3 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java @@ -45,7 +45,7 @@ public class AiKnowledgeDO extends BaseDO { @TableField(typeHandler = JacksonTypeHandler.class) private List visibilityPermissions; /** - * 嵌入模型编号,高质量模式时维护 + * 嵌入模型编号 */ private Long modelId; /** diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java index 2032bfd5e..6d5da06ea 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java @@ -24,10 +24,14 @@ public class AiKnowledgeSegmentDO extends BaseDO { * 向量库的编号 */ private String vectorId; - // TODO @新:knowledgeId 加个,会方便点 + /** + * 知识库编号 + * 关联 {@link AiKnowledgeDO#getId()} + */ + private Long knowledgeId; /** * 文档编号 - * + *

* 关联 {@link AiKnowledgeDocumentDO#getId()} */ private Long documentId; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 2af8b9d90..9be3dd1af 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -6,24 +6,27 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; 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.tokenizer.TokenCountEstimator; import org.springframework.ai.transformer.splitter.TokenTextSplitter; -import org.springframework.ai.vectorstore.RedisVectorStore; +import org.springframework.ai.vectorstore.VectorStore; import org.springframework.core.io.ByteArrayResource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.Objects; /** * AI 知识库-文档 Service 实现类 @@ -42,9 +45,14 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic @Resource private TokenTextSplitter tokenTextSplitter; @Resource - private TokenCountEstimator TOKEN_COUNT_ESTIMATOR; + private TokenCountEstimator tokenCountEstimator; + @Resource - private RedisVectorStore vectorStore; + private AiApiKeyService apiKeyService; + @Resource + private AiKnowledgeService knowledgeService; + @Resource + private AiChatModelService chatModelService; // TODO 芋艿:需要 review 下,代码格式; @@ -53,18 +61,18 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) { // 1.1 下载文档 String url = createReqVO.getUrl(); - TikaDocumentReader loader = new TikaDocumentReader(downloadFile(url)); // 1.2 加载文档 + TikaDocumentReader loader = new TikaDocumentReader(downloadFile(url)); List documents = loader.get(); Document document = CollUtil.getFirst(documents); - // TODO @xin:是不是不存在,就抛出异常呀;厚泽 return 呀; - Integer tokens = Objects.nonNull(document) ? TOKEN_COUNT_ESTIMATOR.estimate(document.getContent()) : 0; - Integer wordCount = Objects.nonNull(document) ? document.getContent().length() : 0; + String content = document.getContent(); + Integer tokens = tokenCountEstimator.estimate(content); + Integer wordCount = content.length(); + // 1.3 文档记录入库 AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class) .setTokens(tokens).setWordCount(wordCount) .setStatus(CommonStatusEnum.ENABLE.getStatus()).setSliceStatus(AiKnowledgeDocumentStatusEnum.SUCCESS.getStatus()); - // 1.2 文档记录入库 documentMapper.insert(documentDO); Long documentId = documentDO.getId(); if (CollUtil.isEmpty(documents)) { @@ -75,11 +83,16 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic List segments = tokenTextSplitter.apply(documents); // 2.2 分段内容入库 List segmentDOList = CollectionUtils.convertList(segments, - segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId) - .setTokens(TOKEN_COUNT_ESTIMATOR.estimate(segment.getContent())).setWordCount(segment.getContent().length()) + segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId).setKnowledgeId(createReqVO.getKnowledgeId()) + .setTokens(tokenCountEstimator.estimate(segment.getContent())).setWordCount(segment.getContent().length()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); segmentMapper.insertBatch(segmentDOList); - // 3 向量化并存储 + + AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId()); + AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId()); + // 3.1 获取向量存储实例 + VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId()); + // 3.2 向量化并存储 vectorStore.add(segments); return documentId; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java index 91b0c9b3e..bf7e8886a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.ai.service.knowledge; + import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeCreateMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdateMyReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; /** * AI 知识库-基础信息 Service 接口 @@ -13,7 +15,7 @@ public interface AiKnowledgeService { * 创建【我的】知识库 * * @param createReqVO 创建信息 - * @param userId 用户编号 + * @param userId 用户编号 * @return 编号 */ Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId); @@ -23,8 +25,16 @@ public interface AiKnowledgeService { * 创建【我的】知识库 * * @param updateReqVO 更新信息 - * @param userId 用户编号 + * @param userId 用户编号 */ void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId); + + /** + * 校验知识库是否存在 + * + * @param id 记录编号 + */ + AiKnowledgeDO validateKnowledgeExists(Long id); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java index 5889bcef7..70442936e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java @@ -29,7 +29,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { private AiChatModelService chatModalService; @Resource - private AiKnowledgeMapper knowledgeBaseMapper; + private AiKnowledgeMapper knowledgeMapper; @Override public Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId) { @@ -39,7 +39,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { // 2. 插入知识库 AiKnowledgeDO knowledgeBase = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class) .setModel(model.getModel()).setUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); - knowledgeBaseMapper.insert(knowledgeBase); + knowledgeMapper.insert(knowledgeBase); return knowledgeBase.getId(); } @@ -56,11 +56,12 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { // 2. 更新知识库 AiKnowledgeDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class); updateDO.setModel(model.getModel()); - knowledgeBaseMapper.updateById(updateDO); + knowledgeMapper.updateById(updateDO); } + @Override public AiKnowledgeDO validateKnowledgeExists(Long id) { - AiKnowledgeDO knowledgeBase = knowledgeBaseMapper.selectById(id); + AiKnowledgeDO knowledgeBase = knowledgeMapper.selectById(id); if (knowledgeBase == null) { throw exception(KNOWLEDGE_NOT_EXISTS); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java index fe8fdd194..603325da6 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java @@ -9,7 +9,9 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveR import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; import jakarta.validation.Valid; import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; +import org.springframework.ai.vectorstore.VectorStore; import java.util.List; @@ -83,6 +85,14 @@ public interface AiApiKeyService { */ ChatModel getChatModel(Long id); + /** + * 获得 EmbeddingModel 对象 + * + * @param id 编号 + * @return EmbeddingModel 对象 + */ + EmbeddingModel getEmbeddingModel(Long id); + /** * 获得 ImageModel 对象 * @@ -111,4 +121,12 @@ public interface AiApiKeyService { */ SunoApi getSunoApi(); + /** + * 获得 vector 对象 + * + * @param id 编号 + * @return VectorStore 对象 + */ + VectorStore getOrCreateVectorStore(Long id); + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java index 590b10a4c..d3e9b7cfb 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.service.model; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; +import cn.iocoder.yudao.framework.ai.core.factory.AiVectorFactory; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; @@ -13,7 +14,9 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper; import jakarta.annotation.Resource; import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; +import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -36,6 +39,8 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { @Resource private AiModelFactory modelFactory; + @Resource + private AiVectorFactory vectorFactory; @Override public Long createApiKey(AiApiKeySaveReqVO createReqVO) { @@ -104,6 +109,13 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { return modelFactory.getOrCreateChatModel(platform, apiKey.getApiKey(), apiKey.getUrl()); } + @Override + public EmbeddingModel getEmbeddingModel(Long id) { + AiApiKeyDO apiKey = validateApiKey(id); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); + return modelFactory.getOrCreateEmbeddingModel(platform, apiKey.getApiKey(), apiKey.getUrl()); + } + @Override public ImageModel getImageModel(AiPlatformEnum platform) { AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); @@ -132,4 +144,11 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { } return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl()); } + + @Override + public VectorStore getOrCreateVectorStore(Long id) { + AiApiKeyDO apiKey = validateApiKey(id); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); + return vectorFactory.getOrCreateVectorStore(getEmbeddingModel(id), platform, apiKey.getApiKey(), apiKey.getUrl()); + } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 8566a0941..50eacd00e 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -2,6 +2,8 @@ package cn.iocoder.yudao.framework.ai.config; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl; +import cn.iocoder.yudao.framework.ai.core.factory.AiVectorFactory; +import cn.iocoder.yudao.framework.ai.core.factory.AiVectorFactoryImpl; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; @@ -10,22 +12,15 @@ 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.tokenizer.JTokkitTokenCountEstimator; import org.springframework.ai.tokenizer.TokenCountEstimator; 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 自动配置 @@ -43,6 +38,12 @@ public class YudaoAiAutoConfiguration { return new AiModelFactoryImpl(); } + @Bean + public AiVectorFactory aiVectorFactory() { + return new AiVectorFactoryImpl(); + } + + // ========== 各种 AI Client 创建 ========== @Bean @@ -85,30 +86,31 @@ public class YudaoAiAutoConfiguration { } // ========== rag 相关 ========== - @Bean - @Lazy // TODO 芋艿:临时注释,避免无法启动 - public EmbeddingModel transformersEmbeddingClient() { - return new TransformersEmbeddingModel(MetadataMode.EMBED); - } + // TODO @xin 免费版本 +// @Bean +// @Lazy // TODO 芋艿:临时注释,避免无法启动」 +// public EmbeddingModel transformersEmbeddingClient() { +// return new TransformersEmbeddingModel(MetadataMode.EMBED); +// } /** - * TODO @xin 抽离出去,根据具体模型走 + * TODO @xin 默认版本先不弄,目前都先取对应的 EmbeddingModel */ - @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 RedisVectorStore vectorStore(TongYiTextEmbeddingModel tongYiTextEmbeddingModel, RedisVectorStoreProperties properties, +// RedisProperties redisProperties) { +// var config = RedisVectorStore.RedisVectorStoreConfig.builder() +// .withIndexName(properties.getIndex()) +// .withPrefix(properties.getPrefix()) +// .build(); +// +// RedisVectorStore redisVectorStore = new RedisVectorStore(config, tongYiTextEmbeddingModel, +// new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), +// properties.isInitializeSchema()); +// redisVectorStore.afterPropertiesSet(); +// return redisVectorStore; +// } @Bean @Lazy // TODO 芋艿:临时注释,避免无法启动 diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java index b6d7b3dd0..6f628ea4d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java @@ -4,6 +4,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.ai.core.model.suno.api.SunoApi; import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; /** @@ -25,6 +26,18 @@ public interface AiModelFactory { */ ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url); + /** + * 基于指定配置,获得 EmbeddingModel 对象 + *

+ * 如果不存在,则进行创建 + * + * @param platform 平台 + * @param apiKey API KEY + * @param url API URL + * @return ChatModel 对象 + */ + EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url); + /** * 基于默认配置,获得 ChatModel 对象 * diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index c9b04dc1e..5c3524e66 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -21,6 +21,7 @@ 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.alibaba.dashscope.embeddings.TextEmbedding; import com.azure.ai.openai.OpenAIClient; import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration; import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties; @@ -37,6 +38,7 @@ 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.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; import org.springframework.ai.model.function.FunctionCallbackContext; import org.springframework.ai.ollama.OllamaChatModel; @@ -97,6 +99,21 @@ public class AiModelFactoryImpl implements AiModelFactory { }); } + @Override + public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url) { + String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url); + return Singleton.get(cacheKey, (Func0) () -> { + // TODO @xin 先测试一个 + switch (platform) { + case TONG_YI: + return buildTongYiEmbeddingModel(apiKey); + default: + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); + } + }); + } + + @Override public ChatModel getDefaultChatModel(AiPlatformEnum platform) { //noinspection EnhancedSwitchMigration @@ -239,7 +256,7 @@ public class AiModelFactoryImpl implements AiModelFactory { /** * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel( - * ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)} + *ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)} */ private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); @@ -249,7 +266,7 @@ public class AiModelFactoryImpl implements AiModelFactory { /** * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel( - * ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + *ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} */ private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); @@ -315,4 +332,15 @@ public class AiModelFactoryImpl implements AiModelFactory { return new StabilityAiImageModel(stabilityAiApi); } + // ========== 各种创建 EmbeddingModel 的方法 ========== + + /** + * 可参考 {@link TongYiAutoConfiguration#tongYiTextEmbeddingClient(TextEmbedding, TongYiConnectionProperties)} + */ + private EmbeddingModel buildTongYiEmbeddingModel(String apiKey) { + TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); + connectionProperties.setApiKey(apiKey); + return new TongYiAutoConfiguration().tongYiTextEmbeddingClient(SpringUtil.getBean(TextEmbedding.class), connectionProperties); + } + } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactory.java new file mode 100644 index 000000000..5e43c9bab --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactory.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.ai.core.factory; + +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.vectorstore.VectorStore; + +/** + * AI Vector 模型工厂的接口类 + * @author xiaoxin + */ +public interface AiVectorFactory { + + + /** + * 基于指定配置,获得 VectorStore 对象 + *

+ * 如果不存在,则进行创建 + * + * @param embeddingModel 嵌入模型 + * @param platform 平台 + * @param apiKey API KEY + * @param url API URL + * @return VectorStore 对象 + */ + VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url); + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactoryImpl.java new file mode 100644 index 000000000..d16b595f3 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactoryImpl.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.framework.ai.core.factory; + +import cn.hutool.core.lang.Singleton; +import cn.hutool.core.lang.func.Func0; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.vectorstore.RedisVectorStore; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import redis.clients.jedis.JedisPooled; + +/** + * AI Vector 模型工厂的实现类 + * 使用 redisVectorStore 实现 VectorStore + * + * @author xiaoxin + */ +public class AiVectorFactoryImpl implements AiVectorFactory { + + @Override + public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url) { + String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url); + return Singleton.get(cacheKey, (Func0) () -> { + // TODO 芋艿 @xin 这两个配置取哪好呢 + // TODO 不同模型的向量维度可能会不一样,目前看貌似是以 index 来做区分的,维度不一样存不到一个 index 上 + String index = "default-index"; + String prefix = "default:"; + var config = RedisVectorStore.RedisVectorStoreConfig.builder() + .withIndexName(index) + .withPrefix(prefix) + .build(); + RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); + RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel, + new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), + true); + redisVectorStore.afterPropertiesSet(); + return redisVectorStore; + }); + } + + + private static String buildClientCacheKey(Class clazz, Object... params) { + if (ArrayUtil.isEmpty(params)) { + return clazz.getName(); + } + return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_")); + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java index 615b05f78..a72d50c4a 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java @@ -19,6 +19,7 @@ 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.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; @@ -38,7 +39,7 @@ import redis.clients.jedis.JedisPooled; */ @AutoConfiguration(after = RedisAutoConfiguration.class) @ConditionalOnClass({JedisPooled.class, JedisConnectionFactory.class, RedisVectorStore.class, EmbeddingModel.class}) -//@ConditionalOnBean(JedisConnectionFactory.class) +@ConditionalOnBean(JedisConnectionFactory.class) @EnableConfigurationProperties(RedisVectorStoreProperties.class) public class RedisVectorStoreAutoConfiguration { From abf5a22cd09304e106ba733eb5e615293a021afa Mon Sep 17 00:00:00 2001 From: xiaoxin <718949661@qq.com> Date: Thu, 29 Aug 2024 14:16:23 +0800 Subject: [PATCH 074/136] =?UTF-8?q?=E3=80=90=E4=BC=98=E5=8C=96=E3=80=91AI?= =?UTF-8?q?=20=E7=9F=A5=E8=AF=86=E5=BA=93:=20=E9=87=8D=E5=91=BD=E5=90=8D?= =?UTF-8?q?=20AiVectorFactory=20->=20AiVectorStoreFactory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/service/model/AiApiKeyServiceImpl.java | 4 ++-- .../framework/ai/config/YudaoAiAutoConfiguration.java | 8 ++++---- .../{AiVectorFactory.java => AiVectorStoreFactory.java} | 2 +- ...ctorFactoryImpl.java => AiVectorStoreFactoryImpl.java} | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/{AiVectorFactory.java => AiVectorStoreFactory.java} (94%) rename yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/{AiVectorFactoryImpl.java => AiVectorStoreFactoryImpl.java} (96%) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java index d3e9b7cfb..25eec75b7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.ai.service.model; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; -import cn.iocoder.yudao.framework.ai.core.factory.AiVectorFactory; +import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactory; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; @@ -40,7 +40,7 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { @Resource private AiModelFactory modelFactory; @Resource - private AiVectorFactory vectorFactory; + private AiVectorStoreFactory vectorFactory; @Override public Long createApiKey(AiApiKeySaveReqVO createReqVO) { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 50eacd00e..79a1f345b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.ai.config; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl; -import cn.iocoder.yudao.framework.ai.core.factory.AiVectorFactory; -import cn.iocoder.yudao.framework.ai.core.factory.AiVectorFactoryImpl; +import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactory; +import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactoryImpl; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; @@ -39,8 +39,8 @@ public class YudaoAiAutoConfiguration { } @Bean - public AiVectorFactory aiVectorFactory() { - return new AiVectorFactoryImpl(); + public AiVectorStoreFactory aiVectorFactory() { + return new AiVectorStoreFactoryImpl(); } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java similarity index 94% rename from yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactory.java rename to yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java index 5e43c9bab..3e138cbd3 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactory.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java @@ -8,7 +8,7 @@ import org.springframework.ai.vectorstore.VectorStore; * AI Vector 模型工厂的接口类 * @author xiaoxin */ -public interface AiVectorFactory { +public interface AiVectorStoreFactory { /** diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java similarity index 96% rename from yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactoryImpl.java rename to yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java index d16b595f3..b3291c6b7 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java @@ -18,7 +18,7 @@ import redis.clients.jedis.JedisPooled; * * @author xiaoxin */ -public class AiVectorFactoryImpl implements AiVectorFactory { +public class AiVectorStoreFactoryImpl implements AiVectorStoreFactory { @Override public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url) { From 7d53a6dd527d0bc381f12abe51977679320d99c3 Mon Sep 17 00:00:00 2001 From: xiaoxin <718949661@qq.com> Date: Thu, 29 Aug 2024 15:56:26 +0800 Subject: [PATCH 075/136] =?UTF-8?q?=E3=80=90=E4=BC=98=E5=8C=96=E3=80=91AI?= =?UTF-8?q?=20=E7=9F=A5=E8=AF=86=E5=BA=93:=20=E5=90=91=E9=87=8F=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E8=A1=A5=E5=85=85=20knowledgeId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataobject/knowledge/AiKnowledgeSegmentDO.java | 2 ++ .../knowledge/AiKnowledgeDocumentServiceImpl.java | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java index 6d5da06ea..fc758ce31 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java @@ -15,6 +15,8 @@ import lombok.Data; @Data public class AiKnowledgeSegmentDO extends BaseDO { + public static final String FIELD_KNOWLEDGE_ID = "knowledgeId"; + /** * 编号 */ diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 9be3dd1af..bcfb64c55 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -27,6 +27,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Map; /** * AI 知识库-文档 Service 实现类 @@ -83,16 +84,22 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic List segments = tokenTextSplitter.apply(documents); // 2.2 分段内容入库 List segmentDOList = CollectionUtils.convertList(segments, - segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId).setKnowledgeId(createReqVO.getKnowledgeId()) + segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId).setKnowledgeId(createReqVO.getKnowledgeId()).setVectorId(segment.getId()) .setTokens(tokenCountEstimator.estimate(segment.getContent())).setWordCount(segment.getContent().length()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); segmentMapper.insertBatch(segmentDOList); + // 3.1 document 补充源数据 + segments.forEach(segment -> { + Map metadata = segment.getMetadata(); + metadata.put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, createReqVO.getKnowledgeId()); + }); + AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId()); AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId()); - // 3.1 获取向量存储实例 + // 3.2 获取向量存储实例 VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId()); - // 3.2 向量化并存储 + // 3.3 向量化并存储 vectorStore.add(segments); return documentId; } From 4208339d4d3206f7f011582100ea295d01f54491 Mon Sep 17 00:00:00 2001 From: xiaoxin <718949661@qq.com> Date: Thu, 29 Aug 2024 16:03:20 +0800 Subject: [PATCH 076/136] =?UTF-8?q?=E3=80=90=E4=BC=98=E5=8C=96=E3=80=91AI?= =?UTF-8?q?=20:=20=E5=88=A0=E9=99=A4=20AiChatRoleEnum#role?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/module/ai/enums/AiChatRoleEnum.java | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java index 811189efe..029961bf3 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java @@ -1,11 +1,8 @@ package cn.iocoder.yudao.module.ai.enums; -import cn.iocoder.yudao.framework.common.core.IntArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; -import java.util.Arrays; - /** * AI 内置聊天角色的枚举 * @@ -13,16 +10,16 @@ import java.util.Arrays; */ @AllArgsConstructor @Getter -public enum AiChatRoleEnum implements IntArrayValuable { +public enum AiChatRoleEnum { - AI_WRITE_ROLE(1, "写作助手", """ + AI_WRITE_ROLE("写作助手", """ 你是一位出色的写作助手,能够帮助用户生成创意和灵感,并在用户提供场景和提示词时生成对应的回复。你的任务包括: 1. 撰写建议:根据用户提供的主题或问题,提供详细的写作建议、情节发展方向、角色设定以及背景描写,确保内容结构清晰、有逻辑。 2. 回复生成:根据用户提供的场景和提示词,生成合适的对话或文字回复,确保语气和风格符合场景需求。 除此之外不需要除了正文内容外的其他回复,如标题、开头、任何解释性语句或道歉。 """), - AI_MIND_MAP_ROLE(2, "导图助手", """ + AI_MIND_MAP_ROLE("导图助手", """ 你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子: # Geek-AI 助手 ## 完整的开源系统 @@ -39,11 +36,6 @@ public enum AiChatRoleEnum implements IntArrayValuable { 除此之外不要任何解释性语句。 """); - // TODO @xin:这个 role 是不是删除掉好点哈。= = 目前主要是没做角色枚举。这里多了 role 反倒容易误解哈 - /** - * 角色 - */ - private final Integer role; /** * 角色名 */ @@ -54,11 +46,4 @@ public enum AiChatRoleEnum implements IntArrayValuable { */ private final String systemMessage; - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiChatRoleEnum::getRole).toArray(); - - @Override - public int[] array() { - return ARRAYS; - } - } From e0d9f7cfbaccf578b9b0351f8eec077404183c7e Mon Sep 17 00:00:00 2001 From: puhui999 Date: Thu, 29 Aug 2024 18:19:54 +0800 Subject: [PATCH 077/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91=E5=95=86=E5=9F=8E:=20=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E6=B4=BB=E5=8A=A8=E4=B8=8B=E5=8D=95=E5=90=8E=EF=BC=8C?= =?UTF-8?q?=E8=B5=A0=E9=80=81=E7=A7=AF=E5=88=86=E3=80=81=E4=BC=98=E6=83=A0?= =?UTF-8?q?=E5=8A=B5=E3=80=81=E5=8C=85=E9=82=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/reward/vo/RewardActivityBaseVO.java | 2 +- .../CombinationRecordServiceImpl.java | 2 +- .../module/trade/api/order/TradeOrderApi.java | 5 +- .../trade/enums/ErrorCodeConstants.java | 1 + .../trade/api/order/TradeOrderApiImpl.java | 5 +- .../dataobject/order/TradeOrderItemDO.java | 13 ++++ .../order/TradeOrderUpdateService.java | 6 +- .../order/TradeOrderUpdateServiceImpl.java | 11 +-- .../handler/TradeCouponOrderHandler.java | 7 +- .../price/bo/TradePriceCalculateRespBO.java | 30 +++++++- .../TradeDeliveryPriceCalculator.java | 20 +++--- .../TradePriceCalculatorHelper.java | 6 +- .../TradeRewardActivityPriceCalculator.java | 68 ++++++++++++++++--- 13 files changed, 135 insertions(+), 41 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java index d498b5e9f..f932a58d6 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java @@ -96,7 +96,7 @@ public class RewardActivityBaseVO { @AssertTrue(message = "优惠劵和数量必须一一对应") @JsonIgnore public boolean isCouponCountsValid() { - return BooleanUtil.isFalse(givePoint) || CollUtil.size(couponIds) == CollUtil.size(couponCounts); + return BooleanUtil.isFalse(giveCoupon) || CollUtil.size(couponIds) == CollUtil.size(couponCounts); } @AssertTrue(message = "赠送的积分不能小于 1") diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java index c1449d60d..6f5ac3f62 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java @@ -340,7 +340,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService { CombinationRecordStatusEnum.FAILED); // 2. 订单取消 headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId(), - TradeOrderCancelTypeEnum.COMBINATION_CLOSE)); + TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType())); } /** diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java index d21e88a44..64a269482 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.trade.api.order; import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO; -import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum; import java.util.Collection; import java.util.List; @@ -34,8 +33,8 @@ public interface TradeOrderApi { * * @param userId 用户编号 * @param orderId 订单编号 - * @param cancelTypeEnum 取消类型 + * @param cancelType 取消类型 */ - void cancelPaidOrder(Long userId, Long orderId, TradeOrderCancelTypeEnum cancelTypeEnum); + void cancelPaidOrder(Long userId, Long orderId, Integer cancelType); } diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java index 696eeba1b..a797fa5bd 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java @@ -60,6 +60,7 @@ public interface ErrorCodeConstants { ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板"); ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵"); ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量"); + ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_006, "计算快递运费异常,配送方式不匹配"); // ========== 物流 Express 模块 1-011-004-000 ========== ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在"); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java index edb675f29..5e50f43ab 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.trade.api.order; import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO; import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert; -import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum; import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService; import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService; import jakarta.annotation.Resource; @@ -37,8 +36,8 @@ public class TradeOrderApiImpl implements TradeOrderApi { } @Override - public void cancelPaidOrder(Long userId, Long orderId, TradeOrderCancelTypeEnum cancelTypeEnum) { - tradeOrderUpdateService.cancelPaidOrder(userId, orderId, cancelTypeEnum); + public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) { + tradeOrderUpdateService.cancelPaidOrder(userId, orderId, cancelType); } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java index 450bc764f..b69997605 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java @@ -160,6 +160,19 @@ public class TradeOrderItemDO extends BaseDO { */ private Integer vipPrice; + /** + * 赠送的优惠劵编号的数组 + * + * 目的:用于后续取消或者售后订单时,需要扣减赠送 + */ + private List couponIds; + /** + * 赠送的优惠券数量的数组 + * + * 目的:用于后续取消或者售后订单时,需要扣减赠送 + */ + private List couponCounts; + // ========== 售后基本信息 ========== /** diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java index d03826924..4508138ff 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java @@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettle import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO; import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; -import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum; import jakarta.validation.constraints.NotNull; /** @@ -186,14 +185,13 @@ public interface TradeOrderUpdateService { */ void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId); - // TODO @puhui999:不传递枚举哈。因为 rpc 不好支持。 /** * 取消支付订单 * * @param userId 用户编号 * @param orderId 订单编号 - * @param cancelType 取消类型 + * @param cancelType 取消类型 */ - void cancelPaidOrder(Long userId, Long orderId, TradeOrderCancelTypeEnum cancelType); + void cancelPaidOrder(Long userId, Long orderId, Integer cancelType); } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index 3eda99411..a4f29a5b8 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -858,8 +858,11 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @Override @Transactional(rollbackFor = Exception.class) - public void cancelPaidOrder(Long userId, Long orderId, TradeOrderCancelTypeEnum cancelType) { - // TODO @puhui999:这里校验下 cancelType 只允许拼团关闭; + public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) { + // 1. 这里校验下 cancelType 只允许拼团关闭; + if (!TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType().equals(cancelType)) { + return; + } // 1.1 检验订单存在 TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); if (order == null) { @@ -876,13 +879,13 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { } // 2.1 取消订单 - cancelOrder0(order, cancelType); + cancelOrder0(order, TradeOrderCancelTypeEnum.COMBINATION_CLOSE); // 2.2 创建退款单 payRefundApi.createRefund(new PayRefundCreateReqDTO() .setAppKey(tradeOrderProperties.getPayAppKey()).setUserIp(getClientIP()) // 支付应用 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 .setMerchantRefundId(String.valueOf(order.getId())) - .setReason(cancelType.getName()).setPrice(order.getPayPrice()));// 价格信息 + .setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice()));// 价格信息 } /** diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java index 478de450f..eb4816719 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java @@ -4,9 +4,9 @@ import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO; +import jakarta.annotation.Resource; import org.springframework.stereotype.Component; -import jakarta.annotation.Resource; import java.util.List; /** @@ -30,6 +30,11 @@ public class TradeCouponOrderHandler implements TradeOrderHandler { .setOrderId(order.getId())); } + @Override + public void afterPayOrder(TradeOrderDO order, List orderItems) { + + } + @Override public void afterCancelOrder(TradeOrderDO order, List orderItems) { if (order.getCouponId() == null || order.getCouponId() <= 0) { diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java index b7482407c..32043df0e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java @@ -67,6 +67,21 @@ public class TradePriceCalculateRespBO { */ private Long bargainActivityId; + /** + * 是否包邮 + */ + private Boolean freeDelivery; + + // TODO @puhui999: 订单保存时需要保存 + /** + * 赠送的优惠劵编号的数组 + */ + private List couponIds; + /** + * 赠送的优惠券数量的数组 + */ + private List couponCounts; + /** * 订单价格 */ @@ -213,8 +228,19 @@ public class TradePriceCalculateRespBO { */ private Long categoryId; + // ========== 物流相关字段 ========= + /** - * 运费模板 Id + * 配送方式数组 + * + * 对应 DeliveryTypeEnum 枚举 + */ + private List deliveryTypes; + + /** + * 物流配置模板编号 + * + * 对应 TradeDeliveryExpressTemplateDO 的 id 编号 */ private Long deliveryTemplateId; @@ -234,7 +260,7 @@ public class TradePriceCalculateRespBO { private List properties; /** - * 使用的积分 + * 赠送的积分 */ private Integer givePoint; diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java index 2fa0d44af..d0dcf9cfd 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java @@ -6,8 +6,6 @@ import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.module.member.api.address.MemberAddressApi; import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; -import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; -import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO; import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryExpressChargeModeEnum; @@ -30,8 +28,7 @@ import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; -import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PICK_UP_STORE_NOT_EXISTS; -import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*; /** * 运费的 {@link TradePriceCalculator} 实现类 @@ -52,19 +49,15 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { private DeliveryExpressTemplateService deliveryExpressTemplateService; @Resource private TradeConfigService tradeConfigService; - @Resource - private ProductSpuApi productSpuApi; @Override public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { if (param.getDeliveryType() == null) { return; } - // TODO @puhui999:1)TradePriceCalculateRespBO 传递进来 delveryType 配送方式,减少读取;2)如果不匹配,抛出业务异常; = = 不然就不扣钱啦。 // 校验是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈 - List spuList = productSpuApi.getSpuList(convertSet(result.getItems(), OrderItem::getSpuId)); - if (anyMatch(spuList, item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) { - return; + if (anyMatch(result.getItems(), item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) { + throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL); } if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) { @@ -101,7 +94,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { return; } - // 情况二:快递模版 + // 情况二:活动包邮 + if (Boolean.TRUE.equals(result.getFreeDelivery())) { + return; + } + + // 情况三:快递模版 // 2.1 过滤出已选中的商品 SKU List selectedItem = filterList(result.getItems(), OrderItem::getSelected); Set deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java index 891f1e0dc..cb8f97c11 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java @@ -31,8 +31,8 @@ public class TradePriceCalculatorHelper { List spuList, List skuList) { // 创建 PriceCalculateRespDTO 对象 TradePriceCalculateRespBO result = new TradePriceCalculateRespBO(); - result.setType(getOrderType(param)); - result.setPromotions(new ArrayList<>()); + result.setType(getOrderType(param)).setPromotions(new ArrayList<>()) + .setCouponIds(new ArrayList<>()).setCouponCounts(new ArrayList<>()); // 创建它的 OrderItem 属性 result.setItems(new ArrayList<>(param.getItems().size())); @@ -60,7 +60,7 @@ public class TradePriceCalculatorHelper { .setWeight(sku.getWeight()).setVolume(sku.getVolume()); // spu 信息 orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId()) - .setDeliveryTemplateId(spu.getDeliveryTemplateId()) + .setDeliveryTypes(spu.getDeliveryTypes()).setDeliveryTemplateId(spu.getDeliveryTemplateId()) .setGivePoint(spu.getGiveIntegral()).setUsePoint(0); if (StrUtil.isBlank(orderItem.getPicUrl())) { orderItem.setPicUrl(spu.getPicUrl()); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java index 490c2aea7..fa5a61f3a 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -3,9 +3,11 @@ package cn.iocoder.yudao.module.trade.service.price.calculator; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi; import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; @@ -14,6 +16,8 @@ import jakarta.annotation.Resource; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; @@ -61,7 +65,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator if (rule == null) { TradePriceCalculatorHelper.addNotMatchPromotion(result, orderItems, rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(), - getRewardActivityNotMeetTip(rewardActivity)); + getRewardActivityNotMeetTip(rewardActivity, orderItems)); return; } @@ -84,6 +88,26 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator TradePriceCalculatorHelper.recountPayPrice(orderItem); } TradePriceCalculatorHelper.recountAllPrice(result); + + // 4.1 记录赠送的积分 + if (rule.getGivePoint()) { + List dividePoints = TradePriceCalculatorHelper.dividePrice(orderItems, rule.getPoint()); + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); + // 商品可能赠送了积分,所以这里要加上 + orderItem.setGivePoint(orderItem.getGivePoint() + dividePoints.get(i)); + } + } + // 4.2 记录订单是否包邮 + if (rule.getFreeDelivery()) { + // 只要满足一个活动包邮那么这单就包邮 + result.setFreeDelivery(true); + } + // 4.3 记录赠送的优惠券 + if (rule.getGiveCoupon()) { + // TODO @puhui999: 需要考虑赠送的优惠券是否重叠,重叠则对数量进行累加 + result.setCouponIds(rule.getCouponIds()).setCouponCounts(rule.getCouponCounts()); + } } /** @@ -95,9 +119,21 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator */ private List filterMatchActivityOrderItems(TradePriceCalculateRespBO result, RewardActivityMatchRespDTO rewardActivity) { - // TODO @puhui999:是不是得根据类型过滤哈 - return filterList(result.getItems(), - orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getSpuId())); + // 情况一:全部商品都可以参与 + if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) { + return result.getItems(); + } + // 情况二:指定商品参与 + if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) { + return filterList(result.getItems(), + orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getSpuId())); + } + // 情况三:指定商品类型参与 + if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) { + return filterList(result.getItems(), + orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getCategoryId())); + } + return List.of(); } /** @@ -130,14 +166,30 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator } /** - * 获得满减送活动部匹配时的提示 + * 获得满减送活动不匹配时的提示 * * @param rewardActivity 满减送活动 * @return 提示 */ - private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity) { - // TODO 芋艿:后面再想想;应该找第一个规则,算下还差多少即可。 - return "TODO"; + private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity, + List orderItems) { + // 1. 计算数量和价格 + Integer count = TradePriceCalculatorHelper.calculateTotalCount(orderItems); + Integer price = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); + assert count != null && price != null; + + // 2. 构建不满足时的提示信息-按最低档规则算 + String meetTip = "满减送:购满 {} {},可以减 {} 元"; + List rules = new ArrayList<>(rewardActivity.getRules()); + rules.sort(Comparator.comparing(RewardActivityMatchRespDTO.Rule::getLimit)); // 按优惠门槛降序 + RewardActivityMatchRespDTO.Rule rule = rules.get(0); + if (PromotionConditionTypeEnum.PRICE.getType().equals(rewardActivity.getConditionType())) { + return StrUtil.format(meetTip, rule.getLimit(), "元", MoneyUtils.fenToYuanStr(rule.getDiscountPrice())); + } + if (PromotionConditionTypeEnum.COUNT.getType().equals(rewardActivity.getConditionType())) { + return StrUtil.format(meetTip, rule.getLimit(), "件", MoneyUtils.fenToYuanStr(rule.getDiscountPrice())); + } + return StrUtil.EMPTY; } } From 3e66a922bf37a0446aaa244620ef5e0b99644c77 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Thu, 29 Aug 2024 21:45:44 +0800 Subject: [PATCH 078/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91=E5=95=86=E5=9F=8E:=20=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E6=B4=BB=E5=8A=A8=E4=B8=8B=E5=8D=95=E5=90=8E=EF=BC=8C?= =?UTF-8?q?=E8=B5=A0=E9=80=81=E4=BC=98=E6=83=A0=E5=8A=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../promotion/api/coupon/CouponApi.java | 12 +++++++- .../promotion/api/coupon/CouponApiImpl.java | 8 ++++- .../service/coupon/CouponService.java | 9 ++++++ .../service/coupon/CouponServiceImpl.java | 30 +++++++++++++++++-- .../dal/dataobject/order/TradeOrderDO.java | 13 ++++++++ .../dataobject/order/TradeOrderItemDO.java | 13 -------- .../order/TradeOrderUpdateServiceImpl.java | 2 ++ .../handler/TradeCouponOrderHandler.java | 18 ++++++++--- .../price/bo/TradePriceCalculateRespBO.java | 1 - .../TradeRewardActivityPriceCalculator.java | 15 ++++++++-- 10 files changed, 96 insertions(+), 25 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java index 27e5b6fb8..09fa7f9a8 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java @@ -3,9 +3,10 @@ package cn.iocoder.yudao.module.promotion.api.coupon; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO; - import jakarta.validation.Valid; +import java.util.List; + /** * 优惠劵 API 接口 * @@ -35,4 +36,13 @@ public interface CouponApi { */ CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO); + /** + * 【管理员】给指定用户批量发送优惠券 + * + * @param templateIds 优惠劵编号的数组 + * @param counts 优惠券数量的数组 + * @param userId 用户编号 + */ + void takeCouponsByAdmin(List templateIds, List counts, Long userId); + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java index b7f904583..23d088a74 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java @@ -7,10 +7,11 @@ import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO; import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; import cn.iocoder.yudao.module.promotion.service.coupon.CouponService; +import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import jakarta.annotation.Resource; +import java.util.List; /** * 优惠劵 API 实现类 @@ -41,4 +42,9 @@ public class CouponApiImpl implements CouponApi { return CouponConvert.INSTANCE.convert(coupon); } + @Override + public void takeCouponsByAdmin(List templateIds, List counts, Long userId) { + couponService.takeCouponsByAdmin(templateIds, counts, userId); + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java index edd654275..5220a6da7 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java @@ -105,6 +105,15 @@ public interface CouponService { takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN); } + /** + * 【管理员】给指定用户批量发送优惠券 + * + * @param templateIds 优惠劵编号的数组 + * @param counts 优惠券数量的数组 + * @param userId 用户编号 + */ + void takeCouponsByAdmin(List templateIds, List counts, Long userId); + /** * 【会员】领取优惠券 * diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index abf933d83..dcca8344f 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java @@ -19,19 +19,19 @@ import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponMapper; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum; +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 jakarta.annotation.Resource; - import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; import static java.util.Arrays.asList; @@ -175,10 +175,34 @@ public class CouponServiceImpl implements CouponService { // 3. 批量保存优惠劵 couponMapper.insertBatch(convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId))); - // 3. 增加优惠劵模板的领取数量 + // 4. 增加优惠劵模板的领取数量 couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size()); } + @Override + public void takeCouponsByAdmin(List templateIds, List counts, Long userId) { + // 1. 获得优惠券模版 + List templateList = couponTemplateService.getCouponTemplateList(templateIds); + if (CollUtil.isEmpty(templateList)) { + return; + } + + Map templateMap = convertMap(templateList, CouponTemplateDO::getId); + // 2.1 批量构建优惠券 + List couponList = new ArrayList<>(); + for (int i = 0; i < templateIds.size(); i++) { + int finalI = i; + findAndThen(templateMap, templateIds.get(i), template -> { + for (int j = 0; j < counts.get(finalI); j++) { + couponList.add(CouponConvert.INSTANCE.convert(template, userId) + .setTakeType(CouponTakeTypeEnum.ADMIN.getValue())); + } + }); + } + // 2.2 批量保存优惠券 + couponMapper.insertBatch(couponList); + } + @Override @Transactional(rollbackFor = Exception.class) public void takeCouponByRegister(Long userId) { diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java index b127004aa..495287edf 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java @@ -16,6 +16,7 @@ import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.time.LocalDateTime; +import java.util.List; /** * 交易订单 DO @@ -290,6 +291,18 @@ public class TradeOrderDO extends BaseDO { * VIP 减免金额,单位:分 */ private Integer vipPrice; + /** + * 赠送的优惠劵编号的数组 + * + * 目的:用于后续取消或者售后订单时,需要扣减赠送 + */ + private List couponIds; + /** + * 赠送的优惠券数量的数组 + * + * 目的:用于后续取消或者售后订单时,需要扣减赠送 + */ + private List couponCounts; /** * 秒杀活动编号 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java index b69997605..450bc764f 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java @@ -160,19 +160,6 @@ public class TradeOrderItemDO extends BaseDO { */ private Integer vipPrice; - /** - * 赠送的优惠劵编号的数组 - * - * 目的:用于后续取消或者售后订单时,需要扣减赠送 - */ - private List couponIds; - /** - * 赠送的优惠券数量的数组 - * - * 目的:用于后续取消或者售后订单时,需要扣减赠送 - */ - private List couponCounts; - // ========== 售后基本信息 ========== /** diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index a4f29a5b8..ee6cb90b0 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -201,6 +201,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()); order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum)); order.setUserIp(getClientIP()).setTerminal(getTerminal()); + // 优惠券 + order.setCouponIds(calculateRespBO.getCouponIds()).setCouponCounts(calculateRespBO.getCouponCounts()); // 支付 + 退款信息 order.setAdjustPrice(0).setPayStatus(false); order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java index eb4816719..6da931b6e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.trade.service.order.handler; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; @@ -32,16 +33,25 @@ public class TradeCouponOrderHandler implements TradeOrderHandler { @Override public void afterPayOrder(TradeOrderDO order, List orderItems) { - + if (CollUtil.isEmpty(order.getCouponIds())) { + return; + } + // 赠送优惠券 + couponApi.takeCouponsByAdmin(order.getCouponIds(), order.getCouponCounts(), order.getUserId()); } @Override public void afterCancelOrder(TradeOrderDO order, List orderItems) { - if (order.getCouponId() == null || order.getCouponId() <= 0) { + // 情况一:退还订单使用的优惠券 + if (order.getCouponId() != null && order.getCouponId() > 0) { + // 退回优惠劵 + couponApi.returnUsedCoupon(order.getCouponId()); + } + // 情况二:收回赠送的优惠券 + if (CollUtil.isEmpty(order.getCouponIds())) { return; } - // 退回优惠劵 - couponApi.returnUsedCoupon(order.getCouponId()); + // TODO @puhui999: 收回优惠券再考虑一下,是直接删除券还是改个状态 } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java index 32043df0e..119b68ec8 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java @@ -72,7 +72,6 @@ public class TradePriceCalculateRespBO { */ private Boolean freeDelivery; - // TODO @puhui999: 订单保存时需要保存 /** * 赠送的优惠劵编号的数组 */ diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java index fa5a61f3a..47420e24e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -19,6 +19,7 @@ import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Objects; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; @@ -105,8 +106,18 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator } // 4.3 记录赠送的优惠券 if (rule.getGiveCoupon()) { - // TODO @puhui999: 需要考虑赠送的优惠券是否重叠,重叠则对数量进行累加 - result.setCouponIds(rule.getCouponIds()).setCouponCounts(rule.getCouponCounts()); + for (int i = 0; i < rule.getCouponIds().size(); i++) { + Long couponId = result.getCouponIds().get(i); + Integer couponCount = result.getCouponCounts().get(i); + int index = CollUtil.indexOf(result.getCouponIds(), id -> Objects.equals(couponId, id)); + if (index != -1) { // 情况一:别的满减活动送过同类优惠券,则直接增加数量 + List couponCounts = result.getCouponCounts(); + couponCounts.set(index, couponCounts.get(index) + couponCount); + result.setCouponCounts(couponCounts); + } else { // 情况二:还没有赠送的优惠券 + result.setCouponIds(rule.getCouponIds()).setCouponCounts(rule.getCouponCounts()); + } + } } } From d60374d646e8087b1e3be0d405d2c4f09ddecc0c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 29 Aug 2024 23:30:59 +0800 Subject: [PATCH 079/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/module/promotion/api/coupon/CouponApi.java | 1 + .../promotion/service/coupon/CouponServiceImpl.java | 1 + .../yudao/module/trade/api/order/TradeOrderApi.java | 6 +++--- .../trade/dal/dataobject/order/TradeOrderDO.java | 2 ++ .../service/order/TradeOrderUpdateServiceImpl.java | 10 +++++----- .../service/order/handler/TradeCouponOrderHandler.java | 2 +- .../service/price/bo/TradePriceCalculateRespBO.java | 1 + .../price/calculator/TradeDeliveryPriceCalculator.java | 3 ++- .../calculator/TradeRewardActivityPriceCalculator.java | 5 +++-- 9 files changed, 19 insertions(+), 12 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java index 09fa7f9a8..f7b741ddb 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java @@ -36,6 +36,7 @@ public interface CouponApi { */ CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO); + // TODO @puhui999:Map 优惠劵 会不会好点。 /** * 【管理员】给指定用户批量发送优惠券 * diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index dcca8344f..222843c31 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java @@ -181,6 +181,7 @@ public class CouponServiceImpl implements CouponService { @Override public void takeCouponsByAdmin(List templateIds, List counts, Long userId) { + // TODO @puhui999:要不要循环调用上面的 takeCoupon 方法?按道理说,赠送也不会很多张。如果某次发卷失败,可以打个 error log; // 1. 获得优惠券模版 List templateList = couponTemplateService.getCouponTemplateList(templateIds); if (CollUtil.isEmpty(templateList)) { diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java index 64a269482..4bf1f5bf9 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java @@ -31,9 +31,9 @@ public interface TradeOrderApi { /** * 取消支付订单 * - * @param userId 用户编号 - * @param orderId 订单编号 - * @param cancelType 取消类型 + * @param userId 用户编号 + * @param orderId 订单编号 + * @param cancelType 取消类型 */ void cancelPaidOrder(Long userId, Long orderId, Integer cancelType); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java index 495287edf..4ce025408 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java @@ -291,6 +291,8 @@ public class TradeOrderDO extends BaseDO { * VIP 减免金额,单位:分 */ private Integer vipPrice; + + // TODO @puhui999::1)建议命名要 giveXXX;不然不好理解哈;2)是不是搞成 Map 好点哈。 /** * 赠送的优惠劵编号的数组 * diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index ee6cb90b0..5cb932e4e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -201,7 +201,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()); order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum)); order.setUserIp(getClientIP()).setTerminal(getTerminal()); - // 优惠券 + // 使用 + 赠送优惠券 order.setCouponIds(calculateRespBO.getCouponIds()).setCouponCounts(calculateRespBO.getCouponCounts()); // 支付 + 退款信息 order.setAdjustPrice(0).setPayStatus(false); @@ -861,17 +861,17 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @Override @Transactional(rollbackFor = Exception.class) public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) { - // 1. 这里校验下 cancelType 只允许拼团关闭; - if (!TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType().equals(cancelType)) { + // 1.1 这里校验下 cancelType 只允许拼团关闭; + if (ObjUtil.notEqual(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType(), cancelType)) { return; } - // 1.1 检验订单存在 + // 1.2 检验订单存在 TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); if (order == null) { throw exception(ORDER_NOT_FOUND); } - // 1.2 校验订单是否支付 + // 1.3 校验订单是否支付 if (!order.getPayStatus()) { throw exception(ORDER_CANCEL_PAID_FAIL, "已支付"); } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java index 6da931b6e..9428c6412 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java @@ -51,7 +51,7 @@ public class TradeCouponOrderHandler implements TradeOrderHandler { if (CollUtil.isEmpty(order.getCouponIds())) { return; } - // TODO @puhui999: 收回优惠券再考虑一下,是直接删除券还是改个状态 + // TODO @puhui999: 收回优惠券再考虑一下,是直接删除券还是改个状态;建议是【已作废】 } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java index 119b68ec8..95f85aea8 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java @@ -72,6 +72,7 @@ public class TradePriceCalculateRespBO { */ private Boolean freeDelivery; + // TODO @puhui999:感觉要不要试着改成 Map giveCoupons?貌似整体会更好理解一点。 /** * 赠送的优惠劵编号的数组 */ diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java index d0dcf9cfd..8c0829f9a 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.module.member.api.address.MemberAddressApi; import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO; @@ -56,7 +57,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator { return; } // 校验是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈 - if (anyMatch(result.getItems(), item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) { + if (CollectionUtils.anyMatch(result.getItems(), item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) { throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL); } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java index 47420e24e..d543dc1f4 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -25,6 +25,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice; +// TODO @puhui999:相关的单测,建议改一改 /** * 满减送活动的 {@link TradePriceCalculator} 实现类 * @@ -94,8 +95,8 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator if (rule.getGivePoint()) { List dividePoints = TradePriceCalculatorHelper.dividePrice(orderItems, rule.getPoint()); for (int i = 0; i < orderItems.size(); i++) { - TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); // 商品可能赠送了积分,所以这里要加上 + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); orderItem.setGivePoint(orderItem.getGivePoint() + dividePoints.get(i)); } } @@ -189,7 +190,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator Integer price = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); assert count != null && price != null; - // 2. 构建不满足时的提示信息-按最低档规则算 + // 2. 构建不满足时的提示信息:按最低档规则算 String meetTip = "满减送:购满 {} {},可以减 {} 元"; List rules = new ArrayList<>(rewardActivity.getRules()); rules.sort(Comparator.comparing(RewardActivityMatchRespDTO.Rule::getLimit)); // 按优惠门槛降序 From 806c828bb50fb7077803b19a5b8a96325a279053 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Fri, 30 Aug 2024 14:58:22 +0800 Subject: [PATCH 080/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E:=20=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E6=B4=BB=E5=8A=A8=E4=BC=98=E6=83=A0=E5=88=B8=E7=9B=B8?= =?UTF-8?q?=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../promotion/api/coupon/CouponApi.java | 16 ++-- .../dto/RewardActivityMatchRespDTO.java | 11 ++- .../enums/coupon/CouponStatusEnum.java | 2 +- .../promotion/api/coupon/CouponApiImpl.java | 11 ++- .../admin/reward/vo/RewardActivityBaseVO.java | 12 +-- .../dal/dataobject/coupon/CouponDO.java | 1 - .../dataobject/reward/RewardActivityDO.java | 11 ++- .../dal/mysql/coupon/CouponMapper.java | 9 +++ .../service/coupon/CouponService.java | 13 +++- .../service/coupon/CouponServiceImpl.java | 74 ++++++++++++++----- .../dal/dataobject/order/TradeOrderDO.java | 17 ++--- .../order/TradeOrderUpdateServiceImpl.java | 2 +- .../handler/TradeCouponOrderHandler.java | 8 +- .../price/bo/TradePriceCalculateRespBO.java | 13 ++-- .../TradePriceCalculatorHelper.java | 4 +- .../TradeRewardActivityPriceCalculator.java | 21 +++--- 16 files changed, 136 insertions(+), 89 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java index f7b741ddb..bda835678 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java @@ -5,7 +5,7 @@ import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO; import jakarta.validation.Valid; -import java.util.List; +import java.util.Map; /** * 优惠劵 API 接口 @@ -36,14 +36,20 @@ public interface CouponApi { */ CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO); - // TODO @puhui999:Map 优惠劵 会不会好点。 /** * 【管理员】给指定用户批量发送优惠券 * - * @param templateIds 优惠劵编号的数组 - * @param counts 优惠券数量的数组 + * @param giveCouponsMap key: 优惠劵编号,value:对应的优惠券数量 * @param userId 用户编号 */ - void takeCouponsByAdmin(List templateIds, List counts, Long userId); + void takeCouponsByAdmin(Map giveCouponsMap, Long userId); + + /** + * 【管理员】收回给指定用户批量发送优惠券 + * + * @param giveCouponsMap key: 优惠劵编号,value:对应的优惠券数量 + * @param userId 用户编号 + */ + void takeBackCouponsByAdmin(Map giveCouponsMap, Long userId); } diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java index a174637af..9cdb922f1 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java @@ -8,6 +8,7 @@ import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; /** * 满减送活动的匹配 Response DTO @@ -98,13 +99,11 @@ public class RewardActivityMatchRespDTO { */ private Boolean giveCoupon; /** - * 赠送的优惠劵编号的数组 + * 赠送的优惠劵 + * + * key: 优惠劵编号,value:对应的优惠券数量 */ - private List couponIds; - /** - * 赠送的优惠券数量的数组 - */ - private List couponCounts; + private Map giveCouponsMap; } diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java index 320345d85..3edb3897f 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java @@ -18,7 +18,7 @@ public enum CouponStatusEnum implements IntArrayValuable { UNUSED(1, "未使用"), USED(2, "已使用"), EXPIRE(3, "已过期"), - ; + INVALID(4, "已作废"); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponStatusEnum::getStatus).toArray(); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java index 23d088a74..b4778d0fe 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java @@ -11,7 +11,7 @@ import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import java.util.List; +import java.util.Map; /** * 优惠劵 API 实现类 @@ -43,8 +43,13 @@ public class CouponApiImpl implements CouponApi { } @Override - public void takeCouponsByAdmin(List templateIds, List counts, Long userId) { - couponService.takeCouponsByAdmin(templateIds, counts, userId); + public void takeCouponsByAdmin(Map giveCouponsMap, Long userId) { + couponService.takeCouponsByAdmin(giveCouponsMap, userId); + } + + @Override + public void takeBackCouponsByAdmin(Map giveCouponsMap, Long userId) { + couponService.takeBackCouponsByAdmin(giveCouponsMap, userId); } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java index f932a58d6..0ed4b7d52 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java @@ -16,6 +16,7 @@ import lombok.Data; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -88,16 +89,7 @@ public class RewardActivityBaseVO { private Boolean giveCoupon; @Schema(description = "赠送的优惠劵编号的数组", example = "1,2,3") - private List couponIds; - - @Schema(description = "赠送的优惠券数量的数组", example = "1,2,3") - private List couponCounts; - - @AssertTrue(message = "优惠劵和数量必须一一对应") - @JsonIgnore - public boolean isCouponCountsValid() { - return BooleanUtil.isFalse(giveCoupon) || CollUtil.size(couponIds) == CollUtil.size(couponCounts); - } + private Map giveCouponsMap; @AssertTrue(message = "赠送的积分不能小于 1") @JsonIgnore diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java index 296d2a2fd..7182f0ea0 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java @@ -50,7 +50,6 @@ public class CouponDO extends BaseDO { * * 枚举 {@link CouponStatusEnum} */ - // TODO 芋艿:已作废? private Integer status; // TODO 芋艿:发放 adminid? diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java index 9a7135063..b1332cb3f 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java @@ -16,6 +16,7 @@ import lombok.EqualsAndHashCode; import java.io.Serializable; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; /** * 满减送活动 DO @@ -114,13 +115,11 @@ public class RewardActivityDO extends BaseDO { */ private Boolean giveCoupon; /** - * 赠送的优惠劵编号的数组 + * 赠送的优惠劵 + * + * key: 优惠劵编号,value:对应的优惠券数量 */ - private List couponIds; - /** - * 赠送的优惠券数量的数组 - */ - private List couponCounts; + private Map giveCouponsMap; } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java index e5f1daf6c..913b84510 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java @@ -72,6 +72,15 @@ public interface CouponMapper extends BaseMapperX { ); } + default List selectListByTemplateIdAndUserIdAndTakeType(Long templateId, Collection userIds, + Integer takeType) { + return selectList(new LambdaQueryWrapperX() + .eq(CouponDO::getTemplateId, templateId) + .eq(CouponDO::getTakeType, takeType) + .in(CouponDO::getUserId, userIds) + ); + } + default Map selectCountByUserIdAndTemplateIdIn(Long userId, Collection templateIds) { String templateIdAlias = "templateId"; String countAlias = "count"; diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java index 5220a6da7..628a42e7f 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java @@ -108,11 +108,18 @@ public interface CouponService { /** * 【管理员】给指定用户批量发送优惠券 * - * @param templateIds 优惠劵编号的数组 - * @param counts 优惠券数量的数组 + * @param giveCouponsMap key: 优惠劵编号,value:对应的优惠券数量 * @param userId 用户编号 */ - void takeCouponsByAdmin(List templateIds, List counts, Long userId); + void takeCouponsByAdmin(Map giveCouponsMap, Long userId); + + /** + * 【管理员】收回给指定用户批量发送优惠券 + * + * @param giveCouponsMap key: 优惠劵编号,value:对应的优惠券数量 + * @param userId 用户编号 + */ + void takeBackCouponsByAdmin(Map giveCouponsMap, Long userId); /** * 【会员】领取优惠券 diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index 222843c31..aff7579de 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java @@ -31,7 +31,6 @@ import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; -import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; import static java.util.Arrays.asList; @@ -165,6 +164,7 @@ public class CouponServiceImpl implements CouponService { } @Override + @Transactional(rollbackFor = Exception.class) public void takeCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType) { CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId); // 1. 过滤掉达到领取限制的用户 @@ -180,28 +180,66 @@ public class CouponServiceImpl implements CouponService { } @Override - public void takeCouponsByAdmin(List templateIds, List counts, Long userId) { - // TODO @puhui999:要不要循环调用上面的 takeCoupon 方法?按道理说,赠送也不会很多张。如果某次发卷失败,可以打个 error log; - // 1. 获得优惠券模版 - List templateList = couponTemplateService.getCouponTemplateList(templateIds); - if (CollUtil.isEmpty(templateList)) { + public void takeCouponsByAdmin(Map giveCouponsMap, Long userId) { + if (CollUtil.isEmpty(giveCouponsMap)) { return; } - Map templateMap = convertMap(templateList, CouponTemplateDO::getId); - // 2.1 批量构建优惠券 - List couponList = new ArrayList<>(); - for (int i = 0; i < templateIds.size(); i++) { - int finalI = i; - findAndThen(templateMap, templateIds.get(i), template -> { - for (int j = 0; j < counts.get(finalI); j++) { - couponList.add(CouponConvert.INSTANCE.convert(template, userId) - .setTakeType(CouponTakeTypeEnum.ADMIN.getValue())); + // 循环发放 + for (Map.Entry entry : giveCouponsMap.entrySet()) { + try { + for (int i = 0; i < entry.getValue(); i++) { + getSelf().takeCoupon(entry.getKey(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.ADMIN); } - }); + } catch (Exception e) { + log.error("[takeCouponsByAdmin][coupon({}) 优惠券发放失败]", entry, e); + } } - // 2.2 批量保存优惠券 - couponMapper.insertBatch(couponList); + } + + @Override + public void takeBackCouponsByAdmin(Map giveCouponsMap, Long userId) { + // 循环收回 + for (Map.Entry entry : giveCouponsMap.entrySet()) { + try { + for (int i = 0; i < entry.getValue(); i++) { + getSelf().takeBackCoupon(entry.getKey(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.ADMIN); + } + } catch (Exception e) { + log.error("[takeBackCouponsByAdmin][coupon({}) 收回优惠券失败]", entry, e); + } + } + } + + /** + * 【管理员】收回优惠券 + * + * @param templateId 模版编号 + * @param userIds 用户编号列表 + * @param takeType 领取方式 + */ + @Transactional(rollbackFor = Exception.class) + public void takeBackCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType) { + CouponTemplateDO couponTemplate = couponTemplateService.getCouponTemplate(templateId); + // 1.1 校验模板 + if (couponTemplate == null) { + throw exception(COUPON_TEMPLATE_NOT_EXISTS); + } + // 1.2 校验领取方式 + if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getValue())) { + throw exception(COUPON_TEMPLATE_CANNOT_TAKE); + } + + // 2.1 过滤出还未使用的赠送的优惠券 + List couponList = couponMapper.selectListByTemplateIdAndUserIdAndTakeType(templateId, userIds, + takeType.getValue()); + List unUsedCouponList = filterList(couponList, item -> !CouponStatusEnum.USED.getStatus().equals(item.getStatus())); + // 2.2 减少优惠劵模板的领取数量 + couponTemplateService.updateCouponTemplateTakeCount(templateId, unUsedCouponList.size() * -1); + // 2.3 批量更新优惠劵状态 + couponMapper.updateById(convertList(unUsedCouponList, item -> new CouponDO().setId(item.getId()) + .setStatus(CouponStatusEnum.INVALID.getStatus()))); + } @Override diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java index 4ce025408..710d8dc3f 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java @@ -12,11 +12,13 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.*; import java.time.LocalDateTime; -import java.util.List; +import java.util.Map; /** * 交易订单 DO @@ -292,19 +294,14 @@ public class TradeOrderDO extends BaseDO { */ private Integer vipPrice; - // TODO @puhui999::1)建议命名要 giveXXX;不然不好理解哈;2)是不是搞成 Map 好点哈。 /** - * 赠送的优惠劵编号的数组 + * 赠送的优惠劵 * + * key: 优惠劵编号,value:对应的优惠券数量 * 目的:用于后续取消或者售后订单时,需要扣减赠送 */ - private List couponIds; - /** - * 赠送的优惠券数量的数组 - * - * 目的:用于后续取消或者售后订单时,需要扣减赠送 - */ - private List couponCounts; + @TableField(typeHandler = JacksonTypeHandler.class) + private Map giveCouponsMap; /** * 秒杀活动编号 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index 5cb932e4e..c9c1e685b 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -202,7 +202,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum)); order.setUserIp(getClientIP()).setTerminal(getTerminal()); // 使用 + 赠送优惠券 - order.setCouponIds(calculateRespBO.getCouponIds()).setCouponCounts(calculateRespBO.getCouponCounts()); + order.setGiveCouponsMap(calculateRespBO.getGiveCouponsMap()); // 支付 + 退款信息 order.setAdjustPrice(0).setPayStatus(false); order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java index 9428c6412..e364bc007 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java @@ -33,11 +33,11 @@ public class TradeCouponOrderHandler implements TradeOrderHandler { @Override public void afterPayOrder(TradeOrderDO order, List orderItems) { - if (CollUtil.isEmpty(order.getCouponIds())) { + if (CollUtil.isEmpty(order.getGiveCouponsMap())) { return; } // 赠送优惠券 - couponApi.takeCouponsByAdmin(order.getCouponIds(), order.getCouponCounts(), order.getUserId()); + couponApi.takeCouponsByAdmin(order.getGiveCouponsMap(), order.getUserId()); } @Override @@ -48,10 +48,10 @@ public class TradeCouponOrderHandler implements TradeOrderHandler { couponApi.returnUsedCoupon(order.getCouponId()); } // 情况二:收回赠送的优惠券 - if (CollUtil.isEmpty(order.getCouponIds())) { + if (CollUtil.isEmpty(order.getGiveCouponsMap())) { return; } - // TODO @puhui999: 收回优惠券再考虑一下,是直接删除券还是改个状态;建议是【已作废】 + couponApi.takeBackCouponsByAdmin(order.getGiveCouponsMap(), order.getUserId()); } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java index 95f85aea8..e53613d26 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import lombok.Data; import java.util.List; +import java.util.Map; /** * 价格计算 Response BO @@ -72,15 +73,13 @@ public class TradePriceCalculateRespBO { */ private Boolean freeDelivery; - // TODO @puhui999:感觉要不要试着改成 Map giveCoupons?貌似整体会更好理解一点。 /** - * 赠送的优惠劵编号的数组 + * 赠送的优惠劵 + * + * key: 优惠劵编号,value:对应的优惠券数量 + * 目的:用于后续取消或者售后订单时,需要扣减赠送 */ - private List couponIds; - /** - * 赠送的优惠券数量的数组 - */ - private List couponCounts; + private Map giveCouponsMap; /** * 订单价格 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java index cb8f97c11..6fa639c5a 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -31,8 +32,7 @@ public class TradePriceCalculatorHelper { List spuList, List skuList) { // 创建 PriceCalculateRespDTO 对象 TradePriceCalculateRespBO result = new TradePriceCalculateRespBO(); - result.setType(getOrderType(param)).setPromotions(new ArrayList<>()) - .setCouponIds(new ArrayList<>()).setCouponCounts(new ArrayList<>()); + result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCouponsMap(new LinkedHashMap<>()); // 创建它的 OrderItem 属性 result.setItems(new ArrayList<>(param.getItems().size())); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java index d543dc1f4..05679d836 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -19,13 +19,14 @@ import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.Objects; +import java.util.Map; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice; // TODO @puhui999:相关的单测,建议改一改 + /** * 满减送活动的 {@link TradePriceCalculator} 实现类 * @@ -107,16 +108,12 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator } // 4.3 记录赠送的优惠券 if (rule.getGiveCoupon()) { - for (int i = 0; i < rule.getCouponIds().size(); i++) { - Long couponId = result.getCouponIds().get(i); - Integer couponCount = result.getCouponCounts().get(i); - int index = CollUtil.indexOf(result.getCouponIds(), id -> Objects.equals(couponId, id)); - if (index != -1) { // 情况一:别的满减活动送过同类优惠券,则直接增加数量 - List couponCounts = result.getCouponCounts(); - couponCounts.set(index, couponCounts.get(index) + couponCount); - result.setCouponCounts(couponCounts); - } else { // 情况二:还没有赠送的优惠券 - result.setCouponIds(rule.getCouponIds()).setCouponCounts(rule.getCouponCounts()); + for (Map.Entry entry : rule.getGiveCouponsMap().entrySet()) { + Map giveCouponsMap = result.getGiveCouponsMap(); + if (giveCouponsMap.get(entry.getKey()) == null) { // 情况一:还没有赠送的优惠券 + result.setGiveCouponsMap(rule.getGiveCouponsMap()); + } else { // 情况二:别的满减活动送过同类优惠券,则直接增加数量 + giveCouponsMap.put(entry.getKey(), giveCouponsMap.get(entry.getKey()) + entry.getValue()); } } } @@ -193,7 +190,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator // 2. 构建不满足时的提示信息:按最低档规则算 String meetTip = "满减送:购满 {} {},可以减 {} 元"; List rules = new ArrayList<>(rewardActivity.getRules()); - rules.sort(Comparator.comparing(RewardActivityMatchRespDTO.Rule::getLimit)); // 按优惠门槛降序 + rules.sort(Comparator.comparing(RewardActivityMatchRespDTO.Rule::getLimit)); // 按优惠门槛升序 RewardActivityMatchRespDTO.Rule rule = rules.get(0); if (PromotionConditionTypeEnum.PRICE.getType().equals(rewardActivity.getConditionType())) { return StrUtil.format(meetTip, rule.getLimit(), "元", MoneyUtils.fenToYuanStr(rule.getDiscountPrice())); From 96fb953730433d999486bac28594c0b0300b4418 Mon Sep 17 00:00:00 2001 From: xiaoxin <718949661@qq.com> Date: Fri, 30 Aug 2024 15:10:53 +0800 Subject: [PATCH 081/136] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91AI?= =?UTF-8?q?=20=E7=9F=A5=E8=AF=86=E5=BA=93:=20=E5=A2=9E=E5=8A=A0=E9=83=A8?= =?UTF-8?q?=E5=88=86=E7=AE=A1=E7=90=86=E7=B1=BB=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/enums/ErrorCodeConstants.java | 4 +- .../knowledge/AiKnowledgeController.java | 24 +++++++-- .../AiKnowledgeDocumentController.java | 54 +++++++++++++++++++ .../AiKnowledgeSegmentController.java | 51 ++++++++++++++++++ .../AiKnowledgeDocumentPageReqVO.java | 13 +++++ .../document/AiKnowledgeDocumentRespVO.java | 37 +++++++++++++ .../AiKnowledgeDocumentUpdateReqVO.java | 23 ++++++++ .../AiKnowledgeCreateMyReqVO.java | 2 +- .../AiKnowledgeDocumentCreateReqVO.java | 8 ++- .../vo/knowledge/AiKnowledgeRespVO.java | 25 +++++++++ .../AiKnowledgeUpdateMyReqVO.java | 4 +- .../segment/AiKnowledgeSegmentPageReqVO.java | 21 ++++++++ .../vo/segment/AiKnowledgeSegmentRespVO.java | 33 ++++++++++++ .../AiKnowledgeSegmentUpdateReqVO.java | 17 ++++++ .../AiKnowledgeSegmentUpdateStatusReqVO.java | 17 ++++++ .../knowledge/AiKnowledgeDocumentMapper.java | 10 ++++ .../mysql/knowledge/AiKnowledgeMapper.java | 11 ++++ .../knowledge/AiKnowledgeSegmentMapper.java | 11 ++++ .../knowledge/AiKnowledgeDocumentService.java | 21 +++++++- .../AiKnowledgeDocumentServiceImpl.java | 34 +++++++++++- .../knowledge/AiKnowledgeSegmentService.java | 27 ++++++++++ .../AiKnowledgeSegmentServiceImpl.java | 27 ++++++++++ .../service/knowledge/AiKnowledgeService.java | 14 ++++- .../knowledge/AiKnowledgeServiceImpl.java | 11 +++- 24 files changed, 479 insertions(+), 20 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/{ => knowledge}/AiKnowledgeCreateMyReqVO.java (98%) rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/{ => knowledge}/AiKnowledgeDocumentCreateReqVO.java (90%) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/{ => knowledge}/AiKnowledgeUpdateMyReqVO.java (94%) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index 714d49adb..c3158a1aa 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode; /** * AI 错误码枚举类 - * + *

* ai 系统,使用 1-040-000-000 段 */ public interface ErrorCodeConstants { @@ -55,5 +55,7 @@ public interface ErrorCodeConstants { // ========== API 知识库 1-022-008-000 ========== ErrorCode KNOWLEDGE_NOT_EXISTS = new ErrorCode(1_022_008_000, "知识库不存在!"); + ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_022_008_001, "文档不存在!"); + ErrorCode KNOWLEDGE_SEGMENT_NOT_EXISTS = new ErrorCode(1_022_008_002, "段落不存在!"); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java index 9eae6b70c..a7b49b413 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java @@ -1,13 +1,19 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeCreateMyReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdateMyReqVO; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +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.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @@ -19,19 +25,27 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti public class AiKnowledgeController { @Resource - private AiKnowledgeService knowledgeBaseService; + private AiKnowledgeService knowledgeService; + + @GetMapping("/my-page") + @Operation(summary = "获取【我的】知识库分页") + public CommonResult> getKnowledgePageMy(@Validated PageParam pageReqVO) { + PageResult pageResult = knowledgeService.getKnowledgePageMy(getLoginUserId(), pageReqVO); + return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class)); + } + @PostMapping("/create-my") @Operation(summary = "创建【我的】知识库") public CommonResult createKnowledgeMy(@RequestBody @Valid AiKnowledgeCreateMyReqVO createReqVO) { - return success(knowledgeBaseService.createKnowledgeMy(createReqVO, getLoginUserId())); + return success(knowledgeService.createKnowledgeMy(createReqVO, getLoginUserId())); } @PutMapping("/update-my") @Operation(summary = "更新【我的】知识库") public CommonResult updateKnowledgeMy(@RequestBody @Valid AiKnowledgeUpdateMyReqVO updateReqVO) { - knowledgeBaseService.updateKnowledgeMy(updateReqVO, getLoginUserId()); + knowledgeService.updateKnowledgeMy(updateReqVO, getLoginUserId()); return success(true); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java new file mode 100644 index 000000000..58a53a19c --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge; + +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.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + + +@Tag(name = "管理后台 - AI 知识库-文档") +@RestController +@RequestMapping("/ai/knowledge/document") +public class AiKnowledgeDocumentController { + + @Resource + private AiKnowledgeDocumentService documentService; + + + @PostMapping("/create") + @Operation(summary = "新建文档") + public CommonResult createKnowledgeDocument(@Validated AiKnowledgeDocumentCreateReqVO reqVO) { + Long knowledgeDocumentId = documentService.createKnowledgeDocument(reqVO); + return success(knowledgeDocumentId); + } + + + @GetMapping("/page") + @Operation(summary = "获取文档分页") + public CommonResult> getKnowledgeDocumentPageMy(@Validated AiKnowledgeDocumentPageReqVO pageReqVO) { + PageResult pageResult = documentService.getKnowledgeDocumentPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class)); + } + + + @PutMapping("/update") + @Operation(summary = "更新文档") + public CommonResult updateKnowledgeDocument(@Validated @RequestBody AiKnowledgeDocumentUpdateReqVO reqVO) { + documentService.updateKnowledgeDocument(reqVO); + return success(true); + } + + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java new file mode 100644 index 000000000..a19f38eb7 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge; + +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.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + + +@Tag(name = "管理后台 - AI 知识库-段落") +@RestController +@RequestMapping("/ai/knowledge/segment") +public class AiKnowledgeSegmentController { + + @Resource + private AiKnowledgeSegmentService segmentService; + + @GetMapping("/page") + @Operation(summary = "获取段落分页") + public CommonResult> getKnowledgeSegmentPageMy(@Validated AiKnowledgeSegmentPageReqVO pageReqVO) { + PageResult pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class)); + } + + + @PutMapping("/update") + @Operation(summary = "更新段落内容") + public CommonResult updateKnowledgeSegment(@Validated @RequestBody AiKnowledgeSegmentUpdateReqVO reqVO) { + segmentService.updateKnowledgeSegment(reqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "启禁用段落内容") + public CommonResult updateKnowledgeSegmentStatus(@Validated @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) { + segmentService.updateKnowledgeSegmentStatus(reqVO); + return success(true); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java new file mode 100644 index 000000000..c1e7947f5 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document; + +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 AiKnowledgeDocumentPageReqVO extends PageParam { + + @Schema(description = "文档名称", example = "Java 开发手册") + private String name; +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java new file mode 100644 index 000000000..94a022363 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - AI 知识库-文档 Response VO") +@Data +public class AiKnowledgeDocumentRespVO extends PageParam { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + private Long id; + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + private Long knowledgeId; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") + private String name; + + @Schema(description = "内容", example = "Java 是一门面向对象的语言.....") + private String content; + + @Schema(description = "文档 url", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn") + private String url; + + @Schema(description = "token 数量", example = "1024") + private Integer tokens; + + @Schema(description = "字符数", example = "1008") + private Integer wordCount; + + @Schema(description = "切片状态", example = "1") + private Integer sliceStatus; + + @Schema(description = "文档状态", example = "1") + private Integer status; +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java new file mode 100644 index 000000000..6fb42c774 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + + +@Schema(description = "管理后台 - AI 更新 知识库-文档 Request VO") +@Data +public class AiKnowledgeDocumentUpdateReqVO { + + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "是否启用", example = "1") + private Integer status; + + @Schema(description = "名称", example = "Java 开发手册") + private String name; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateMyReqVO.java similarity index 98% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateMyReqVO.java index ac94a4c15..44a5e87ee 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeCreateMyReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateMyReqVO.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo; +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeDocumentCreateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java similarity index 90% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeDocumentCreateReqVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java index 10ad036b2..660c573ba 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeDocumentCreateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo; +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; @@ -6,10 +6,8 @@ import jakarta.validation.constraints.NotNull; import lombok.Data; import org.hibernate.validator.constraints.URL; -/** - * @author xiaoxin - */ -@Schema(description = "管理后台 - AI 知识库【创建文档】 Request VO") + +@Schema(description = "管理后台 - AI 知识库创建【文档】 Request VO") @Data public class AiKnowledgeDocumentCreateReqVO { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java new file mode 100644 index 000000000..2eb08717e --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + + +@Schema(description = "管理后台 - AI 知识库 Response VO") +@Data +public class AiKnowledgeRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + private Long id; + + @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南") + private String name; + + @Schema(description = "知识库描述", example = "ruoyi-vue-pro 用户指南") + private String description; + + @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "14") + private Long modelId; + + @Schema(description = "模型标识", example = "qwen-72b-chat") + private String model; +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeUpdateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateMyReqVO.java similarity index 94% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeUpdateMyReqVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateMyReqVO.java index e1f6a31af..987c9bf4a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/AiKnowledgeUpdateMyReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateMyReqVO.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo; +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; @@ -7,7 +7,7 @@ import lombok.Data; import java.util.List; -@Schema(description = "管理后台 - AI 知识库创建【我的】 Request VO") +@Schema(description = "管理后台 - AI 知识库更新【我的】 Request VO") @Data public class AiKnowledgeUpdateMyReqVO { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java new file mode 100644 index 000000000..125cb80b1 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; + +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 AiKnowledgeSegmentPageReqVO extends PageParam { + + + @Schema(description = "分段状态", example = "1") + private Integer status; + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer documentId; + + @Schema(description = "分段内容关键字", example = "Java 开发") + private String keyword; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java new file mode 100644 index 000000000..d8411618b --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - AI 知识库-文档 Response VO") +@Data +public class AiKnowledgeSegmentRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + private Long id; + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + private Long documentId; + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + private Long knowledgeId; + + @Schema(description = "向量库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1858496a-1dde-4edf-a43e-0aed08f37f8c") + private String vectorId; + + @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") + private String content; + + @Schema(description = "token 数量", example = "1024") + private Integer tokens; + + @Schema(description = "字符数", example = "1008") + private Integer wordCount; + + @Schema(description = "文档状态", example = "1") + private Integer status; +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java new file mode 100644 index 000000000..23b1461e2 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + + +@Schema(description = "管理后台 - AI 更新 知识库-段落 request VO") +@Data +public class AiKnowledgeSegmentUpdateReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + private Long id; + + @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") + private String content; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java new file mode 100644 index 000000000..409ce0146 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + + +@Schema(description = "管理后台 - AI 更新 知识库-段落 request VO") +@Data +public class AiKnowledgeSegmentUpdateStatusReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + private Long id; + + @Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java index af55f545a..7692d1ced 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java @@ -1,6 +1,9 @@ package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; +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.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import org.apache.ibatis.annotations.Mapper; @@ -11,4 +14,11 @@ import org.apache.ibatis.annotations.Mapper; */ @Mapper public interface AiKnowledgeDocumentMapper extends BaseMapperX { + + default PageResult selectPage(AiKnowledgeDocumentPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(AiKnowledgeDocumentDO::getName, reqVO.getName()) + .orderByDesc(AiKnowledgeDocumentDO::getId)); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java index 41e71ccad..2bf23411a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java @@ -1,6 +1,10 @@ package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +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.dal.dataobject.knowledge.AiKnowledgeDO; import org.apache.ibatis.annotations.Mapper; @@ -11,4 +15,11 @@ import org.apache.ibatis.annotations.Mapper; */ @Mapper public interface AiKnowledgeMapper extends BaseMapperX { + + default PageResult selectPageByMy(Long userId, PageParam pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eq(AiKnowledgeDO::getUserId, userId) + .eq(AiKnowledgeDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) + .orderByDesc(AiKnowledgeDO::getId)); + } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java index 5043ee0ca..912d18cbc 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java @@ -1,6 +1,9 @@ package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; +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.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import org.apache.ibatis.annotations.Mapper; @@ -11,4 +14,12 @@ import org.apache.ibatis.annotations.Mapper; */ @Mapper public interface AiKnowledgeSegmentMapper extends BaseMapperX { + + default PageResult selectPage(AiKnowledgeSegmentPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eq(AiKnowledgeSegmentDO::getDocumentId, reqVO.getDocumentId()) + .eqIfPresent(AiKnowledgeSegmentDO::getStatus, reqVO.getStatus()) + .likeIfPresent(AiKnowledgeSegmentDO::getContent, reqVO.getKeyword()) + .orderByDesc(AiKnowledgeSegmentDO::getId)); + } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 82c4f7b91..3de0ac01d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -1,6 +1,10 @@ package cn.iocoder.yudao.module.ai.service.knowledge; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; /** * AI 知识库-文档 Service 接口 @@ -17,4 +21,19 @@ public interface AiKnowledgeDocumentService { */ Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO); + + /** + * 获取文档分页 + * + * @param pageReqVO 分页参数 + * @return 文档分页 + */ + PageResult getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO); + + /** + * 更新文档 + * + * @param reqVO 更新信息 + */ + void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index bcfb64c55..3758c3bfd 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -3,9 +3,12 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.hutool.core.collection.CollUtil; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; @@ -29,6 +32,9 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Map; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_NOT_EXISTS; + /** * AI 知识库-文档 Service 实现类 * @@ -104,6 +110,32 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic return documentId; } + @Override + public PageResult getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO) { + return documentMapper.selectPage(pageReqVO); + } + + @Override + public void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO) { + validateKnowledgeDocumentExists(reqVO.getId()); + AiKnowledgeDocumentDO document = BeanUtils.toBean(reqVO, AiKnowledgeDocumentDO.class); + documentMapper.updateById(document); + } + + /** + * 校验文档是否存在 + * + * @param id 文档编号 + * @return 文档信息 + */ + private AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id) { + AiKnowledgeDocumentDO knowledgeDocument = documentMapper.selectById(id); + if (knowledgeDocument == null) { + throw exception(KNOWLEDGE_DOCUMENT_NOT_EXISTS); + } + return knowledgeDocument; + } + private org.springframework.core.io.Resource downloadFile(String url) { try { byte[] bytes = HttpUtil.downloadBytes(url); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index 7caea9ff4..22f634907 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -1,5 +1,11 @@ package cn.iocoder.yudao.module.ai.service.knowledge; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; + /** * AI 知识库分片 Service 接口 * @@ -7,4 +13,25 @@ package cn.iocoder.yudao.module.ai.service.knowledge; */ public interface AiKnowledgeSegmentService { + /** + * 获取段落分页 + * + * @param pageReqVO 分页查询 + * @return 文档分页 + */ + PageResult getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO); + + /** + * 更新段落内容 + * + * @param reqVO 更新内容 + */ + void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO); + + /** + * 更新状态 + * + * @param reqVO 更新内容 + */ + void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index 226c5f8fb..7f751b176 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -1,5 +1,13 @@ package cn.iocoder.yudao.module.ai.service.knowledge; +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.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -12,4 +20,23 @@ import org.springframework.stereotype.Service; @Slf4j public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService { + @Resource + private AiKnowledgeSegmentMapper segmentMapper; + + @Override + public PageResult getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) { + return segmentMapper.selectPage(pageReqVO); + } + + @Override + public void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO) { + segmentMapper.updateById(BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class)); + // TODO @xin 重新向量化 + } + + @Override + public void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO) { + segmentMapper.updateById(BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class)); + // TODO @xin 1.禁用删除向量 2.启用重新向量化 + } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java index bf7e8886a..9f43c5328 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.ai.service.knowledge; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeCreateMyReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdateMyReqVO; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; /** @@ -37,4 +39,12 @@ public interface AiKnowledgeService { */ AiKnowledgeDO validateKnowledgeExists(Long id); + /** + * 获得【我的】知识库分页 + * + * @param userId 用户编号 + * @param pageReqVO 分页查询 + * @return 知识库分页 + */ + PageResult getKnowledgePageMy(Long userId, PageParam pageReqVO); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java index 70442936e..1948bb00e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java @@ -2,9 +2,11 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +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.knowledge.vo.AiKnowledgeCreateMyReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.AiKnowledgeUpdateMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper; @@ -68,4 +70,9 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { return knowledgeBase; } + @Override + public PageResult getKnowledgePageMy(Long userId, PageParam pageReqVO) { + return knowledgeMapper.selectPageByMy(userId, pageReqVO); + } + } From d26ef6b89e2c3748360a92255ffbee61822c6429 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Fri, 30 Aug 2024 16:17:19 +0800 Subject: [PATCH 082/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E:=20TradeRewardActivityPric?= =?UTF-8?q?eCalculatorTest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TradeRewardActivityPriceCalculator.java | 6 ++--- ...radeRewardActivityPriceCalculatorTest.java | 27 +++++++++++++------ .../src/test/resources/sql/create_tables.sql | 1 + 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java index 05679d836..6b333df47 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -93,7 +93,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator TradePriceCalculatorHelper.recountAllPrice(result); // 4.1 记录赠送的积分 - if (rule.getGivePoint()) { + if (Boolean.TRUE.equals(rule.getGivePoint())) { List dividePoints = TradePriceCalculatorHelper.dividePrice(orderItems, rule.getPoint()); for (int i = 0; i < orderItems.size(); i++) { // 商品可能赠送了积分,所以这里要加上 @@ -102,12 +102,12 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator } } // 4.2 记录订单是否包邮 - if (rule.getFreeDelivery()) { + if (Boolean.TRUE.equals(rule.getFreeDelivery())) { // 只要满足一个活动包邮那么这单就包邮 result.setFreeDelivery(true); } // 4.3 记录赠送的优惠券 - if (rule.getGiveCoupon()) { + if (Boolean.TRUE.equals(rule.getGiveCoupon())) { for (Map.Entry entry : rule.getGiveCouponsMap().entrySet()) { Map giveCouponsMap = result.getGiveCouponsMap(); if (giveCouponsMap.get(entry.getKey()) == null) { // 情况一:还没有赠送的优惠券 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java index 219ae727e..3ae34514d 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi; import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; @@ -13,6 +14,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import java.util.ArrayList; +import java.util.LinkedHashMap; import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; @@ -47,7 +49,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest TradePriceCalculateRespBO result = new TradePriceCalculateRespBO() .setType(TradeOrderTypeEnum.NORMAL.getType()) .setPrice(new TradePriceCalculateRespBO.Price()) - .setPromotions(new ArrayList<>()) + .setPromotions(new ArrayList<>()).setGiveCouponsMap(new LinkedHashMap<>()) .setItems(asList( new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true) .setPrice(100).setSpuId(1L), @@ -60,16 +62,22 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest TradePriceCalculatorHelper.recountPayPrice(result.getItems()); TradePriceCalculatorHelper.recountAllPrice(result); - // mock 方法(限时折扣 DiscountActivity 信息) + // mock 方法(满减送 RewardActivity 信息) when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L, 3L)))).thenReturn(asList( randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号") - .setProductScopeValues(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType()) - .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(200).setDiscountPrice(70)))), + .setConditionType(PromotionConditionTypeEnum.PRICE.getType()) + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)) + .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(20).setDiscountPrice(70) + .setGivePoint(false).setFreeDelivery(false)))), randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号") - .setProductScopeValues(singletonList(3L)).setConditionType(PromotionConditionTypeEnum.COUNT.getType()) - .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10), - new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60), // 最大可满足,因为是 4 个 - new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100)))) + .setConditionType(PromotionConditionTypeEnum.COUNT.getType()) + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L)) + .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10) + .setGivePoint(true).setPoint(50).setFreeDelivery(false), + new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60).setGivePoint(true) + .setPoint(100).setFreeDelivery(false), // 最大可满足,因为是 4 个 + new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100) + .setGivePoint(false).setFreeDelivery(false)))) )); // 调用 @@ -94,6 +102,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest assertEquals(orderItem01.getCouponPrice(), 0); assertEquals(orderItem01.getPointPrice(), 0); assertEquals(orderItem01.getPayPrice(), 160); + assertEquals(orderItem01.getGivePoint(), 0); // 断言:SKU 2 TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1); assertEquals(orderItem02.getSkuId(), 20L); @@ -104,6 +113,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest assertEquals(orderItem02.getCouponPrice(), 0); assertEquals(orderItem02.getPointPrice(), 0); assertEquals(orderItem02.getPayPrice(), 120); + assertEquals(orderItem02.getGivePoint(), 0); // 断言:SKU 3 TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2); assertEquals(orderItem03.getSkuId(), 30L); @@ -114,6 +124,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest assertEquals(orderItem03.getCouponPrice(), 0); assertEquals(orderItem03.getPointPrice(), 0); assertEquals(orderItem03.getPayPrice(), 60); + assertEquals(orderItem03.getGivePoint(), 100); // 断言:Promotion 部分(第一个) assertEquals(result.getPromotions().size(), 2); TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql b/yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql index f619c01de..1d7ed24ee 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql +++ b/yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql @@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS "trade_order" "give_point" int NULL, "refund_point" int NULL, "vip_price" int NULL, + "give_coupons_map" varchar NULL, "seckill_activity_id" long NULL, "bargain_activity_id" long NULL, "bargain_record_id" long NULL, From 88cc4c987b26c2e9ebb37da4c944932f3ab729b6 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 30 Aug 2024 21:37:51 +0800 Subject: [PATCH 083/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/module/promotion/api/coupon/CouponApi.java | 7 +++++-- .../api/reward/dto/RewardActivityMatchRespDTO.java | 5 ++++- .../module/promotion/enums/coupon/CouponStatusEnum.java | 1 + .../yudao/module/promotion/api/coupon/CouponApiImpl.java | 4 ++-- .../module/promotion/service/coupon/CouponService.java | 2 +- .../module/promotion/service/coupon/CouponServiceImpl.java | 2 +- .../module/trade/dal/dataobject/order/TradeOrderDO.java | 5 ++++- .../service/order/handler/TradeCouponOrderHandler.java | 2 +- 8 files changed, 19 insertions(+), 9 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java index bda835678..c724df8c1 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java @@ -36,20 +36,23 @@ public interface CouponApi { */ CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO); + // TODO @puhui999:可能需要根据 TradeOrderDO 的建议,进行修改;需要返回优惠劵编号 /** * 【管理员】给指定用户批量发送优惠券 * * @param giveCouponsMap key: 优惠劵编号,value:对应的优惠券数量 * @param userId 用户编号 */ + // TODO @puhui999:giveCouponsMap 可能改成 giveCoupons 更合适?优惠劵模版编号、数量 void takeCouponsByAdmin(Map giveCouponsMap, Long userId); + // TODO @puhui999:可能需要根据 TradeOrderDO 的建议,进行修改 giveCouponsMap 参数 /** - * 【管理员】收回给指定用户批量发送优惠券 + * 【管理员】作废指定用户的指定优惠劵 * * @param giveCouponsMap key: 优惠劵编号,value:对应的优惠券数量 * @param userId 用户编号 */ - void takeBackCouponsByAdmin(Map giveCouponsMap, Long userId); + void invalidateCouponsByAdmin(Map giveCouponsMap, Long userId); } diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java index 9cdb922f1..93b5691fb 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java @@ -86,6 +86,7 @@ public class RewardActivityMatchRespDTO { * 是否包邮 */ private Boolean freeDelivery; + // TODO @puhui999:建议不返回 + 去掉 givePoint、giveCoupon 字段哈。 /** * 是否赠送积分 */ @@ -98,10 +99,12 @@ public class RewardActivityMatchRespDTO { * 是否赠送优惠券 */ private Boolean giveCoupon; + // TODO @puhui999:giveCoupons 即可 /** * 赠送的优惠劵 * - * key: 优惠劵编号,value:对应的优惠券数量 + * key: 优惠劵模版编号 + * value:对应的优惠券数量 */ private Map giveCouponsMap; diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java index 3edb3897f..831d4b5a0 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java @@ -18,6 +18,7 @@ public enum CouponStatusEnum implements IntArrayValuable { UNUSED(1, "未使用"), USED(2, "已使用"), EXPIRE(3, "已过期"), + // TODO @puhui999:捉摸了下,貌似搞成逻辑删除好了?不然好多地方的 status 都要做一些变动。可能未来加个 invalidateType 来标识,是管理后台删除,还是取消回收。或者优惠劵的 change log 可能更好。 INVALID(4, "已作废"); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponStatusEnum::getStatus).toArray(); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java index b4778d0fe..22fea4525 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java @@ -48,8 +48,8 @@ public class CouponApiImpl implements CouponApi { } @Override - public void takeBackCouponsByAdmin(Map giveCouponsMap, Long userId) { - couponService.takeBackCouponsByAdmin(giveCouponsMap, userId); + public void invalidateCouponsByAdmin(Map giveCouponsMap, Long userId) { + couponService.invalidateCouponsByAdmin(giveCouponsMap, userId); } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java index 628a42e7f..97c1412ca 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java @@ -119,7 +119,7 @@ public interface CouponService { * @param giveCouponsMap key: 优惠劵编号,value:对应的优惠券数量 * @param userId 用户编号 */ - void takeBackCouponsByAdmin(Map giveCouponsMap, Long userId); + void invalidateCouponsByAdmin(Map giveCouponsMap, Long userId); /** * 【会员】领取优惠券 diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index aff7579de..666a310e7 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java @@ -198,7 +198,7 @@ public class CouponServiceImpl implements CouponService { } @Override - public void takeBackCouponsByAdmin(Map giveCouponsMap, Long userId) { + public void invalidateCouponsByAdmin(Map giveCouponsMap, Long userId) { // 循环收回 for (Map.Entry entry : giveCouponsMap.entrySet()) { try { diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java index 710d8dc3f..82b6d6117 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java @@ -294,10 +294,13 @@ public class TradeOrderDO extends BaseDO { */ private Integer vipPrice; + // TODO @puhui999:项了下,貌似这里存储 List giveCouponIds 更合适。因为优惠劵赠送到最后是对应的编号,然后从而进行取消? /** * 赠送的优惠劵 * - * key: 优惠劵编号,value:对应的优惠券数量 + * key: 优惠劵编号 + * value:对应的优惠券数量 + * * 目的:用于后续取消或者售后订单时,需要扣减赠送 */ @TableField(typeHandler = JacksonTypeHandler.class) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java index e364bc007..3b1df5e0e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java @@ -51,7 +51,7 @@ public class TradeCouponOrderHandler implements TradeOrderHandler { if (CollUtil.isEmpty(order.getGiveCouponsMap())) { return; } - couponApi.takeBackCouponsByAdmin(order.getGiveCouponsMap(), order.getUserId()); + couponApi.invalidateCouponsByAdmin(order.getGiveCouponsMap(), order.getUserId()); } } From 86a413f57d2a8575a030ccfc9d650fcfc5293694 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 30 Aug 2024 23:19:00 +0800 Subject: [PATCH 084/136] =?UTF-8?q?1059=20=E3=80=90=E8=BD=BB=E9=87=8F?= =?UTF-8?q?=E7=BA=A7=20PR=E3=80=91=EF=BC=9A=E6=97=A5=E5=BF=97=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E7=A7=9F=E6=88=B7Job=E9=94=99=E8=AF=AF=E4=BF=A1?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/framework/tenant/core/job/TenantJobAspect.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java index 76fd98ecb..ce9eb1631 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java @@ -46,7 +46,7 @@ public class TenantJobAspect { try { joinPoint.proceed(); } catch (Throwable e) { - log.error("occur error while executing job with tenant {}", tenantId, e); + log.error("[execute][租户({}) 执行 Job 发生异常", tenantId, e); results.put(tenantId, ExceptionUtil.getRootCauseMessage(e)); } }); From e5f439aecd04dc759dbbcde6c1f71c875f7b91af Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 31 Aug 2024 08:16:31 +0800 Subject: [PATCH 085/136] =?UTF-8?q?1063=20=E3=80=90=E8=BD=BB=E9=87=8F?= =?UTF-8?q?=E7=BA=A7=20PR=E3=80=91=EF=BC=9A=E5=85=B3=E9=97=AD=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E8=BF=98=E6=98=AF=E4=BC=9A=E6=98=BE=E7=A4=BA=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/service/permission/MenuServiceImpl.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java index 730958f82..98052eb65 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java @@ -140,17 +140,19 @@ public class MenuServiceImpl implements MenuService { return true; } - // 1. 遍历到 parentId 为根节点,则无需判断 + // 1. 先判断自身是否禁用 + if (CommonStatusEnum.isDisable(node.getStatus())) { + disabledMenuCache.add(node.getId()); + return true; + } + + // 2. 遍历到 parentId 为根节点,则无需判断 Long parentId = node.getParentId(); if (ObjUtil.equal(parentId, ID_ROOT)) { - if (CommonStatusEnum.isDisable(node.getStatus())) { - disabledMenuCache.add(node.getId()); - return true; - } return false; } - // 2. 继续遍历 parent 节点 + // 3. 继续遍历 parent 节点 MenuDO parent = menuMap.get(parentId); if (parent == null || isMenuDisabled(parent, menuMap, disabledMenuCache)) { disabledMenuCache.add(node.getId()); From 39ec137047d92d3b4de889b384ea64eedc950e60 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 31 Aug 2024 08:18:24 +0800 Subject: [PATCH 086/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91apiErrorLogFrameworkService=20=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E5=90=8D=E5=B0=8F=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/framework/web/config/YudaoWebAutoConfiguration.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java index 8c784d9f2..1bdda5723 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java @@ -59,8 +59,8 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer { } @Bean - public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) { - return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService); + public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService apiErrorLogFrameworkService) { + return new GlobalExceptionHandler(applicationName, apiErrorLogFrameworkService); } @Bean From d6ecc032c2ec342522622afe358383eee81f2230 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 31 Aug 2024 08:53:05 +0800 Subject: [PATCH 087/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91SYSTEM=EF=BC=9A=E4=B8=83=E7=89=9B=E4=BA=91?= =?UTF-8?q?=E7=9F=AD=E4=BF=A1=E7=9A=84=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sms/core/client/impl/QiniuSmsClient.java | 92 +++++++++---------- .../client/impl/SmsClientFactoryImpl.java | 1 + .../core/client/impl/TencentSmsClient.java | 2 +- .../core/client/impl/QiniuSmsClientTest.java | 20 ++-- .../sms/core/client/impl/SmsClientTests.java | 9 +- 5 files changed, 60 insertions(+), 64 deletions(-) diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java index 4fbb8649d..a041970be 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java @@ -1,11 +1,9 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; import cn.hutool.core.collection.CollStreamUtil; -import cn.hutool.core.collection.ListUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.digest.HmacAlgorithm; @@ -22,6 +20,9 @@ import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import java.util.*; +import java.util.function.Function; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; /** * 七牛云短信客户端的实现类 @@ -34,19 +35,12 @@ public class QiniuSmsClient extends AbstractSmsClient { private static final String HOST = "sms.qiniuapi.com"; - private static final String PATH = "/v1/message/single"; - - private static final String TEMPLATE_PATH = "/v1/template"; - public QiniuSmsClient(SmsChannelProperties properties) { super(properties); Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); } - protected void doInit() { - } - public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { // 1. 执行请求 @@ -56,16 +50,16 @@ public class QiniuSmsClient extends AbstractSmsClient { body.put("mobile", mobile); body.put("parameters", CollStreamUtil.toMap(templateParams, KeyValue::getKey, KeyValue::getValue)); body.put("seq", Long.toString(sendLogId)); + JSONObject response = request("POST", body, "/v1/message/single"); - JSONObject response = request("POST", body, PATH); // 2. 解析请求 - if (ObjectUtil.isNotEmpty(response.getStr("error"))){//短信请求失败 + if (ObjectUtil.isNotEmpty(response.getStr("error"))) { + // 短信请求失败 return new SmsSendRespDTO().setSuccess(false) .setApiCode(response.getStr("error")) .setApiRequestId(response.getStr("request_id")) .setApiMsg(response.getStr("message")); } - return new SmsSendRespDTO().setSuccess(response.containsKey("message_id")) .setSerialNo(response.getStr("message_id")); } @@ -81,62 +75,66 @@ public class QiniuSmsClient extends AbstractSmsClient { */ private JSONObject request(String httpMethod, LinkedHashMap body, String path) { String signDate = DateUtil.date().setTimeZone(TimeZone.getTimeZone("UTC")).toString("yyyyMMdd'T'HHmmss'Z'"); - //请求头 + // 1. 请求头 Map header = new HashMap<>(4); header.put("HOST", HOST); - header.put("Authorization", getSignature(httpMethod, HOST, path, body != null ? JSONUtil.toJsonStr(body) : "", signDate)); + header.put("Authorization", getSignature(httpMethod, path, body != null ? JSONUtil.toJsonStr(body) : "", signDate)); header.put("Content-Type", "application/json"); header.put("X-Qiniu-Date", signDate); - String responseBody =""; - if (Objects.equals(httpMethod, "POST")){// POST 发送短消息用POST请求 + // 2. 发起请求 + String responseBody; + if (Objects.equals(httpMethod, "POST")){ responseBody = HttpUtils.post("https://" + HOST + path, header, JSONUtil.toJsonStr(body)); - }else { // GET 查询template状态用GET请求 + } else { responseBody = HttpUtils.get("https://" + HOST + path, header); } return JSONUtil.parseObj(responseBody); } - public String getSignature(String method, String host, String path, String body, String signDate) { + private String getSignature(String method, String path, String body, String signDate) { StringBuilder dataToSign = new StringBuilder(); - dataToSign.append(method.toUpperCase()).append(" ").append(path); - dataToSign.append("\nHost: ").append(host); - dataToSign.append("\n").append("Content-Type").append(": ").append("application/json"); - dataToSign.append("\n").append("X-Qiniu-Date").append(": ").append(signDate); - dataToSign.append("\n\n"); + dataToSign.append(method.toUpperCase()).append(" ").append(path) + .append("\nHost: ").append(HOST) + .append("\n").append("Content-Type").append(": ").append("application/json") + .append("\n").append("X-Qiniu-Date").append(": ").append(signDate) + .append("\n\n"); if (ObjectUtil.isNotEmpty(body)) { dataToSign.append(body); } - String encodedSignature = SecureUtil.hmac(HmacAlgorithm.HmacSHA1, properties.getApiSecret()).digestBase64(dataToSign.toString(), true); - - return "Qiniu " + properties.getApiKey() + ":" + encodedSignature; + String signature = SecureUtil.hmac(HmacAlgorithm.HmacSHA1, properties.getApiSecret()) + .digestBase64(dataToSign.toString(), true); + return "Qiniu " + properties.getApiKey() + ":" + signature; } @Override public List parseSmsReceiveStatus(String text) { JSONObject status = JSONUtil.parseObj(text); // 字段参考 https://developer.qiniu.com/sms/5910/message-push - return ListUtil.of(new SmsReceiveRespDTO() - .setSuccess("DELIVRD".equals(status.getJSONArray("items").getJSONObject(0).getStr("status"))) // 是否接收成功 - .setErrorMsg(status.getJSONArray("items").getJSONObject(0).getStr("status")) - .setMobile(status.getJSONArray("items").getJSONObject(0).getStr("mobile")) // 手机号 - .setReceiveTime(LocalDateTimeUtil.of(status.getJSONArray("items").getJSONObject(0).getLong("delivrd_at")*1000L)) - .setSerialNo(status.getJSONArray("items").getJSONObject(0).getStr("message_id")) // 发送序列号 - .setLogId(Long.valueOf(status.getJSONArray("items").getJSONObject(0).getStr("seq")))); // logId + return convertList(status.getJSONArray("items"), new Function() { + + @Override + public SmsReceiveRespDTO apply(Object item) { + JSONObject statusObj = (JSONObject) item; + return new SmsReceiveRespDTO() + .setSuccess("DELIVRD".equals(statusObj.getStr("status"))) // 是否接收成功 + .setErrorMsg(statusObj.getStr("status")) // 状态报告编码 + .setMobile(statusObj.getStr("mobile")) // 手机号 + .setReceiveTime(LocalDateTimeUtil.of(statusObj.getLong("delivrd_at") * 1000L)) // 状态报告时间 + .setSerialNo(statusObj.getStr("message_id")) // 发送序列号 + .setLogId(statusObj.getLong("seq")); // 用户序列号 + } + + }); } @Override public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { // 1. 执行请求 // 参考链接 https://developer.qiniu.com/sms/5969/query-a-single-template - JSONObject response = request("GET", null, TEMPLATE_PATH + "/" + apiTemplateId); - // 2.1 请求失败 - if (ObjUtil.notEqual(response.getStr("audit_status"), "passed")) { - log.error("[getSmsTemplate][模版编号({}) 响应不正确({})]", apiTemplateId, response); - return null; - } + JSONObject response = request("GET", null, "/v1/template/" + apiTemplateId); - // 2.2 请求成功 + // 2.2 解析请求 return new SmsTemplateRespDTO() .setId(response.getStr("id")) .setContent(response.getStr("template")) @@ -146,12 +144,12 @@ public class QiniuSmsClient extends AbstractSmsClient { @VisibleForTesting Integer convertSmsTemplateAuditStatus(String templateStatus) { - return switch (templateStatus) { - case "passed" -> SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); - case "reviewing" -> SmsTemplateAuditStatusEnum.CHECKING.getStatus(); - case "rejected" -> SmsTemplateAuditStatusEnum.FAIL.getStatus(); - case null, default -> - throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus)); - }; + switch (templateStatus) { + case "passed": return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + case "reviewing": return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); + case "rejected": return SmsTemplateAuditStatusEnum.FAIL.getStatus(); + default: + throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus)); + } } } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java index dde1475d4..da783189b 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java @@ -80,6 +80,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory { case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); case TENCENT: return new TencentSmsClient(properties); case HUAWEI: return new HuaweiSmsClient(properties); + case QINIU: return new QiniuSmsClient(properties); } // 创建失败,错误日志 + 抛出异常 log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java index ae3138362..19cde8c26 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java @@ -162,7 +162,7 @@ public class TencentSmsClient extends AbstractSmsClient { * @param body 请求参数 * @return 请求结果 */ - private JSONObject request(String action, TreeMap body) throws Exception { + private JSONObject request(String action, TreeMap body) { // 1.1 请求 Header Map headers = new HashMap<>(); headers.put("Content-Type", "application/json; charset=utf-8"); diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java index c3e896695..93b99bcc5 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.mockStatic; * @author scholar */ public class QiniuSmsClientTest extends BaseMockitoUnitTest { + private final SmsChannelProperties properties = new SmsChannelProperties() .setApiKey(randomString())// 随机一个 apiKey,避免构建报错 .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 @@ -37,12 +38,6 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { @InjectMocks private QiniuSmsClient smsClient = new QiniuSmsClient(properties); - @Test - public void testDoInit() { - // 调用 - smsClient.doInit(); - } - @Test public void testDoSendSms_success() throws Throwable { try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { @@ -113,12 +108,13 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { List statuses = smsClient.parseSmsReceiveStatus(text); // 断言 assertEquals(1, statuses.size()); - assertTrue(statuses.getFirst().getSuccess()); - assertEquals("DELIVRD", statuses.getFirst().getErrorMsg()); - assertEquals(LocalDateTime.of(2024, 8, 25, 21, 14, 26), statuses.getFirst().getReceiveTime()); - assertEquals("18881234567", statuses.getFirst().getMobile()); - assertEquals("10135515063508004167", statuses.getFirst().getSerialNo()); - assertEquals(123, statuses.getFirst().getLogId()); + SmsReceiveRespDTO status = statuses.get(0); + assertTrue(status.getSuccess()); + assertEquals("DELIVRD", status.getErrorMsg()); + assertEquals(LocalDateTime.of(2024, 8, 25, 21, 14, 26), status.getReceiveTime()); + assertEquals("18881234567", status.getMobile()); + assertEquals("10135515063508004167", status.getSerialNo()); + assertEquals(123, status.getLogId()); } @Test diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java index 09608fea0..faba754a2 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java @@ -1,5 +1,6 @@ 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.SmsClient; import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; @@ -47,7 +48,7 @@ public class SmsClientTests { String mobile = "15601691323"; String apiTemplateId = "SMS_207945135"; // 调用 - SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024"))); + SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024"))); // 打印结果 System.out.println(sendRespDTO); } @@ -68,7 +69,7 @@ public class SmsClientTests { String mobile = "15601691323"; String apiTemplateId = "358212"; // 调用 - SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024"))); + SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024"))); // 打印结果 System.out.println(sendRespDTO); } @@ -105,7 +106,7 @@ public class SmsClientTests { Long sendLogId = System.currentTimeMillis(); String mobile = "17321315478"; String apiTemplateId = "3644cdab863546a3b718d488659a99ef"; - List> templateParams = List.of(new KeyValue<>("code", "1024")); + List> templateParams = ListUtil.of(new KeyValue<>("code", "1024")); // 调用 SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); // 打印结果 @@ -125,7 +126,7 @@ public class SmsClientTests { Long sendLogId = System.currentTimeMillis(); String mobile = "17321315478"; String apiTemplateId = "3644cdab863546a3b718d488659a99ef"; - List> templateParams = List.of(new KeyValue<>("code", "1122")); + List> templateParams = ListUtil.of(new KeyValue<>("code", "1122")); // 调用 SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); // 打印结果 From 476101189ea1a6eeaba555bfadcd7e90793e1340 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 31 Aug 2024 09:07:11 +0800 Subject: [PATCH 088/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E4=BC=98=E6=83=A0?= =?UTF-8?q?=E5=8A=B5=E7=9A=84=E6=8F=8F=E8=BF=B0=E5=AD=97=E6=AE=B5=E6=96=B0?= =?UTF-8?q?=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/coupon/vo/template/CouponTemplateBaseVO.java | 6 +++--- .../app/coupon/vo/template/AppCouponTemplateRespVO.java | 9 +++------ .../dal/dataobject/coupon/CouponTemplateDO.java | 8 +++++--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java index 715982aa0..6885246b4 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java @@ -33,6 +33,9 @@ public class CouponTemplateBaseVO { @NotNull(message = "优惠劵名不能为空") private String name; + @Schema(description = "优惠券说明", example = "优惠券使用说明") + private String description; + @Schema(description = "发行总量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") // -1 - 则表示不限制发放数量 @NotNull(message = "发行总量不能为空") private Integer totalCount; @@ -95,9 +98,6 @@ public class CouponTemplateBaseVO { @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用 private Integer discountLimitPrice; - @Schema(description = "优惠券说明", example = "优惠券使用说明") // 单位:分,仅在 discountType 为 PERCENT 使用 - private String description; - @AssertTrue(message = "商品范围编号的数组不能为空") @JsonIgnore public boolean isProductScopeValuesValid() { diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java index 8ca62935e..a57fc0472 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java @@ -1,8 +1,5 @@ package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.template; -import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; -import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; -import com.baomidou.mybatisplus.annotation.TableField; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -20,6 +17,9 @@ public class AppCouponTemplateRespVO { @Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送") private String name; + @Schema(description = "优惠券说明", example = "优惠券使用说明") + private String description; + @Schema(description = "每人限领个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "66") // -1 - 则表示不限制 private Integer takeLimitCount; @@ -67,7 +67,4 @@ public class AppCouponTemplateRespVO { @Schema(description = "是否可以领取", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean canTake; - @Schema(description = "优惠券说明", example = "优惠券使用说明") // 单位:分,仅在 discountType 为 PERCENT 使用 - private String description; - } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java index 93970ab9b..10fe302a3 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java @@ -11,7 +11,6 @@ import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -41,6 +40,10 @@ public class CouponTemplateDO extends BaseDO { * 优惠劵名 */ private String name; + /** + * 优惠券说明 + */ + private String description; /** * 状态 * @@ -159,10 +162,9 @@ public class CouponTemplateDO extends BaseDO { * 使用优惠券的次数 */ private Integer useCount; + // ========== 统计信息 END ========== // TODO 芋艿:领取开始时间、领取结束时间 - @Schema(description = "优惠券说明", example = "优惠券使用说明") // 单位:分,仅在 discountType 为 PERCENT 使用 - private String description; } From f1dd7999db9832697c0975e5a1e1719e451046f1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 31 Aug 2024 09:23:02 +0800 Subject: [PATCH 089/136] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/ruoyi-vue-pro.sql | 86 +++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index 274e17a5e..405fd743f 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -11,7 +11,7 @@ Target Server Version : 80200 (8.2.0) File Encoding : 65001 - Date: 28/07/2024 23:20:28 + Date: 31/08/2024 09:22:45 */ SET NAMES utf8mb4; @@ -62,7 +62,7 @@ COMMIT; -- ---------------------------- DROP TABLE IF EXISTS `infra_api_error_log`; CREATE TABLE `infra_api_error_log` ( - `id` int NOT NULL AUTO_INCREMENT COMMENT '编号', + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', `trace_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '链路追踪编号', `user_id` int NOT NULL DEFAULT 0 COMMENT '用户编号', `user_type` tinyint NOT NULL DEFAULT 0 COMMENT '用户类型', @@ -91,7 +91,7 @@ CREATE TABLE `infra_api_error_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 19166 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志'; +) ENGINE = InnoDB AUTO_INCREMENT = 20014 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志'; -- ---------------------------- -- Records of infra_api_error_log @@ -128,7 +128,7 @@ CREATE TABLE `infra_codegen_column` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2470 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义'; +) ENGINE = InnoDB AUTO_INCREMENT = 2483 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义'; -- ---------------------------- -- Records of infra_codegen_column @@ -166,7 +166,7 @@ CREATE TABLE `infra_codegen_table` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 186 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义'; +) ENGINE = InnoDB AUTO_INCREMENT = 187 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义'; -- ---------------------------- -- Records of infra_codegen_table @@ -179,7 +179,7 @@ COMMIT; -- ---------------------------- DROP TABLE IF EXISTS `infra_config`; CREATE TABLE `infra_config` ( - `id` int NOT NULL AUTO_INCREMENT COMMENT '参数主键', + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '参数主键', `category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '参数分组', `type` tinyint NOT NULL COMMENT '参数类型', `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '参数名称', @@ -250,7 +250,7 @@ CREATE TABLE `infra_file` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1447 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; +) ENGINE = InnoDB AUTO_INCREMENT = 1472 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; -- ---------------------------- -- Records of infra_file @@ -438,7 +438,7 @@ CREATE TABLE `system_dict_data` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1588 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表'; +) ENGINE = InnoDB AUTO_INCREMENT = 1592 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表'; -- ---------------------------- -- Records of system_dict_data @@ -858,6 +858,10 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1585, 2, '回复', '2', 'ai_write_type', 0, '', '', '', '1', '2024-07-10 21:26:06', '1', '2024-07-10 21:26:06', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1586, 2, '腾讯云', 'TENCENT', 'system_sms_channel_code', 0, '', '', '', '1', '2024-07-22 22:23:16', '1', '2024-07-22 22:23:16', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1587, 3, '华为云', 'HUAWEI', 'system_sms_channel_code', 0, '', '', '', '1', '2024-07-22 22:23:46', '1', '2024-07-22 22:23:53', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1588, 1, 'OpenAI 微软', 'AzureOpenAI', 'ai_platform', 0, '', '', '', '1', '2024-08-10 14:07:41', '1', '2024-08-10 14:07:41', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1589, 10, 'BPMN 设计器', '10', 'bpm_model_type', 0, 'primary', '', '', '1', '2024-08-26 15:22:17', '1', '2024-08-26 16:46:02', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1590, 20, 'SIMPLE 设计器', '20', 'bpm_model_type', 0, 'success', '', '', '1', '2024-08-26 15:22:27', '1', '2024-08-26 16:45:58', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1591, 4, '七牛云', 'QINIU', 'system_sms_channel_code', 0, '', '', '', '1', '2024-08-31 08:45:03', '1', '2024-08-31 08:45:24', b'0'); COMMIT; -- ---------------------------- @@ -877,7 +881,7 @@ CREATE TABLE `system_dict_type` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 629 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; +) ENGINE = InnoDB AUTO_INCREMENT = 630 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; -- ---------------------------- -- Records of system_dict_type @@ -975,6 +979,7 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (626, '写作长度', 'ai_write_length', 0, '', '1', '2024-07-07 15:18:41', '1', '2024-07-07 15:18:41', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (627, '写作格式', 'ai_write_format', 0, '', '1', '2024-07-07 15:14:34', '1', '2024-07-07 15:14:34', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (628, 'AI 写作类型', 'ai_write_type', 0, '', '1', '2024-07-10 21:25:29', '1', '2024-07-10 21:25:29', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (629, 'BPM 流程模型类型', 'bpm_model_type', 0, '', '1', '2024-08-26 15:21:43', '1', '2024-08-26 15:21:43', b'0', '1970-01-01 00:00:00'); COMMIT; -- ---------------------------- @@ -998,7 +1003,7 @@ CREATE TABLE `system_login_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 3261 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录'; +) ENGINE = InnoDB AUTO_INCREMENT = 3289 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录'; -- ---------------------------- -- Records of system_login_log @@ -1129,7 +1134,7 @@ CREATE TABLE `system_menu` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2798 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表'; +) ENGINE = InnoDB AUTO_INCREMENT = 2808 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表'; -- ---------------------------- -- Records of system_menu @@ -1293,7 +1298,6 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1193, '流程模型', '', 2, 1, 1186, 'model', 'fa-solid:project-diagram', 'bpm/model/index', 'BpmModel', 0, b'1', b'1', b'1', '1', '2021-12-31 23:24:58', '1', '2024-03-19 12:25:19', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1194, '模型查询', 'bpm:model:query', 3, 1, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:10', '1', '2022-04-20 17:03:10', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1195, '模型创建', 'bpm:model:create', 3, 2, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:24', '1', '2022-04-20 17:03:10', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1196, '模型导入', 'bpm:model:import', 3, 3, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:35', '1', '2022-04-20 17:03:10', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1197, '模型更新', 'bpm:model:update', 3, 4, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:02:28', '1', '2022-04-20 17:03:10', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1198, '模型删除', 'bpm:model:delete', 3, 5, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:02:43', '1', '2022-04-20 17:03:10', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1199, '模型发布', 'bpm:model:deploy', 3, 6, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:03:24', '1', '2022-04-20 17:03:10', b'0'); @@ -1952,7 +1956,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2784, '绘画管理', '', 2, 11, 2760, 'image', 'fa:file-image-o', 'ai/image/manager/index.vue', 'AiImageManager', 0, b'1', b'1', b'1', '', '2024-06-26 13:32:31', '1', '2024-06-26 21:37:13', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2785, '绘画查询', 'ai:image:query', 3, 1, 2784, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-06-26 13:32:31', '1', '2024-06-26 22:21:57', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2786, '绘画删除', 'ai:image:delete', 3, 4, 2784, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-06-26 13:32:31', '1', '2024-06-26 22:22:08', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2787, '会话更新公开状态', 'ai:image:update-public-status', 3, 2, 2784, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-06-26 22:47:56', '1', '2024-06-26 22:47:56', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2787, '绘图更新', 'ai:image:update', 3, 2, 2784, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-06-26 22:47:56', '1', '2024-08-31 09:21:35', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2788, '音乐管理', '', 2, 12, 2760, 'music', 'fa:music', 'ai/music/manager/index.vue', 'AiMusicManager', 0, b'1', b'1', b'1', '', '2024-06-27 15:03:33', '1', '2024-06-27 23:04:19', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2789, '音乐查询', 'ai:music:query', 3, 1, 2788, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-06-27 15:03:33', '', '2024-06-27 15:03:33', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2790, '音乐更新', 'ai:music:update', 3, 3, 2788, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-06-27 15:03:33', '', '2024-06-27 15:03:33', b'0'); @@ -1961,8 +1965,18 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2793, '写作管理', '', 2, 13, 2760, 'write', 'fa:bookmark-o', 'ai/write/manager/index.vue', 'AiWriteManager', 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '1', '2024-07-10 21:31:59', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2794, 'AI 写作查询', 'ai:write:query', 3, 1, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2795, 'AI 写作删除', 'ai:write:delete', 3, 4, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiMusic', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-17 09:36:12', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiMusic', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-29 21:11:52', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2797, '客服中心', '', 2, 100, 2362, 'kefu', 'fa-solid:user-alt', 'mall/promotion/kefu/index', 'KeFu', 0, b'1', b'1', b'1', '1', '2024-07-17 23:49:05', '1', '2024-07-17 23:49:16', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2798, 'AI 思维导图', '', 2, 5, 2758, 'mind-map', 'fa:sitemap', 'ai/mindmap/index/index.vue', 'AiMindMap', 0, b'1', b'1', b'1', '1', '2024-07-29 21:31:59', '1', '2024-07-29 21:33:20', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2799, '导图管理', '', 2, 14, 2760, 'mind-map', 'fa:map', 'ai/mindmap/manager/index', 'AiMindMapManager', 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '1', '2024-08-10 17:24:28', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2800, '思维导图查询', 'ai:mind-map:query', 3, 1, 2799, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '', '2024-08-10 09:15:09', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2801, '思维导图删除', 'ai:mind-map:delete', 3, 4, 2799, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '', '2024-08-10 09:15:09', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2802, '会话查询', 'promotion:kefu-conversation:query', 3, 1, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:17:52', '1', '2024-08-31 09:18:52', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2803, '会话更新', 'promotion:kefu-conversation:update', 3, 2, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:18:15', '1', '2024-08-31 09:19:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2804, '消息查询', 'promotion:kefu-message:query', 3, 10, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:18:42', '1', '2024-08-31 09:18:42', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2805, '会话删除', 'promotion:kefu-conversation:delete', 3, 3, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:19:51', '1', '2024-08-31 09:20:32', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2806, '消息发送', 'promotion:kefu-message:send', 3, 12, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:20:06', '1', '2024-08-31 09:20:06', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2807, '消息更新', 'promotion:kefu-message:update', 3, 11, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:20:22', '1', '2024-08-31 09:20:22', b'0'); COMMIT; -- ---------------------------- @@ -2084,7 +2098,7 @@ CREATE TABLE `system_oauth2_access_token` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_access_token`(`access_token` ASC) USING BTREE, INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 8784 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌'; +) ENGINE = InnoDB AUTO_INCREMENT = 9563 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌'; -- ---------------------------- -- Records of system_oauth2_access_token @@ -2206,7 +2220,7 @@ CREATE TABLE `system_oauth2_refresh_token` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1598 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌'; +) ENGINE = InnoDB AUTO_INCREMENT = 1620 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌'; -- ---------------------------- -- Records of system_oauth2_refresh_token @@ -2239,7 +2253,7 @@ CREATE TABLE `system_operate_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 9053 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本'; +) ENGINE = InnoDB AUTO_INCREMENT = 9056 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本'; -- ---------------------------- -- Records of system_operate_log @@ -2298,7 +2312,7 @@ CREATE TABLE `system_role` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 153 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表'; +) ENGINE = InnoDB AUTO_INCREMENT = 154 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表'; -- ---------------------------- -- Records of system_role @@ -2307,9 +2321,10 @@ BEGIN; INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '超级管理员', 'super_admin', 1, 1, '', 0, 1, '超级管理员', 'admin', '2021-01-05 17:03:48', '', '2022-02-22 05:08:21', b'0', 1); INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '普通角色', 'common', 2, 2, '', 0, 1, '普通角色', 'admin', '2021-01-05 17:03:48', '', '2022-02-22 05:08:20', b'0', 1); INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, 'CRM 管理员', 'crm_admin', 2, 1, '', 0, 1, 'CRM 专属角色', '1', '2024-02-24 10:51:13', '1', '2024-02-24 02:51:32', b'0', 1); -INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '测试账号', 'test', 0, 2, '[105,106,107]', 0, 2, '', '', '2021-01-06 13:49:35', '1', '2024-07-27 23:30:47', b'0', 1); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '测试账号', 'test', 0, 1, '[]', 0, 2, '', '', '2021-01-06 13:49:35', '1', '2024-08-11 10:41:10', b'0', 1); INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-02-22 00:56:14', '1', '2022-02-22 00:56:14', b'0', 121); INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (153, '某角色', 'tt', 4, 1, '', 0, 2, '', '1', '2024-08-17 14:09:35', '1', '2024-08-17 14:09:35', b'0', 1); COMMIT; -- ---------------------------- @@ -2390,7 +2405,6 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1623, 101, 1193, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1624, 101, 1194, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1625, 101, 1195, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1626, 101, 1196, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1627, 101, 1197, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1628, 101, 1198, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1629, 101, 1199, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); @@ -3195,9 +3209,8 @@ CREATE TABLE `system_sms_channel` ( -- Records of system_sms_channel -- ---------------------------- BEGIN; -INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'Ballcat', 'ALIYUN', 0, '你要改哦,只有我可以用!!!!', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2023-12-02 22:10:17', b'0'); +INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'Ballcat', 'ALIYUN', 0, '你要改哦,只有我可以用!!!!', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2024-08-04 08:53:26', b'0'); INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '测试渠道', 'DEBUG_DING_TALK', 0, '123', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2021-04-13 00:23:14', '1', '2022-03-27 20:29:49', b'0'); -INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, '测试演示', 'DEBUG_DING_TALK', 0, '仅测试', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2022-04-10 23:07:59', '1', '2023-12-02 22:10:08', b'0'); COMMIT; -- ---------------------------- @@ -3222,7 +3235,7 @@ CREATE TABLE `system_sms_code` ( `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号' -) ENGINE = InnoDB AUTO_INCREMENT = 628 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码'; +) ENGINE = InnoDB AUTO_INCREMENT = 632 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码'; -- ---------------------------- -- Records of system_sms_code @@ -3263,7 +3276,7 @@ CREATE TABLE `system_sms_log` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 987 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志'; +) ENGINE = InnoDB AUTO_INCREMENT = 1088 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志'; -- ---------------------------- -- Records of system_sms_log @@ -3293,24 +3306,25 @@ CREATE TABLE `system_sms_template` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板'; +) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板'; -- ---------------------------- -- Records of system_sms_template -- ---------------------------- BEGIN; -INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 1, 0, 'test_01', '测试验证码短信', '正在进行登录操作{operation},您的验证码是{code}', '[\"operation\",\"code\"]', '测试备注', '4383920', 6, 'DEBUG_DING_TALK', '', '2021-03-31 10:49:38', '1', '2023-12-02 22:32:47', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 1, 0, 'test_01', '测试验证码短信', '正在进行登录操作{operation},您的验证码是{code}', '[\"operation\",\"code\"]', '测试备注', '4383920', 4, 'DEBUG_DING_TALK', '', '2021-03-31 10:49:38', '1', '2024-08-18 11:57:18', b'0'); INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3, 1, 0, 'test_02', '公告通知', '您的验证码{code},该验证码5分钟内有效,请勿泄漏于他人!', '[\"code\"]', NULL, 'SMS_207945135', 2, 'ALIYUN', '', '2021-03-31 11:56:30', '1', '2021-04-10 01:22:02', b'0'); -INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, 3, 0, 'test-01', '测试模板', '哈哈哈 {name}', '[\"name\"]', 'f哈哈哈', '4383920', 6, 'DEBUG_DING_TALK', '1', '2021-04-10 01:07:21', '1', '2022-12-10 21:26:09', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, 3, 0, 'test-01', '测试模板', '哈哈哈 {name}', '[\"name\"]', 'f哈哈哈', '4383920', 4, 'DEBUG_DING_TALK', '1', '2021-04-10 01:07:21', '1', '2024-08-18 11:57:07', b'0'); INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, 3, 0, 'test-04', '测试下', '老鸡{name},牛逼{code}', '[\"name\",\"code\"]', '哈哈哈哈', 'suibian', 4, 'DEBUG_DING_TALK', '1', '2021-04-13 00:29:53', '1', '2023-12-02 22:35:34', b'0'); -INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 1, 0, 'user-sms-login', '前台用户短信登录', '您的验证码是{code}', '[\"code\"]', NULL, '4372216', 6, 'DEBUG_DING_TALK', '1', '2021-10-11 08:10:00', '1', '2022-12-10 21:25:59', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 1, 0, 'user-sms-login', '前台用户短信登录', '您的验证码是{code}', '[\"code\"]', NULL, '4372216', 4, 'DEBUG_DING_TALK', '1', '2021-10-11 08:10:00', '1', '2024-08-18 11:57:06', b'0'); INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (9, 2, 0, 'bpm_task_assigned', '【工作流】任务被分配', '您收到了一条新的待办任务:{processInstanceName}-{taskName},申请人:{startUserNickname},处理链接:{detailUrl}', '[\"processInstanceName\",\"taskName\",\"startUserNickname\",\"detailUrl\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2022-01-21 22:31:19', '1', '2022-01-22 00:03:36', b'0'); INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (10, 2, 0, 'bpm_process_instance_reject', '【工作流】流程被不通过', '您的流程被审批不通过:{processInstanceName},原因:{reason},查看链接:{detailUrl}', '[\"processInstanceName\",\"reason\",\"detailUrl\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2022-01-22 00:03:31', '1', '2022-05-01 12:33:14', b'0'); INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (11, 2, 0, 'bpm_process_instance_approve', '【工作流】流程被通过', '您的流程被审批通过:{processInstanceName},查看链接:{detailUrl}', '[\"processInstanceName\",\"detailUrl\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2022-01-22 00:04:31', '1', '2022-03-27 20:32:21', b'0'); -INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (12, 2, 0, 'demo', '演示模板', '我就是测试一下下', '[]', NULL, 'biubiubiu', 6, 'DEBUG_DING_TALK', '1', '2022-04-10 23:22:49', '1', '2023-03-24 23:45:07', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (12, 2, 0, 'demo', '演示模板', '我就是测试一下下', '[]', NULL, 'biubiubiu', 4, 'DEBUG_DING_TALK', '1', '2022-04-10 23:22:49', '1', '2024-08-18 11:57:04', b'0'); INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (14, 1, 0, 'user-update-mobile', '会员用户 - 修改手机', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-08-19 11:34:04', b'0'); INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (15, 1, 0, 'user-update-password', '会员用户 - 修改密码', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-08-19 11:34:18', b'0'); INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (16, 1, 0, 'user-reset-password', '会员用户 - 重置密码', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-12-02 22:35:27', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (17, 2, 0, 'bpm_task_timeout', '【工作流】任务审批超时', '您收到了一条超时的待办任务:{processInstanceName}-{taskName},处理链接:{detailUrl}', '[\"processInstanceName\",\"taskName\",\"detailUrl\"]', '', 'X', 4, 'DEBUG_DING_TALK', '1', '2024-08-16 21:59:15', '1', '2024-08-16 21:59:34', b'0'); COMMIT; -- ---------------------------- @@ -3367,7 +3381,7 @@ CREATE TABLE `system_social_user` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 36 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表'; +) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表'; -- ---------------------------- -- Records of system_social_user @@ -3392,7 +3406,7 @@ CREATE TABLE `system_social_user_bind` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 119 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表'; +) ENGINE = InnoDB AUTO_INCREMENT = 120 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表'; -- ---------------------------- -- Records of system_social_user_bind @@ -3559,10 +3573,10 @@ CREATE TABLE `system_users` ( -- Records of system_users -- ---------------------------- BEGIN; -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2024-07-28 11:35:00', 'admin', '2021-01-05 17:03:47', NULL, '2024-07-28 11:35:00', b'0', 1); -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', NULL, '2022-07-09 23:03:33', b'0', 1); -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$10$YMpimV4T6BtDhIaA8jSW.u8UTGBeGhc/qwXP4oxoMr4mOw9.qttt6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '0:0:0:0:0:0:0:1', '2024-03-18 21:09:04', '', '2021-01-13 23:50:35', NULL, '2024-03-18 21:09:04', b'0', 1); -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$jDFLttgfik0QqJKAbfhMa.2A9xXoZmAIxakdFJUzkX.MgBKT6ddo6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-07-13 23:13:16', '', '2021-01-21 02:13:53', NULL, '2024-07-13 23:13:16', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2024-08-26 16:54:00', 'admin', '2021-01-05 17:03:47', NULL, '2024-08-26 16:54:00', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 0, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', '1', '2024-08-17 11:06:13', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$04$fUBSmjKCPYAUmnMzOb6qE.eZCGPhHi1JmAKclODbfS/O7fHOl2bH6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '0:0:0:0:0:0:0:1', '2024-08-11 17:48:12', '', '2021-01-13 23:50:35', NULL, '2024-08-11 17:48:12', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$jDFLttgfik0QqJKAbfhMa.2A9xXoZmAIxakdFJUzkX.MgBKT6ddo6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-08-11 09:38:08', '', '2021-01-21 02:13:53', NULL, '2024-08-11 09:38:08', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, 'admin107', '$2a$10$dYOOBKMO93v/.ReCqzyFg.o67Tqk.bbc2bhrpyBGkIw9aypCtr2pm', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 22:59:33', '1', '2022-02-27 08:26:51', b'0', 118); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, 'admin108', '$2a$10$y6mfvKoNYL1GXWak8nYwVOH.kCWqjactkzdoIDgiKl93WN3Ejg.Lu', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 23:00:50', '1', '2022-02-27 08:26:53', b'0', 119); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, 'admin109', '$2a$10$JAqvH0tEc0I7dfDVBI7zyuB4E3j.uH6daIjV53.vUS6PknFkDJkuK', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 23:11:50', '1', '2022-02-27 08:26:56', b'0', 120); @@ -3572,7 +3586,7 @@ INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, 'aoteman', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋道', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '127.0.0.1', '2022-03-19 18:38:51', '1', '2022-03-07 21:37:58', NULL, '2022-03-19 18:38:51', b'0', 122); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (114, 'hrmgr', '$2a$10$TR4eybBioGRhBmDBWkqWLO6NIh3mzYa8KBKDDB5woiGYFVlRAi.fu', 'hr 小姐姐', NULL, NULL, '[5]', '', '15601691236', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-03-24 22:21:05', '1', '2022-03-19 21:50:58', NULL, '2024-03-24 22:21:05', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, 'aotemane', '$2a$04$GcyP0Vyzb2F2Yni5PuIK9ueGxM0tkZGMtDwVRwrNbtMvorzbpNsV2', '阿呆', '11222', 102, '[1,2]', '7648@qq.com', '15601691229', 2, '', 0, '', NULL, '1', '2022-04-30 02:55:43', '1', '2024-04-04 09:37:14', b'0', 1); -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, 'admin123', '$2a$10$WI8Gg/lpZQIrOEZMHqka7OdFaD4Nx.B/qY8ZGTTUKrOJwaHFqibaC', '测试号', '1111', 100, '[2]', '', '15601691234', 1, '', 0, '', NULL, '1', '2022-07-09 17:40:26', '1', '2022-07-09 17:40:26', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, 'admin123', '$2a$10$WI8Gg/lpZQIrOEZMHqka7OdFaD4Nx.B/qY8ZGTTUKrOJwaHFqibaC', '测试号02', '1111', 100, '[2]', '', '15601691234', 1, '', 0, '', NULL, '1', '2022-07-09 17:40:26', '1', '2024-08-11 10:12:03', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (118, 'goudan', '$2a$04$OB1SuphCdiLVRpiYRKeqH.8NYS7UIp5vmIv1W7U4w6toiFeOAATVK', '狗蛋', NULL, 103, '[1]', '', '15601691239', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-03-17 09:10:27', '1', '2022-07-09 17:44:43', '1', '2024-04-04 09:48:05', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (131, 'hh', '$2a$04$jyH9h6.gaw8mpOjPfHIpx.8as2Rzfcmdlj5rlJFwgCw4rsv/MTb2K', '呵呵', NULL, 100, '[]', '777@qq.com', '15601882312', 1, '', 0, '', NULL, '1', '2024-04-27 08:45:56', '1', '2024-04-27 08:45:56', b'0', 1); COMMIT; From a677e98de0ca16b7884fd8ecf04a0d0af77cd923 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 31 Aug 2024 10:05:33 +0800 Subject: [PATCH 090/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91SYSTEM=EF=BC=9A=E8=A7=92=E8=89=B2=E6=A0=87?= =?UTF-8?q?=E8=AF=86=E7=9A=84=E6=8F=90=E7=A4=BA=E4=B8=8D=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/handler/GlobalExceptionHandler.java | 75 ++++++++++++------- .../system/enums/DictTypeConstants.java | 3 +- .../system/enums/ErrorCodeConstants.java | 4 +- .../admin/permission/vo/role/RoleRespVO.java | 5 +- .../permission/vo/role/RoleSaveReqVO.java | 14 +++- 5 files changed, 66 insertions(+), 35 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index 3b0a17fa4..41646d7ef 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -4,6 +4,7 @@ import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.JakartaServletUtil; import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; @@ -14,13 +15,14 @@ import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.validation.ValidationException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.access.AccessDeniedException; import org.springframework.util.Assert; import org.springframework.validation.BindException; @@ -38,7 +40,12 @@ import java.time.LocalDateTime; import java.util.Map; import java.util.Set; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.METHOD_NOT_ALLOWED; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_FOUND; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; /** * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 @@ -88,7 +95,7 @@ public class GlobalExceptionHandler { return validationException((ValidationException) ex); } if (ex instanceof NoHandlerFoundException) { - return noHandlerFoundExceptionHandler(request, (NoHandlerFoundException) ex); + return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex); } if (ex instanceof NoResourceFoundException) { return noResourceFoundExceptionHandler(request, (NoResourceFoundException) ex); @@ -123,7 +130,7 @@ public class GlobalExceptionHandler { */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) public CommonResult methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { - log.warn("[missingServletRequestParameterExceptionHandler]", ex); + log.warn("[methodArgumentTypeMismatchExceptionHandler]", ex); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage())); } @@ -149,6 +156,22 @@ public class GlobalExceptionHandler { return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); } + /** + * 处理 SpringMVC 请求参数类型错误 + * + * 例如说,接口上设置了 @RequestBody实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public CommonResult methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) { + log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex); + if(ex.getCause() instanceof InvalidFormatException) { + InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause(); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue())); + }else { + return defaultExceptionHandler(ServletUtils.getRequest(), ex); + } + } + /** * 处理 Validator 校验不通过产生的异常 */ @@ -177,7 +200,7 @@ public class GlobalExceptionHandler { * 2. spring.mvc.static-path-pattern 为 /statics/** */ @ExceptionHandler(NoHandlerFoundException.class) - public CommonResult noHandlerFoundExceptionHandler(HttpServletRequest req, NoHandlerFoundException ex) { + public CommonResult noHandlerFoundExceptionHandler(NoHandlerFoundException ex) { log.warn("[noHandlerFoundExceptionHandler]", ex); return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); } @@ -253,7 +276,7 @@ public class GlobalExceptionHandler { // 情况二:处理异常 log.error("[defaultExceptionHandler]", ex); // 插入异常日志 - this.createExceptionLog(req, ex); + createExceptionLog(req, ex); // 返回 ERROR CommonResult return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); } @@ -279,7 +302,7 @@ public class GlobalExceptionHandler { errorLog.setExceptionName(e.getClass().getName()); errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); - errorLog.setExceptionStackTrace(ExceptionUtils.getStackTrace(e)); + errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e)); StackTraceElement[] stackTraceElements = e.getStackTrace(); Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); StackTraceElement stackTraceElement = stackTraceElements[0]; @@ -292,12 +315,12 @@ public class GlobalExceptionHandler { errorLog.setApplicationName(applicationName); errorLog.setRequestUrl(request.getRequestURI()); Map requestParams = MapUtil.builder() - .put("query", ServletUtils.getParamMap(request)) - .put("body", ServletUtils.getBody(request)).build(); + .put("query", JakartaServletUtil.getParamMap(request)) + .put("body", JakartaServletUtil.getBody(request)).build(); errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); errorLog.setRequestMethod(request.getMethod()); errorLog.setUserAgent(ServletUtils.getUserAgent(request)); - errorLog.setUserIp(ServletUtils.getClientIP(request)); + errorLog.setUserIp(JakartaServletUtil.getClientIP(request)); errorLog.setExceptionTime(LocalDateTime.now()); } @@ -314,51 +337,51 @@ public class GlobalExceptionHandler { } // 1. 数据报表 if (message.contains("report_")) { - log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]"); + log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[报表模块 yudao-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]"); + "[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]"); } // 2. 工作流 if (message.contains("bpm_")) { - log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]"); + log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]"); + "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); } // 3. 微信公众号 if (message.contains("mp_")) { - log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); } // 4. 商城系统 if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) { - log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); + log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); + "[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); } // 5. ERP 系统 if (message.contains("erp_")) { - log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://doc.iocoder.cn/erp/build/ 开启]"); + log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://doc.iocoder.cn/erp/build/ 开启]"); + "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); } // 6. CRM 系统 if (message.contains("crm_")) { - log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://doc.iocoder.cn/crm/build/ 开启]"); + log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://doc.iocoder.cn/crm/build/ 开启]"); + "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); } // 7. 支付平台 if (message.contains("pay_")) { - log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[支付模块 yudao-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); } // 8. AI 大模型 if (message.contains("ai_")) { - log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://doc.iocoder.cn/ai/build/ 开启]"); + log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://doc.iocoder.cn/ai/build/ 开启]"); + "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); } return null; } diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/DictTypeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/DictTypeConstants.java index d7967fe28..d7592c34c 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/DictTypeConstants.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/DictTypeConstants.java @@ -13,12 +13,11 @@ public interface DictTypeConstants { // ========== SYSTEM 模块 ========== String USER_SEX = "system_user_sex"; // 用户性别 + String DATA_SCOPE = "system_data_scope"; // 数据范围 String LOGIN_TYPE = "system_login_type"; // 登录日志的类型 String LOGIN_RESULT = "system_login_result"; // 登录结果 - String ERROR_CODE_TYPE = "system_error_code_type"; // 错误码的类型枚举 - String SMS_CHANNEL_CODE = "system_sms_channel_code"; // 短信渠道编码 String SMS_TEMPLATE_TYPE = "system_sms_template_type"; // 短信模板类型 String SMS_SEND_STATUS = "system_sms_send_status"; // 短信发送状态 diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java index e360a426b..5a44a9869 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java @@ -27,10 +27,10 @@ public interface ErrorCodeConstants { // ========== 角色模块 1-002-002-000 ========== ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1_002_002_000, "角色不存在"); ErrorCode ROLE_NAME_DUPLICATE = new ErrorCode(1_002_002_001, "已经存在名为【{}】的角色"); - ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1_002_002_002, "已经存在编码为【{}】的角色"); + ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1_002_002_002, "已经存在标识为【{}】的角色"); ErrorCode ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE = new ErrorCode(1_002_002_003, "不能操作类型为系统内置的角色"); ErrorCode ROLE_IS_DISABLE = new ErrorCode(1_002_002_004, "名字为【{}】的角色已被禁用"); - ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1_002_002_005, "编码【{}】不能使用"); + ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1_002_002_005, "标识【{}】不能使用"); // ========== 用户模块 1-002-003-000 ========== ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1_002_003_000, "用户账号已经存在"); diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java index e7b48c8bc..89f80c672 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java @@ -6,9 +6,9 @@ import cn.iocoder.yudao.module.system.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import lombok.Data; -import jakarta.validation.constraints.NotBlank; import java.time.LocalDateTime; import java.util.Set; @@ -46,7 +46,8 @@ public class RoleRespVO { private String remark; @Schema(description = "数据范围,参见 DataScopeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @ExcelProperty("数据范围") + @ExcelProperty(value = "数据范围", converter = DictConvert.class) + @DictFormat(DictTypeConstants.DATA_SCOPE) private Integer dataScope; @Schema(description = "数据范围(指定部门数组)", example = "1") diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java index ee5951fc0..2d273f360 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java @@ -1,12 +1,13 @@ package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; import com.mzt.logapi.starter.annotation.DiffLogField; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import lombok.Data; @Schema(description = "管理后台 - 角色创建/更新 Request VO") @Data @@ -23,7 +24,7 @@ public class RoleSaveReqVO { @NotBlank(message = "角色标志不能为空") @Size(max = 100, message = "角色标志长度不能超过 100 个字符") - @Schema(description = "角色编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ADMIN") + @Schema(description = "角色标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "ADMIN") @DiffLogField(name = "角色标志") private String code; @@ -32,7 +33,14 @@ public class RoleSaveReqVO { @DiffLogField(name = "显示顺序") private Integer sort; + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @DiffLogField(name = "状态") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") + private Integer status; + @Schema(description = "备注", example = "我是一个角色") + @Size(max = 500, message = "备注长度不能超过 500 个字符") @DiffLogField(name = "备注") private String remark; From 424922b488f1c30b7daac0c51abfdd84e92abd82 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 31 Aug 2024 10:14:48 +0800 Subject: [PATCH 091/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91SYSTEM=EF=BC=9A=E8=A7=92=E8=89=B2=E6=A0=87?= =?UTF-8?q?=E8=AF=86=E7=9A=84=E6=8F=90=E7=A4=BA=E4=B8=8D=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/system/service/permission/RoleServiceImpl.java | 3 ++- .../system/service/permission/RoleServiceImplTest.java | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java index 35db06706..53d6b7e72 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.service.permission; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; @@ -61,7 +62,7 @@ public class RoleServiceImpl implements RoleService { // 2. 插入到数据库 RoleDO role = BeanUtils.toBean(createReqVO, RoleDO.class) .setType(ObjectUtil.defaultIfNull(type, RoleTypeEnum.CUSTOM.getType())) - .setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setStatus(ObjUtil.defaultIfNull(createReqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) .setDataScope(DataScopeEnum.ALL.getScope()); // 默认可查看所有数据。原因是,可能一些项目不需要项目权限 roleMapper.insert(role); diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImplTest.java index 941b7bca1..fc87193c4 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImplTest.java @@ -51,7 +51,8 @@ public class RoleServiceImplTest extends BaseDbUnitTest { public void testCreateRole() { // 准备参数 RoleSaveReqVO reqVO = randomPojo(RoleSaveReqVO.class) - .setId(null); // 防止 id 被赋值 + .setId(null) // 防止 id 被赋值 + .setStatus(randomCommonStatus()); // 调用 Long roleId = roleService.createRole(reqVO, null); @@ -59,7 +60,6 @@ public class RoleServiceImplTest extends BaseDbUnitTest { RoleDO roleDO = roleMapper.selectById(roleId); assertPojoEquals(reqVO, roleDO, "id"); assertEquals(RoleTypeEnum.CUSTOM.getType(), roleDO.getType()); - assertEquals(CommonStatusEnum.ENABLE.getStatus(), roleDO.getStatus()); assertEquals(DataScopeEnum.ALL.getScope(), roleDO.getDataScope()); } @@ -70,7 +70,8 @@ public class RoleServiceImplTest extends BaseDbUnitTest { roleMapper.insert(roleDO); // 准备参数 Long id = roleDO.getId(); - RoleSaveReqVO reqVO = randomPojo(RoleSaveReqVO.class, o -> o.setId(id)); + RoleSaveReqVO reqVO = randomPojo(RoleSaveReqVO.class, o -> o.setId(id) + .setStatus(randomCommonStatus())); // 调用 roleService.updateRole(reqVO); From 56ae4503a619dd52068c42ddcfae0dcf87a6cb91 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 31 Aug 2024 13:28:30 +0800 Subject: [PATCH 092/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91SYSTEM=EF=BC=9A=E6=93=8D=E4=BD=9C=E6=97=A5?= =?UTF-8?q?=E5=BF=97=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=BC=82=E6=AD=A5=E8=AE=B0?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/YudaoMybatisAutoConfiguration.java | 14 +++++++++- .../core/aop/ApiSignatureAspect.java | 7 +++-- .../core/redis/ApiSignatureRedisDAO.java | 14 +++++----- .../signature/core/ApiSignatureTest.java | 4 +-- .../core/service/LogRecordServiceImpl.java | 26 ++++++++++++------- .../definition/BpmModelServiceImpl.java | 2 +- .../bpm/service/task/BpmTaskServiceImpl.java | 6 ++--- 7 files changed, 45 insertions(+), 28 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java index d685fd81a..ab2992184 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java @@ -7,6 +7,8 @@ import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; import com.baomidou.mybatisplus.extension.incrementer.*; +import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal; +import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.apache.ibatis.annotations.Mapper; @@ -16,6 +18,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.core.env.ConfigurableEnvironment; +import java.util.concurrent.TimeUnit; + /** * MyBaits 配置类 * @@ -26,6 +30,14 @@ import org.springframework.core.env.ConfigurableEnvironment; lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试 public class YudaoMybatisAutoConfiguration { + static { + // 动态 SQL 智能优化支持本地缓存加速解析,更完善的租户复杂 XML 动态 SQL 支持,静态注入缓存 + JsqlParserGlobal.setJsqlParseCache(new JdkSerialCaffeineJsqlParseCache( + (cache) -> cache.maximumSize(1024) + .expireAfterWrite(5, TimeUnit.SECONDS)) + ); + } + @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); @@ -34,7 +46,7 @@ public class YudaoMybatisAutoConfiguration { } @Bean - public MetaObjectHandler defaultMetaObjectHandler(){ + public MetaObjectHandler defaultMetaObjectHandler() { return new DefaultDBFieldHandler(); // 自动填充参数类 } diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java index 3259dac11..c1c78ac57 100644 --- a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java @@ -69,7 +69,7 @@ public class ApiSignatureAspect { // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ) String nonce = request.getHeader(signature.nonce()); - signatureRedisDAO.setNonce(nonce, signature.timeout() * 2, signature.timeUnit()); + signatureRedisDAO.setNonce(appId, nonce, signature.timeout() * 2, signature.timeUnit()); return true; } @@ -113,7 +113,7 @@ public class ApiSignatureAspect { } // 3. 检查 nonce 是否存在,有且仅能使用一次 - return signatureRedisDAO.getNonce(nonce) == null; + return signatureRedisDAO.getNonce(appId, nonce) == null; } /** @@ -165,5 +165,4 @@ public class ApiSignatureAspect { return sortedMap; } -} - +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java index f4aa84910..11fe384da 100644 --- a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java @@ -22,7 +22,7 @@ public class ApiSignatureRedisDAO { * VALUE 格式:String * 过期时间:不固定 */ - private static final String SIGNATURE_NONCE = "api_signature_nonce:%s"; + private static final String SIGNATURE_NONCE = "api_signature_nonce:%s:%s"; /** * 签名密钥 @@ -36,16 +36,16 @@ public class ApiSignatureRedisDAO { // ========== 验签随机数 ========== - public String getNonce(String nonce) { - return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce)); + public String getNonce(String appId, String nonce) { + return stringRedisTemplate.opsForValue().get(formatNonceKey(appId, nonce)); } - public void setNonce(String nonce, int time, TimeUnit timeUnit) { - stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), "", time, timeUnit); + public void setNonce(String appId, String nonce, int time, TimeUnit timeUnit) { + stringRedisTemplate.opsForValue().set(formatNonceKey(appId, nonce), "", time, timeUnit); } - private static String formatNonceKey(String key) { - return String.format(SIGNATURE_NONCE, key); + private static String formatNonceKey(String appId, String nonce) { + return String.format(SIGNATURE_NONCE, appId, nonce); } // ========== 签名密钥 ========== diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java b/yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java index c9a3dfff4..2b1c5ca44 100644 --- a/yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java @@ -69,7 +69,7 @@ public class ApiSignatureTest { // 断言结果 assertTrue(result); // 断言调用 - verify(signatureRedisDAO).setNonce(eq(nonce), eq(120), eq(TimeUnit.SECONDS)); + verify(signatureRedisDAO).setNonce(eq(appId), eq(nonce), eq(120), eq(TimeUnit.SECONDS)); } -} +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java index d6aeb3bf0..e2ed4c314 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java @@ -11,6 +11,7 @@ import com.mzt.logapi.service.ILogRecordService; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; import java.util.List; @@ -28,19 +29,24 @@ public class LogRecordServiceImpl implements ILogRecordService { private OperateLogApi operateLogApi; @Override + @Async public void record(LogRecord logRecord) { - // 1. 补全通用字段 OperateLogCreateReqDTO reqDTO = new OperateLogCreateReqDTO(); - reqDTO.setTraceId(TracerUtils.getTraceId()); - // 补充用户信息 - fillUserFields(reqDTO); - // 补全模块信息 - fillModuleFields(reqDTO, logRecord); - // 补全请求信息 - fillRequestFields(reqDTO); + try { + reqDTO.setTraceId(TracerUtils.getTraceId()); + // 补充用户信息 + fillUserFields(reqDTO); + // 补全模块信息 + fillModuleFields(reqDTO, logRecord); + // 补全请求信息 + fillRequestFields(reqDTO); - // 2. 异步记录日志 - operateLogApi.createOperateLog(reqDTO); + // 2. 异步记录日志 + operateLogApi.createOperateLog(reqDTO); + } catch (Throwable ex) { + // 由于 @Async 异步调用,这里打印下日志,更容易跟进 + log.error("[record][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex); + } } private static void fillUserFields(OperateLogCreateReqDTO reqDTO) { diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java index 7c4dae618..3770e7ca7 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java @@ -63,6 +63,7 @@ public class BpmModelServiceImpl implements BpmModelService { @Override public PageResult getModelPage(BpmModelPageReqVO pageVO) { ModelQuery modelQuery = repositoryService.createModelQuery(); + modelQuery.modelTenantId(FlowableUtils.getTenantId()); if (StrUtil.isNotBlank(pageVO.getKey())) { modelQuery.modelKey(pageVO.getKey()); } @@ -78,7 +79,6 @@ public class BpmModelServiceImpl implements BpmModelService { return PageResult.empty(count); } List models = modelQuery - .modelTenantId(FlowableUtils.getTenantId()) .orderByCreateTime().desc() .listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); return new PageResult<>(models, count); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index aa5326ddd..3b0c742fd 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -97,7 +97,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { } if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); - taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[1])); + taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); } long count = taskQuery.count(); if (count == 0) { @@ -119,7 +119,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { } if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); - taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[1])); + taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); } // 执行查询 long count = taskQuery.count(); @@ -141,7 +141,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { } if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); - taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[1])); + taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); } // 执行查询 long count = taskQuery.count(); From e30795fb69ccbb1f77271abafa4b8bc75e35d3d7 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 31 Aug 2024 14:18:35 +0800 Subject: [PATCH 093/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91AI=20=E5=A4=A7=E6=A8=A1=E5=9E=8B=EF=BC=9A?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/enums/ErrorCodeConstants.java | 1 - .../knowledge/AiKnowledgeController.java | 4 +- .../AiKnowledgeDocumentController.java | 15 +++----- .../AiKnowledgeSegmentController.java | 12 +++--- .../AiKnowledgeDocumentPageReqVO.java | 3 +- .../document/AiKnowledgeDocumentRespVO.java | 11 +++--- .../AiKnowledgeDocumentUpdateReqVO.java | 3 ++ .../AiKnowledgeDocumentCreateReqVO.java | 2 +- .../vo/knowledge/AiKnowledgeRespVO.java | 5 ++- .../segment/AiKnowledgeSegmentPageReqVO.java | 5 +-- .../vo/segment/AiKnowledgeSegmentRespVO.java | 7 ++-- .../AiKnowledgeSegmentUpdateStatusReqVO.java | 7 +++- .../knowledge/AiKnowledgeSegmentDO.java | 5 ++- .../AiKnowledgeDocumentServiceImpl.java | 38 ++++++++----------- .../knowledge/AiKnowledgeSegmentService.java | 7 ++-- .../ai/service/model/AiApiKeyService.java | 18 ++++----- .../ai/service/model/AiApiKeyServiceImpl.java | 15 ++++---- .../ai/core/factory/AiModelFactory.java | 24 ++++++------ .../ai/core/factory/AiModelFactoryImpl.java | 35 ++++++++--------- .../ai/core/factory/AiVectorStoreFactory.java | 3 +- .../factory/AiVectorStoreFactoryImpl.java | 3 +- 21 files changed, 111 insertions(+), 112 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index c3158a1aa..e1dd1a956 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -52,7 +52,6 @@ public interface ErrorCodeConstants { // ========== API 思维导图 1-040-008-000 ========== ErrorCode MIND_MAP_NOT_EXISTS = new ErrorCode(1_040_008_000, "思维导图不存在!"); - // ========== API 知识库 1-022-008-000 ========== ErrorCode KNOWLEDGE_NOT_EXISTS = new ErrorCode(1_022_008_000, "知识库不存在!"); ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_022_008_001, "文档不存在!"); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java index a7b49b413..dc2c8e3ae 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java @@ -22,6 +22,7 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti @Tag(name = "管理后台 - AI 知识库") @RestController @RequestMapping("/ai/knowledge") +@Validated public class AiKnowledgeController { @Resource @@ -34,14 +35,12 @@ public class AiKnowledgeController { return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class)); } - @PostMapping("/create-my") @Operation(summary = "创建【我的】知识库") public CommonResult createKnowledgeMy(@RequestBody @Valid AiKnowledgeCreateMyReqVO createReqVO) { return success(knowledgeService.createKnowledgeMy(createReqVO, getLoginUserId())); } - @PutMapping("/update-my") @Operation(summary = "更新【我的】知识库") public CommonResult updateKnowledgeMy(@RequestBody @Valid AiKnowledgeUpdateMyReqVO updateReqVO) { @@ -49,5 +48,4 @@ public class AiKnowledgeController { return success(true); } - } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java index 58a53a19c..d86210556 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java @@ -12,43 +12,40 @@ import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.validation.Valid; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -@Tag(name = "管理后台 - AI 知识库-文档") +@Tag(name = "管理后台 - AI 知识库文档") @RestController @RequestMapping("/ai/knowledge/document") +@Validated public class AiKnowledgeDocumentController { @Resource private AiKnowledgeDocumentService documentService; - @PostMapping("/create") @Operation(summary = "新建文档") - public CommonResult createKnowledgeDocument(@Validated AiKnowledgeDocumentCreateReqVO reqVO) { + public CommonResult createKnowledgeDocument(@Valid AiKnowledgeDocumentCreateReqVO reqVO) { Long knowledgeDocumentId = documentService.createKnowledgeDocument(reqVO); return success(knowledgeDocumentId); } - @GetMapping("/page") @Operation(summary = "获取文档分页") - public CommonResult> getKnowledgeDocumentPageMy(@Validated AiKnowledgeDocumentPageReqVO pageReqVO) { + public CommonResult> getKnowledgeDocumentPageMy(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) { PageResult pageResult = documentService.getKnowledgeDocumentPage(pageReqVO); return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class)); } - @PutMapping("/update") @Operation(summary = "更新文档") - public CommonResult updateKnowledgeDocument(@Validated @RequestBody AiKnowledgeDocumentUpdateReqVO reqVO) { + public CommonResult updateKnowledgeDocument(@Valid @RequestBody AiKnowledgeDocumentUpdateReqVO reqVO) { documentService.updateKnowledgeDocument(reqVO); return success(true); } - } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java index a19f38eb7..a0d0952a8 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java @@ -12,15 +12,16 @@ import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.validation.Valid; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -@Tag(name = "管理后台 - AI 知识库-段落") +@Tag(name = "管理后台 - AI 知识库段落") @RestController @RequestMapping("/ai/knowledge/segment") +@Validated public class AiKnowledgeSegmentController { @Resource @@ -28,22 +29,21 @@ public class AiKnowledgeSegmentController { @GetMapping("/page") @Operation(summary = "获取段落分页") - public CommonResult> getKnowledgeSegmentPageMy(@Validated AiKnowledgeSegmentPageReqVO pageReqVO) { + public CommonResult> getKnowledgeSegmentPageMy(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) { PageResult pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO); return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class)); } - @PutMapping("/update") @Operation(summary = "更新段落内容") - public CommonResult updateKnowledgeSegment(@Validated @RequestBody AiKnowledgeSegmentUpdateReqVO reqVO) { + public CommonResult updateKnowledgeSegment(@Valid @RequestBody AiKnowledgeSegmentUpdateReqVO reqVO) { segmentService.updateKnowledgeSegment(reqVO); return success(true); } @PutMapping("/update-status") @Operation(summary = "启禁用段落内容") - public CommonResult updateKnowledgeSegmentStatus(@Validated @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) { + public CommonResult updateKnowledgeSegmentStatus(@Valid @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) { segmentService.updateKnowledgeSegmentStatus(reqVO); return success(true); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java index c1e7947f5..76c001bd3 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java @@ -4,10 +4,11 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -@Schema(description = "管理后台 - AI 知识库-文档 分页 Request VO") +@Schema(description = "管理后台 - AI 知识库文档的分页 Request VO") @Data public class AiKnowledgeDocumentPageReqVO extends PageParam { @Schema(description = "文档名称", example = "Java 开发手册") private String name; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java index 94a022363..96ca61b3d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java @@ -17,21 +17,22 @@ public class AiKnowledgeDocumentRespVO extends PageParam { @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") private String name; - @Schema(description = "内容", example = "Java 是一门面向对象的语言.....") + @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 是一门面向对象的语言.....") private String content; @Schema(description = "文档 url", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn") private String url; - @Schema(description = "token 数量", example = "1024") + @Schema(description = "token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Integer tokens; - @Schema(description = "字符数", example = "1008") + @Schema(description = "字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1008") private Integer wordCount; - @Schema(description = "切片状态", example = "1") + @Schema(description = "切片状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer sliceStatus; - @Schema(description = "文档状态", example = "1") + @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java index 6fb42c774..2cc6a32f3 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -15,6 +17,7 @@ public class AiKnowledgeDocumentUpdateReqVO { private Long id; @Schema(description = "是否启用", example = "1") + @InEnum(CommonStatusEnum.class) private Integer status; @Schema(description = "名称", example = "Java 开发手册") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java index 660c573ba..9cc5290ab 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java @@ -7,7 +7,7 @@ import lombok.Data; import org.hibernate.validator.constraints.URL; -@Schema(description = "管理后台 - AI 知识库创建【文档】 Request VO") +@Schema(description = "管理后台 - AI 知识库文档的创建 Request VO") @Data public class AiKnowledgeDocumentCreateReqVO { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java index 2eb08717e..3ff8a1c75 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java @@ -14,12 +14,13 @@ public class AiKnowledgeRespVO { @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南") private String name; - @Schema(description = "知识库描述", example = "ruoyi-vue-pro 用户指南") + @Schema(description = "知识库描述", example = "帮助你快速构建系统") private String description; @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "14") private Long modelId; - @Schema(description = "模型标识", example = "qwen-72b-chat") + @Schema(description = "模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "qwen-72b-chat") private String model; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java index 125cb80b1..8be3db501 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java @@ -4,15 +4,14 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -@Schema(description = "管理后台 - AI 知识库分页 Request VO") +@Schema(description = "管理后台 - AI 知识库分段的分页 Request VO") @Data public class AiKnowledgeSegmentPageReqVO extends PageParam { - @Schema(description = "分段状态", example = "1") private Integer status; - @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @Schema(description = "文档编号", example = "1") private Integer documentId; @Schema(description = "分段内容关键字", example = "Java 开发") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java index d8411618b..5e3f2d8cb 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java @@ -22,12 +22,13 @@ public class AiKnowledgeSegmentRespVO { @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") private String content; - @Schema(description = "token 数量", example = "1024") + @Schema(description = "token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Integer tokens; - @Schema(description = "字符数", example = "1008") + @Schema(description = "字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1008") private Integer wordCount; - @Schema(description = "文档状态", example = "1") + @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java index 409ce0146..2516c7dfb 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java @@ -1,10 +1,13 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import lombok.Data; -@Schema(description = "管理后台 - AI 更新 知识库-段落 request VO") +@Schema(description = "管理后台 - AI 知识库段落的更新状态 Request VO") @Data public class AiKnowledgeSegmentUpdateStatusReqVO { @@ -12,6 +15,8 @@ public class AiKnowledgeSegmentUpdateStatusReqVO { private Long id; @Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "是否启用不能为空") + @InEnum(CommonStatusEnum.class) private Integer status; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java index fc758ce31..84f7de654 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java @@ -28,12 +28,13 @@ public class AiKnowledgeSegmentDO extends BaseDO { private String vectorId; /** * 知识库编号 + * * 关联 {@link AiKnowledgeDO#getId()} */ private Long knowledgeId; /** * 文档编号 - *

+ * * 关联 {@link AiKnowledgeDocumentDO#getId()} */ private Long documentId; @@ -51,7 +52,7 @@ public class AiKnowledgeSegmentDO extends BaseDO { private Integer tokens; /** * 状态 - *

+ * * 枚举 {@link CommonStatusEnum} */ private Integer status; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 3758c3bfd..99f0621c8 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -30,13 +30,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_NOT_EXISTS; /** - * AI 知识库-文档 Service 实现类 + * AI 知识库文档 Service 实现类 * * @author xiaoxin */ @@ -61,24 +60,21 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic @Resource private AiChatModelService chatModelService; - - // TODO 芋艿:需要 review 下,代码格式; @Override @Transactional(rollbackFor = Exception.class) public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) { + // 0. 校验 + AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId()); + AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId()); + // 1.1 下载文档 - String url = createReqVO.getUrl(); - // 1.2 加载文档 - TikaDocumentReader loader = new TikaDocumentReader(downloadFile(url)); + TikaDocumentReader loader = new TikaDocumentReader(downloadFile(createReqVO.getUrl())); List documents = loader.get(); Document document = CollUtil.getFirst(documents); + // 1.2 文档记录入库 String content = document.getContent(); - Integer tokens = tokenCountEstimator.estimate(content); - Integer wordCount = content.length(); - - // 1.3 文档记录入库 AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class) - .setTokens(tokens).setWordCount(wordCount) + .setTokens(tokenCountEstimator.estimate(content)).setWordCount(content.length()) .setStatus(CommonStatusEnum.ENABLE.getStatus()).setSliceStatus(AiKnowledgeDocumentStatusEnum.SUCCESS.getStatus()); documentMapper.insert(documentDO); Long documentId = documentDO.getId(); @@ -90,22 +86,16 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic List segments = tokenTextSplitter.apply(documents); // 2.2 分段内容入库 List segmentDOList = CollectionUtils.convertList(segments, - segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId).setKnowledgeId(createReqVO.getKnowledgeId()).setVectorId(segment.getId()) + segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId) + .setKnowledgeId(createReqVO.getKnowledgeId()).setVectorId(segment.getId()) .setTokens(tokenCountEstimator.estimate(segment.getContent())).setWordCount(segment.getContent().length()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); segmentMapper.insertBatch(segmentDOList); - // 3.1 document 补充源数据 - segments.forEach(segment -> { - Map metadata = segment.getMetadata(); - metadata.put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, createReqVO.getKnowledgeId()); - }); - - AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId()); - AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId()); - // 3.2 获取向量存储实例 + // 3.1 获取向量存储实例 VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId()); - // 3.3 向量化并存储 + // 3.2 向量化并存储 + segments.forEach(segment -> segment.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, createReqVO.getKnowledgeId())); vectorStore.add(segments); return documentId; } @@ -117,7 +107,9 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic @Override public void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO) { + // 1. 校验文档是否存在 validateKnowledgeDocumentExists(reqVO.getId()); + // 2. 更新文档 AiKnowledgeDocumentDO document = BeanUtils.toBean(reqVO, AiKnowledgeDocumentDO.class); documentMapper.updateById(document); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index 22f634907..8ecb2d24a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -7,7 +7,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowle import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; /** - * AI 知识库分片 Service 接口 + * AI 知识库段落 Service 接口 * * @author xiaoxin */ @@ -22,16 +22,17 @@ public interface AiKnowledgeSegmentService { PageResult getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO); /** - * 更新段落内容 + * 更新段落的内容 * * @param reqVO 更新内容 */ void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO); /** - * 更新状态 + * 更新段落的状态 * * @param reqVO 更新内容 */ void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java index 603325da6..f5f881349 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java @@ -85,14 +85,6 @@ public interface AiApiKeyService { */ ChatModel getChatModel(Long id); - /** - * 获得 EmbeddingModel 对象 - * - * @param id 编号 - * @return EmbeddingModel 对象 - */ - EmbeddingModel getEmbeddingModel(Long id); - /** * 获得 ImageModel 对象 * @@ -122,7 +114,15 @@ public interface AiApiKeyService { SunoApi getSunoApi(); /** - * 获得 vector 对象 + * 获得 EmbeddingModel 对象 + * + * @param id 编号 + * @return EmbeddingModel 对象 + */ + EmbeddingModel getEmbeddingModel(Long id); + + /** + * 获得 VectorStore 对象 * * @param id 编号 * @return VectorStore 对象 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java index 25eec75b7..bf11ec218 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java @@ -109,13 +109,6 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { return modelFactory.getOrCreateChatModel(platform, apiKey.getApiKey(), apiKey.getUrl()); } - @Override - public EmbeddingModel getEmbeddingModel(Long id) { - AiApiKeyDO apiKey = validateApiKey(id); - AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); - return modelFactory.getOrCreateEmbeddingModel(platform, apiKey.getApiKey(), apiKey.getUrl()); - } - @Override public ImageModel getImageModel(AiPlatformEnum platform) { AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); @@ -145,10 +138,18 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl()); } + @Override + public EmbeddingModel getEmbeddingModel(Long id) { + AiApiKeyDO apiKey = validateApiKey(id); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); + return modelFactory.getOrCreateEmbeddingModel(platform, apiKey.getApiKey(), apiKey.getUrl()); + } + @Override public VectorStore getOrCreateVectorStore(Long id) { AiApiKeyDO apiKey = validateApiKey(id); AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); return vectorFactory.getOrCreateVectorStore(getEmbeddingModel(id), platform, apiKey.getApiKey(), apiKey.getUrl()); } + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java index 6f628ea4d..7e8465375 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java @@ -26,18 +26,6 @@ public interface AiModelFactory { */ ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url); - /** - * 基于指定配置,获得 EmbeddingModel 对象 - *

- * 如果不存在,则进行创建 - * - * @param platform 平台 - * @param apiKey API KEY - * @param url API URL - * @return ChatModel 对象 - */ - EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url); - /** * 基于默认配置,获得 ChatModel 对象 * @@ -92,4 +80,16 @@ public interface AiModelFactory { */ SunoApi getOrCreateSunoApi(String apiKey, String url); + /** + * 基于指定配置,获得 EmbeddingModel 对象 + * + * 如果不存在,则进行创建 + * + * @param platform 平台 + * @param apiKey API KEY + * @param url API URL + * @return ChatModel 对象 + */ + EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url); + } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 5c3524e66..aa46c45f2 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -99,21 +99,6 @@ public class AiModelFactoryImpl implements AiModelFactory { }); } - @Override - public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url) { - String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url); - return Singleton.get(cacheKey, (Func0) () -> { - // TODO @xin 先测试一个 - switch (platform) { - case TONG_YI: - return buildTongYiEmbeddingModel(apiKey); - default: - throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); - } - }); - } - - @Override public ChatModel getDefaultChatModel(AiPlatformEnum platform) { //noinspection EnhancedSwitchMigration @@ -192,6 +177,20 @@ public class AiModelFactoryImpl implements AiModelFactory { return Singleton.get(cacheKey, (Func0) () -> new SunoApi(url)); } + @Override + public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url) { + String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url); + return Singleton.get(cacheKey, (Func0) () -> { + // TODO @xin 先测试一个 + switch (platform) { + case TONG_YI: + return buildTongYiEmbeddingModel(apiKey); + default: + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); + } + }); + } + private static String buildClientCacheKey(Class clazz, Object... params) { if (ArrayUtil.isEmpty(params)) { return clazz.getName(); @@ -255,8 +254,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel( - *ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel(ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)} */ private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); @@ -265,8 +263,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel( - *ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel(ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} */ private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java index 3e138cbd3..dad58a2c0 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java @@ -4,13 +4,14 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.VectorStore; +// TODO @xin:也放到 AiModelFactory 里面好了,后续改成 AiFactory /** * AI Vector 模型工厂的接口类 + * * @author xiaoxin */ public interface AiVectorStoreFactory { - /** * 基于指定配置,获得 VectorStore 对象 *

diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java index b3291c6b7..ec04c5e88 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java @@ -26,6 +26,7 @@ public class AiVectorStoreFactoryImpl implements AiVectorStoreFactory { return Singleton.get(cacheKey, (Func0) () -> { // TODO 芋艿 @xin 这两个配置取哪好呢 // TODO 不同模型的向量维度可能会不一样,目前看貌似是以 index 来做区分的,维度不一样存不到一个 index 上 + // TODO 回复:好的哈 String index = "default-index"; String prefix = "default:"; var config = RedisVectorStore.RedisVectorStoreConfig.builder() @@ -41,11 +42,11 @@ public class AiVectorStoreFactoryImpl implements AiVectorStoreFactory { }); } - private static String buildClientCacheKey(Class clazz, Object... params) { if (ArrayUtil.isEmpty(params)) { return clazz.getName(); } return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_")); } + } From 34231bc5f8a7346b4f2fdb525cc8aba36d7b2d64 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Sep 2024 08:55:58 +0800 Subject: [PATCH 094/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=20=E5=A4=A7=E6=A8=A1=E5=9E=8B=EF=BC=9Atran?= =?UTF-8?q?sformer=20=E7=9A=84=20onnx=E3=80=81tokenizer=20=E8=B5=B0=20CDN?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=20github?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-server/src/main/resources/application.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 23c622ea9..8df1268c5 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -157,6 +157,12 @@ spring: redis: index: default-index prefix: "default:" + embedding: + transformer: + onnx: + model-uri: http://test.yudao.iocoder.cn/model.onnx + tokenizer: + uri: http://test.yudao.iocoder.cn/tokenizer.json qianfan: # 文心一言 api-key: x0cuLZ7XsaTCU08vuJWO87Lg secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK From 059f64acb0e9b77d249d3909de48c08630efd515 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Sep 2024 08:58:40 +0800 Subject: [PATCH 095/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=20=E5=A4=A7=E6=A8=A1=E5=9E=8B=EF=BC=9A?= =?UTF-8?q?=E5=B0=86=20spring-ai=20=E8=B0=83=E6=95=B4=E6=88=90=20group.spr?= =?UTF-8?q?ingframework.ai=EF=BC=8C=E8=A7=A3=E5=86=B3=20spring-ai=20?= =?UTF-8?q?=E6=9A=82=E6=97=B6=E6=97=A0=E6=B3=95=E4=BD=BF=E7=94=A8=E9=98=BF?= =?UTF-8?q?=E9=87=8C=E4=BA=91=20maven=20=E5=8A=A0=E9=80=9F=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao-spring-boot-starter-ai/pom.xml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index 69a3cdbda..85996cb82 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -14,49 +14,50 @@ ${project.artifactId} AI 大模型拓展,接入国内外大模型 - 1.0.0-M1 + group.springframework.ai + 1.1.0 - org.springframework.ai + ${spring-ai.groupId} spring-ai-zhipuai-spring-boot-starter ${spring-ai.version} - org.springframework.ai + ${spring-ai.groupId} spring-ai-openai-spring-boot-starter ${spring-ai.version} - org.springframework.ai + ${spring-ai.groupId} spring-ai-azure-openai-spring-boot-starter ${spring-ai.version} - org.springframework.ai + ${spring-ai.groupId} spring-ai-ollama-spring-boot-starter ${spring-ai.version} - org.springframework.ai + ${spring-ai.groupId} spring-ai-stability-ai-spring-boot-starter ${spring-ai.version} - org.springframework.ai + ${spring-ai.groupId} spring-ai-transformers-spring-boot-starter ${spring-ai.version} - org.springframework.ai + ${spring-ai.groupId} spring-ai-tika-document-reader ${spring-ai.version} - org.springframework.ai + ${spring-ai.groupId} spring-ai-redis-store ${spring-ai.version} @@ -71,11 +72,10 @@ yudao-common - - group.springframework.ai + ${spring-ai.groupId} spring-ai-qianfan-spring-boot-starter - 1.1.0 + ${spring-ai.version} From 87adf15a934b7c9560c3f20dd7a80849510a19cd Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 1 Sep 2024 09:38:45 +0800 Subject: [PATCH 096/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9AApp=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=A2=9E=E5=8A=A0=E9=83=A8=E5=88=86=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E7=9A=84=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/aftersale/AppAfterSaleController.java | 12 +++++---- .../aftersale/AppAfterSaleLogController.java | 2 ++ .../AppBrokerageRecordController.java | 1 + .../brokerage/AppBrokerageUserController.java | 1 + .../delivery/AppDeliverConfigController.java | 27 ------------------- .../app/order/AppTradeOrderController.java | 9 +++++++ 6 files changed, 20 insertions(+), 32 deletions(-) delete mode 100644 yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/delivery/AppDeliverConfigController.java diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleController.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleController.java index 9b60d8c24..89a805ec6 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleController.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleController.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.trade.controller.app.aftersale; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.log.AfterSaleLogRespVO; +import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated; import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO; import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO; import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleRespVO; @@ -12,14 +12,11 @@ import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService; 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 lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import jakarta.annotation.Resource; - -import java.util.List; - import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @@ -35,6 +32,7 @@ public class AppAfterSaleController { @GetMapping(value = "/page") @Operation(summary = "获得售后分页") + @PreAuthenticated public CommonResult> getAfterSalePage(PageParam pageParam) { return success(AfterSaleConvert.INSTANCE.convertPage02( afterSaleService.getAfterSalePage(getLoginUserId(), pageParam))); @@ -43,18 +41,21 @@ public class AppAfterSaleController { @GetMapping(value = "/get") @Operation(summary = "获得售后订单") @Parameter(name = "id", description = "售后编号", required = true, example = "1") + @PreAuthenticated public CommonResult getAfterSale(@RequestParam("id") Long id) { return success(AfterSaleConvert.INSTANCE.convert(afterSaleService.getAfterSale(getLoginUserId(), id))); } @PostMapping(value = "/create") @Operation(summary = "申请售后") + @PreAuthenticated public CommonResult createAfterSale(@RequestBody AppAfterSaleCreateReqVO createReqVO) { return success(afterSaleService.createAfterSale(getLoginUserId(), createReqVO)); } @PutMapping(value = "/delivery") @Operation(summary = "退回货物") + @PreAuthenticated public CommonResult deliveryAfterSale(@RequestBody AppAfterSaleDeliveryReqVO deliveryReqVO) { afterSaleService.deliveryAfterSale(getLoginUserId(), deliveryReqVO); return success(true); @@ -63,6 +64,7 @@ public class AppAfterSaleController { @DeleteMapping(value = "/cancel") @Operation(summary = "取消售后") @Parameter(name = "id", description = "售后编号", required = true, example = "1") + @PreAuthenticated public CommonResult cancelAfterSale(@RequestParam("id") Long id) { afterSaleService.cancelAfterSale(getLoginUserId(), id); return success(true); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleLogController.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleLogController.java index 667733442..142e6608f 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleLogController.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleLogController.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.controller.app.aftersale; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated; import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.log.AppAfterSaleLogRespVO; import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleLogDO; import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleLogService; @@ -33,6 +34,7 @@ public class AppAfterSaleLogController { @GetMapping("/list") @Operation(summary = "获得售后日志列表") @Parameter(name = "afterSaleId", description = "售后编号", required = true, example = "1") + @PreAuthenticated public CommonResult> getAfterSaleLogList( @RequestParam("afterSaleId") Long afterSaleId) { List logs = afterSaleLogService.getAfterSaleLogList(afterSaleId); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageRecordController.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageRecordController.java index 28666cb43..74e68b4fd 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageRecordController.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageRecordController.java @@ -45,6 +45,7 @@ public class AppBrokerageRecordController { @GetMapping("/get-product-brokerage-price") @Operation(summary = "获得商品的分销金额") + @PreAuthenticated public CommonResult getProductBrokeragePrice(@RequestParam("spuId") Long spuId) { return success(brokerageRecordService.calculateProductBrokeragePrice(getLoginUserId(), spuId)); } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageUserController.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageUserController.java index 202ed3c42..1eaed1344 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageUserController.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageUserController.java @@ -133,6 +133,7 @@ public class AppBrokerageUserController { @GetMapping("/get-rank-by-price") @Operation(summary = "获得分销用户排行(基于佣金)") @Parameter(name = "times", description = "时间段", required = true) + @PreAuthenticated public CommonResult getRankByPrice( @RequestParam("times") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) LocalDateTime[] times) { return success(brokerageRecordService.getUserRankByPrice(getLoginUserId(), times)); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/delivery/AppDeliverConfigController.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/delivery/AppDeliverConfigController.java deleted file mode 100644 index 1d4e36f90..000000000 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/delivery/AppDeliverConfigController.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.iocoder.yudao.module.trade.controller.app.delivery; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.trade.controller.app.delivery.vo.config.AppDeliveryConfigRespVO; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -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.RestController; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -@Tag(name = "用户 App - 配送配置") -@RestController -@RequestMapping("/trade/delivery/config") -@Validated -public class AppDeliverConfigController { - - // TODO @芋艿:这里后面干掉,合并到 AppTradeConfigController 中 - @GetMapping("/get") - @Operation(summary = "获得配送配置") - public CommonResult getDeliveryConfig() { - return success(new AppDeliveryConfigRespVO().setPickUpEnable(true).setTencentLbsKey("123456")); - } - -} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java index daa5e8e15..b1280d8c1 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java @@ -80,6 +80,7 @@ public class AppTradeOrderController { @GetMapping("/get-detail") @Operation(summary = "获得交易订单") @Parameter(name = "id", description = "交易订单编号") + @PreAuthenticated public CommonResult getOrder(@RequestParam("id") Long id) { // 查询订单 TradeOrderDO order = tradeOrderQueryService.getOrder(getLoginUserId(), id); @@ -99,6 +100,7 @@ public class AppTradeOrderController { @GetMapping("/get-express-track-list") @Operation(summary = "获得交易订单的物流轨迹") @Parameter(name = "id", description = "交易订单编号") + @PreAuthenticated public CommonResult> getOrderExpressTrackList(@RequestParam("id") Long id) { return success(TradeOrderConvert.INSTANCE.convertList02( tradeOrderQueryService.getExpressTrackList(id, getLoginUserId()))); @@ -106,6 +108,7 @@ public class AppTradeOrderController { @GetMapping("/page") @Operation(summary = "获得交易订单分页") + @PreAuthenticated public CommonResult> getOrderPage(AppTradeOrderPageReqVO reqVO) { // 查询订单 PageResult pageResult = tradeOrderQueryService.getOrderPage(getLoginUserId(), reqVO); @@ -118,6 +121,7 @@ public class AppTradeOrderController { @GetMapping("/get-count") @Operation(summary = "获得交易订单数量") + @PreAuthenticated public CommonResult> getOrderCount() { Map orderCount = Maps.newLinkedHashMapWithExpectedSize(5); // 全部 @@ -142,6 +146,7 @@ public class AppTradeOrderController { @PutMapping("/receive") @Operation(summary = "确认交易订单收货") @Parameter(name = "id", description = "交易订单编号") + @PreAuthenticated public CommonResult receiveOrder(@RequestParam("id") Long id) { tradeOrderUpdateService.receiveOrderByMember(getLoginUserId(), id); return success(true); @@ -150,6 +155,7 @@ public class AppTradeOrderController { @DeleteMapping("/cancel") @Operation(summary = "取消交易订单") @Parameter(name = "id", description = "交易订单编号") + @PreAuthenticated public CommonResult cancelOrder(@RequestParam("id") Long id) { tradeOrderUpdateService.cancelOrderByMember(getLoginUserId(), id); return success(true); @@ -158,6 +164,7 @@ public class AppTradeOrderController { @DeleteMapping("/delete") @Operation(summary = "删除交易订单") @Parameter(name = "id", description = "交易订单编号") + @PreAuthenticated public CommonResult deleteOrder(@RequestParam("id") Long id) { tradeOrderUpdateService.deleteOrder(getLoginUserId(), id); return success(true); @@ -168,6 +175,7 @@ public class AppTradeOrderController { @GetMapping("/item/get") @Operation(summary = "获得交易订单项") @Parameter(name = "id", description = "交易订单项编号") + @PreAuthenticated public CommonResult getOrderItem(@RequestParam("id") Long id) { TradeOrderItemDO item = tradeOrderQueryService.getOrderItem(getLoginUserId(), id); return success(TradeOrderConvert.INSTANCE.convert03(item)); @@ -175,6 +183,7 @@ public class AppTradeOrderController { @PostMapping("/item/create-comment") @Operation(summary = "创建交易订单项的评价") + @PreAuthenticated public CommonResult createOrderItemComment(@RequestBody AppTradeOrderItemCommentCreateReqVO createReqVO) { return success(tradeOrderUpdateService.createOrderItemCommentByMember(getLoginUserId(), createReqVO)); } From 5ea3e5db0da5c4a4bb7c9c9dc9f6bf5c92f25a22 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Mon, 2 Sep 2024 11:23:13 +0800 Subject: [PATCH 097/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E:=20=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E6=B4=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../promotion/api/coupon/CouponApi.java | 13 +- .../dto/RewardActivityMatchRespDTO.java | 12 +- .../enums/coupon/CouponStatusEnum.java | 4 +- .../promotion/api/coupon/CouponApiImpl.java | 9 +- .../dal/mysql/coupon/CouponMapper.java | 17 +- .../service/coupon/CouponService.java | 103 +++++---- .../service/coupon/CouponServiceImpl.java | 212 ++++++++++-------- .../dal/dataobject/order/TradeOrderDO.java | 12 +- .../order/TradeOrderUpdateService.java | 11 + .../order/TradeOrderUpdateServiceImpl.java | 18 +- .../handler/TradeCouponOrderHandler.java | 19 +- .../price/bo/TradePriceCalculateRespBO.java | 2 +- .../TradePriceCalculatorHelper.java | 2 +- .../TradeRewardActivityPriceCalculator.java | 14 +- ...radeRewardActivityPriceCalculatorTest.java | 10 +- 15 files changed, 256 insertions(+), 202 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java index c724df8c1..789a4526d 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO; import jakarta.validation.Valid; +import java.util.List; import java.util.Map; /** @@ -36,23 +37,21 @@ public interface CouponApi { */ CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO); - // TODO @puhui999:可能需要根据 TradeOrderDO 的建议,进行修改;需要返回优惠劵编号 /** * 【管理员】给指定用户批量发送优惠券 * - * @param giveCouponsMap key: 优惠劵编号,value:对应的优惠券数量 + * @param giveCoupons key: 优惠劵模版编号,value:对应的数量 * @param userId 用户编号 + * @return 优惠券编号列表 */ - // TODO @puhui999:giveCouponsMap 可能改成 giveCoupons 更合适?优惠劵模版编号、数量 - void takeCouponsByAdmin(Map giveCouponsMap, Long userId); + List takeCouponsByAdmin(Map giveCoupons, Long userId); - // TODO @puhui999:可能需要根据 TradeOrderDO 的建议,进行修改 giveCouponsMap 参数 /** * 【管理员】作废指定用户的指定优惠劵 * - * @param giveCouponsMap key: 优惠劵编号,value:对应的优惠券数量 + * @param giveCouponIds 赠送的优惠券编号 * @param userId 用户编号 */ - void invalidateCouponsByAdmin(Map giveCouponsMap, Long userId); + void invalidateCouponsByAdmin(List giveCouponIds, Long userId); } diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java index 93b5691fb..d8d5ef135 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java @@ -86,27 +86,17 @@ public class RewardActivityMatchRespDTO { * 是否包邮 */ private Boolean freeDelivery; - // TODO @puhui999:建议不返回 + 去掉 givePoint、giveCoupon 字段哈。 - /** - * 是否赠送积分 - */ - private Boolean givePoint; /** * 赠送的积分 */ private Integer point; - /** - * 是否赠送优惠券 - */ - private Boolean giveCoupon; - // TODO @puhui999:giveCoupons 即可 /** * 赠送的优惠劵 * * key: 优惠劵模版编号 * value:对应的优惠券数量 */ - private Map giveCouponsMap; + private Map giveCoupons; } diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java index 831d4b5a0..bef4db225 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java @@ -17,9 +17,7 @@ public enum CouponStatusEnum implements IntArrayValuable { UNUSED(1, "未使用"), USED(2, "已使用"), - EXPIRE(3, "已过期"), - // TODO @puhui999:捉摸了下,貌似搞成逻辑删除好了?不然好多地方的 status 都要做一些变动。可能未来加个 invalidateType 来标识,是管理后台删除,还是取消回收。或者优惠劵的 change log 可能更好。 - INVALID(4, "已作废"); + EXPIRE(3, "已过期"); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponStatusEnum::getStatus).toArray(); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java index 22fea4525..edc8f1b7f 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java @@ -11,6 +11,7 @@ import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.util.List; import java.util.Map; /** @@ -43,13 +44,13 @@ public class CouponApiImpl implements CouponApi { } @Override - public void takeCouponsByAdmin(Map giveCouponsMap, Long userId) { - couponService.takeCouponsByAdmin(giveCouponsMap, userId); + public List takeCouponsByAdmin(Map giveCoupons, Long userId) { + return couponService.takeCouponsByAdmin(giveCoupons, userId); } @Override - public void invalidateCouponsByAdmin(Map giveCouponsMap, Long userId) { - couponService.invalidateCouponsByAdmin(giveCouponsMap, userId); + public void invalidateCouponsByAdmin(List giveCouponIds, Long userId) { + couponService.invalidateCouponsByAdmin(giveCouponIds, userId); } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java index 913b84510..a06b92338 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java @@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.github.yulichang.toolkit.MPJWrappers; import org.apache.ibatis.annotations.Mapper; @@ -72,15 +73,6 @@ public interface CouponMapper extends BaseMapperX { ); } - default List selectListByTemplateIdAndUserIdAndTakeType(Long templateId, Collection userIds, - Integer takeType) { - return selectList(new LambdaQueryWrapperX() - .eq(CouponDO::getTemplateId, templateId) - .eq(CouponDO::getTakeType, takeType) - .in(CouponDO::getUserId, userIds) - ); - } - default Map selectCountByUserIdAndTemplateIdIn(Long userId, Collection templateIds) { String templateIdAlias = "templateId"; String countAlias = "count"; @@ -116,4 +108,11 @@ public interface CouponMapper extends BaseMapperX { ); } + default List selectListByIdAndUserIdAndTakeType(Long couponId, Long userId, Integer takeType) { + return selectList(new LambdaQueryWrapper() + .eq(CouponDO::getId, couponId) + .eq(CouponDO::getUserId, userId) + .eq(CouponDO::getTakeType, takeType)); + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java index 97c1412ca..622b09a5b 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java @@ -38,14 +38,6 @@ public interface CouponService { */ void validCoupon(CouponDO coupon); - /** - * 获得优惠劵分页 - * - * @param pageReqVO 分页查询 - * @return 优惠劵分页 - */ - PageResult getCouponPage(CouponPageReqVO pageReqVO); - /** * 使用优惠劵 * @@ -69,57 +61,43 @@ public interface CouponService { */ void deleteCoupon(Long id); - /** - * 获得用户的优惠劵列表 - * - * @param userId 用户编号 - * @param status 优惠劵状态 - * @return 优惠劵列表 - */ - List getCouponList(Long userId, Integer status); - - /** - * 获得未使用的优惠劵数量 - * - * @param userId 用户编号 - * @return 未使用的优惠劵数量 - */ - Long getUnusedCouponCount(Long userId); - /** * 领取优惠券 * * @param templateId 优惠券模板编号 * @param userIds 用户编号列表 * @param takeType 领取方式 + * @return key: userId, value: 优惠券编号列表 */ - void takeCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType); + Map> takeCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType); /** * 【管理员】给用户发送优惠券 * * @param templateId 优惠券模板编号 * @param userIds 用户编号列表 + * @return key: userId, value: 优惠券编号列表 */ - default void takeCouponByAdmin(Long templateId, Set userIds) { - takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN); + default Map> takeCouponByAdmin(Long templateId, Set userIds) { + return takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN); } /** * 【管理员】给指定用户批量发送优惠券 * - * @param giveCouponsMap key: 优惠劵编号,value:对应的优惠券数量 + * @param giveCoupons key: 优惠劵模版编号,value:对应的数量 * @param userId 用户编号 + * @return 优惠券编号列表 */ - void takeCouponsByAdmin(Map giveCouponsMap, Long userId); + List takeCouponsByAdmin(Map giveCoupons, Long userId); /** - * 【管理员】收回给指定用户批量发送优惠券 + * 【管理员】作废指定用户的指定优惠劵 * - * @param giveCouponsMap key: 优惠劵编号,value:对应的优惠券数量 + * @param giveCouponIds 赠送的优惠券编号 * @param userId 用户编号 */ - void invalidateCouponsByAdmin(Map giveCouponsMap, Long userId); + void invalidateCouponsByAdmin(List giveCouponIds, Long userId); /** * 【会员】领取优惠券 @@ -138,6 +116,49 @@ public interface CouponService { */ void takeCouponByRegister(Long userId); + /** + * 过期优惠券 + * + * @return 过期数量 + */ + int expireCoupon(); + + //======================= 查询相关 ======================= + + /** + * 获得未使用的优惠劵数量 + * + * @param userId 用户编号 + * @return 未使用的优惠劵数量 + */ + Long getUnusedCouponCount(Long userId); + + /** + * 获得优惠劵分页 + * + * @param pageReqVO 分页查询 + * @return 优惠劵分页 + */ + PageResult getCouponPage(CouponPageReqVO pageReqVO); + + /** + * 获得用户的优惠劵列表 + * + * @param userId 用户编号 + * @param status 优惠劵状态 + * @return 优惠劵列表 + */ + List getCouponList(Long userId, Integer status); + + /** + * 统计会员领取优惠券的数量 + * + * @param templateIds 优惠券模板编号列表 + * @param userId 用户编号 + * @return 领取优惠券的数量 + */ + Map getTakeCountMapByTemplateIds(Collection templateIds, Long userId); + /** * 获取会员领取指定优惠券的数量 * @@ -150,15 +171,6 @@ public interface CouponService { return MapUtil.getInt(map, templateId, 0); } - /** - * 统计会员领取优惠券的数量 - * - * @param templateIds 优惠券模板编号列表 - * @param userId 用户编号 - * @return 领取优惠券的数量 - */ - Map getTakeCountMapByTemplateIds(Collection templateIds, Long userId); - /** * 获取用户匹配的优惠券列表 * @@ -168,13 +180,6 @@ public interface CouponService { */ List getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO); - /** - * 过期优惠券 - * - * @return 过期数量 - */ - int expireCoupon(); - /** * 获取用户是否可以领取优惠券 * diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index 666a310e7..ecc1adb46 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.promotion.service.coupon; import cn.hutool.core.collection.CollStreamUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; @@ -31,6 +32,7 @@ import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; import static java.util.Arrays.asList; @@ -75,20 +77,6 @@ public class CouponServiceImpl implements CouponService { } } - @Override - public PageResult getCouponPage(CouponPageReqVO pageReqVO) { - // 获得用户编号 - if (StrUtil.isNotEmpty(pageReqVO.getNickname())) { - List users = memberUserApi.getUserListByNickname(pageReqVO.getNickname()); - if (CollUtil.isEmpty(users)) { - return PageResult.empty(); - } - pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId)); - } - // 分页查询 - return couponMapper.selectPage(pageReqVO); - } - @Override public void useCoupon(Long id, Long userId, Long orderId) { // 校验优惠劵 @@ -145,27 +133,9 @@ public class CouponServiceImpl implements CouponService { couponTemplateService.updateCouponTemplateTakeCount(coupon.getTemplateId(), -1); } - @Override - public List getCouponList(Long userId, Integer status) { - return couponMapper.selectListByUserIdAndStatus(userId, status); - } - - private CouponDO validateCouponExists(Long id) { - CouponDO coupon = couponMapper.selectById(id); - if (coupon == null) { - throw exception(COUPON_NOT_EXISTS); - } - return coupon; - } - - @Override - public Long getUnusedCouponCount(Long userId) { - return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus()); - } - @Override @Transactional(rollbackFor = Exception.class) - public void takeCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType) { + public Map> takeCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType) { CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId); // 1. 过滤掉达到领取限制的用户 removeTakeLimitUser(userIds, template); @@ -173,40 +143,45 @@ public class CouponServiceImpl implements CouponService { validateCouponTemplateCanTake(template, userIds, takeType); // 3. 批量保存优惠劵 - couponMapper.insertBatch(convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId))); + List couponList = convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId)); + couponMapper.insertBatch(couponList); // 4. 增加优惠劵模板的领取数量 couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size()); + + return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId); } @Override - public void takeCouponsByAdmin(Map giveCouponsMap, Long userId) { - if (CollUtil.isEmpty(giveCouponsMap)) { - return; + public List takeCouponsByAdmin(Map giveCoupons, Long userId) { + if (CollUtil.isEmpty(giveCoupons)) { + return Collections.emptyList(); } + List couponIds = new ArrayList<>(); // 循环发放 - for (Map.Entry entry : giveCouponsMap.entrySet()) { + for (Map.Entry entry : giveCoupons.entrySet()) { try { for (int i = 0; i < entry.getValue(); i++) { - getSelf().takeCoupon(entry.getKey(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.ADMIN); + Map> userCouponIdsMap = getSelf().takeCoupon(entry.getKey(), CollUtil.newHashSet(userId), + CouponTakeTypeEnum.ADMIN); + findAndThen(userCouponIdsMap, userId, couponIds::addAll); } } catch (Exception e) { log.error("[takeCouponsByAdmin][coupon({}) 优惠券发放失败]", entry, e); } } + return couponIds; } @Override - public void invalidateCouponsByAdmin(Map giveCouponsMap, Long userId) { + public void invalidateCouponsByAdmin(List giveCouponIds, Long userId) { // 循环收回 - for (Map.Entry entry : giveCouponsMap.entrySet()) { + for (Long couponId : giveCouponIds) { try { - for (int i = 0; i < entry.getValue(); i++) { - getSelf().takeBackCoupon(entry.getKey(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.ADMIN); - } + getSelf().takeBackCoupon(couponId, userId, CouponTakeTypeEnum.ADMIN); } catch (Exception e) { - log.error("[takeBackCouponsByAdmin][coupon({}) 收回优惠券失败]", entry, e); + log.error("[invalidateCouponsByAdmin][couponId({}) 收回优惠券失败]", couponId, e); } } } @@ -214,32 +189,36 @@ public class CouponServiceImpl implements CouponService { /** * 【管理员】收回优惠券 * - * @param templateId 模版编号 - * @param userIds 用户编号列表 + * @param couponId 模版编号 + * @param userId 用户编号 * @param takeType 领取方式 */ @Transactional(rollbackFor = Exception.class) - public void takeBackCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType) { - CouponTemplateDO couponTemplate = couponTemplateService.getCouponTemplate(templateId); - // 1.1 校验模板 + public void takeBackCoupon(Long couponId, Long userId, CouponTakeTypeEnum takeType) { + // 1.1 校验优惠券 + CouponDO coupon = couponMapper.selectByIdAndUserId(couponId, userId); + if (coupon == null) { + throw exception(COUPON_NOT_EXISTS); + } + // 1.2 校验模板 + CouponTemplateDO couponTemplate = couponTemplateService.getCouponTemplate(coupon.getTemplateId()); if (couponTemplate == null) { throw exception(COUPON_TEMPLATE_NOT_EXISTS); } - // 1.2 校验领取方式 + // 1.3 校验领取方式 if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getValue())) { throw exception(COUPON_TEMPLATE_CANNOT_TAKE); } - // 2.1 过滤出还未使用的赠送的优惠券 - List couponList = couponMapper.selectListByTemplateIdAndUserIdAndTakeType(templateId, userIds, - takeType.getValue()); - List unUsedCouponList = filterList(couponList, item -> !CouponStatusEnum.USED.getStatus().equals(item.getStatus())); + // 2.1 校验优惠券是否已经使用,如若使用则先不管 + if (ObjUtil.equal(coupon.getStatus(), CouponStatusEnum.USED.getStatus())) { + return; + } // 2.2 减少优惠劵模板的领取数量 - couponTemplateService.updateCouponTemplateTakeCount(templateId, unUsedCouponList.size() * -1); - // 2.3 批量更新优惠劵状态 - couponMapper.updateById(convertList(unUsedCouponList, item -> new CouponDO().setId(item.getId()) - .setStatus(CouponStatusEnum.INVALID.getStatus()))); - + couponTemplateService.updateCouponTemplateTakeCount(couponTemplate.getId(), -1); + // 2.3 批量作废优惠劵 + // TODO @puhui999:捉摸了下,貌似搞成逻辑删除好了?不然好多地方的 status 都要做一些变动。可能未来加个 invalidateType 来标识,是管理后台删除,还是取消回收。或者优惠劵的 change log 可能更好。 + couponMapper.deleteById(couponId); } @Override @@ -251,24 +230,6 @@ public class CouponServiceImpl implements CouponService { } } - @Override - public Map getTakeCountMapByTemplateIds(Collection templateIds, Long userId) { - if (CollUtil.isEmpty(templateIds)) { - return Collections.emptyMap(); - } - return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds); - } - - @Override - public List getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO) { - List list = couponMapper.selectListByUserIdAndStatusAndUsePriceLeAndProductScope(userId, - CouponStatusEnum.UNUSED.getStatus(), - matchReqVO.getPrice(), matchReqVO.getSpuIds(), matchReqVO.getCategoryIds()); - // 兜底逻辑:如果 CouponExpireJob 未执行,status 未变成 EXPIRE ,但是 validEndTime 已经过期了,需要进行过滤 - list.removeIf(coupon -> !LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime())); - return list; - } - @Override public int expireCoupon() { // 1. 查询待过期的优惠券 @@ -293,27 +254,6 @@ public class CouponServiceImpl implements CouponService { return count; } - @Override - public Map getUserCanCanTakeMap(Long userId, List templates) { - // 1. 未登录时,都显示可以领取 - Map userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true); - if (userId == null) { - return userCanTakeMap; - } - - // 2.1 过滤领取数量无限制的 - Set templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1); - // 2.2 检查用户领取的数量是否超过限制 - if (CollUtil.isNotEmpty(templateIds)) { - Map couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId); - for (CouponTemplateDO template : templates) { - Integer takeCount = couponTakeCountMap.get(template.getId()); - userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount()); - } - } - return userCanTakeMap; - } - /** * 过期单个优惠劵 * @@ -385,11 +325,84 @@ public class CouponServiceImpl implements CouponService { userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount()); } + //======================= 查询相关 ======================= + + @Override + public Long getUnusedCouponCount(Long userId) { + return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus()); + } + + @Override + public PageResult getCouponPage(CouponPageReqVO pageReqVO) { + // 获得用户编号 + if (StrUtil.isNotEmpty(pageReqVO.getNickname())) { + List users = memberUserApi.getUserListByNickname(pageReqVO.getNickname()); + if (CollUtil.isEmpty(users)) { + return PageResult.empty(); + } + pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId)); + } + // 分页查询 + return couponMapper.selectPage(pageReqVO); + } + + @Override + public List getCouponList(Long userId, Integer status) { + return couponMapper.selectListByUserIdAndStatus(userId, status); + } + + @Override + public Map getTakeCountMapByTemplateIds(Collection templateIds, Long userId) { + if (CollUtil.isEmpty(templateIds)) { + return Collections.emptyMap(); + } + return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds); + } + + @Override + public List getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO) { + List list = couponMapper.selectListByUserIdAndStatusAndUsePriceLeAndProductScope(userId, + CouponStatusEnum.UNUSED.getStatus(), + matchReqVO.getPrice(), matchReqVO.getSpuIds(), matchReqVO.getCategoryIds()); + // 兜底逻辑:如果 CouponExpireJob 未执行,status 未变成 EXPIRE ,但是 validEndTime 已经过期了,需要进行过滤 + list.removeIf(coupon -> !LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime())); + return list; + } + + @Override + public Map getUserCanCanTakeMap(Long userId, List templates) { + // 1. 未登录时,都显示可以领取 + Map userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true); + if (userId == null) { + return userCanTakeMap; + } + + // 2.1 过滤领取数量无限制的 + Set templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1); + // 2.2 检查用户领取的数量是否超过限制 + if (CollUtil.isNotEmpty(templateIds)) { + Map couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId); + for (CouponTemplateDO template : templates) { + Integer takeCount = couponTakeCountMap.get(template.getId()); + userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount()); + } + } + return userCanTakeMap; + } + @Override public CouponDO getCoupon(Long userId, Long id) { return couponMapper.selectByIdAndUserId(id, userId); } + private CouponDO validateCouponExists(Long id) { + CouponDO coupon = couponMapper.selectById(id); + if (coupon == null) { + throw exception(COUPON_NOT_EXISTS); + } + return coupon; + } + /** * 获得自身的代理对象,解决 AOP 生效问题 * @@ -398,4 +411,5 @@ public class CouponServiceImpl implements CouponService { private CouponServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } + } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java index 82b6d6117..1409561d5 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.dal.dataobject.order; import cn.iocoder.yudao.framework.common.enums.TerminalEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO; import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO; @@ -18,6 +19,7 @@ import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.*; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; /** @@ -294,17 +296,23 @@ public class TradeOrderDO extends BaseDO { */ private Integer vipPrice; - // TODO @puhui999:项了下,貌似这里存储 List giveCouponIds 更合适。因为优惠劵赠送到最后是对应的编号,然后从而进行取消? /** * 赠送的优惠劵 * * key: 优惠劵编号 * value:对应的优惠券数量 * - * 目的:用于后续取消或者售后订单时,需要扣减赠送 + * 目的:用于订单支付后赠送优惠券 */ @TableField(typeHandler = JacksonTypeHandler.class) private Map giveCouponsMap; + /** + * 赠送的优惠劵编号 + * + * 目的:用于后续取消或者售后订单时,需要扣减赠送 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List giveCouponIds; /** * 秒杀活动编号 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java index 4508138ff..56b7cbc56 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java @@ -11,6 +11,8 @@ import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderI import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; import jakarta.validation.constraints.NotNull; +import java.util.List; + /** * 交易订单【写】Service 接口 * @@ -194,4 +196,13 @@ public interface TradeOrderUpdateService { */ void cancelPaidOrder(Long userId, Long orderId, Integer cancelType); + /** + * 更新下单赠送的优惠券编号到订单 + * + * @param userId 用户编号 + * @param orderId 订单编号 + * @param giveCouponIds 赠送的优惠券编号列表 + */ + void updateOrderGiveCouponIds(Long userId, Long orderId, List giveCouponIds); + } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index c9c1e685b..bdae8f227 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -202,7 +202,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum)); order.setUserIp(getClientIP()).setTerminal(getTerminal()); // 使用 + 赠送优惠券 - order.setGiveCouponsMap(calculateRespBO.getGiveCouponsMap()); + order.setGiveCouponsMap(calculateRespBO.getGiveCoupons()); // 支付 + 退款信息 order.setAdjustPrice(0).setPayStatus(false); order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0); @@ -890,6 +890,22 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { .setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice()));// 价格信息 } + @Override + public void updateOrderGiveCouponIds(Long userId, Long orderId, List giveCouponIds) { + // 1.1 检验订单存在 + TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // 1.2 校验订单是否支付 + if (!order.getPayStatus()) { + throw exception(ORDER_CANCEL_PAID_FAIL, "已支付"); + } + + // 2. 更新订单赠送的优惠券编号列表 + tradeOrderMapper.updateById(new TradeOrderDO().setId(orderId).setGiveCouponIds(giveCouponIds)); + } + /** * 创建单个订单的评论 * diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java index 3b1df5e0e..3a98a6c9e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java @@ -5,7 +5,10 @@ import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService; +import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService; import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import java.util.List; @@ -18,6 +21,12 @@ import java.util.List; @Component public class TradeCouponOrderHandler implements TradeOrderHandler { + @Resource + @Lazy // 延迟加载,避免循环依赖 + private TradeOrderUpdateService orderUpdateService; + @Resource + private TradeOrderQueryService orderQueryService; + @Resource private CouponApi couponApi; @@ -37,7 +46,11 @@ public class TradeCouponOrderHandler implements TradeOrderHandler { return; } // 赠送优惠券 - couponApi.takeCouponsByAdmin(order.getGiveCouponsMap(), order.getUserId()); + List couponIds = couponApi.takeCouponsByAdmin(order.getGiveCouponsMap(), order.getUserId()); + if (CollUtil.isEmpty(couponIds)) { + return; + } + orderUpdateService.updateOrderGiveCouponIds(order.getUserId(), order.getId(), couponIds); } @Override @@ -48,10 +61,10 @@ public class TradeCouponOrderHandler implements TradeOrderHandler { couponApi.returnUsedCoupon(order.getCouponId()); } // 情况二:收回赠送的优惠券 - if (CollUtil.isEmpty(order.getGiveCouponsMap())) { + if (CollUtil.isEmpty(order.getGiveCouponIds())) { return; } - couponApi.invalidateCouponsByAdmin(order.getGiveCouponsMap(), order.getUserId()); + couponApi.invalidateCouponsByAdmin(order.getGiveCouponIds(), order.getUserId()); } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java index e53613d26..68fa58b37 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java @@ -79,7 +79,7 @@ public class TradePriceCalculateRespBO { * key: 优惠劵编号,value:对应的优惠券数量 * 目的:用于后续取消或者售后订单时,需要扣减赠送 */ - private Map giveCouponsMap; + private Map giveCoupons; /** * 订单价格 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java index 6fa639c5a..195ef8718 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java @@ -32,7 +32,7 @@ public class TradePriceCalculatorHelper { List spuList, List skuList) { // 创建 PriceCalculateRespDTO 对象 TradePriceCalculateRespBO result = new TradePriceCalculateRespBO(); - result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCouponsMap(new LinkedHashMap<>()); + result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCoupons(new LinkedHashMap<>()); // 创建它的 OrderItem 属性 result.setItems(new ArrayList<>(param.getItems().size())); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java index 6b333df47..f62b65eb9 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -93,7 +93,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator TradePriceCalculatorHelper.recountAllPrice(result); // 4.1 记录赠送的积分 - if (Boolean.TRUE.equals(rule.getGivePoint())) { + if (rule.getPoint() != null && rule.getPoint() > 0) { List dividePoints = TradePriceCalculatorHelper.dividePrice(orderItems, rule.getPoint()); for (int i = 0; i < orderItems.size(); i++) { // 商品可能赠送了积分,所以这里要加上 @@ -107,13 +107,13 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator result.setFreeDelivery(true); } // 4.3 记录赠送的优惠券 - if (Boolean.TRUE.equals(rule.getGiveCoupon())) { - for (Map.Entry entry : rule.getGiveCouponsMap().entrySet()) { - Map giveCouponsMap = result.getGiveCouponsMap(); - if (giveCouponsMap.get(entry.getKey()) == null) { // 情况一:还没有赠送的优惠券 - result.setGiveCouponsMap(rule.getGiveCouponsMap()); + if (CollUtil.isNotEmpty(rule.getGiveCoupons())) { + for (Map.Entry entry : rule.getGiveCoupons().entrySet()) { + Map giveCoupons = result.getGiveCoupons(); + if (giveCoupons.get(entry.getKey()) == null) { // 情况一:还没有赠送的优惠券 + result.setGiveCoupons(rule.getGiveCoupons()); } else { // 情况二:别的满减活动送过同类优惠券,则直接增加数量 - giveCouponsMap.put(entry.getKey(), giveCouponsMap.get(entry.getKey()) + entry.getValue()); + giveCoupons.put(entry.getKey(), giveCoupons.get(entry.getKey()) + entry.getValue()); } } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java index 3ae34514d..f1f31e3c8 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java @@ -49,7 +49,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest TradePriceCalculateRespBO result = new TradePriceCalculateRespBO() .setType(TradeOrderTypeEnum.NORMAL.getType()) .setPrice(new TradePriceCalculateRespBO.Price()) - .setPromotions(new ArrayList<>()).setGiveCouponsMap(new LinkedHashMap<>()) + .setPromotions(new ArrayList<>()).setGiveCoupons(new LinkedHashMap<>()) .setItems(asList( new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true) .setPrice(100).setSpuId(1L), @@ -68,16 +68,16 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest .setConditionType(PromotionConditionTypeEnum.PRICE.getType()) .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)) .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(20).setDiscountPrice(70) - .setGivePoint(false).setFreeDelivery(false)))), + .setFreeDelivery(false)))), randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号") .setConditionType(PromotionConditionTypeEnum.COUNT.getType()) .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L)) .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10) - .setGivePoint(true).setPoint(50).setFreeDelivery(false), - new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60).setGivePoint(true) + .setPoint(50).setFreeDelivery(false), + new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60) .setPoint(100).setFreeDelivery(false), // 最大可满足,因为是 4 个 new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100) - .setGivePoint(false).setFreeDelivery(false)))) + .setFreeDelivery(false)))) )); // 调用 From 81e38666659cea502f9d1c921fa5278f8d12764c Mon Sep 17 00:00:00 2001 From: puhui999 Date: Mon, 2 Sep 2024 11:35:33 +0800 Subject: [PATCH 098/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E:=20=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E6=B4=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/reward/vo/RewardActivityBaseVO.java | 11 +---------- .../dal/dataobject/reward/RewardActivityDO.java | 10 ---------- .../service/reward/RewardActivityServiceImpl.java | 1 - 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java index 0ed4b7d52..7f68ee123 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.promotion.controller.admin.reward.vo; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.BooleanUtil; import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; @@ -77,24 +76,16 @@ public class RewardActivityBaseVO { @NotNull(message = "规则是否包邮不能为空") private Boolean freeDelivery; - @Schema(description = "是否赠送积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") - @NotNull(message = "规则是否赠送积分不能为空") - private Boolean givePoint; - @Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") private Integer point; - @Schema(description = "是否赠送优惠券", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") - @NotNull(message = "规则是否赠送优惠券不能为空") - private Boolean giveCoupon; - @Schema(description = "赠送的优惠劵编号的数组", example = "1,2,3") private Map giveCouponsMap; @AssertTrue(message = "赠送的积分不能小于 1") @JsonIgnore public boolean isPointValid() { - return BooleanUtil.isFalse(givePoint) || (point != null && point >= 1); + return point == null || point >= 1; } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java index b1332cb3f..f5d60c4e6 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java @@ -100,20 +100,10 @@ public class RewardActivityDO extends BaseDO { * 是否包邮 */ private Boolean freeDelivery; - // TODO @puhui999:是不是大于零,就认为赠送积分哈;简洁一点; - /** - * 是否赠送积分 - */ - private Boolean givePoint; /** * 赠送的积分 */ private Integer point; - // TODO @puhui999:非空,就认为赠送优惠劵 - /** - * 是否赠送优惠券 - */ - private Boolean giveCoupon; /** * 赠送的优惠劵 * diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java index 1ad0ae48f..d35142c19 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java @@ -159,7 +159,6 @@ public class RewardActivityServiceImpl implements RewardActivityService { @Override public List getMatchRewardActivityList(Collection spuIds) { - // TODO 芋艿:待实现;先指定,然后再全局的; List list = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, CommonStatusEnum.ENABLE.getStatus()); return BeanUtils.toBean(list, RewardActivityMatchRespDTO.class); } From 0ddee9036632be24b8f307f104f456fdab1b7599 Mon Sep 17 00:00:00 2001 From: heyho Date: Mon, 2 Sep 2024 03:52:29 +0000 Subject: [PATCH 099/136] =?UTF-8?q?=E5=90=8E=E5=90=8E=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E4=B8=BA=E2=80=9C=E6=8C=87=E5=AE=9A=E5=88=86=E9=94=80=E2=80=9D?= =?UTF-8?q?=E6=97=B6=EF=BC=8C=E8=AE=A9=E6=99=AE=E9=80=9A=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=97=A0=E9=9C=80=E6=88=90=E4=B8=BA=E6=8E=A8=E5=B9=BF=E8=80=85?= =?UTF-8?q?=E4=B9=9F=E5=8F=AF=E4=BB=A5=E7=BB=91=E5=AE=9A=E6=88=90=E4=B8=BA?= =?UTF-8?q?=20=E6=8E=A8=E5=B9=BF=E8=80=85=E7=9A=84=E4=B8=8B=E7=BA=A7?= =?UTF-8?q?=E4=BB=A5=E4=BE=BF=E8=AE=A9=E6=8C=87=E5=AE=9A=E7=9A=84=E6=8E=A8?= =?UTF-8?q?=E5=B9=BF=E8=80=85=E8=B5=9A=E5=8F=96=E4=BD=A3=E9=87=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: heyho --- .../trade/service/brokerage/BrokerageUserServiceImpl.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java index c874f06ca..cf56a5bce 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java @@ -192,6 +192,8 @@ public class BrokerageUserServiceImpl implements BrokerageUserService { Integer enabledCondition = tradeConfigService.getTradeConfig().getBrokerageEnabledCondition(); if (BrokerageEnabledConditionEnum.ALL.getCondition().equals(enabledCondition)) { // 人人分销:用户默认就有分销资格 brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now()); + } else { + brokerageUser.setBrokerageEnabled(false).setBrokerageTime(LocalDateTime.now()); } brokerageUserMapper.insert(fillBindUserData(bindUserId, brokerageUser)); } else { @@ -267,9 +269,9 @@ public class BrokerageUserServiceImpl implements BrokerageUserService { } // 校验分佣模式:仅可后台手动设置推广员 - if (BrokerageEnabledConditionEnum.ADMIN.getCondition().equals(tradeConfig.getBrokerageEnabledCondition())) { - throw exception(BROKERAGE_BIND_CONDITION_ADMIN); - } + // if (BrokerageEnabledConditionEnum.ADMIN.getCondition().equals(tradeConfig.getBrokerageEnabledCondition())) { + // throw exception(BROKERAGE_BIND_CONDITION_ADMIN); + // } // 校验分销关系绑定模式 if (BrokerageBindModeEnum.REGISTER.getMode().equals(tradeConfig.getBrokerageBindMode())) { From 98e6124c2f8add8e069d38a2dc4b8c0b941cab02 Mon Sep 17 00:00:00 2001 From: heyho Date: Mon, 2 Sep 2024 03:55:45 +0000 Subject: [PATCH 100/136] =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=B4=AD=E4=B9=B0?= =?UTF-8?q?=E4=B8=80=E4=BB=B6=E4=BB=A5=E4=B8=8A=E6=95=B0=E9=87=8F=E6=97=B6?= =?UTF-8?q?=E6=8E=A8=E5=B9=BF=E8=80=85=E8=BF=94=E4=BD=A3=E9=87=91=E9=A2=9D?= =?UTF-8?q?=E5=B0=B1=E5=87=BA=E9=94=99(=E7=BF=BB=E5=80=8D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: heyho --- .../yudao/module/trade/convert/order/TradeOrderConvert.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java index d91969481..c8b68ebec 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java @@ -261,7 +261,7 @@ public interface TradeOrderConvert { default BrokerageAddReqBO convert(MemberUserRespDTO user, TradeOrderItemDO item, ProductSpuRespDTO spu, ProductSkuRespDTO sku) { BrokerageAddReqBO bo = new BrokerageAddReqBO().setBizId(String.valueOf(item.getId())).setSourceUserId(item.getUserId()) - .setBasePrice(item.getPayPrice() * item.getCount()) + .setBasePrice(item.getPrice() * item.getCount()) .setTitle(StrUtil.format("{}成功购买{}", user.getNickname(), item.getSpuName())) .setFirstFixedPrice(0).setSecondFixedPrice(0); if (BooleanUtil.isTrue(spu.getSubCommissionType())) { From ae26ca5d00f4f7a6ad34245b7054f6629ee8fe6d Mon Sep 17 00:00:00 2001 From: heyho Date: Mon, 2 Sep 2024 03:57:11 +0000 Subject: [PATCH 101/136] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E9=A9=B3=E5=9B=9E?= =?UTF-8?q?=E4=BD=A3=E9=87=91=E6=8F=90=E7=8E=B0=E6=97=B6,=E6=8A=A5"?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E5=8F=82=E6=95=B0(reason)=E7=BC=BA=E5=A4=B1"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: heyho --- .../trade/service/brokerage/BrokerageWithdrawServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java index c735163a5..86814f8a5 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java @@ -96,7 +96,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService { Map templateParams = MapUtil.builder() .put("createTime", LocalDateTimeUtil.formatNormal(withdraw.getCreateTime())) .put("price", MoneyUtils.fenToYuanStr(withdraw.getPrice())) - .put("reason", withdraw.getAuditReason()) + .put("reason", auditReason) .build(); notifyMessageSendApi.sendSingleMessageToMember(new NotifySendSingleToUserReqDTO() .setUserId(withdraw.getUserId()).setTemplateCode(templateCode).setTemplateParams(templateParams)); From eaeeb34e74c14d1b89c95c727bae8a2f64ca6aaa Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 2 Sep 2024 12:25:53 +0800 Subject: [PATCH 102/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dal/mysql/coupon/CouponMapper.java | 8 -------- .../service/coupon/CouponService.java | 2 +- .../service/coupon/CouponServiceImpl.java | 19 +++++++------------ .../dal/dataobject/order/TradeOrderDO.java | 3 ++- .../order/TradeOrderUpdateServiceImpl.java | 6 +----- 5 files changed, 11 insertions(+), 27 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java index a06b92338..e5f1daf6c 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java @@ -8,7 +8,6 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.github.yulichang.toolkit.MPJWrappers; import org.apache.ibatis.annotations.Mapper; @@ -108,11 +107,4 @@ public interface CouponMapper extends BaseMapperX { ); } - default List selectListByIdAndUserIdAndTakeType(Long couponId, Long userId, Integer takeType) { - return selectList(new LambdaQueryWrapper() - .eq(CouponDO::getId, couponId) - .eq(CouponDO::getUserId, userId) - .eq(CouponDO::getTakeType, takeType)); - } - } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java index 622b09a5b..5fdcd0669 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java @@ -123,7 +123,7 @@ public interface CouponService { */ int expireCoupon(); - //======================= 查询相关 ======================= + // ======================= 查询相关 ======================= /** * 获得未使用的优惠劵数量 diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index ecc1adb46..e6cd4ba0e 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java @@ -179,7 +179,7 @@ public class CouponServiceImpl implements CouponService { // 循环收回 for (Long couponId : giveCouponIds) { try { - getSelf().takeBackCoupon(couponId, userId, CouponTakeTypeEnum.ADMIN); + getSelf().invalidateCoupon(couponId, userId); } catch (Exception e) { log.error("[invalidateCouponsByAdmin][couponId({}) 收回优惠券失败]", couponId, e); } @@ -191,10 +191,9 @@ public class CouponServiceImpl implements CouponService { * * @param couponId 模版编号 * @param userId 用户编号 - * @param takeType 领取方式 */ @Transactional(rollbackFor = Exception.class) - public void takeBackCoupon(Long couponId, Long userId, CouponTakeTypeEnum takeType) { + public void invalidateCoupon(Long couponId, Long userId) { // 1.1 校验优惠券 CouponDO coupon = couponMapper.selectByIdAndUserId(couponId, userId); if (coupon == null) { @@ -205,19 +204,15 @@ public class CouponServiceImpl implements CouponService { if (couponTemplate == null) { throw exception(COUPON_TEMPLATE_NOT_EXISTS); } - // 1.3 校验领取方式 - if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getValue())) { - throw exception(COUPON_TEMPLATE_CANNOT_TAKE); - } - - // 2.1 校验优惠券是否已经使用,如若使用则先不管 + // 1.3 校验优惠券是否已经使用,如若使用则先不管 if (ObjUtil.equal(coupon.getStatus(), CouponStatusEnum.USED.getStatus())) { + log.info("[invalidateCoupon][coupon({}) 已经使用,无法作废]", couponId); return; } - // 2.2 减少优惠劵模板的领取数量 + + // 2.1 减少优惠劵模板的领取数量 couponTemplateService.updateCouponTemplateTakeCount(couponTemplate.getId(), -1); - // 2.3 批量作废优惠劵 - // TODO @puhui999:捉摸了下,貌似搞成逻辑删除好了?不然好多地方的 status 都要做一些变动。可能未来加个 invalidateType 来标识,是管理后台删除,还是取消回收。或者优惠劵的 change log 可能更好。 + // 2.2 作废优惠劵 couponMapper.deleteById(couponId); } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java index 1409561d5..4cfee5e17 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java @@ -296,10 +296,11 @@ public class TradeOrderDO extends BaseDO { */ private Integer vipPrice; + // TODO @puhui999:我们要不要把相关的字段,定义的更明确一点?例如说,giveCouponTemplateCounts 赠送的优惠劵模版数量,或者 giveCouponCounts 赠送的优惠劵数量。感受上,Coupons 和 Map 有点点重叠哈。 /** * 赠送的优惠劵 * - * key: 优惠劵编号 + * key: 优惠劵模版编号 * value:对应的优惠券数量 * * 目的:用于订单支付后赠送优惠券 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index bdae8f227..379be205f 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -892,15 +892,11 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @Override public void updateOrderGiveCouponIds(Long userId, Long orderId, List giveCouponIds) { - // 1.1 检验订单存在 + // 1. 检验订单存在 TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); if (order == null) { throw exception(ORDER_NOT_FOUND); } - // 1.2 校验订单是否支付 - if (!order.getPayStatus()) { - throw exception(ORDER_CANCEL_PAID_FAIL, "已支付"); - } // 2. 更新订单赠送的优惠券编号列表 tradeOrderMapper.updateById(new TradeOrderDO().setId(orderId).setGiveCouponIds(giveCouponIds)); From d1fc18a4098f290db2d1f778c78f49fac349742f Mon Sep 17 00:00:00 2001 From: heyho Date: Mon, 2 Sep 2024 05:08:17 +0000 Subject: [PATCH 103/136] =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=B4=AD=E4=B9=B0?= =?UTF-8?q?=E4=B8=80=E4=BB=B6=E4=BB=A5=E4=B8=8A=E6=95=B0=E9=87=8F=E6=97=B6?= =?UTF-8?q?=E6=8E=A8=E5=B9=BF=E8=80=85=E8=BF=94=E4=BD=A3=E9=87=91=E9=A2=9D?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E5=87=BA=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: heyho --- .../yudao/module/trade/convert/order/TradeOrderConvert.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java index c8b68ebec..60b81057f 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java @@ -261,7 +261,7 @@ public interface TradeOrderConvert { default BrokerageAddReqBO convert(MemberUserRespDTO user, TradeOrderItemDO item, ProductSpuRespDTO spu, ProductSkuRespDTO sku) { BrokerageAddReqBO bo = new BrokerageAddReqBO().setBizId(String.valueOf(item.getId())).setSourceUserId(item.getUserId()) - .setBasePrice(item.getPrice() * item.getCount()) + .setBasePrice(item.getPayPrice()) .setTitle(StrUtil.format("{}成功购买{}", user.getNickname(), item.getSpuName())) .setFirstFixedPrice(0).setSecondFixedPrice(0); if (BooleanUtil.isTrue(spu.getSubCommissionType())) { From 0ce0c3f3d2c6a82550eaf14c64798c50557d3510 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 2 Sep 2024 13:15:10 +0800 Subject: [PATCH 104/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E5=B7=A5=E4=BD=9C=E6=B5=81=EF=BC=9AbpmnBytes?= =?UTF-8?q?=20=E5=9C=A8=E9=9D=9E=20UTF-8=20=E7=8E=AF=E5=A2=83=E4=B8=8B?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E8=83=BD=E5=AD=98=E5=9C=A8=E4=B9=B1=E7=A0=81?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/bpm/convert/definition/BpmModelConvert.java | 3 ++- .../framework/flowable/core/util/BpmnModelUtils.java | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java index 3fe5cc068..ec053b8d0 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java @@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModel import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO; import org.flowable.common.engine.impl.db.SuspensionState; import org.flowable.engine.repository.Deployment; @@ -55,7 +56,7 @@ public interface BpmModelConvert { BpmModelMetaInfoRespDTO metaInfo = buildMetaInfo(model); BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null); if (ArrayUtil.isNotEmpty(bpmnBytes)) { - modelVO.setBpmnXml(new String(bpmnBytes)); + modelVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnBytes)); } return modelVO; } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index bcf82d731..c046011b0 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; import org.flowable.bpmn.converter.BpmnXMLConverter; @@ -108,7 +109,14 @@ public class BpmnModelUtils { return null; } BpmnXMLConverter converter = new BpmnXMLConverter(); - return new String(converter.convertToXML(model)); + return StrUtil.utf8Str(converter.convertToXML(model)); + } + + public static String getBpmnXml(byte[] bpmnBytes) { + if (ArrayUtil.isEmpty(bpmnBytes)) { + return null; + } + return StrUtil.utf8Str(bpmnBytes); } // ========== 遍历相关的方法 ========== From fdaf5e50ca39367377c9225924ac47606d951598 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Mon, 2 Sep 2024 16:28:13 +0800 Subject: [PATCH 105/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E:=20=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E6=B4=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/reward/RewardActivityController.java | 11 +++++------ .../admin/reward/vo/RewardActivityBaseVO.java | 8 ++++---- .../convert/reward/RewardActivityConvert.java | 13 ------------- .../dal/dataobject/reward/RewardActivityDO.java | 4 ++-- .../service/reward/RewardActivityServiceImpl.java | 5 ++--- 5 files changed, 13 insertions(+), 28 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/RewardActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/RewardActivityController.java index d41912337..0e50ffc14 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/RewardActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/RewardActivityController.java @@ -2,23 +2,22 @@ package cn.iocoder.yudao.module.promotion.controller.admin.reward; 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.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; -import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert; import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService; 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.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; - import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - 满减送活动") @@ -69,7 +68,7 @@ public class RewardActivityController { @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')") public CommonResult getRewardActivity(@RequestParam("id") Long id) { RewardActivityDO rewardActivity = rewardActivityService.getRewardActivity(id); - return success(RewardActivityConvert.INSTANCE.convert(rewardActivity)); + return success(BeanUtils.toBean(rewardActivity, RewardActivityRespVO.class)); } @GetMapping("/page") @@ -77,7 +76,7 @@ public class RewardActivityController { @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')") public CommonResult> getRewardActivityPage(@Valid RewardActivityPageReqVO pageVO) { PageResult pageResult = rewardActivityService.getRewardActivityPage(pageVO); - return success(RewardActivityConvert.INSTANCE.convertPage(pageResult)); + return success(BeanUtils.toBean(pageResult, RewardActivityRespVO.class)); } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java index 7f68ee123..31c40d9de 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java @@ -79,13 +79,13 @@ public class RewardActivityBaseVO { @Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") private Integer point; - @Schema(description = "赠送的优惠劵编号的数组", example = "1,2,3") - private Map giveCouponsMap; + @Schema(description = "赠送的优惠劵编号的数组") + private Map giveCoupons; - @AssertTrue(message = "赠送的积分不能小于 1") + @AssertTrue(message = "赠送的积分不能小于 0") @JsonIgnore public boolean isPointValid() { - return point == null || point >= 1; + return point == null || point >= 0; } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java index 5343656ed..c954100c5 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java @@ -1,10 +1,5 @@ package cn.iocoder.yudao.module.promotion.convert.reward; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; -import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO; -import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; -import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; @@ -18,12 +13,4 @@ public interface RewardActivityConvert { RewardActivityConvert INSTANCE = Mappers.getMapper(RewardActivityConvert.class); - RewardActivityDO convert(RewardActivityCreateReqVO bean); - - RewardActivityDO convert(RewardActivityUpdateReqVO bean); - - RewardActivityRespVO convert(RewardActivityDO bean); - - PageResult convertPage(PageResult page); - } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java index f5d60c4e6..03e052a69 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java @@ -107,9 +107,9 @@ public class RewardActivityDO extends BaseDO { /** * 赠送的优惠劵 * - * key: 优惠劵编号,value:对应的优惠券数量 + * key: 优惠劵模版编号,value:对应的数量 */ - private Map giveCouponsMap; + private Map giveCoupons; } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java index d35142c19..eefbc6dee 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java @@ -10,7 +10,6 @@ import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivi import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; -import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert; import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO; import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; @@ -54,7 +53,7 @@ public class RewardActivityServiceImpl implements RewardActivityService { validateRewardActivitySpuConflicts(null, createReqVO); // 2. 插入 - RewardActivityDO rewardActivity = RewardActivityConvert.INSTANCE.convert(createReqVO) + RewardActivityDO rewardActivity = BeanUtils.toBean(createReqVO, RewardActivityDO.class) .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime())); rewardActivityMapper.insert(rewardActivity); // 返回 @@ -74,7 +73,7 @@ public class RewardActivityServiceImpl implements RewardActivityService { validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO); // 2. 更新 - RewardActivityDO updateObj = RewardActivityConvert.INSTANCE.convert(updateReqVO) + RewardActivityDO updateObj = BeanUtils.toBean(updateReqVO, RewardActivityDO.class) .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime())); rewardActivityMapper.updateById(updateObj); } From 79cb96702ad4144aac1a365a12df03918a7093b5 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Mon, 2 Sep 2024 17:20:41 +0800 Subject: [PATCH 106/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E:=20=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E6=B4=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/reward/dto/RewardActivityMatchRespDTO.java | 4 +++- .../admin/reward/vo/RewardActivityBaseVO.java | 2 +- .../dal/dataobject/reward/RewardActivityDO.java | 7 +++++-- .../trade/dal/dataobject/order/TradeOrderDO.java | 3 +-- .../service/order/TradeOrderUpdateServiceImpl.java | 2 +- .../order/handler/TradeCouponOrderHandler.java | 4 ++-- .../service/price/bo/TradePriceCalculateRespBO.java | 8 +++++--- .../price/calculator/TradePriceCalculatorHelper.java | 2 +- .../TradeRewardActivityPriceCalculator.java | 12 ++++++------ .../TradeRewardActivityPriceCalculatorTest.java | 2 +- 10 files changed, 26 insertions(+), 20 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java index d8d5ef135..958668461 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java @@ -95,8 +95,10 @@ public class RewardActivityMatchRespDTO { * * key: 优惠劵模版编号 * value:对应的优惠券数量 + * + * 目的:用于订单支付后赠送优惠券 */ - private Map giveCoupons; + private Map giveCouponTemplateCounts; } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java index 31c40d9de..590e9a7f2 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java @@ -80,7 +80,7 @@ public class RewardActivityBaseVO { private Integer point; @Schema(description = "赠送的优惠劵编号的数组") - private Map giveCoupons; + private Map giveCouponTemplateCounts; @AssertTrue(message = "赠送的积分不能小于 0") @JsonIgnore diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java index 03e052a69..a2f1e7e88 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java @@ -107,9 +107,12 @@ public class RewardActivityDO extends BaseDO { /** * 赠送的优惠劵 * - * key: 优惠劵模版编号,value:对应的数量 + * key: 优惠劵模版编号 + * value:对应的优惠券数量 + * + * 目的:用于订单支付后赠送优惠券 */ - private Map giveCoupons; + private Map giveCouponTemplateCounts; } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java index 4cfee5e17..399b692ed 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java @@ -296,7 +296,6 @@ public class TradeOrderDO extends BaseDO { */ private Integer vipPrice; - // TODO @puhui999:我们要不要把相关的字段,定义的更明确一点?例如说,giveCouponTemplateCounts 赠送的优惠劵模版数量,或者 giveCouponCounts 赠送的优惠劵数量。感受上,Coupons 和 Map 有点点重叠哈。 /** * 赠送的优惠劵 * @@ -306,7 +305,7 @@ public class TradeOrderDO extends BaseDO { * 目的:用于订单支付后赠送优惠券 */ @TableField(typeHandler = JacksonTypeHandler.class) - private Map giveCouponsMap; + private Map giveCouponTemplateCounts; /** * 赠送的优惠劵编号 * diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index 379be205f..ce0c953e1 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -202,7 +202,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum)); order.setUserIp(getClientIP()).setTerminal(getTerminal()); // 使用 + 赠送优惠券 - order.setGiveCouponsMap(calculateRespBO.getGiveCoupons()); + order.setGiveCouponTemplateCounts(calculateRespBO.getGiveCouponTemplateCounts()); // 支付 + 退款信息 order.setAdjustPrice(0).setPayStatus(false); order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java index 3a98a6c9e..f5d7da4d4 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java @@ -42,11 +42,11 @@ public class TradeCouponOrderHandler implements TradeOrderHandler { @Override public void afterPayOrder(TradeOrderDO order, List orderItems) { - if (CollUtil.isEmpty(order.getGiveCouponsMap())) { + if (CollUtil.isEmpty(order.getGiveCouponTemplateCounts())) { return; } // 赠送优惠券 - List couponIds = couponApi.takeCouponsByAdmin(order.getGiveCouponsMap(), order.getUserId()); + List couponIds = couponApi.takeCouponsByAdmin(order.getGiveCouponTemplateCounts(), order.getUserId()); if (CollUtil.isEmpty(couponIds)) { return; } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java index 68fa58b37..4f65f33d1 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java @@ -76,10 +76,12 @@ public class TradePriceCalculateRespBO { /** * 赠送的优惠劵 * - * key: 优惠劵编号,value:对应的优惠券数量 - * 目的:用于后续取消或者售后订单时,需要扣减赠送 + * key: 优惠劵模版编号 + * value:对应的优惠券数量 + * + * 目的:用于订单支付后赠送优惠券 */ - private Map giveCoupons; + private Map giveCouponTemplateCounts; /** * 订单价格 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java index 195ef8718..323b50e93 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java @@ -32,7 +32,7 @@ public class TradePriceCalculatorHelper { List spuList, List skuList) { // 创建 PriceCalculateRespDTO 对象 TradePriceCalculateRespBO result = new TradePriceCalculateRespBO(); - result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCoupons(new LinkedHashMap<>()); + result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>()); // 创建它的 OrderItem 属性 result.setItems(new ArrayList<>(param.getItems().size())); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java index f62b65eb9..ddb24e9bd 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -107,13 +107,13 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator result.setFreeDelivery(true); } // 4.3 记录赠送的优惠券 - if (CollUtil.isNotEmpty(rule.getGiveCoupons())) { - for (Map.Entry entry : rule.getGiveCoupons().entrySet()) { - Map giveCoupons = result.getGiveCoupons(); - if (giveCoupons.get(entry.getKey()) == null) { // 情况一:还没有赠送的优惠券 - result.setGiveCoupons(rule.getGiveCoupons()); + if (CollUtil.isNotEmpty(rule.getGiveCouponTemplateCounts())) { + for (Map.Entry entry : rule.getGiveCouponTemplateCounts().entrySet()) { + Map giveCouponTemplateCounts = result.getGiveCouponTemplateCounts(); + if (giveCouponTemplateCounts.get(entry.getKey()) == null) { // 情况一:还没有赠送的优惠券 + result.setGiveCouponTemplateCounts(rule.getGiveCouponTemplateCounts()); } else { // 情况二:别的满减活动送过同类优惠券,则直接增加数量 - giveCoupons.put(entry.getKey(), giveCoupons.get(entry.getKey()) + entry.getValue()); + giveCouponTemplateCounts.put(entry.getKey(), giveCouponTemplateCounts.get(entry.getKey()) + entry.getValue()); } } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java index f1f31e3c8..ba93fc10e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java @@ -49,7 +49,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest TradePriceCalculateRespBO result = new TradePriceCalculateRespBO() .setType(TradeOrderTypeEnum.NORMAL.getType()) .setPrice(new TradePriceCalculateRespBO.Price()) - .setPromotions(new ArrayList<>()).setGiveCoupons(new LinkedHashMap<>()) + .setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>()) .setItems(asList( new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true) .setPrice(100).setSpuId(1L), From e5453028f10c1e7879739ebeca1dc5966edc1fd7 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 2 Sep 2024 21:53:41 +0800 Subject: [PATCH 107/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E6=BB=A1=E5=87=8F?= =?UTF-8?q?=E9=80=81=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../convert/reward/RewardActivityConvert.java | 16 ---------------- .../TradeRewardActivityPriceCalculator.java | 4 ++++ 2 files changed, 4 insertions(+), 16 deletions(-) delete mode 100755 yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java deleted file mode 100755 index c954100c5..000000000 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.iocoder.yudao.module.promotion.convert.reward; - -import org.mapstruct.Mapper; -import org.mapstruct.factory.Mappers; - -/** - * 满减送活动 Convert - * - * @author 芋道源码 - */ -@Mapper -public interface RewardActivityConvert { - - RewardActivityConvert INSTANCE = Mappers.getMapper(RewardActivityConvert.class); - -} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java index ddb24e9bd..50d424c29 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -110,6 +110,10 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator if (CollUtil.isNotEmpty(rule.getGiveCouponTemplateCounts())) { for (Map.Entry entry : rule.getGiveCouponTemplateCounts().entrySet()) { Map giveCouponTemplateCounts = result.getGiveCouponTemplateCounts(); + // TODO @puhui999:是不是有一种可能性,这个 key 没有,别的 key 有哈。 + // TODO 这里还有一种简化的写法。就是下面,大概两行就可以啦 +// result.getGiveCouponTemplateCounts().put(entry.getKey(), +// result.getGiveCouponTemplateCounts().getOrDefault(entry.getKey(), 0) + entry.getValue()); if (giveCouponTemplateCounts.get(entry.getKey()) == null) { // 情况一:还没有赠送的优惠券 result.setGiveCouponTemplateCounts(rule.getGiveCouponTemplateCounts()); } else { // 情况二:别的满减活动送过同类优惠券,则直接增加数量 From 9fbc953dd1ff6f11470bcda12f48e4845982ce59 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 3 Sep 2024 08:54:56 +0800 Subject: [PATCH 108/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91INFRA=EF=BC=9A=E4=BB=A3=E7=A0=81=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=9C=A8=20ERP=20=E6=A8=A1=E5=BC=8F=E6=97=B6=EF=BC=8C?= =?UTF-8?q?updateTime=20=E6=97=A0=E6=B3=95=E8=A2=AB=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/codegen/java/service/serviceImpl.vm | 1 + 1 file changed, 1 insertion(+) diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm index a8184e4d7..80bc71b02 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm @@ -286,6 +286,7 @@ public class ${table.className}ServiceImpl implements ${table.className}Service // 校验存在 validate${subSimpleClassName}Exists(${subClassNameVar}.getId()); // 更新 + ${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新 ${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar}); } From d3f28b92a76af82d78977bd14650a13e9fc10f20 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 3 Sep 2024 10:34:44 +0800 Subject: [PATCH 109/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91SYSTEM=EF=BC=9A=E8=AE=A4=E8=AF=81=E4=BB=A4?= =?UTF-8?q?=E7=89=8C=E7=9A=84=E6=93=8D=E4=BD=9C=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=20@Transactional=20=E6=B3=A8=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 6 +- .../oauth2/OAuth2TokenServiceImpl.java | 6 +- yudao-server/pom.xml | 60 +++++++++---------- 3 files changed, 37 insertions(+), 35 deletions(-) diff --git a/pom.xml b/pom.xml index 86dfebcc3..4634d345e 100644 --- a/pom.xml +++ b/pom.xml @@ -15,12 +15,12 @@ yudao-module-system yudao-module-infra - + yudao-module-member - - + yudao-module-pay + yudao-module-mall diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java index cb3bf409f..8918e7ede 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java @@ -56,7 +56,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { private AdminUserService adminUserService; @Override - @Transactional + @Transactional(rollbackFor = Exception.class) public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes) { OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); // 创建刷新令牌 @@ -66,6 +66,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { } @Override + @Transactional(rollbackFor = Exception.class) public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) { // 查询访问令牌 OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); @@ -82,7 +83,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { // 移除相关的访问令牌 List accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken); if (CollUtil.isNotEmpty(accessTokenDOs)) { - oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); + oauth2AccessTokenMapper.deleteByIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken)); } @@ -126,6 +127,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { } @Override + @Transactional(rollbackFor = Exception.class) public OAuth2AccessTokenDO removeAccessToken(String accessToken) { // 删除访问令牌 OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml index 3b16fa192..d0c429aab 100644 --- a/yudao-server/pom.xml +++ b/yudao-server/pom.xml @@ -33,11 +33,11 @@ - - - - - + + cn.iocoder.boot + yudao-module-member-biz + ${revision} + @@ -52,11 +52,11 @@ - - - - - + + cn.iocoder.boot + yudao-module-pay-biz + ${revision} + @@ -66,26 +66,26 @@ - - - - - - - - - - - - - - - - - - - - + + cn.iocoder.boot + yudao-module-promotion-biz + ${revision} + + + cn.iocoder.boot + yudao-module-product-biz + ${revision} + + + cn.iocoder.boot + yudao-module-trade-biz + ${revision} + + + cn.iocoder.boot + yudao-module-statistics-biz + ${revision} + From 5692571a9c02c9801a5e2b1208463349e6c50365 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 3 Sep 2024 10:47:20 +0800 Subject: [PATCH 110/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E6=97=B6=EF=BC=8C=E5=A2=9E=E5=8A=A0=E2=80=9C?= =?UTF-8?q?=E8=AF=A5=E4=BC=98=E6=83=A0=E5=8A=B5=E6=97=A0=E6=B3=95=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=EF=BC=8C=E5=8E=9F=E5=9B=A0=EF=BC=9A=E4=BC=98=E6=83=A0?= =?UTF-8?q?=E9=87=91=E9=A2=9D=E8=B6=85=E8=BF=87=E8=AE=A2=E5=8D=95=E9=87=91?= =?UTF-8?q?=E9=A2=9D=E2=80=9D=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/module/trade/enums/ErrorCodeConstants.java | 1 + .../trade/service/brokerage/BrokerageUserServiceImpl.java | 5 ----- .../price/calculator/TradeCouponPriceCalculator.java | 6 ++++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java index a797fa5bd..5613cae8e 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java @@ -61,6 +61,7 @@ public interface ErrorCodeConstants { ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵"); ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量"); ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_006, "计算快递运费异常,配送方式不匹配"); + ErrorCode PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:优惠金额超过订单金额"); // ========== 物流 Express 模块 1-011-004-000 ========== ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在"); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java index cf56a5bce..751151fe8 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java @@ -268,11 +268,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService { return false; } - // 校验分佣模式:仅可后台手动设置推广员 - // if (BrokerageEnabledConditionEnum.ADMIN.getCondition().equals(tradeConfig.getBrokerageEnabledCondition())) { - // throw exception(BROKERAGE_BIND_CONDITION_ADMIN); - // } - // 校验分销关系绑定模式 if (BrokerageBindModeEnum.REGISTER.getMode().equals(tradeConfig.getBrokerageBindMode())) { // 判断是否为新用户:注册时间在 30 秒内的,都算新用户 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java index 3bdfe509f..1c7294be5 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java @@ -25,6 +25,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU; import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH; /** * 优惠劵的 {@link TradePriceCalculator} 实现类 @@ -65,8 +66,9 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator { // 3.1 计算可以优惠的金额 Integer couponPrice = getCouponPrice(coupon, totalPayPrice); - Assert.isTrue(couponPrice < totalPayPrice, - "优惠劵({}) 的优惠金额({}),不能大于订单总金额({})", coupon.getId(), couponPrice, totalPayPrice); + if (couponPrice <= totalPayPrice) { + throw exception(PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH); + } // 3.2 计算分摊的优惠金额 List divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice); From acf0e401d3c188901f0d811a2af043ecef496b1c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 3 Sep 2024 16:11:08 +0800 Subject: [PATCH 111/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91SYSTEM=EF=BC=9Auser=5Frole=5Fids=20=E5=BF=BD?= =?UTF-8?q?=E7=95=A5=E5=A4=9A=E7=A7=9F=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-server/src/main/resources/application.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 8d63d9593..efe1eb34f 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -301,6 +301,7 @@ yudao: - tmp_report_data_1 - tmp_report_data_income ignore-caches: + - user_role_ids - permission_menu_ids - oauth_client - notify_template From b8c653d18b0aeb325abff9cbea1d9510843ea93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Tue, 3 Sep 2024 16:16:22 +0800 Subject: [PATCH 112/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E6=8B=BC=E5=9B=A2=E8=A3=85=E4=BF=AE=E9=87=8D?= =?UTF-8?q?=E6=9E=84=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CombinationActivityController.java | 45 +++++++++++++++++-- .../activity/CombinationActivityRespVO.java | 12 +++++ .../AppCombinationActivityController.java | 38 ++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java index 9ba319463..9dbe45503 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java @@ -16,19 +16,21 @@ import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordSe 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.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import static cn.hutool.core.collection.CollectionUtil.newArrayList; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; @Tag(name = "管理后台 - 拼团活动") @RestController @@ -87,6 +89,43 @@ public class CombinationActivityController { return success(CombinationActivityConvert.INSTANCE.convert(activity, products)); } + @GetMapping("/list") + @Operation(summary = "获得拼团活动详情列表") + @Parameter(name = "combinationActivityIds", description = "拼团活动编号列表", required = true, example = "[1,2,3]") + @PreAuthorize("@ss.hasPermission('product:spu:query')") + public CommonResult> getCombinationActivityDetailList(@RequestParam("combinationActivityIds") Collection combinationActivityIds) { + // 查询拼团活动列表 + List activities = combinationActivityService.getCombinationActivityListByIds(combinationActivityIds); + + // 转换活动列表 + List activityVOs = CombinationActivityConvert.INSTANCE.convertList(activities); + + // 获取商品SPU列表和拼团产品列表 + Set spuIds = activities.stream().map(CombinationActivityDO::getSpuId).collect(Collectors.toSet()); + List spuList = productSpuApi.getSpuList(spuIds); + + Set activityIds = activities.stream().map(CombinationActivityDO::getId).collect(Collectors.toSet()); + List productList = combinationActivityService.getCombinationProductListByActivityIds(activityIds); + + // 创建SPU和产品的映射 + Map spuMap = convertMap(spuList, ProductSpuRespDTO::getId); + Map> productMap = convertMultiMap(productList, CombinationProductDO::getActivityId); + + // 更新VO列表 + activityVOs.forEach(vo -> { + ProductSpuRespDTO spu = spuMap.get(vo.getSpuId()); + if (spu != null) { + vo.setSpuName(spu.getName()) + .setPicUrl(spu.getPicUrl()) + .setMarketPrice(spu.getMarketPrice()); + } + vo.setProducts(CombinationActivityConvert.INSTANCE.convertList2(productMap.get(vo.getId()))); + }); + + return success(activityVOs); + } + + @GetMapping("/page") @Operation(summary = "获得拼团活动分页") @PreAuthorize("@ss.hasPermission('promotion:combination-activity:query')") diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java index 0ac77c559..e4880970e 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java @@ -27,4 +27,16 @@ public class CombinationActivityRespVO extends CombinationActivityBaseVO { @Schema(description = "拼团商品", requiredMode = Schema.RequiredMode.REQUIRED) private List products; + // ========== 商品字段 ========== + + @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 name 读取 + example = "618大促") + private String spuName; + @Schema(description = "商品主图", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 picUrl 读取 + example = "https://www.iocoder.cn/xx.png") + private String picUrl; + @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 marketPrice 读取 + example = "50") + private Integer marketPrice; + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java index 867c2d4b8..11a3cfd96 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java @@ -26,9 +26,13 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.annotation.Resource; + import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; @@ -109,4 +113,38 @@ public class AppCombinationActivityController { return success(CombinationActivityConvert.INSTANCE.convert3(activity, products)); } + @GetMapping("/get-detail-list") + @Operation(summary = "获得拼团活动明细") + @Parameter(name = "combinationActivityIds", description = "活动编号列表", required = true, example = "[1024, 1025]") + public CommonResult> getCombinationActivityDetailList(@RequestParam("combinationActivityIds") Collection combinationActivityIds) { + // 1. 获取活动 + List combinationActivityDOList = activityService.getCombinationActivityListByIds(combinationActivityIds); + + // 过滤掉无效的活动 + List validActivities = combinationActivityDOList.stream() + .filter(combinationActivityDO -> combinationActivityDO != null && + !ObjectUtil.equal(combinationActivityDO.getStatus(), CommonStatusEnum.DISABLE.getStatus())) + .toList(); + + // 如果没有有效的活动,返回 null 或者适当的错误信息 + if (validActivities.isEmpty()) { + return success(null); // 或者 return error("没有有效的活动"); + } + + // 2. 构建结果列表 + List detailRespVOList = new ArrayList<>(); + for (CombinationActivityDO activity : validActivities) { + // 获取活动商品 + List products = activityService.getCombinationProductsByActivityId(activity.getId()); + + // 调用转换方法并添加到结果列表 + AppCombinationActivityDetailRespVO detailRespVO = CombinationActivityConvert.INSTANCE.convert3(activity, products); + detailRespVOList.add(detailRespVO); + } + + // 3. 返回转换后的结果 + return success(detailRespVOList); + } + + } From ae9ff74e2353d0709b035f79798bf9f8f4fa01d1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 4 Sep 2024 09:07:16 +0800 Subject: [PATCH 113/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=85=A8=E5=B1=80=EF=BC=9AMySQL=20JDBC=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20rewriteBatchedStatements=20=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E6=8F=92=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-server/src/main/resources/application-dev.yaml | 2 +- yudao-server/src/main/resources/application-local.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index 46399b802..7e2fcc1c1 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -40,7 +40,7 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root password: 123456 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 0c27aeac8..35715d98c 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -45,8 +45,8 @@ spring: primary: master datasource: master: - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 - # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # MySQL Connector/J 5.X 连接的示例 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例 # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例 # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer 连接的示例 From 15f46db7acaf9e3ea85a559a2ad9537eb24f58d0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 4 Sep 2024 09:08:12 +0800 Subject: [PATCH 114/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=85=A8=E5=B1=80=EF=BC=9AMySQL=20JDBC=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20rewriteBatchedStatements=20=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E6=8F=92=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-server/src/main/resources/application-dev.yaml | 2 +- yudao-server/src/main/resources/application-local.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index 7e2fcc1c1..5a4fa9286 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -45,7 +45,7 @@ spring: password: 123456 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root password: 123456 diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 35715d98c..40c0919b7 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -63,7 +63,7 @@ spring: # password: Yudao@2024 # OpenGauss 连接的示例 slave: # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true username: root password: 123456 From 819323d3dba064772478a35b392724e3b987f6c2 Mon Sep 17 00:00:00 2001 From: heyho Date: Wed, 4 Sep 2024 05:24:04 +0000 Subject: [PATCH 115/136] =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E7=AB=AF?= =?UTF-8?q?=E9=87=87=E7=94=A8=E9=93=B6=E8=A1=8C=E5=8D=A1=E6=8F=90=E7=8E=B0?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E4=BA=A4=E5=90=8E=E6=8A=A5=EF=BC=9A=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E5=8F=82=E6=95=B0bankName=E7=B1=BB=E5=9E=8B=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E3=80=82=20=E5=8E=9F=E5=9B=A0=EF=BC=9Ayudao-module-ma?= =?UTF-8?q?ll/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module?= =?UTF-8?q?/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdra?= =?UTF-8?q?wCreateReqVO.java=E4=B8=AD=E7=9A=84=E5=AD=97=E6=AE=B5=EF=BC=9Ap?= =?UTF-8?q?rivate=20Integer=20bankName=E5=AE=9E=E9=99=85=E5=BA=94=E4=B8=BA?= =?UTF-8?q?String=E5=9E=8B=EF=BC=8C=E5=9B=A0=E4=B8=BA=E6=97=A0=E8=AE=BA?= =?UTF-8?q?=E6=98=AF=E7=94=A8=E6=88=B7=E6=89=8B=E5=8A=A8=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E9=93=B6=E8=A1=8C=E5=90=8D=E5=AD=97=EF=BC=8C=E6=88=96=E6=98=AF?= =?UTF-8?q?=E9=87=87=E7=94=A8=E6=9F=A5=E8=AF=A2=E5=AD=97=E5=85=B8=E8=A1=A8?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B"brokerage=5Fbank=5Fname"=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E5=AD=97=E5=85=B8=E5=80=BC"value"=EF=BC=8C=E9=83=BD=E5=BA=94?= =?UTF-8?q?=E8=AF=A5=E6=98=AFString=E5=9E=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 经检查其相所有相关的DO,VO的属性bankName均为String,所以AppBrokerageWithdrawCreateReqVO的bankName改为String为宜。 Signed-off-by: heyho --- .../brokerage/vo/withdraw/AppBrokerageWithdrawCreateReqVO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawCreateReqVO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawCreateReqVO.java index feb6eae89..83d473825 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawCreateReqVO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawCreateReqVO.java @@ -44,7 +44,7 @@ public class AppBrokerageWithdrawCreateReqVO { private String name; @Schema(description = "提现银行", example = "1") @NotNull(message = "提现银行不能为空", groups = {Bank.class}) - private Integer bankName; + private String bankName; @Schema(description = "开户地址", example = "海淀支行") private String bankAddress; From c935312bf805d74651002ce8a3009a55c9d7025d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Wed, 4 Sep 2024 22:32:30 +0800 Subject: [PATCH 116/136] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=97=A0=E7=94=A8?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/combination/AppCombinationActivityController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java index 11a3cfd96..42d6c8535 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java @@ -128,7 +128,7 @@ public class AppCombinationActivityController { // 如果没有有效的活动,返回 null 或者适当的错误信息 if (validActivities.isEmpty()) { - return success(null); // 或者 return error("没有有效的活动"); + return success(null); } // 2. 构建结果列表 From efd0a86007c3b71113383268a2aa386ace5cdb85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Wed, 4 Sep 2024 22:55:12 +0800 Subject: [PATCH 117/136] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/combination/CombinationActivityController.java | 8 ++++---- .../app/combination/AppCombinationActivityController.java | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java index 9dbe45503..1dba7345f 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java @@ -89,13 +89,13 @@ public class CombinationActivityController { return success(CombinationActivityConvert.INSTANCE.convert(activity, products)); } - @GetMapping("/list") + @GetMapping("/detail-list") @Operation(summary = "获得拼团活动详情列表") - @Parameter(name = "combinationActivityIds", description = "拼团活动编号列表", required = true, example = "[1,2,3]") + @Parameter(name = "ids", description = "拼团活动编号列表", required = true, example = "[1,2,3]") @PreAuthorize("@ss.hasPermission('product:spu:query')") - public CommonResult> getCombinationActivityDetailList(@RequestParam("combinationActivityIds") Collection combinationActivityIds) { + public CommonResult> getCombinationActivityDetailList(@RequestParam("ids") Collection ids) { // 查询拼团活动列表 - List activities = combinationActivityService.getCombinationActivityListByIds(combinationActivityIds); + List activities = combinationActivityService.getCombinationActivityListByIds(ids); // 转换活动列表 List activityVOs = CombinationActivityConvert.INSTANCE.convertList(activities); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java index 42d6c8535..9ae93e263 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java @@ -113,12 +113,12 @@ public class AppCombinationActivityController { return success(CombinationActivityConvert.INSTANCE.convert3(activity, products)); } - @GetMapping("/get-detail-list") + @GetMapping("/detail-list") @Operation(summary = "获得拼团活动明细") - @Parameter(name = "combinationActivityIds", description = "活动编号列表", required = true, example = "[1024, 1025]") - public CommonResult> getCombinationActivityDetailList(@RequestParam("combinationActivityIds") Collection combinationActivityIds) { + @Parameter(name = "ids", description = "活动编号列表", required = true, example = "[1024, 1025]") + public CommonResult> getCombinationActivityDetailList(@RequestParam("ids") Collection ids) { // 1. 获取活动 - List combinationActivityDOList = activityService.getCombinationActivityListByIds(combinationActivityIds); + List combinationActivityDOList = activityService.getCombinationActivityListByIds(ids); // 过滤掉无效的活动 List validActivities = combinationActivityDOList.stream() From 39a6eb4792b11df14100a65573a52bc3419c86bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Thu, 5 Sep 2024 10:55:23 +0800 Subject: [PATCH 118/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E6=8B=BC=E5=9B=A2=E6=B4=BB=E5=8A=A8=E5=92=8C?= =?UTF-8?q?SPU=E8=AF=A6=E6=83=85=E5=88=86=E5=BC=80=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CombinationActivityController.java | 19 +++---------------- .../activity/CombinationActivityRespVO.java | 12 ------------ .../AppCombinationActivityController.java | 5 +++-- 3 files changed, 6 insertions(+), 30 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java index 1dba7345f..f166b010d 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java @@ -100,32 +100,19 @@ public class CombinationActivityController { // 转换活动列表 List activityVOs = CombinationActivityConvert.INSTANCE.convertList(activities); - // 获取商品SPU列表和拼团产品列表 - Set spuIds = activities.stream().map(CombinationActivityDO::getSpuId).collect(Collectors.toSet()); - List spuList = productSpuApi.getSpuList(spuIds); - + // 获取拼团产品列表 Set activityIds = activities.stream().map(CombinationActivityDO::getId).collect(Collectors.toSet()); List productList = combinationActivityService.getCombinationProductListByActivityIds(activityIds); // 创建SPU和产品的映射 - Map spuMap = convertMap(spuList, ProductSpuRespDTO::getId); Map> productMap = convertMultiMap(productList, CombinationProductDO::getActivityId); - // 更新VO列表 - activityVOs.forEach(vo -> { - ProductSpuRespDTO spu = spuMap.get(vo.getSpuId()); - if (spu != null) { - vo.setSpuName(spu.getName()) - .setPicUrl(spu.getPicUrl()) - .setMarketPrice(spu.getMarketPrice()); - } - vo.setProducts(CombinationActivityConvert.INSTANCE.convertList2(productMap.get(vo.getId()))); - }); + // 往活动VO赋值产品列表 + activityVOs.forEach(vo -> vo.setProducts(CombinationActivityConvert.INSTANCE.convertList2(productMap.get(vo.getId())))); return success(activityVOs); } - @GetMapping("/page") @Operation(summary = "获得拼团活动分页") @PreAuthorize("@ss.hasPermission('promotion:combination-activity:query')") diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java index e4880970e..0ac77c559 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java @@ -27,16 +27,4 @@ public class CombinationActivityRespVO extends CombinationActivityBaseVO { @Schema(description = "拼团商品", requiredMode = Schema.RequiredMode.REQUIRED) private List products; - // ========== 商品字段 ========== - - @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 name 读取 - example = "618大促") - private String spuName; - @Schema(description = "商品主图", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 picUrl 读取 - example = "https://www.iocoder.cn/xx.png") - private String picUrl; - @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 marketPrice 读取 - example = "50") - private Integer marketPrice; - } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java index 9ae93e263..f113ab0d2 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.promotion.controller.app.combination; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -126,9 +127,9 @@ public class AppCombinationActivityController { !ObjectUtil.equal(combinationActivityDO.getStatus(), CommonStatusEnum.DISABLE.getStatus())) .toList(); - // 如果没有有效的活动,返回 null 或者适当的错误信息 + // 如果没有有效的活动,返回空列表 if (validActivities.isEmpty()) { - return success(null); + return success(ListUtil.empty()); } // 2. 构建结果列表 From 73a7ccbd754fb1b77470fda682d6514a02d30b3a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 5 Sep 2024 13:56:46 +0800 Subject: [PATCH 119/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E6=8B=BC=E5=9B=A2=E6=B4=BB=E5=8A=A8=EF=BC=9A?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8B=BC=E5=9B=A2=E6=B4=BB=E5=8A=A8=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=9A=84=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CombinationActivityController.java | 44 ++++----- .../activity/CombinationActivityRespVO.java | 10 ++ .../AppCombinationActivityController.java | 95 ++++--------------- .../AppCombinationActivityRespVO.java | 11 +-- .../CombinationActivityConvert.java | 43 +++++---- .../CombinationActivityService.java | 8 -- .../CombinationActivityServiceImpl.java | 5 - 7 files changed, 75 insertions(+), 141 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java index f166b010d..2f9e7863f 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.promotion.controller.admin.combination; import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; @@ -22,15 +23,15 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import static cn.hutool.core.collection.CollectionUtil.newArrayList; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; @Tag(name = "管理后台 - 拼团活动") @RestController @@ -89,28 +90,21 @@ public class CombinationActivityController { return success(CombinationActivityConvert.INSTANCE.convert(activity, products)); } - @GetMapping("/detail-list") - @Operation(summary = "获得拼团活动详情列表") - @Parameter(name = "ids", description = "拼团活动编号列表", required = true, example = "[1,2,3]") - @PreAuthorize("@ss.hasPermission('product:spu:query')") - public CommonResult> getCombinationActivityDetailList(@RequestParam("ids") Collection ids) { - // 查询拼团活动列表 - List activities = combinationActivityService.getCombinationActivityListByIds(ids); - - // 转换活动列表 - List activityVOs = CombinationActivityConvert.INSTANCE.convertList(activities); - - // 获取拼团产品列表 - Set activityIds = activities.stream().map(CombinationActivityDO::getId).collect(Collectors.toSet()); - List productList = combinationActivityService.getCombinationProductListByActivityIds(activityIds); - - // 创建SPU和产品的映射 - Map> productMap = convertMultiMap(productList, CombinationProductDO::getActivityId); - - // 往活动VO赋值产品列表 - activityVOs.forEach(vo -> vo.setProducts(CombinationActivityConvert.INSTANCE.convertList2(productMap.get(vo.getId())))); - - return success(activityVOs); + @GetMapping("/list-by-ids") + @Operation(summary = "获得拼团活动列表,基于活动编号数组") + @Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]") + public CommonResult> getCombinationActivityListByIds(@RequestParam("ids") List ids) { + // 1. 获得开启的活动列表 + List activityList = combinationActivityService.getCombinationActivityListByIds(ids); + activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus())); + if (CollUtil.isEmpty(activityList)) { + return success(Collections.emptyList()); + } + // 2. 拼接返回 + List productList = combinationActivityService.getCombinationProductListByActivityIds( + convertList(activityList, CombinationActivityDO::getId)); + List spuList = productSpuApi.getSpuList(convertList(activityList, CombinationActivityDO::getSpuId)); + return success(CombinationActivityConvert.INSTANCE.convertList(activityList, productList, spuList)); } @GetMapping("/page") diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java index 0ac77c559..d65ecfe10 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java @@ -27,4 +27,14 @@ public class CombinationActivityRespVO extends CombinationActivityBaseVO { @Schema(description = "拼团商品", requiredMode = Schema.RequiredMode.REQUIRED) private List products; + @Schema(description = "商品 SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "一个白菜") + private String spuName; // 从 SPU 的 name 读取 + @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096") + private String picUrl; // 从 SPU 的 picUrl 读取 + @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + private Integer marketPrice; // 从 SPU 的 marketPrice 读取 + + @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer combinationPrice; // 从 products 获取最小 price 读取 + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java index f113ab0d2..90a9fd8d7 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.promotion.controller.app.combination; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.collection.ListUtil; import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -15,28 +14,20 @@ import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivity import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationProductDO; import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; 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 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 jakarta.annotation.Resource; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "用户 APP - 拼团活动") @@ -45,45 +36,12 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. @Validated public class AppCombinationActivityController { - /** - * {@link AppCombinationActivityRespVO} 缓存,通过它异步刷新 {@link #getCombinationActivityList0(Integer)} 所要的首页数据 - */ - private final LoadingCache> combinationActivityListCache = buildAsyncReloadingCache(Duration.ofSeconds(10L), - new CacheLoader>() { - - @Override - public List load(Integer count) { - return getCombinationActivityList0(count); - } - - }); - @Resource private CombinationActivityService activityService; @Resource private ProductSpuApi spuApi; - @GetMapping("/list") - @Operation(summary = "获得拼团活动列表", description = "用于小程序首页") - @Parameter(name = "count", description = "需要展示的数量", example = "6") - public CommonResult> getCombinationActivityList( - @RequestParam(name = "count", defaultValue = "6") Integer count) { - return success(combinationActivityListCache.getUnchecked(count)); - } - - private List getCombinationActivityList0(Integer count) { - List activityList = activityService.getCombinationActivityListByCount(count); - if (CollUtil.isEmpty(activityList)) { - return Collections.emptyList(); - } - // 拼接返回 - List productList = activityService.getCombinationProductListByActivityIds( - convertList(activityList, CombinationActivityDO::getId)); - List spuList = spuApi.getSpuList(convertList(activityList, CombinationActivityDO::getSpuId)); - return CombinationActivityConvert.INSTANCE.convertAppList(activityList, productList, spuList); - } - @GetMapping("/page") @Operation(summary = "获得拼团活动分页") public CommonResult> getCombinationActivityPage(PageParam pageParam) { @@ -98,6 +56,23 @@ public class AppCombinationActivityController { return success(CombinationActivityConvert.INSTANCE.convertAppPage(pageResult, productList, spuList)); } + @GetMapping("/list-by-ids") + @Operation(summary = "获得拼团活动列表,基于活动编号数组") + @Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]") + public CommonResult> getCombinationActivityListByIds(@RequestParam("ids") List ids) { + // 1. 获得开启的活动列表 + List activityList = activityService.getCombinationActivityListByIds(ids); + activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus())); + if (CollUtil.isEmpty(activityList)) { + return success(Collections.emptyList()); + } + // 2. 拼接返回 + List productList = activityService.getCombinationProductListByActivityIds( + convertList(activityList, CombinationActivityDO::getId)); + List spuList = spuApi.getSpuList(convertList(activityList, CombinationActivityDO::getSpuId)); + return success(CombinationActivityConvert.INSTANCE.convertAppList(activityList, productList, spuList)); + } + @GetMapping("/get-detail") @Operation(summary = "获得拼团活动明细") @Parameter(name = "id", description = "活动编号", required = true, example = "1024") @@ -114,38 +89,4 @@ public class AppCombinationActivityController { return success(CombinationActivityConvert.INSTANCE.convert3(activity, products)); } - @GetMapping("/detail-list") - @Operation(summary = "获得拼团活动明细") - @Parameter(name = "ids", description = "活动编号列表", required = true, example = "[1024, 1025]") - public CommonResult> getCombinationActivityDetailList(@RequestParam("ids") Collection ids) { - // 1. 获取活动 - List combinationActivityDOList = activityService.getCombinationActivityListByIds(ids); - - // 过滤掉无效的活动 - List validActivities = combinationActivityDOList.stream() - .filter(combinationActivityDO -> combinationActivityDO != null && - !ObjectUtil.equal(combinationActivityDO.getStatus(), CommonStatusEnum.DISABLE.getStatus())) - .toList(); - - // 如果没有有效的活动,返回空列表 - if (validActivities.isEmpty()) { - return success(ListUtil.empty()); - } - - // 2. 构建结果列表 - List detailRespVOList = new ArrayList<>(); - for (CombinationActivityDO activity : validActivities) { - // 获取活动商品 - List products = activityService.getCombinationProductsByActivityId(activity.getId()); - - // 调用转换方法并添加到结果列表 - AppCombinationActivityDetailRespVO detailRespVO = CombinationActivityConvert.INSTANCE.convert3(activity, products); - detailRespVOList.add(detailRespVO); - } - - // 3. 返回转换后的结果 - return success(detailRespVOList); - } - - } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityRespVO.java index 64462a377..8f933fa3e 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityRespVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityRespVO.java @@ -19,15 +19,14 @@ public class AppCombinationActivityRespVO { @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") private Long spuId; + @Schema(description = "商品 SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "一个白菜") + private String spuName; // 从 SPU 的 name 读取 @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096") - // 从 SPU 的 picUrl 读取 - private String picUrl; - + private String picUrl; // 从 SPU 的 picUrl 读取 @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") - // 从 SPU 的 marketPrice 读取 - private Integer marketPrice; + private Integer marketPrice; // 从 SPU 的 marketPrice 读取 @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") - private Integer combinationPrice; + private Integer combinationPrice; // 从 products 获取最小 price 读取 } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java index 8acdac6ee..3ee4a8190 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java @@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; @@ -127,40 +128,42 @@ public interface CombinationActivityConvert { .setSpuName(spu.getName()).setPicUrl(sku.getPicUrl()); } - List convertAppList(List list); - - default List convertAppList(List list, - List productList, - List spuList) { - List activityList = convertAppList(list); + default List convertList(List list, + List productList, + List spuList) { + List activityList = BeanUtils.toBean(list, CombinationActivityRespVO.class); Map spuMap = convertMap(spuList, ProductSpuRespDTO::getId); Map> productMap = convertMultiMap(productList, CombinationProductDO::getActivityId); return CollectionUtils.convertList(activityList, item -> { // 设置 product 信息 item.setCombinationPrice(getMinValue(productMap.get(item.getId()), CombinationProductDO::getCombinationPrice)); // 设置 SPU 信息 - findAndThen(spuMap, item.getSpuId(), spu -> item.setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice())); + findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName()) + .setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice())); return item; }); } - PageResult convertAppPage(PageResult result); + default List convertAppList(List list, + List productList, + List spuList) { + List activityList = BeanUtils.toBean(list, AppCombinationActivityRespVO.class); + Map spuMap = convertMap(spuList, ProductSpuRespDTO::getId); + Map> productMap = convertMultiMap(productList, CombinationProductDO::getActivityId); + return CollectionUtils.convertList(activityList, item -> { + // 设置 product 信息 + item.setCombinationPrice(getMinValue(productMap.get(item.getId()), CombinationProductDO::getCombinationPrice)); + // 设置 SPU 信息 + findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName()) + .setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice())); + return item; + }); + } default PageResult convertAppPage(PageResult result, List productList, List spuList) { - PageResult appPage = convertAppPage(result); - Map spuMap = convertMap(spuList, ProductSpuRespDTO::getId); - Map> productMap = convertMultiMap(productList, CombinationProductDO::getActivityId); - List list = CollectionUtils.convertList(appPage.getList(), item -> { - // 设置 product 信息 - item.setCombinationPrice(getMinValue(productMap.get(item.getId()), CombinationProductDO::getCombinationPrice)); - // 设置 SPU 信息 - findAndThen(spuMap, item.getSpuId(), spu -> item.setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice())); - return item; - }); - appPage.setList(list); - return appPage; + return new PageResult<>(convertAppList(result.getList(), productList, spuList), result.getTotal()); } AppCombinationActivityDetailRespVO convert2(CombinationActivityDO combinationActivity); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityService.java index 8637a9607..6f9b62729 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityService.java @@ -100,14 +100,6 @@ public interface CombinationActivityService { */ List getCombinationActivityListByIds(Collection ids); - /** - * 获取正在进行的活动分页数据 - * - * @param count 需要的数量 - * @return 拼团活动分页 - */ - List getCombinationActivityListByCount(Integer count); - /** * 获取正在进行的活动分页数据 * diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java index 6d51bde6c..f45a2168e 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java @@ -225,11 +225,6 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic return combinationActivityMapper.selectList(CombinationActivityDO::getId, ids); } - @Override - public List getCombinationActivityListByCount(Integer count) { - return combinationActivityMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus(), count); - } - @Override public PageResult getCombinationActivityPage(PageParam pageParam) { return combinationActivityMapper.selectPage(pageParam, CommonStatusEnum.ENABLE.getStatus()); From 12ce87b3056718a28d2f5d9c0e5344bab6dacc8a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 6 Sep 2024 21:43:59 +0800 Subject: [PATCH 120/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E6=A1=86=E6=9E=B6=EF=BC=9A=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=BC=82=E6=AD=A5=E8=AE=B0=E5=BD=95=E6=97=B6?= =?UTF-8?q?=EF=BC=8C=E4=B8=A2=E5=A4=B1=20request=20=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../operatelog/core/service/LogRecordServiceImpl.java | 4 +--- .../crm/dal/mysql/receivable/CrmReceivableMapper.java | 2 +- .../yudao/module/system/api/logger/OperateLogApi.java | 11 +++++++++++ .../module/system/api/logger/OperateLogApiImpl.java | 2 -- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java index e2ed4c314..68cdf65ad 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java @@ -11,7 +11,6 @@ import com.mzt.logapi.service.ILogRecordService; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Async; import java.util.List; @@ -29,7 +28,6 @@ public class LogRecordServiceImpl implements ILogRecordService { private OperateLogApi operateLogApi; @Override - @Async public void record(LogRecord logRecord) { OperateLogCreateReqDTO reqDTO = new OperateLogCreateReqDTO(); try { @@ -42,7 +40,7 @@ public class LogRecordServiceImpl implements ILogRecordService { fillRequestFields(reqDTO); // 2. 异步记录日志 - operateLogApi.createOperateLog(reqDTO); + operateLogApi.createOperateLogAsync(reqDTO); } catch (Throwable ex) { // 由于 @Async 异步调用,这里打印下日志,更容易跟进 log.error("[record][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex); diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java index 0c821c8c2..99bc09f0b 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java +++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java @@ -92,7 +92,7 @@ public interface CrmReceivableMapper extends BaseMapperX { List> result = selectMaps(new QueryWrapper() .select("contract_id, SUM(price) AS total_price") .in("audit_status", CrmAuditStatusEnum.DRAFT.getStatus(), // 草稿 + 审批中 + 审批通过 - CrmAuditStatusEnum.PROCESS, CrmAuditStatusEnum.APPROVE.getStatus()) + CrmAuditStatusEnum.PROCESS.getStatus(), CrmAuditStatusEnum.APPROVE.getStatus()) .groupBy("contract_id") .in("contract_id", contractIds)); // 获得金额 diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApi.java index 2ac5343e2..43ac01d0f 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApi.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApi.java @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogCreateReqDTO; import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogPageReqDTO; import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogRespDTO; import jakarta.validation.Valid; +import org.springframework.scheduling.annotation.Async; /** * 操作日志 API 接口 @@ -20,6 +21,16 @@ public interface OperateLogApi { */ void createOperateLog(@Valid OperateLogCreateReqDTO createReqDTO); + /** + * 【异步】创建操作日志 + * + * @param createReqDTO 请求 + */ + @Async + default void createOperateLogAsync(OperateLogCreateReqDTO createReqDTO) { + createOperateLog(createReqDTO); + } + /** * 获取指定模块的指定数据的操作日志分页 * diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApiImpl.java index 07f70d1cf..7be537e62 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApiImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApiImpl.java @@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO; import cn.iocoder.yudao.module.system.service.logger.OperateLogService; import com.fhs.core.trans.anno.TransMethodResult; import jakarta.annotation.Resource; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -26,7 +25,6 @@ public class OperateLogApiImpl implements OperateLogApi { private OperateLogService operateLogService; @Override - @Async public void createOperateLog(OperateLogCreateReqDTO createReqDTO) { operateLogService.createOperateLog(createReqDTO); } From 2f9d9723b3f5e8143a1d3570657a79bc4376b0f7 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 6 Sep 2024 22:38:42 +0800 Subject: [PATCH 121/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E6=A1=86=E6=9E=B6=EF=BC=9A=E7=AE=80=E5=8C=96?= =?UTF-8?q?=20api=20=E8=AE=BF=E9=97=AE=E6=97=A5=E5=BF=97=E3=80=81=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=97=A5=E5=BF=97=E7=9A=84=E8=AE=B0=E5=BD=95=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/YudaoApiLogAutoConfiguration.java | 21 ++---------- .../core/filter/ApiAccessLogFilter.java | 12 +++---- .../service/ApiAccessLogFrameworkService.java | 19 ----------- .../ApiAccessLogFrameworkServiceImpl.java | 33 ------------------- .../service/ApiErrorLogFrameworkService.java | 19 ----------- .../ApiErrorLogFrameworkServiceImpl.java | 33 ------------------- .../web/config/YudaoWebAutoConfiguration.java | 12 +++---- .../core/handler/GlobalExceptionHandler.java | 13 +++----- .../infra/api/logger/ApiAccessLogApi.java | 11 +++++++ .../infra/api/logger/ApiErrorLogApi.java | 11 +++++++ 10 files changed, 40 insertions(+), 144 deletions(-) delete mode 100644 yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java index d1f7453b6..cf76036c8 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java @@ -2,15 +2,10 @@ package cn.iocoder.yudao.framework.apilog.config; import cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter; import cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor; -import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService; -import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl; -import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService; -import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl; import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; import cn.iocoder.yudao.framework.web.config.WebProperties; import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration; import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi; -import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi; import jakarta.servlet.Filter; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -23,18 +18,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @AutoConfiguration(after = YudaoWebAutoConfiguration.class) public class YudaoApiLogAutoConfiguration implements WebMvcConfigurer { - @Bean - @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") - public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) { - return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi); - } - - @Bean - @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") - public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) { - return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi); - } - /** * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 */ @@ -42,8 +25,8 @@ public class YudaoApiLogAutoConfiguration implements WebMvcConfigurer { @ConditionalOnProperty(prefix = "yudao.access-log", value = "enable", matchIfMissing = true) // 允许使用 yudao.access-log.enable=false 禁用访问日志 public FilterRegistrationBean apiAccessLogFilter(WebProperties webProperties, @Value("${spring.application.name}") String applicationName, - ApiAccessLogFrameworkService apiAccessLogFrameworkService) { - ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService); + ApiAccessLogApi apiAccessLogApi) { + ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogApi); return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER); } diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java index 479a5fb9f..d798b7044 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java @@ -9,7 +9,6 @@ import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; -import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService; import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -18,6 +17,7 @@ import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.web.config.WebProperties; import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi; import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Operation; @@ -36,7 +36,7 @@ import java.time.temporal.ChronoUnit; import java.util.Iterator; import java.util.Map; -import static cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor.*; +import static cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD; import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; /** @@ -53,12 +53,12 @@ public class ApiAccessLogFilter extends ApiRequestFilter { private final String applicationName; - private final ApiAccessLogFrameworkService apiAccessLogFrameworkService; + private final ApiAccessLogApi apiAccessLogApi; - public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) { + public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogApi apiAccessLogApi) { super(webProperties); this.applicationName = applicationName; - this.apiAccessLogFrameworkService = apiAccessLogFrameworkService; + this.apiAccessLogApi = apiAccessLogApi; } @Override @@ -91,7 +91,7 @@ public class ApiAccessLogFilter extends ApiRequestFilter { if (!enable) { return; } - apiAccessLogFrameworkService.createApiAccessLog(accessLog); + apiAccessLogApi.createApiAccessLogAsync(accessLog); } catch (Throwable th) { log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th); } diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java deleted file mode 100644 index 2f3c78f60..000000000 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.iocoder.yudao.framework.apilog.core.service; - -import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; - -/** - * API 访问日志 Framework Service 接口 - * - * @author 芋道源码 - */ -public interface ApiAccessLogFrameworkService { - - /** - * 创建 API 访问日志 - * - * @param reqDTO API 访问日志 - */ - void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java deleted file mode 100644 index 934f8141c..000000000 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.yudao.framework.apilog.core.service; - -import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi; -import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Async; - -/** - * API 访问日志 Framework Service 实现类 - * - * 基于 {@link ApiAccessLogApi} 服务,记录访问日志 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService { - - private final ApiAccessLogApi apiAccessLogApi; - - @Override - @Async - public void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO) { - try { - apiAccessLogApi.createApiAccessLog(reqDTO); - } catch (Throwable ex) { - // 由于 @Async 异步调用,这里打印下日志,更容易跟进 - log.error("[createApiAccessLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex); - } - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java deleted file mode 100644 index 33bebb711..000000000 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.iocoder.yudao.framework.apilog.core.service; - -import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; - -/** - * API 错误日志 Framework Service 接口 - * - * @author 芋道源码 - */ -public interface ApiErrorLogFrameworkService { - - /** - * 创建 API 错误日志 - * - * @param reqDTO API 错误日志 - */ - void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java deleted file mode 100644 index e4e19fb32..000000000 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.yudao.framework.apilog.core.service; - -import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi; -import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Async; - -/** - * API 错误日志 Framework Service 实现类 - * - * 基于 {@link ApiErrorLogApi} 服务,记录错误日志 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService { - - private final ApiErrorLogApi apiErrorLogApi; - - @Override - @Async - public void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO) { - try { - apiErrorLogApi.createApiErrorLog(reqDTO); - } catch (Throwable ex) { - // 由于 @Async 异步调用,这里打印下日志,更容易跟进 - log.error("[createApiErrorLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex); - } - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java index 1bdda5723..e3684dfac 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java @@ -1,12 +1,14 @@ package cn.iocoder.yudao.framework.web.config; -import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService; import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter; import cn.iocoder.yudao.framework.web.core.filter.DemoFilter; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi; +import jakarta.annotation.Resource; +import jakarta.servlet.Filter; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -25,9 +27,6 @@ import org.springframework.web.filter.CorsFilter; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import jakarta.annotation.Resource; -import jakarta.servlet.Filter; - @AutoConfiguration @EnableConfigurationProperties(WebProperties.class) public class YudaoWebAutoConfiguration implements WebMvcConfigurer { @@ -59,8 +58,9 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer { } @Bean - public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService apiErrorLogFrameworkService) { - return new GlobalExceptionHandler(applicationName, apiErrorLogFrameworkService); + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogApi apiErrorLogApi) { + return new GlobalExceptionHandler(applicationName, apiErrorLogApi); } @Bean diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index 41646d7ef..6628f116c 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -5,7 +5,6 @@ import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.servlet.JakartaServletUtil; -import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -14,6 +13,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi; import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import jakarta.servlet.http.HttpServletRequest; @@ -40,12 +40,7 @@ import java.time.LocalDateTime; import java.util.Map; import java.util.Set; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.METHOD_NOT_ALLOWED; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_FOUND; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; /** * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 @@ -65,7 +60,7 @@ public class GlobalExceptionHandler { @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") private final String applicationName; - private final ApiErrorLogFrameworkService apiErrorLogFrameworkService; + private final ApiErrorLogApi apiErrorLogApi; /** * 处理所有异常,主要是提供给 Filter 使用 @@ -288,7 +283,7 @@ public class GlobalExceptionHandler { // 初始化 errorLog buildExceptionLog(errorLog, req, e); // 执行插入 errorLog - apiErrorLogFrameworkService.createApiErrorLog(errorLog); + apiErrorLogApi.createApiErrorLogAsync(errorLog); } catch (Throwable th) { log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); } diff --git a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java index 0a28d2563..84f598959 100644 --- a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java +++ b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.api.logger; import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; import jakarta.validation.Valid; +import org.springframework.scheduling.annotation.Async; /** * API 访问日志的 API 接口 @@ -18,4 +19,14 @@ public interface ApiAccessLogApi { */ void createApiAccessLog(@Valid ApiAccessLogCreateReqDTO createDTO); + /** + * 【异步】创建 API 访问日志 + * + * @param createDTO 访问日志 DTO + */ + @Async + default void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) { + createApiAccessLog(createDTO); + } + } diff --git a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java index 3544a8977..23ce3bd0d 100644 --- a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java +++ b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.api.logger; import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; import jakarta.validation.Valid; +import org.springframework.scheduling.annotation.Async; /** * API 错误日志的 API 接口 @@ -18,4 +19,14 @@ public interface ApiErrorLogApi { */ void createApiErrorLog(@Valid ApiErrorLogCreateReqDTO createDTO); + /** + * 【异步】创建 API 异常日志 + * + * @param createDTO 异常日志 DTO + */ + @Async + default void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) { + createApiErrorLog(createDTO); + } + } From 54956fcbb9fb12fd05c2c8d85c8a82206723f30b Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 7 Sep 2024 08:25:51 +0800 Subject: [PATCH 122/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91INFRA=EF=BC=9AVBEN=20=E7=9A=84=20dict=20?= =?UTF-8?q?=E4=B8=8B=E6=8B=89=E7=B1=BB=E5=9E=8B=E4=B8=8D=E7=B2=BE=E5=87=86?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codegen/vue3_vben/views/data.ts.vm | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm index 92d3b2d75..56f4e82ca 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm @@ -42,9 +42,17 @@ export const searchFormSchema: FormSchema[] = [ #foreach($column in $columns) #if ($column.listOperation) #set ($dictType=$column.dictType) + #set ($javaType = $column.javaType) #set ($javaField = $column.javaField) #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set ($comment=$column.columnComment) + #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") + #set ($dictMethod = "number") + #elseif ($javaType == "String") + #set ($dictMethod = "string") + #elseif ($javaType == "Boolean") + #set ($dictMethod = "boolean") + #end { label: '${comment}', field: '${javaField}', @@ -54,16 +62,16 @@ export const searchFormSchema: FormSchema[] = [ component: 'Select', componentProps: { #if ("" != $dictType)## 设置了 dictType 数据字典的情况 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase()), + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), #else## 未设置 dictType 数据字典的情况 options: [], #end }, #elseif ($column.htmlType == "radio") - component: 'Radio', + component: 'RadioButtonGroup', componentProps: { #if ("" != $dictType)## 设置了 dictType 数据字典的情况 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase()), + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), #else## 未设置 dictType 数据字典的情况 options: [], #end @@ -87,9 +95,17 @@ export const createFormSchema: FormSchema[] = [ #foreach($column in $columns) #if ($column.createOperation) #set ($dictType = $column.dictType) + #set ($javaType = $column.javaType) #set ($javaField = $column.javaField) #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set ($comment = $column.columnComment) + #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") + #set ($dictMethod = "number") + #elseif ($javaType == "String") + #set ($dictMethod = "string") + #elseif ($javaType == "Boolean") + #set ($dictMethod = "boolean") + #end #if (!$column.primaryKey)## 忽略主键,不用在表单里 { label: '${comment}', @@ -117,7 +133,7 @@ export const createFormSchema: FormSchema[] = [ component: 'Select', componentProps: { #if ("" != $dictType)## 有数据字典 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), #else##没数据字典 options:[], #end @@ -126,7 +142,7 @@ export const createFormSchema: FormSchema[] = [ component: 'Checkbox', componentProps: { #if ("" != $dictType)## 有数据字典 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), #else##没数据字典 options:[], #end @@ -135,7 +151,7 @@ export const createFormSchema: FormSchema[] = [ component: 'RadioButtonGroup', componentProps: { #if ("" != $dictType)## 有数据字典 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), #else##没数据字典 options:[], #end @@ -166,9 +182,17 @@ export const updateFormSchema: FormSchema[] = [ #foreach($column in $columns) #if ($column.updateOperation) #set ($dictType = $column.dictType) +#set ($javaType = $column.javaType) #set ($javaField = $column.javaField) #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set ($comment = $column.columnComment) +#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") + #set ($dictMethod = "number") +#elseif ($javaType == "String") + #set ($dictMethod = "string") +#elseif ($javaType == "Boolean") + #set ($dictMethod = "boolean") +#end #if (!$column.primaryKey)## 忽略主键,不用在表单里 { label: '${comment}', @@ -196,7 +220,7 @@ export const updateFormSchema: FormSchema[] = [ component: 'Select', componentProps: { #if ("" != $dictType)## 有数据字典 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), #else##没数据字典 options:[], #end @@ -205,7 +229,7 @@ export const updateFormSchema: FormSchema[] = [ component: 'Checkbox', componentProps: { #if ("" != $dictType)## 有数据字典 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), #else##没数据字典 options:[], #end @@ -214,7 +238,7 @@ export const updateFormSchema: FormSchema[] = [ component: 'RadioButtonGroup', componentProps: { #if ("" != $dictType)## 有数据字典 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), #else##没数据字典 options:[], #end From 2759a323312f9ce66d536f558faa437e55dce3d9 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 7 Sep 2024 08:30:29 +0800 Subject: [PATCH 123/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E3=80=91INFRA=EF=BC=9A=E7=A7=BB=E9=99=A4=20Vue3=20+?= =?UTF-8?q?=20Element=20Plus=20schema=20=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enums/codegen/CodegenFrontTypeEnum.java | 1 - .../service/codegen/inner/CodegenEngine.java | 13 -- .../codegen/vue3_schema/api/api.ts.vm | 46 ------- .../codegen/vue3_schema/views/data.ts.vm | 124 ------------------ .../codegen/vue3_schema/views/form.vue.vm | 65 --------- .../codegen/vue3_schema/views/index.vue.vm | 85 ------------ .../src/main/resources/application.yaml | 2 +- 7 files changed, 1 insertion(+), 335 deletions(-) delete mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm delete mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm delete mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm delete mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java index b7d2403dc..101781c48 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java @@ -14,7 +14,6 @@ public enum CodegenFrontTypeEnum { VUE2(10), // Vue2 Element UI 标准模版 VUE3(20), // Vue3 Element Plus 标准模版 - VUE3_SCHEMA(21), // Vue3 Element Plus Schema 模版 VUE3_VBEN(30), // Vue3 VBEN 模版 ; diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java index 4e742539d..63e0c92ac 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java @@ -135,15 +135,6 @@ public class CodegenEngine { vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("api/api.ts"), vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) - // Vue3 Schema 模版 - .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/data.ts"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) - .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/index.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) - .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/form.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue")) - .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("api/api.ts"), - vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) // Vue3 vben 模版 .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"), vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) @@ -496,10 +487,6 @@ public class CodegenEngine { "src/" + path; } - private static String vue3SchemaTemplatePath(String path) { - return "codegen/vue3_schema/" + path + ".vm"; - } - private static String vue3VbenTemplatePath(String path) { return "codegen/vue3_vben/" + path + ".vm"; } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm deleted file mode 100644 index 48cd5422b..000000000 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm +++ /dev/null @@ -1,46 +0,0 @@ -import request from '@/config/axios' -#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") - -export interface ${simpleClassName}VO { - #foreach ($column in $columns) - #if ($column.createOperation || $column.updateOperation) - #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal") - ${column.javaField}: number - #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdatetime") - ${column.javaField}: Date - #else - ${column.javaField}: ${column.javaType.toLowerCase()} - #end - #end - #end -} - -// 查询${table.classComment}列表 -export const get${simpleClassName}Page = async (params) => { - return await request.get({ url: '${baseURL}/page', params }) -} - -// 查询${table.classComment}详情 -export const get${simpleClassName} = async (id: number) => { - return await request.get({ url: '${baseURL}/get?id=' + id }) -} - -// 新增${table.classComment} -export const create${simpleClassName} = async (data: ${simpleClassName}VO) => { - return await request.post({ url: '${baseURL}/create', data }) -} - -// 修改${table.classComment} -export const update${simpleClassName} = async (data: ${simpleClassName}VO) => { - return await request.put({ url: '${baseURL}/update', data }) -} - -// 删除${table.classComment} -export const delete${simpleClassName} = async (id: number) => { - return await request.delete({ url: '${baseURL}/delete?id=' + id }) -} - -// 导出${table.classComment} Excel -export const export${simpleClassName}Api = async (params) => { - return await request.download({ url: '${baseURL}/export-excel', params }) -} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm deleted file mode 100644 index ff4fa810a..000000000 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm +++ /dev/null @@ -1,124 +0,0 @@ -import type { CrudSchema } from '@/hooks/web/useCrudSchemas' -import { dateFormatter } from '@/utils/formatTime' - -// 表单校验 -export const rules = reactive({ -#foreach ($column in $columns) -#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键 -#set($comment=$column.columnComment) - $column.javaField: [required], -#end -#end -}) - -// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/ -const crudSchemas = reactive([ -#foreach($column in $columns) -#if ($column.listOperation || $column.listOperationResult || $column.createOperation || $column.updateOperation) -#set ($dictType = $column.dictType) -#set ($javaField = $column.javaField) -#set ($javaType = $column.javaType) - { - label: '${column.columnComment}', - field: '${column.javaField}', -## ========= 字典部分 ========= - #if ("" != $dictType)## 有数据字典 - dictType: DICT_TYPE.$dictType.toUpperCase(), - #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") - dictClass: 'number', - #elseif ($javaType == "String") - dictClass: 'string', - #elseif ($javaType == "Boolean") - dictClass: 'boolean', - #end - #end -## ========= Table 表格部分 ========= - #if (!$column.listOperationResult) - isTable: false, - #else - #if ($column.htmlType == "datetime") - formatter: dateFormatter, - #end - #end -## ========= Search 表格部分 ========= - #if ($column.listOperation) - isSearch: true, - #if ($column.htmlType == "datetime") - search: { - component: 'DatePicker', - componentProps: { - valueFormat: 'YYYY-MM-DD HH:mm:ss', - type: 'daterange', - defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')] - } - }, - #end - #end -## ========= Form 表单部分 ========= - #if ((!$column.createOperation && !$column.updateOperation) || $column.primaryKey) - isForm: false, - #else - #if($column.htmlType == "imageUpload")## 图片上传 - form: { - component: 'UploadImg' - }, - #elseif($column.htmlType == "fileUpload")## 文件上传 - form: { - component: 'UploadFile' - }, - #elseif($column.htmlType == "editor")## 文本编辑器 - form: { - component: 'Editor', - componentProps: { - valueHtml: '', - height: 200 - } - }, - #elseif($column.htmlType == "select")## 下拉框 - form: { - component: 'SelectV2' - }, - #elseif($column.htmlType == "checkbox")## 多选框 - form: { - component: 'Checkbox' - }, - #elseif($column.htmlType == "radio")## 单选框 - form: { - component: 'Radio' - }, - #elseif($column.htmlType == "datetime")## 时间框 - form: { - component: 'DatePicker', - componentProps: { - type: 'datetime', - valueFormat: 'x' - } - }, - #elseif($column.htmlType == "textarea")## 文本框 - form: { - component: 'Input', - componentProps: { - type: 'textarea', - rows: 4 - }, - colProps: { - span: 24 - } - }, - #elseif(${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer")## 文本框 - form: { - component: 'InputNumber', - value: 0 - }, - #end - #end - }, -#end -#end - { - label: '操作', - field: 'action', - isForm: false - } -]) -export const { allSchemas } = useCrudSchemas(crudSchemas) diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm deleted file mode 100644 index 52f20a2f5..000000000 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm +++ /dev/null @@ -1,65 +0,0 @@ - - diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm deleted file mode 100644 index 6e8f1403a..000000000 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm +++ /dev/null @@ -1,85 +0,0 @@ - - diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 8d63d9593..72ad7b2ed 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -245,7 +245,7 @@ yudao: codegen: base-package: ${yudao.info.base-package} db-schemas: ${spring.datasource.dynamic.datasource.master.name} - front-type: 10 # 前端模版的类型,参见 CodegenFrontTypeEnum 枚举类 + front-type: 20 # 前端模版的类型,参见 CodegenFrontTypeEnum 枚举类 tenant: # 多租户相关配置项 enable: true ignore-urls: From 840cfad84aa45e70b1ec11f6ce7529b5f6e2a247 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 7 Sep 2024 12:05:42 +0800 Subject: [PATCH 124/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E6=97=B6=EF=BC=8C=E8=BF=94=E5=9B=9E=E5=8F=AF?= =?UTF-8?q?=E7=94=A8=20+=20=E4=B8=8D=E5=8F=AF=E7=94=A8=E7=9A=84=E4=BC=98?= =?UTF-8?q?=E6=83=A0=E5=8A=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 6 +- .../promotion/api/coupon/CouponApi.java | 18 ++-- .../api/coupon/dto/CouponValidReqDTO.java | 27 ------ .../promotion/enums/ErrorCodeConstants.java | 2 - .../promotion/api/coupon/CouponApiImpl.java | 15 ++- .../app/coupon/AppCouponController.java | 17 +--- .../coupon/vo/coupon/AppCouponMatchReqVO.java | 30 ------ .../vo/coupon/AppCouponMatchRespVO.java | 16 ---- .../app/coupon/vo/coupon/AppCouponRespVO.java | 2 - .../convert/coupon/CouponConvert.java | 3 - .../dal/mysql/coupon/CouponMapper.java | 20 ---- .../mysql/reward/RewardActivityMapper.java | 8 +- .../service/coupon/CouponService.java | 30 ------ .../service/coupon/CouponServiceImpl.java | 30 +----- .../trade/enums/ErrorCodeConstants.java | 2 +- .../vo/AppTradeOrderSettlementRespVO.java | 45 ++++++++- .../price/bo/TradePriceCalculateRespBO.java | 65 ++++++++++++- .../TradeCouponPriceCalculator.java | 94 +++++++++++++------ .../TradeCouponPriceCalculatorTest.java | 11 ++- .../oauth2/OAuth2TokenServiceImplTest.java | 2 +- yudao-server/pom.xml | 60 ++++++------ 21 files changed, 240 insertions(+), 263 deletions(-) delete mode 100644 yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponValidReqDTO.java delete mode 100755 yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchReqVO.java delete mode 100755 yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchRespVO.java diff --git a/pom.xml b/pom.xml index 4634d345e..86dfebcc3 100644 --- a/pom.xml +++ b/pom.xml @@ -15,12 +15,12 @@ yudao-module-system yudao-module-infra - yudao-module-member + - yudao-module-pay - yudao-module-mall + + diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java index 789a4526d..10d4eb64c 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.promotion.api.coupon; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; -import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO; import jakarta.validation.Valid; import java.util.List; @@ -15,6 +14,15 @@ import java.util.Map; */ public interface CouponApi { + /** + * 获得用户的优惠劵列表 + * + * @param userId 用户编号 + * @param status 优惠劵状态 + * @return 优惠劵列表 + */ + List getCouponListByUserId(Long userId, Integer status); + /** * 使用优惠劵 * @@ -29,14 +37,6 @@ public interface CouponApi { */ void returnUsedCoupon(Long id); - /** - * 校验优惠劵 - * - * @param validReqDTO 校验请求 - * @return 优惠劵 - */ - CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO); - /** * 【管理员】给指定用户批量发送优惠券 * diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponValidReqDTO.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponValidReqDTO.java deleted file mode 100644 index f219b6fdd..000000000 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponValidReqDTO.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.iocoder.yudao.module.promotion.api.coupon.dto; - -import lombok.Data; - -import jakarta.validation.constraints.NotNull; - -/** - * 优惠劵使用 Request DTO - * - * @author 芋道源码 - */ -@Data -public class CouponValidReqDTO { - - /** - * 优惠劵编号 - */ - @NotNull(message = "优惠劵编号不能为空") - private Long id; - - /** - * 用户编号 - */ - @NotNull(message = "用户编号不能为空") - private Long userId; - -} diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java index e1efb9c91..c1af1b874 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java @@ -20,8 +20,6 @@ public interface ErrorCodeConstants { ErrorCode BANNER_NOT_EXISTS = new ErrorCode(1_013_002_000, "Banner 不存在"); // ========== Coupon 相关 1-013-003-000 ============ - ErrorCode COUPON_NO_MATCH_SPU = new ErrorCode(1_013_003_000, "优惠劵没有可使用的商品!"); - ErrorCode COUPON_NO_MATCH_MIN_PRICE = new ErrorCode(1_013_003_001, "所结算的商品中未满足使用的金额"); // ========== 优惠劵模板 1-013-004-000 ========== ErrorCode COUPON_TEMPLATE_NOT_EXISTS = new ErrorCode(1_013_004_000, "优惠劵模板不存在"); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java index edc8f1b7f..167883e0b 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java @@ -1,11 +1,9 @@ package cn.iocoder.yudao.module.promotion.api.coupon; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; -import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO; -import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert; -import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; import cn.iocoder.yudao.module.promotion.service.coupon.CouponService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; @@ -26,6 +24,11 @@ public class CouponApiImpl implements CouponApi { @Resource private CouponService couponService; + @Override + public List getCouponListByUserId(Long userId, Integer status) { + return BeanUtils.toBean(couponService.getCouponList(userId, status), CouponRespDTO.class); + } + @Override public void useCoupon(CouponUseReqDTO useReqDTO) { couponService.useCoupon(useReqDTO.getId(), useReqDTO.getUserId(), @@ -37,12 +40,6 @@ public class CouponApiImpl implements CouponApi { couponService.returnUsedCoupon(id); } - @Override - public CouponRespDTO validateCoupon(CouponValidReqDTO validReqDTO) { - CouponDO coupon = couponService.validCoupon(validReqDTO.getId(), validReqDTO.getUserId()); - return CouponConvert.INSTANCE.convert(coupon); - } - @Override public List takeCouponsByAdmin(Map giveCoupons, Long userId) { return couponService.takeCouponsByAdmin(giveCoupons, userId); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponController.java index ed19d9141..bde2d8f91 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponController.java @@ -5,7 +5,9 @@ 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.security.core.annotations.PreAuthenticated; -import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.*; +import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponPageReqVO; +import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponRespVO; +import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponTakeReqVO; import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO; @@ -15,13 +17,12 @@ import cn.iocoder.yudao.module.promotion.service.coupon.CouponTemplateService; 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.validation.Valid; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; import java.util.Collections; -import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @@ -56,14 +57,6 @@ public class AppCouponController { return success(canTakeAgain); } - @GetMapping("/match-list") - @Operation(summary = "获得匹配指定商品的优惠劵列表", description = "用于下单页,展示优惠劵列表") - public CommonResult> getMatchCouponList(AppCouponMatchReqVO matchReqVO) { - // todo: 优化:优惠金额倒序 - List list = couponService.getMatchCouponList(getLoginUserId(), matchReqVO); - return success(BeanUtils.toBean(list, AppCouponMatchRespVO.class)); - } - @GetMapping("/page") @Operation(summary = "我的优惠劵列表") @PreAuthenticated diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchReqVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchReqVO.java deleted file mode 100755 index 6dc287d98..000000000 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchReqVO.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import java.util.List; - -@Schema(description = "用户 App - 优惠劵的匹配 Request VO") -@Data -public class AppCouponMatchReqVO { - - @Schema(description = "商品金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "商品金额不能为空") - private Integer price; - - @Schema(description = "商品 SPU 编号的数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]") - @NotEmpty(message = "商品 SPU 编号不能为空") - private List spuIds; - - @Schema(description = "商品 SKU 编号的数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]") - @NotEmpty(message = "商品 SKU 编号不能为空") - private List skuIds; - - @Schema(description = "分类编号的数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[10, 20]") - @NotEmpty(message = "分类编号不能为空") - private List categoryIds; - -} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchRespVO.java deleted file mode 100755 index da60390fe..000000000 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchRespVO.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Schema(description = "用户 App - 优惠劵 Response VO") -@Data -public class AppCouponMatchRespVO extends AppCouponRespVO { - - @Schema(description = "是否匹配", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") - private Boolean match; - - @Schema(description = "匹配条件的提示", example = "所结算商品没有符合条件的商品") - private String description; - -} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponRespVO.java index c0949f671..f6084a2c4 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponRespVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponRespVO.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import jakarta.validation.constraints.Min; import java.time.LocalDateTime; import java.util.List; @@ -42,7 +41,6 @@ public class AppCouponRespVO { private Integer discountPercent; @Schema(description = "优惠金额", example = "10") - @Min(value = 0, message = "优惠金额需要大于等于 0") private Integer discountPrice; @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用 diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java index 542a77e84..0ac9c58da 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java @@ -4,9 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO; import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageItemRespVO; import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO; -import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchRespVO; import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponPageReqVO; -import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponRespVO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum; @@ -16,7 +14,6 @@ import org.mapstruct.factory.Mappers; import java.time.LocalDateTime; import java.util.Collection; -import java.util.List; /** * 优惠劵 Convert diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java index e5f1daf6c..ce89b0593 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java @@ -1,13 +1,11 @@ package cn.iocoder.yudao.module.promotion.dal.mysql.coupon; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; 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.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; -import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.github.yulichang.toolkit.MPJWrappers; import org.apache.ibatis.annotations.Mapper; @@ -16,8 +14,6 @@ import java.time.LocalDateTime; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; @@ -84,22 +80,6 @@ public interface CouponMapper extends BaseMapperX { return convertMap(list, map -> MapUtil.getLong(map, templateIdAlias), map -> MapUtil.getInt(map, countAlias)); } - default List selectListByUserIdAndStatusAndUsePriceLeAndProductScope( - Long userId, Integer status, Integer usePrice, List spuIds, List categoryIds) { - Function, String> productScopeValuesFindInSetFunc = ids -> ids.stream() - .map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id)) - .collect(Collectors.joining(" OR ")); - return selectList(new LambdaQueryWrapperX() - .eq(CouponDO::getUserId, userId) - .eq(CouponDO::getStatus, status) - .le(CouponDO::getUsePrice, usePrice) // 价格小于等于,满足价格使用条件 - .and(w -> w.eq(CouponDO::getProductScope, PromotionProductScopeEnum.ALL.getScope()) // 商品范围一:全部 - .or(ww -> ww.eq(CouponDO::getProductScope, PromotionProductScopeEnum.SPU.getScope()) // 商品范围二:满足指定商品 - .apply(productScopeValuesFindInSetFunc.apply(spuIds))) - .or(ww -> ww.eq(CouponDO::getProductScope, PromotionProductScopeEnum.CATEGORY.getScope()) // 商品范围三:满足指定分类 - .apply(productScopeValuesFindInSetFunc.apply(categoryIds))))); - } - default List selectListByStatusAndValidEndTimeLe(Integer status, LocalDateTime validEndTime) { return selectList(new LambdaQueryWrapperX() .eq(CouponDO::getStatus, status) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java index 915696967..cc9010d93 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java @@ -30,15 +30,9 @@ public interface RewardActivityMapper extends BaseMapperX { .orderByDesc(RewardActivityDO::getId)); } - default List selectListByProductScopeAndStatus(Integer productScope, Integer status) { - return selectList(new LambdaQueryWrapperX() - .eq(RewardActivityDO::getProductScope, productScope) - .eq(RewardActivityDO::getStatus, status)); - } - default List selectListBySpuIdsAndStatus(Collection spuIds, Integer status) { Function, String> productScopeValuesFindInSetFunc = ids -> ids.stream() - .map(id -> StrUtil.format("FIND_IN_SET({}, product_spu_ids) ", id)) + .map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id)) .collect(Collectors.joining(" OR ")); return selectList(new QueryWrapper() .eq("status", status) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java index 5fdcd0669..c24cf3ac9 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java @@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO; -import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchReqVO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum; @@ -18,26 +17,6 @@ import java.util.*; */ public interface CouponService { - /** - * 校验优惠劵,包括状态、有限期 - *

- * 1. 如果校验通过,则返回优惠劵信息 - * 2. 如果校验不通过,则直接抛出业务异常 - * - * @param id 优惠劵编号 - * @param userId 用户编号 - * @return 优惠劵信息 - */ - CouponDO validCoupon(Long id, Long userId); - - /** - * 校验优惠劵,包括状态、有限期 - * - * @param coupon 优惠劵 - * @see #validCoupon(Long, Long) 逻辑相同,只是入参不同 - */ - void validCoupon(CouponDO coupon); - /** * 使用优惠劵 * @@ -171,15 +150,6 @@ public interface CouponService { return MapUtil.getInt(map, templateId, 0); } - /** - * 获取用户匹配的优惠券列表 - * - * @param userId 用户编号 - * @param matchReqVO 匹配参数 - * @return 优惠券列表 - */ - List getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO); - /** * 获取用户是否可以领取优惠券 * diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index e6cd4ba0e..cff17f9da 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java @@ -12,7 +12,6 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; import cn.iocoder.yudao.module.member.api.user.MemberUserApi; import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO; -import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchReqVO; import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO; @@ -56,18 +55,9 @@ public class CouponServiceImpl implements CouponService { private MemberUserApi memberUserApi; @Override - public CouponDO validCoupon(Long id, Long userId) { - CouponDO coupon = couponMapper.selectByIdAndUserId(id, userId); - if (coupon == null) { - throw exception(COUPON_NOT_EXISTS); - } - validCoupon(coupon); - return coupon; - } - - @Override - public void validCoupon(CouponDO coupon) { + public void useCoupon(Long id, Long userId, Long orderId) { // 校验状态 + CouponDO coupon = couponMapper.selectByIdAndUserId(id, userId); if (ObjectUtil.notEqual(coupon.getStatus(), CouponStatusEnum.UNUSED.getStatus())) { throw exception(COUPON_STATUS_NOT_UNUSED); } @@ -75,12 +65,6 @@ public class CouponServiceImpl implements CouponService { if (!LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime())) { throw exception(COUPON_VALID_TIME_NOT_NOW); } - } - - @Override - public void useCoupon(Long id, Long userId, Long orderId) { - // 校验优惠劵 - validCoupon(id, userId); // 更新状态 int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(), @@ -354,16 +338,6 @@ public class CouponServiceImpl implements CouponService { return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds); } - @Override - public List getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO) { - List list = couponMapper.selectListByUserIdAndStatusAndUsePriceLeAndProductScope(userId, - CouponStatusEnum.UNUSED.getStatus(), - matchReqVO.getPrice(), matchReqVO.getSpuIds(), matchReqVO.getCategoryIds()); - // 兜底逻辑:如果 CouponExpireJob 未执行,status 未变成 EXPIRE ,但是 validEndTime 已经过期了,需要进行过滤 - list.removeIf(coupon -> !LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime())); - return list; - } - @Override public Map getUserCanCanTakeMap(Long userId, List templates) { // 1. 未登录时,都显示可以领取 diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java index 5613cae8e..c3a42e40e 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java @@ -61,7 +61,7 @@ public interface ErrorCodeConstants { ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵"); ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量"); ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_006, "计算快递运费异常,配送方式不匹配"); - ErrorCode PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:优惠金额超过订单金额"); + ErrorCode PRICE_CALCULATE_COUPON_CAN_NOT_USE = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:{}」"); // ========== 物流 Express 模块 1-011-004-000 ========== ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在"); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java index 9aab1b68b..42f035a10 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java @@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.trade.controller.app.order.vo; import cn.iocoder.yudao.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; import java.util.List; @Schema(description = "用户 App - 交易订单结算信息 Response VO") @@ -19,6 +19,9 @@ public class AppTradeOrderSettlementRespVO { @Schema(description = "购物项数组", requiredMode = Schema.RequiredMode.REQUIRED) private List items; + @Schema(description = "优惠劵数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List coupons; // 可用 + 不可用 + @Schema(description = "费用", requiredMode = Schema.RequiredMode.REQUIRED) private Price price; @@ -109,7 +112,6 @@ public class AppTradeOrderSettlementRespVO { private String mobile; @Schema(description = "地区编号", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull(message = "地区编号不能为空") private Long areaId; @Schema(description = "地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海上海市普陀区") private String areaName; @@ -122,4 +124,43 @@ public class AppTradeOrderSettlementRespVO { } + @Schema(description = "优惠劵信息") + @Data + public static class Coupon { + + @Schema(description = "优惠劵编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送") + private String name; + + @Schema(description = "是否设置满多少金额可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") // 单位:分;0 - 不限制 + private Integer usePrice; + + @Schema(description = "固定日期 - 生效开始时间") + private LocalDateTime validStartTime; + + @Schema(description = "固定日期 - 生效结束时间") + private LocalDateTime validEndTime; + + @Schema(description = "优惠类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer discountType; + + @Schema(description = "折扣百分比", example = "80") // 例如说,80% 为 80 + private Integer discountPercent; + + @Schema(description = "优惠金额", example = "10") + private Integer discountPrice; + + @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用 + private Integer discountLimitPrice; + + @Schema(description = "是否可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean match; + + @Schema(description = "不可用原因", example = "优惠劵已过期") + private String mismatchReason; + + } + } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java index 4f65f33d1..7fed25899 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import lombok.Data; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -45,9 +46,13 @@ public class TradePriceCalculateRespBO { private List promotions; /** - * 优惠劵编号 + * 使用的优惠劵编号 */ private Long couponId; + /** + * 用户的优惠劵列表(可用 + 不可用) + */ + private List coupons; /** * 会员剩余积分 @@ -339,4 +344,62 @@ public class TradePriceCalculateRespBO { } + /** + * 优惠劵信息 + */ + @Data + public static class Coupon { + + /** + * 优惠劵编号 + */ + private Long id; + /** + * 优惠劵名 + */ + private String name; + + /** + * 是否设置满多少金额可用,单位:分 + */ + private Integer usePrice; + + /** + * 生效开始时间 + */ + private LocalDateTime validStartTime; + /** + * 生效结束时间 + */ + private LocalDateTime validEndTime; + + /** + * 优惠类型 + */ + private Integer discountType; + /** + * 折扣百分比 + */ + private Integer discountPercent; + /** + * 优惠金额,单位:分 + */ + private Integer discountPrice; + /** + * 折扣上限,单位:分 + */ + private Integer discountLimitPrice; + + /** + * 是否匹配 + */ + private Boolean match; + /** + * 不匹配的原因 + */ + private String mismatchReason; + + } + + } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java index 1c7294be5..3f3b7f70d 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java @@ -1,31 +1,31 @@ package cn.iocoder.yudao.module.trade.service.price.calculator; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO; -import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO; import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; +import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; +import jakarta.annotation.Resource; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import jakarta.annotation.Resource; import java.util.List; import java.util.function.Predicate; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; -import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE; -import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_CAN_NOT_USE; import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER; -import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH; /** * 优惠劵的 {@link TradePriceCalculator} 实现类 @@ -41,34 +41,37 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator { @Override public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { - // 1.1 校验优惠劵 + // 只有【普通】订单,才允许使用优惠劵 + if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) { + if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) { + throw exception(PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER); + } + return; + } + + // 1.1 加载用户的优惠劵列表 + List coupons = couponApi.getCouponListByUserId(param.getUserId(), CouponStatusEnum.UNUSED.getStatus()); + coupons.removeIf(coupon -> LocalDateTimeUtils.beforeNow(coupon.getValidEndTime())); + // 1.2 计算优惠劵的使用条件 + result.setCoupons(calculateCoupons(coupons, result)); + + // 2. 校验优惠劵是否可用 if (param.getCouponId() == null) { return; } - CouponRespDTO coupon = couponApi.validateCoupon(new CouponValidReqDTO() - .setId(param.getCouponId()).setUserId(param.getUserId())); - Assert.notNull(coupon, "校验通过的优惠劵({}),不能为空", param.getCouponId()); - // 1.2 只有【普通】订单,才允许使用优惠劵 - if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) { - throw exception(PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER); + TradePriceCalculateRespBO.Coupon couponBO = CollUtil.findOne(result.getCoupons(), item -> item.getId().equals(param.getCouponId())); + CouponRespDTO coupon = CollUtil.findOne(coupons, item -> item.getId().equals(param.getCouponId())); + if (couponBO == null || coupon == null) { + throw exception(PRICE_CALCULATE_COUPON_CAN_NOT_USE, "优惠劵不存在"); } - - // 2.1 获得匹配的商品 SKU 数组 - List orderItems = filterMatchCouponOrderItems(result, coupon); - if (CollUtil.isEmpty(orderItems)) { - throw exception(COUPON_NO_MATCH_SPU); - } - // 2.2 计算是否满足优惠劵的使用金额 - Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); - if (totalPayPrice < coupon.getUsePrice()) { - throw exception(COUPON_NO_MATCH_MIN_PRICE); + if (Boolean.FALSE.equals(couponBO.getMatch())) { + throw exception(PRICE_CALCULATE_COUPON_CAN_NOT_USE, couponBO.getMismatchReason()); } // 3.1 计算可以优惠的金额 + List orderItems = filterMatchCouponOrderItems(result, coupon); + Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); Integer couponPrice = getCouponPrice(coupon, totalPayPrice); - if (couponPrice <= totalPayPrice) { - throw exception(PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH); - } // 3.2 计算分摊的优惠金额 List divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice); @@ -76,7 +79,7 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator { result.setCouponId(param.getCouponId()); // 4.2 记录优惠明细 TradePriceCalculatorHelper.addPromotion(result, orderItems, - param.getCouponId(), coupon.getName(), PromotionTypeEnum.COUPON.getType(), + param.getCouponId(), couponBO.getName(), PromotionTypeEnum.COUPON.getType(), StrUtil.format("优惠劵:省 {} 元", TradePriceCalculatorHelper.formatPrice(couponPrice)), divideCouponPrices); // 4.3 更新 SKU 优惠金额 @@ -88,6 +91,43 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator { TradePriceCalculatorHelper.recountAllPrice(result); } + /** + * 计算用户的优惠劵列表(可用 + 不可用) + * + * @param coupons 优惠劵 + * @param result 计算结果 + * @return 优惠劵列表 + */ + private List calculateCoupons(List coupons, + TradePriceCalculateRespBO result) { + return convertList(coupons, coupon -> { + TradePriceCalculateRespBO.Coupon matchCoupon = BeanUtils.toBean(coupon, TradePriceCalculateRespBO.Coupon.class); + // 1.1 优惠劵未到使用时间 + if (LocalDateTimeUtils.afterNow(coupon.getValidStartTime())) { + return matchCoupon.setMatch(false).setMismatchReason("优惠劵未到使用时间"); + } + // 1.2 优惠劵没有匹配的商品 + List orderItems = filterMatchCouponOrderItems(result, coupon); + if (CollUtil.isEmpty(orderItems)) { + return matchCoupon.setMatch(false).setMismatchReason("优惠劵没有匹配的商品"); + } + // 1.3 差 %1$,.2f 元可用优惠劵 + Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); + if (totalPayPrice < coupon.getUsePrice()) { + return matchCoupon.setMatch(false) + .setMismatchReason(String.format("差 %1$,.2f 元可用优惠劵", (coupon.getUsePrice() - totalPayPrice) / 100D)); + } + // 1.4 优惠金额超过订单金额 + Integer couponPrice = getCouponPrice(coupon, totalPayPrice); + if (couponPrice >= totalPayPrice) { + return matchCoupon.setMatch(false).setMismatchReason("优惠金额超过订单金额"); + } + + // 2. 满足条件 + return matchCoupon.setMatch(true); + }); + } + private Integer getCouponPrice(CouponRespDTO coupon, Integer totalPayPrice) { if (PromotionDiscountTypeEnum.PRICE.getType().equals(coupon.getDiscountType())) { // 减价 return coupon.getDiscountPrice(); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculatorTest.java b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculatorTest.java index 06655e0b2..373a4581d 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculatorTest.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculatorTest.java @@ -1,12 +1,13 @@ package cn.iocoder.yudao.module.trade.service.price.calculator; +import cn.hutool.core.collection.ListUtil; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi; import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO; -import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO; import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; +import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; @@ -14,8 +15,10 @@ import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; +import java.time.Duration; import java.util.ArrayList; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -69,8 +72,10 @@ public class TradeCouponPriceCalculatorTest extends BaseMockitoUnitTest { CouponRespDTO coupon = randomPojo(CouponRespDTO.class, o -> o.setId(1024L).setName("程序员节") .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)) .setUsePrice(350).setDiscountType(PromotionDiscountTypeEnum.PERCENT.getType()) - .setDiscountPercent(50).setDiscountLimitPrice(70)); - when(couponApi.validateCoupon(eq(new CouponValidReqDTO().setId(1024L).setUserId(233L)))).thenReturn(coupon); + .setDiscountPercent(50).setDiscountLimitPrice(70)) + .setValidStartTime(addTime(Duration.ofDays(1))).setValidEndTime(addTime(Duration.ofDays(2))); + when(couponApi.getCouponListByUserId(eq(233L), eq(CouponStatusEnum.UNUSED.getStatus()))) + .thenReturn(ListUtil.toList(coupon)); // 调用 tradeCouponPriceCalculator.calculate(param, result); diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java index c548940d6..89c59b7ee 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java @@ -144,7 +144,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { // 调用,并断言 assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId), new ErrorCode(401, "刷新令牌已过期")); - assertEquals(0, oauth2RefreshTokenMapper.selectCount()); + assertEquals(0, oauth2AccessTokenMapper.selectCount()); } @Test diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml index d0c429aab..3b16fa192 100644 --- a/yudao-server/pom.xml +++ b/yudao-server/pom.xml @@ -33,11 +33,11 @@ - - cn.iocoder.boot - yudao-module-member-biz - ${revision} - + + + + + @@ -52,11 +52,11 @@ - - cn.iocoder.boot - yudao-module-pay-biz - ${revision} - + + + + + @@ -66,26 +66,26 @@ - - cn.iocoder.boot - yudao-module-promotion-biz - ${revision} - - - cn.iocoder.boot - yudao-module-product-biz - ${revision} - - - cn.iocoder.boot - yudao-module-trade-biz - ${revision} - - - cn.iocoder.boot - yudao-module-statistics-biz - ${revision} - + + + + + + + + + + + + + + + + + + + + From 75ed4486c47ec2fe0cddba58e9bd01ee690deb99 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 7 Sep 2024 14:07:07 +0800 Subject: [PATCH 125/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E6=8B=BC=E5=9B=A2?= =?UTF-8?q?=E4=B8=AD=E6=97=B6=EF=BC=8C=E7=A6=81=E6=AD=A2=E5=8F=91=E8=B5=B7?= =?UTF-8?q?=E5=94=AE=E5=90=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/combination/CombinationRecordApi.java | 8 +- .../dto/CombinationRecordRespDTO.java | 110 ++++++++++++++++++ .../combination/CombinationRecordApiImpl.java | 13 +-- .../trade/enums/ErrorCodeConstants.java | 1 + .../aftersale/AfterSaleServiceImpl.java | 14 +++ .../order/TradeOrderUpdateServiceImpl.java | 2 +- .../handler/TradeCombinationOrderHandler.java | 6 +- .../TradeCouponPriceCalculator.java | 2 +- 8 files changed, 140 insertions(+), 16 deletions(-) create mode 100644 yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/dto/CombinationRecordRespDTO.java diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java index 942ededec..50dae948f 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.promotion.api.combination; import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO; import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateRespDTO; +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO; import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO; - import jakarta.validation.Valid; /** @@ -33,13 +33,13 @@ public interface CombinationRecordApi { CombinationRecordCreateRespDTO createCombinationRecord(@Valid CombinationRecordCreateReqDTO reqDTO); /** - * 查询拼团记录是否成功 + * 基于订单编号,查询拼团记录 * * @param userId 用户编号 * @param orderId 订单编号 - * @return 拼团是否成功 + * @return 拼团记录 */ - boolean isCombinationRecordSuccess(Long userId, Long orderId); + CombinationRecordRespDTO getCombinationRecordByOrderId(Long userId, Long orderId); /** * 【下单前】校验是否满足拼团活动条件 diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/dto/CombinationRecordRespDTO.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/dto/CombinationRecordRespDTO.java new file mode 100644 index 000000000..82fe21257 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/dto/CombinationRecordRespDTO.java @@ -0,0 +1,110 @@ +package cn.iocoder.yudao.module.promotion.api.combination.dto; + +import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 拼团记录 Response DTO + * + * @author 芋道源码 + */ +@Data +public class CombinationRecordRespDTO { + + /** + * 编号,主键自增 + */ + private Long id; + + /** + * 拼团活动编号 + * + * 关联 CombinationActivityDO 的 id 字段 + */ + private Long activityId; + /** + * 拼团商品单价 + * + * 冗余 CombinationProductDO 的 combinationPrice 字段 + */ + private Integer combinationPrice; + /** + * SPU 编号 + */ + private Long spuId; + /** + * 商品名字 + */ + private String spuName; + /** + * 商品图片 + */ + private String picUrl; + /** + * SKU 编号 + */ + private Long skuId; + /** + * 购买的商品数量 + */ + private Integer count; + + /** + * 用户编号 + */ + private Long userId; + + /** + * 用户昵称 + */ + private String nickname; + /** + * 用户头像 + */ + private String avatar; + + /** + * 团长编号 + */ + private Long headId; + /** + * 开团状态 + * + * 关联 {@link CombinationRecordStatusEnum} + */ + private Integer status; + /** + * 订单编号 + */ + private Long orderId; + /** + * 开团需要人数 + * + * 关联 CombinationActivityDO 的 userSize 字段 + */ + private Integer userSize; + /** + * 已加入拼团人数 + */ + private Integer userCount; + /** + * 是否虚拟成团 + */ + private Boolean virtualGroup; + + /** + * 过期时间 + */ + private LocalDateTime expireTime; + /** + * 开始时间 (订单付款后开始的时间) + */ + private LocalDateTime startTime; + /** + * 结束时间(成团时间/失败时间) + */ + private LocalDateTime endTime; + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java index 354f5b359..32f9ea426 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java @@ -1,19 +1,17 @@ package cn.iocoder.yudao.module.promotion.api.combination; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO; import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateRespDTO; +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO; import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO; 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 jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COMBINATION_RECORD_NOT_EXISTS; - /** * 拼团活动 API 实现类 * @@ -37,12 +35,9 @@ public class CombinationRecordApiImpl implements CombinationRecordApi { } @Override - public boolean isCombinationRecordSuccess(Long userId, Long orderId) { + public CombinationRecordRespDTO getCombinationRecordByOrderId(Long userId, Long orderId) { CombinationRecordDO record = combinationRecordService.getCombinationRecord(userId, orderId); - if (record == null) { - throw exception(COMBINATION_RECORD_NOT_EXISTS); - } - return CombinationRecordStatusEnum.isSuccess(record.getStatus()); + return BeanUtils.toBean(record, CombinationRecordRespDTO.class); } @Override diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java index c3a42e40e..2ab726ec4 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java @@ -51,6 +51,7 @@ public interface ErrorCodeConstants { ErrorCode AFTER_SALE_REFUND_FAIL_STATUS_NOT_WAIT_REFUND = new ErrorCode(1_011_000_110, "退款失败,售后单状态不是【待退款】"); ErrorCode AFTER_SALE_CANCEL_FAIL_STATUS_NOT_APPLY_OR_AGREE_OR_BUYER_DELIVERY = new ErrorCode(1_011_000_111, "取消售后单失败,售后单状态不是【待审核】或【卖家同意】或【商家待收货】"); + ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_COMBINATION_IN_PROGRESS = new ErrorCode(1_011_000_112, "订单拼团中,无法申请售后"); // ========== Cart 模块 1-011-002-000 ========== ErrorCode CARD_ITEM_NOT_FOUND = new ErrorCode(1_011_002_000, "购物车项不存在"); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java index df3d2db60..7d57ead11 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java @@ -8,6 +8,9 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi; import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi; +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO; +import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum; import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleDisagreeReqVO; import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePageReqVO; import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleRefuseReqVO; @@ -26,6 +29,7 @@ import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleTypeEnum; import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleWayEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import cn.iocoder.yudao.module.trade.framework.aftersale.core.annotations.AfterSaleLog; import cn.iocoder.yudao.module.trade.framework.aftersale.core.utils.AfterSaleLogUtils; import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties; @@ -71,6 +75,8 @@ public class AfterSaleServiceImpl implements AfterSaleService { @Resource private PayRefundApi payRefundApi; + @Resource + private CombinationRecordApi combinationRecordApi; @Resource private TradeOrderProperties tradeOrderProperties; @@ -148,6 +154,14 @@ public class AfterSaleServiceImpl implements AfterSaleService { && !TradeOrderStatusEnum.haveDelivered(order.getStatus())) { throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_NO_DELIVERED); } + // 如果是拼团订单,则进行中不允许售后 + if (TradeOrderTypeEnum.isCombination(order.getType())) { + CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId( + order.getUserId(), order.getId()); + if (combinationRecord != null && CombinationRecordStatusEnum.isInProgress(combinationRecord.getStatus())) { + throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_COMBINATION_IN_PROGRESS); + } + } return orderItem; } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index ce0c953e1..300da3f9f 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -887,7 +887,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { .setAppKey(tradeOrderProperties.getPayAppKey()).setUserIp(getClientIP()) // 支付应用 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 .setMerchantRefundId(String.valueOf(order.getId())) - .setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice()));// 价格信息 + .setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice())); // 价格信息 } @Override diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationOrderHandler.java index 9216258db..6fdcf24f6 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationOrderHandler.java @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.trade.service.order.handler; import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi; import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateRespDTO; +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO; +import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum; import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO; @@ -84,7 +86,9 @@ public class TradeCombinationOrderHandler implements TradeOrderHandler { return; } // 校验订单拼团是否成功 - if (!combinationRecordApi.isCombinationRecordSuccess(order.getUserId(), order.getId())) { + CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId(order.getUserId(), order.getId()); + Assert.notNull(combinationRecord, "订单({})对应的拼团记录不存在", order.getId()); + if (!CombinationRecordStatusEnum.isSuccess(combinationRecord.getStatus())) { throw exception(ORDER_DELIVERY_FAIL_COMBINATION_RECORD_STATUS_NOT_SUCCESS); } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java index 3f3b7f70d..1292a2f85 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java @@ -43,7 +43,7 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator { public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { // 只有【普通】订单,才允许使用优惠劵 if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) { - if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) { + if (param.getCouponId() != null) { throw exception(PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER); } return; From 74492d65f03c8749fc13cfbdf610e352ad2230ed Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 7 Sep 2024 14:57:36 +0800 Subject: [PATCH 126/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=EF=BC=9Amodel-uri=E3=80=81tokenizer=20?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-server/src/main/resources/application.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index fe04d1ee4..baf68657e 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -154,9 +154,9 @@ spring: embedding: transformer: onnx: - model-uri: http://test.yudao.iocoder.cn/model.onnx + model-uri: https://raw.gitcode.com/yudaocode/yudao-demo/raw/master/yudao-static/ai/model.onnx tokenizer: - uri: http://test.yudao.iocoder.cn/tokenizer.json + uri: https://raw.gitcode.com/yudaocode/yudao-demo/raw/master/yudao-static/ai/tokenizer.json qianfan: # 文心一言 api-key: x0cuLZ7XsaTCU08vuJWO87Lg secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK From 9f271c3d962d9b96caac650240fc845a718076a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Sun, 8 Sep 2024 11:11:22 +0800 Subject: [PATCH 127/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E7=A7=92=E6=9D=80?= =?UTF-8?q?=E8=A3=85=E4=BF=AE=E9=87=8D=E6=9E=84=EF=BC=88PC=E7=AB=AF?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seckill/SeckillActivityController.java | 25 ++++++++++++++++--- .../vo/activity/SeckillActivityRespVO.java | 4 +++ .../seckill/AppSeckillActivityController.java | 6 ++--- .../seckill/SeckillActivityConvert.java | 17 +++++++++++++ .../seckill/SeckillActivityService.java | 12 +++++++-- .../seckill/SeckillActivityServiceImpl.java | 9 +++++-- 6 files changed, 63 insertions(+), 10 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/SeckillActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/SeckillActivityController.java index dd64870e4..0c41071e8 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/SeckillActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/SeckillActivityController.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.promotion.controller.admin.seckill; import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; @@ -13,15 +14,17 @@ import cn.iocoder.yudao.module.promotion.service.seckill.SeckillActivityService; 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.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; +import java.util.Collections; import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; @Tag(name = "管理后台 - 秒杀活动") @@ -89,11 +92,27 @@ public class SeckillActivityController { } // 拼接数据 - List products = seckillActivityService.getSeckillProductListByActivityId( + List products = seckillActivityService.getSeckillProductListByActivityIds( convertSet(pageResult.getList(), SeckillActivityDO::getId)); List spuList = productSpuApi.getSpuList( convertSet(pageResult.getList(), SeckillActivityDO::getSpuId)); return success(SeckillActivityConvert.INSTANCE.convertPage(pageResult, products, spuList)); } + @GetMapping("/list-by-ids") + @Operation(summary = "获得秒杀活动列表,基于活动编号数组") + @Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]") + public CommonResult> getCombinationActivityListByIds(@RequestParam("ids") List ids) { + // 1. 获得开启的活动列表 + List activityList = seckillActivityService.getSeckillActivityListByIds(ids); + activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus())); + if (CollUtil.isEmpty(activityList)) { + return success(Collections.emptyList()); + } + // 2. 拼接返回 + List productList = seckillActivityService.getSeckillProductListByActivityIds( + convertList(activityList, SeckillActivityDO::getId)); + List spuList = productSpuApi.getSpuList(convertList(activityList, SeckillActivityDO::getSpuId)); + return success(SeckillActivityConvert.INSTANCE.convertList(activityList, productList, spuList)); + } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java index 742c73ba6..b6a868585 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java @@ -54,4 +54,8 @@ public class SeckillActivityRespVO extends SeckillActivityBaseVO { example = "50") private Integer marketPrice; + @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer seckillPrice; // 从 products 获取最小 price 读取 + + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/AppSeckillActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/AppSeckillActivityController.java index c91de0ee7..62627a203 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/AppSeckillActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/AppSeckillActivityController.java @@ -23,6 +23,7 @@ import com.google.common.cache.LoadingCache; 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 org.springframework.context.annotation.Lazy; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; @@ -30,7 +31,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import jakarta.annotation.Resource; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; @@ -86,7 +86,7 @@ public class AppSeckillActivityController { // 2.1 查询满足当前阶段的活动 List activityList = activityService.getSeckillActivityListByConfigIdAndStatus(config.getId(), CommonStatusEnum.ENABLE.getStatus()); - List productList = activityService.getSeckillProductListByActivityId( + List productList = activityService.getSeckillProductListByActivityIds( convertList(activityList, SeckillActivityDO::getId)); // 2.2 获取 spu 信息 List spuList = spuApi.getSpuList(convertList(activityList, SeckillActivityDO::getSpuId)); @@ -101,7 +101,7 @@ public class AppSeckillActivityController { if (CollUtil.isEmpty(pageResult.getList())) { return success(PageResult.empty(pageResult.getTotal())); } - List productList = activityService.getSeckillProductListByActivityId( + List productList = activityService.getSeckillProductListByActivityIds( convertList(pageResult.getList(), SeckillActivityDO::getId)); // 2. 拼接数据 diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/seckill/SeckillActivityConvert.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/seckill/SeckillActivityConvert.java index 10259cb69..eca4ae9b0 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/seckill/SeckillActivityConvert.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/seckill/SeckillActivityConvert.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.promotion.convert.seckill; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; import cn.iocoder.yudao.module.promotion.api.seckill.dto.SeckillValidateJoinRespDTO; import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityCreateReqVO; @@ -87,6 +88,22 @@ public interface SeckillActivityConvert { return CollectionUtils.convertList(products, item -> convert(activity, item).setActivityStatus(activity.getStatus())); } + default List convertList(List list, + List productList, + List spuList) { + List activityList = BeanUtils.toBean(list, SeckillActivityRespVO.class); + Map spuMap = convertMap(spuList, ProductSpuRespDTO::getId); + Map> productMap = convertMultiMap(productList, SeckillProductDO::getActivityId); + return CollectionUtils.convertList(activityList, item -> { + // 设置 product 信息 + item.setSeckillPrice(getMinValue(productMap.get(item.getId()), SeckillProductDO::getSeckillPrice)); + // 设置 SPU 信息 + findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName()) + .setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice())); + return item; + }); + } + List convertList2(List list); List convertList3(List activityList); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java index a47bbec7c..65d20f87d 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java @@ -8,8 +8,8 @@ import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.Se import cn.iocoder.yudao.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityPageReqVO; import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillProductDO; - import jakarta.validation.Valid; + import java.time.LocalDateTime; import java.util.Collection; import java.util.List; @@ -98,7 +98,7 @@ public interface SeckillActivityService { * @param activityIds 活动编号 * @return 活动商品列表 */ - List getSeckillProductListByActivityId(Collection activityIds); + List getSeckillProductListByActivityIds(Collection activityIds); /** * 通过活动时段编号获取指定 status 的秒杀活动 @@ -139,4 +139,12 @@ public interface SeckillActivityService { */ List getSeckillActivityBySpuIdsAndStatusAndDateTimeLt(Collection spuIds, Integer status, LocalDateTime dateTime); + /** + * 获得拼团活动列表 + * + * @param ids 拼团活动 ids + * @return 拼团活动的列表 + */ + List getSeckillActivityListByIds(Collection ids); + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java index dff4d7c7b..56e5135f7 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java @@ -23,11 +23,11 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillConfigDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillProductDO; import cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity.SeckillActivityMapper; import cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity.SeckillProductMapper; +import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; -import jakarta.annotation.Resource; import java.time.LocalDateTime; import java.util.Collection; import java.util.Collections; @@ -276,7 +276,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService { } @Override - public List getSeckillProductListByActivityId(Collection activityIds) { + public List getSeckillProductListByActivityIds(Collection activityIds) { return seckillProductMapper.selectListByActivityId(activityIds); } @@ -336,4 +336,9 @@ public class SeckillActivityServiceImpl implements SeckillActivityService { convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")), dateTime); } + @Override + public List getSeckillActivityListByIds(Collection ids) { + return seckillActivityMapper.selectList(SeckillActivityDO::getId, ids); + } + } From 19c4a5c9616f8598db2abb9552cce6e738bbee1a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 9 Sep 2024 09:16:56 +0800 Subject: [PATCH 128/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E7=A7=92=E6=9D=80?= =?UTF-8?q?=E8=A3=85=E4=BF=AE=E9=87=8D=E6=9E=84=EF=BC=88PC=E7=AB=AF?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/seckill/SeckillActivityController.java | 1 + .../admin/seckill/vo/activity/SeckillActivityRespVO.java | 1 - .../promotion/service/seckill/SeckillActivityService.java | 2 +- .../price/calculator/TradeRewardActivityPriceCalculator.java | 3 ++- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/SeckillActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/SeckillActivityController.java index 0c41071e8..de90c0977 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/SeckillActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/SeckillActivityController.java @@ -115,4 +115,5 @@ public class SeckillActivityController { List spuList = productSpuApi.getSpuList(convertList(activityList, SeckillActivityDO::getSpuId)); return success(SeckillActivityConvert.INSTANCE.convertList(activityList, productList, spuList)); } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java index b6a868585..18b2170e3 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java @@ -57,5 +57,4 @@ public class SeckillActivityRespVO extends SeckillActivityBaseVO { @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") private Integer seckillPrice; // 从 products 获取最小 price 读取 - } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java index 65d20f87d..48b2a4264 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java @@ -142,7 +142,7 @@ public interface SeckillActivityService { /** * 获得拼团活动列表 * - * @param ids 拼团活动 ids + * @param ids 拼团活动编号数组 * @return 拼团活动的列表 */ List getSeckillActivityListByIds(Collection ids); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java index 50d424c29..9abb69cd2 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.trade.service.price.calculator; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; @@ -146,7 +147,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator return filterList(result.getItems(), orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getCategoryId())); } - return List.of(); + return ListUtil.of(); } /** From 8283f203dd174986d981b7a700f9ebcd56ae5f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Mon, 9 Sep 2024 13:37:52 +0800 Subject: [PATCH 129/136] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=A1=A5?= =?UTF-8?q?=E5=85=A8=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9Auni-app=E7=AB=AF?= =?UTF-8?q?=E7=A7=92=E6=9D=80=E5=88=97=E8=A1=A8=E7=9A=84=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seckill/AppSeckillActivityController.java | 18 ++++++++++++++++++ .../vo/activity/AppSeckillActivityRespVO.java | 3 +++ .../seckill/SeckillActivityConvert.java | 16 ++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/AppSeckillActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/AppSeckillActivityController.java index 62627a203..6105f9516 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/AppSeckillActivityController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/AppSeckillActivityController.java @@ -35,6 +35,7 @@ import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.Collections; import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @@ -149,4 +150,21 @@ public class AppSeckillActivityController { return success(SeckillActivityConvert.INSTANCE.convert3(activity, productList, startTime, endTime)); } + @GetMapping("/list-by-ids") + @Operation(summary = "获得拼团活动列表,基于活动编号数组") + @Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]") + public CommonResult> getCombinationActivityListByIds(@RequestParam("ids") List ids) { + // 1. 获得开启的活动列表 + List activityList = activityService.getSeckillActivityListByIds(ids); + activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus())); + if (CollUtil.isEmpty(activityList)) { + return success(Collections.emptyList()); + } + // 2. 拼接返回 + List productList = activityService.getSeckillProductListByActivityIds( + convertList(activityList, SeckillActivityDO::getId)); + List spuList = spuApi.getSpuList(convertList(activityList, SeckillActivityDO::getSpuId)); + return success(SeckillActivityConvert.INSTANCE.convertAppList(activityList, productList, spuList)); + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityRespVO.java index 68e7ff829..907a3ce08 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityRespVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityRespVO.java @@ -16,6 +16,9 @@ public class AppSeckillActivityRespVO { @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") private Long spuId; + @Schema(description = "商品 SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "一个白菜") + private String spuName; // 从 SPU 的 name 读取 + @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 picUrl 读取 example = "https://www.iocoder.cn/xx.png") private String picUrl; diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/seckill/SeckillActivityConvert.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/seckill/SeckillActivityConvert.java index eca4ae9b0..5c3277d0e 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/seckill/SeckillActivityConvert.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/seckill/SeckillActivityConvert.java @@ -104,6 +104,22 @@ public interface SeckillActivityConvert { }); } + default List convertAppList(List list, + List productList, + List spuList) { + List activityList = BeanUtils.toBean(list, AppSeckillActivityRespVO.class); + Map spuMap = convertMap(spuList, ProductSpuRespDTO::getId); + Map> productMap = convertMultiMap(productList, SeckillProductDO::getActivityId); + return CollectionUtils.convertList(activityList, item -> { + // 设置 product 信息 + item.setSeckillPrice(getMinValue(productMap.get(item.getId()), SeckillProductDO::getSeckillPrice)); + // 设置 SPU 信息 + findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName()) + .setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice())); + return item; + }); + } + List convertList2(List list); List convertList3(List activityList); From e5dcf0f1cd5e9abd1e8892297b40b2e4460e7825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=AE=87=E5=BA=86?= Date: Wed, 11 Sep 2024 06:37:44 +0000 Subject: [PATCH 130/136] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E5=8D=95=E4=BB=B7=E6=A0=BC=E7=9A=84=E6=A3=80?= =?UTF-8?q?=E8=A7=86=E6=84=8F=E8=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 杨宇庆 --- .../yudao/module/pay/service/order/PayOrderServiceImpl.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java index 31c1f8b55..1111daa26 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java @@ -431,9 +431,7 @@ public class PayOrderServiceImpl implements PayOrderService { return; } - // TODO 芋艿:应该 new 出来更新 - order.setPrice(payPrice); - orderMapper.updateById(order); + orderMapper.updateById(new PayOrderDO().setId(order.getId()).setPrice(payPrice)); } @Override From d8d385e489f3ee87a76fd5ace123fc7bad4a56ed Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 12 Sep 2024 13:39:19 +0800 Subject: [PATCH 131/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=E5=9C=A8?= =?UTF-8?q?=E5=A4=9A=E7=A7=9F=E6=88=B7=E4=B8=8B=EF=BC=8C=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E6=89=A7=E8=A1=8C=E7=9A=84=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/framework/tenant/core/job/TenantJobAspect.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java index ce9eb1631..de409a4a3 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.tenant.core.job; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; @@ -44,7 +45,8 @@ public class TenantJobAspect { // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况 TenantUtils.execute(tenantId, () -> { try { - joinPoint.proceed(); + Object result = joinPoint.proceed(); + results.put(tenantId, StrUtil.toStringOrNull(result)); } catch (Throwable e) { log.error("[execute][租户({}) 执行 Job 发生异常", tenantId, e); results.put(tenantId, ExceptionUtil.getRootCauseMessage(e)); From c71182dda98cdb1993cf9d439cf12ff3f58a485b Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 12 Sep 2024 13:44:13 +0800 Subject: [PATCH 132/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=E7=9A=84?= =?UTF-8?q?=20Bean=20=E4=B8=8D=E5=AD=98=E5=9C=A8=E6=97=B6=EF=BC=8C?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E6=8A=A5=E9=94=99=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/infra/enums/ErrorCodeConstants.java | 2 +- .../module/infra/service/job/JobServiceImpl.java | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java index e9f39a81f..4cce820b7 100644 --- a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java +++ b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java @@ -22,7 +22,7 @@ public interface ErrorCodeConstants { ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1_001_001_003, "定时任务已经处于该状态,无需修改"); ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1_001_001_004, "只有开启状态的任务,才可以修改"); ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1_001_001_005, "CRON 表达式不正确"); - ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在"); + ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在,注意 Bean 默认首字母小写"); ErrorCode JOB_HANDLER_BEAN_TYPE_ERROR = new ErrorCode(1_001_001_007, "定时任务的处理器 Bean 类型不正确,未实现 JobHandler 接口"); // ========== API 错误日志 1-001-002-000 ========== diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java index cfc52d29d..2ebf06619 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java @@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.quartz.SchedulerException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; @@ -91,13 +92,15 @@ public class JobServiceImpl implements JobService { } private void validateJobHandlerExists(String handlerName) { - Object handler = SpringUtil.getBean(handlerName); - if (handler == null) { + try { + Object handler = SpringUtil.getBean(handlerName); + assert handler != null; + if (!(handler instanceof JobHandler)) { + throw exception(JOB_HANDLER_BEAN_TYPE_ERROR); + } + } catch (NoSuchBeanDefinitionException e) { throw exception(JOB_HANDLER_BEAN_NOT_EXISTS); } - if (!(handler instanceof JobHandler)) { - throw exception(JOB_HANDLER_BEAN_TYPE_ERROR); - } } @Override From 01660355ccabee1f50ba376d1dc0cc2ba30c0775 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 12 Sep 2024 13:59:42 +0800 Subject: [PATCH 133/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91websocket=20=E5=85=81=E8=AE=B8=E4=B8=8D?= =?UTF-8?q?=E4=BC=A0=E9=80=92=20token=20=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../websocket/config/YudaoWebSocketAutoConfiguration.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java index 0f08b7cf5..cabceb807 100644 --- a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler; import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener; import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor; +import cn.iocoder.yudao.framework.websocket.core.security.WebSocketAuthorizeRequestsCustomizer; import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer; import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender; import cn.iocoder.yudao.framework.websocket.core.sender.local.LocalWebSocketMessageSender; @@ -76,6 +77,11 @@ public class YudaoWebSocketAutoConfiguration { return new WebSocketSessionManagerImpl(); } + @Bean + public WebSocketAuthorizeRequestsCustomizer webSocketAuthorizeRequestsCustomizer(WebSocketProperties webSocketProperties) { + return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties); + } + // ==================== Sender 相关 ==================== @Configuration From 57a562b8e3dbdb1107d66f0bf381db858eddac04 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 12 Sep 2024 19:39:29 +0800 Subject: [PATCH 134/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E6=94=AF=E4=BB=98=EF=BC=9A=E5=8F=91=E8=B5=B7?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E9=80=80=E6=AC=BE=E8=B0=83=E7=94=A8=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E6=97=B6=EF=BC=8C=E8=AE=BE=E7=BD=AE=E7=9A=84=20outNo?= =?UTF-8?q?=20=E4=B8=8D=E6=AD=A3=E7=A1=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pay/core/client/impl/weixin/AbstractWxPayClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java index 36c305553..298e314d8 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java @@ -266,7 +266,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient Date: Tue, 17 Sep 2024 12:09:12 +0800 Subject: [PATCH 135/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91=E5=95=86=E5=9F=8E=EF=BC=9A=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=8F=91=E8=B4=A7=E5=90=8E=E8=AE=A2=E5=8D=95=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=BF=AB=E9=80=92=E5=85=AC=E5=8F=B8=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E9=94=99=E8=AF=AF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/trade/service/order/TradeOrderUpdateServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index 300da3f9f..9ca167cc3 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -375,7 +375,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { // 3. 记录订单日志 TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.DELIVERED.getStatus(), - MapUtil.builder().put("expressName", express != null ? express.getName() : "") + MapUtil.builder().put("deliveryName", express != null ? express.getName() : "") .put("logisticsNo", express != null ? deliveryReqVO.getLogisticsNo() : "").build()); // 4.1 发送站内信 From a6e5b2880b0548f35f300e47f558da57f440fc74 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 17 Sep 2024 15:28:53 +0800 Subject: [PATCH 136/136] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E5=B7=A5=E4=BD=9C=E6=B5=81=EF=BC=9A=E4=BC=9A?= =?UTF-8?q?=E7=AD=BE=E3=80=81=E6=88=96=E7=AD=BE=E5=88=86=E9=85=8D=E4=BA=BA?= =?UTF-8?q?=E7=9A=84=E6=97=B6=E5=80=99=EF=BC=8C=E5=A6=82=E6=9E=9C=E5=B7=B2?= =?UTF-8?q?=E7=BB=8F=E5=88=86=E9=85=8D=E8=BF=87=EF=BC=8C=E5=88=99=E4=B8=8D?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../behavior/BpmParallelMultiInstanceBehavior.java | 9 +++++++-- .../behavior/BpmSequentialMultiInstanceBehavior.java | 12 ++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java index 64ebb1aac..ec392e496 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java @@ -48,8 +48,13 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); // 第二步,获取任务的所有处理人 - Set assigneeUserIds = taskCandidateInvoker.calculateUsers(execution); - execution.setVariable(super.collectionVariable, assigneeUserIds); + // 由于每次审批(会签、或签等情况)后都会执行一次,所以 variable 已经有结果,不重复计算 + @SuppressWarnings("unchecked") + Set assigneeUserIds = (Set) execution.getVariable(super.collectionVariable, Set.class); + if (assigneeUserIds == null) { + assigneeUserIds = taskCandidateInvoker.calculateUsers(execution); + execution.setVariable(super.collectionVariable, assigneeUserIds); + } return assigneeUserIds.size(); } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java index a214e2625..16a54481d 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java @@ -1,14 +1,13 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import lombok.Setter; import org.flowable.bpmn.model.Activity; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior; -import java.util.LinkedHashSet; import java.util.Set; /** @@ -42,8 +41,13 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); // 第二步,获取任务的所有处理人 - Set assigneeUserIds = new LinkedHashSet<>(taskCandidateInvoker.calculateUsers(execution)); // 保证有序!!! - execution.setVariable(super.collectionVariable, assigneeUserIds); + // 由于每次审批(会签、或签等情况)后都会执行一次,所以 variable 已经有结果,不重复计算 + @SuppressWarnings("unchecked") + Set assigneeUserIds = (Set) execution.getVariable(super.collectionVariable, Set.class); + if (assigneeUserIds == null) { + assigneeUserIds = taskCandidateInvoker.calculateUsers(execution); + execution.setVariable(super.collectionVariable, assigneeUserIds); + } return assigneeUserIds.size(); }

Ql{C5jDPsKZ(&1FAJne`d?X^bb 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 z zSTS)o@7G8M<{$wn;rR2vu<;sH1&MaX+-YUirjAUBpJhQ@RQ&Yosn$--0}*^j3exzH z!Fb@e9OL~gmh^?zT=z#TaVm9sKV%|8zhBpOKcnUR^i}vNe~RqOF4B|s0^IREXg+aj zkf7xN8u#*V{71k2iVl*ttUtF_cKWbzIn6CadqR|4UNpcjsQ)bC zw8oJ+ca`b2V9C!5h{1hbJFBwng5*BGb@p4W`aUjK8kAW?)0nl@(<aH#x=`}MUpdF4E$EQ84tJp*vefArjs6iI}Be~vjt{;O_3*28mN6C+H$~(Uqt^44#LpqpMu>`sKs#rurPH)Mn*Qw zHhS+^!&iKku-QC4zBqF>2>WM+Vp!M%0D6tucO?U*9G-_s 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 S&vVgA3mzVJ7i;d^|firQX1! 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