diff --git a/README.md b/README.md
index 4f4c802e..8f8d1ac8 100755
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
通过简单的配置后,你的项目就具备了和支付宝开放平台的一样的接口提供能力。
-SOP封装了开放平台大部分功能包括:签名验证、统一异常处理、统一返回内容 、业务参数验证(JSR-303)、秘钥管理等,未来还会实现更多功能。
+SOP封装了开放平台大部分功能包括:签名验证、统一异常处理、统一返回内容 、业务参数验证(JSR-303)、秘钥管理、接口回调等。
## 项目特点
diff --git a/changelog.md b/changelog.md
index ab004f29..6af1bc99 100755
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,7 @@
## 日常更新
+- 2025-11-01:添加回调处理。有升级SQL,见:[sop-20251101.sql](./upgrade/sop-20251101.sql)
- 2025-09-12:修复推送文档报找不到@Open注解问题
- 2025-08-29:smart-doc升级到3.1.1
- 2025-08-17:admin后台可关联商户;fastmybatis升级到3.1.7。有升级SQL,见:[sop-20250817.sql](./upgrade/sop-20250817.sql)
diff --git a/pom.xml b/pom.xml
index 3c57eaeb..393d4f70 100755
--- a/pom.xml
+++ b/pom.xml
@@ -24,6 +24,7 @@
sop-gateway
sop-registry
sop-support
+ sop-notify
@@ -127,7 +128,7 @@
com.alibaba
transmittable-thread-local
- 2.14.5
+ 2.14.2
com.google.guava
diff --git a/sop-admin/sop-admin-backend/admin-common/src/main/java/com/gitee/sop/admin/common/enums/NotifyStatusEnum.java b/sop-admin/sop-admin-backend/admin-common/src/main/java/com/gitee/sop/admin/common/enums/NotifyStatusEnum.java
new file mode 100644
index 00000000..2b9af6e8
--- /dev/null
+++ b/sop-admin/sop-admin-backend/admin-common/src/main/java/com/gitee/sop/admin/common/enums/NotifyStatusEnum.java
@@ -0,0 +1,23 @@
+package com.gitee.sop.admin.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author 六如
+ */
+@AllArgsConstructor
+@Getter
+public enum NotifyStatusEnum implements IntEnum {
+
+ // 状态,1-发送成功,2-发送失败,3-重试结束
+ SEND_SUCCESS(1, "发送成功"),
+ SEND_FAIL(2, "发送失败"),
+ RETRY_OVER(3, "重试结束"),
+ END(4, "手动结束");
+
+ private final Integer value;
+
+ private final String description;
+
+}
diff --git a/sop-admin/sop-admin-backend/admin-dao/src/main/java/com/gitee/sop/admin/dao/entity/IsvInfo.java b/sop-admin/sop-admin-backend/admin-dao/src/main/java/com/gitee/sop/admin/dao/entity/IsvInfo.java
index e76d2aa5..9b672bc9 100755
--- a/sop-admin/sop-admin-backend/admin-dao/src/main/java/com/gitee/sop/admin/dao/entity/IsvInfo.java
+++ b/sop-admin/sop-admin-backend/admin-dao/src/main/java/com/gitee/sop/admin/dao/entity/IsvInfo.java
@@ -38,6 +38,11 @@ public class IsvInfo {
*/
private String remark;
+ /**
+ * 回调接口
+ */
+ private String notifyUrl;
+
/**
* 添加时间
*/
diff --git a/sop-admin/sop-admin-backend/admin-dao/src/main/java/com/gitee/sop/admin/dao/entity/NotifyInfo.java b/sop-admin/sop-admin-backend/admin-dao/src/main/java/com/gitee/sop/admin/dao/entity/NotifyInfo.java
new file mode 100644
index 00000000..0bc182c3
--- /dev/null
+++ b/sop-admin/sop-admin-backend/admin-dao/src/main/java/com/gitee/sop/admin/dao/entity/NotifyInfo.java
@@ -0,0 +1,99 @@
+package com.gitee.sop.admin.dao.entity;
+
+import java.time.LocalDateTime;
+
+import com.gitee.fastmybatis.annotation.Pk;
+import com.gitee.fastmybatis.annotation.PkStrategy;
+import com.gitee.fastmybatis.annotation.Table;
+
+import lombok.Data;
+
+
+/**
+ * 表名:notify_info
+ * 备注:回调信息
+ *
+ * @author 六如
+ */
+@Table(name = "notify_info", pk = @Pk(name = "id", strategy = PkStrategy.INCREMENT))
+@Data
+public class NotifyInfo {
+
+ private Long id;
+
+ /**
+ * app_id
+ */
+ private String appId;
+
+ /**
+ * api_name
+ */
+ private String apiName;
+
+ /**
+ * api_version
+ */
+ private String apiVersion;
+
+ /**
+ * 最近一次发送时间
+ */
+ private LocalDateTime lastSendTime;
+
+ /**
+ * 下一次发送时间
+ */
+ private LocalDateTime nextSendTime;
+
+ /**
+ * 已发送次数
+ */
+ private Integer sendCnt;
+
+ /**
+ * 发送内容
+ */
+ private String content;
+
+ /**
+ * 状态,1-发送成功,2-发送失败,3-重试结束
+ */
+ private Integer notifyStatus;
+
+ /**
+ * 失败原因
+ */
+ private String errorMsg;
+
+ /**
+ * 返回结果
+ */
+ private String resultContent;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ /**
+ * 回调url
+ */
+ private String notifyUrl;
+
+ private LocalDateTime addTime;
+
+ private LocalDateTime updateTime;
+
+ /**
+ * 创建人id
+ */
+ private Long addBy;
+
+ /**
+ * 修改人id
+ */
+ private Long updateBy;
+
+
+}
diff --git a/sop-admin/sop-admin-backend/admin-dao/src/main/java/com/gitee/sop/admin/dao/mapper/NotifyInfoMapper.java b/sop-admin/sop-admin-backend/admin-dao/src/main/java/com/gitee/sop/admin/dao/mapper/NotifyInfoMapper.java
new file mode 100644
index 00000000..6ace3dd2
--- /dev/null
+++ b/sop-admin/sop-admin-backend/admin-dao/src/main/java/com/gitee/sop/admin/dao/mapper/NotifyInfoMapper.java
@@ -0,0 +1,13 @@
+package com.gitee.sop.admin.dao.mapper;
+
+import com.gitee.fastmybatis.core.mapper.BaseMapper;
+import com.gitee.sop.admin.dao.entity.NotifyInfo;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author 六如
+ */
+@Mapper
+public interface NotifyInfoMapper extends BaseMapper {
+
+}
diff --git a/sop-admin/sop-admin-backend/admin-service/pom.xml b/sop-admin/sop-admin-backend/admin-service/pom.xml
index 4489abd1..a17e46c3 100755
--- a/sop-admin/sop-admin-backend/admin-service/pom.xml
+++ b/sop-admin/sop-admin-backend/admin-service/pom.xml
@@ -28,6 +28,11 @@
admin-dao
5.0.0-SNAPSHOT
+
+ com.gitee.sop
+ sop-notify-api
+ 5.0.0-SNAPSHOT
+
org.apache.dubbo
dubbo
diff --git a/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/isv/IsvInfoService.java b/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/isv/IsvInfoService.java
index a53588c8..0629f205 100755
--- a/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/isv/IsvInfoService.java
+++ b/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/isv/IsvInfoService.java
@@ -119,10 +119,7 @@ public class IsvInfoService implements ServiceSupport {
@Transactional(rollbackFor = Exception.class)
public int update(IsvInfoUpdateDTO isvInfoUpdateDTO) {
- IsvInfo isvInfo = new IsvInfo();
- isvInfo.setId(isvInfoUpdateDTO.getId());
- isvInfo.setStatus(isvInfoUpdateDTO.getStatus());
- isvInfo.setRemark(isvInfoUpdateDTO.getRemark());
+ IsvInfo isvInfo = CopyUtil.copyBean(isvInfoUpdateDTO, IsvInfo::new);
int cnt = this.update(isvInfo);
sendChangeEvent(isvInfoUpdateDTO.getId());
return cnt;
@@ -185,4 +182,11 @@ public class IsvInfoService implements ServiceSupport {
sendChangeEvent(isvId);
return i;
}
+
+ public Integer setNotifyUrl(Long id, String url) {
+ return this.query()
+ .eq(IsvInfo::getId, id)
+ .set(IsvInfo::getNotifyUrl, url)
+ .update();
+ }
}
diff --git a/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/isv/dto/IsvInfoAddDTO.java b/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/isv/dto/IsvInfoAddDTO.java
index b7935343..2517b77e 100755
--- a/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/isv/dto/IsvInfoAddDTO.java
+++ b/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/isv/dto/IsvInfoAddDTO.java
@@ -24,4 +24,9 @@ public class IsvInfoAddDTO {
@Length(max = 500)
private String remark;
+ /**
+ * 回调接口
+ */
+ private String notifyUrl;
+
}
diff --git a/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/isv/dto/IsvInfoDTO.java b/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/isv/dto/IsvInfoDTO.java
index 2fed0ed3..3cbff588 100755
--- a/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/isv/dto/IsvInfoDTO.java
+++ b/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/isv/dto/IsvInfoDTO.java
@@ -58,4 +58,9 @@ public class IsvInfoDTO {
*/
private String merchantCode;
+ /**
+ * 回调接口
+ */
+ private String notifyUrl;
+
}
diff --git a/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/serve/NotifyInfoService.java b/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/serve/NotifyInfoService.java
new file mode 100644
index 00000000..2ec99829
--- /dev/null
+++ b/sop-admin/sop-admin-backend/admin-service/src/main/java/com/gitee/sop/admin/service/serve/NotifyInfoService.java
@@ -0,0 +1,61 @@
+package com.gitee.sop.admin.service.serve;
+
+import com.gitee.fastmybatis.core.PageInfo;
+import com.gitee.fastmybatis.core.query.LambdaQuery;
+import com.gitee.fastmybatis.core.support.LambdaService;
+import com.gitee.sop.admin.common.enums.NotifyStatusEnum;
+import com.gitee.sop.admin.common.exception.BizException;
+import com.gitee.sop.admin.dao.entity.NotifyInfo;
+import com.gitee.sop.admin.dao.mapper.NotifyInfoMapper;
+import com.gitee.sop.notify.api.NotifyService;
+import com.gitee.sop.notify.api.resp.NotifyResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.springframework.stereotype.Service;
+
+
+/**
+ * @author thc
+ */
+@Slf4j
+@Service
+public class NotifyInfoService implements LambdaService {
+
+ @DubboReference
+ private NotifyService notifyService;
+
+ public PageInfo doPage(LambdaQuery query) {
+ query.orderByDesc(NotifyInfo::getId);
+ PageInfo page = this.page(query);
+
+ // 格式转换
+ return page.convert(isvInfo -> {
+
+ return isvInfo;
+ });
+ }
+
+ public int push(Long id, String url) {
+ if (StringUtils.isNotBlank(url)) {
+ this.query()
+ .eq(NotifyInfo::getId, id)
+ .set(NotifyInfo::getNotifyUrl, url)
+ .update();
+ }
+ NotifyResponse notifyResponse = notifyService.notifyImmediately(id);
+ log.info("重新推送结果, notifyResponse={}", notifyResponse);
+ if (!notifyResponse.getSuccess()) {
+ throw new BizException(notifyResponse.getMsg());
+ }
+ return 1;
+ }
+
+ public int end(Long id) {
+ return this.query()
+ .eq(NotifyInfo::getId, id)
+ .eq(NotifyInfo::getNotifyStatus, NotifyStatusEnum.SEND_FAIL.getValue())
+ .set(NotifyInfo::getNotifyStatus, NotifyStatusEnum.END.getValue())
+ .update();
+ }
+}
diff --git a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/isv/param/IsvInfoAddParam.java b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/isv/param/IsvInfoAddParam.java
index 306a97ea..969ec36c 100755
--- a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/isv/param/IsvInfoAddParam.java
+++ b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/isv/param/IsvInfoAddParam.java
@@ -24,4 +24,5 @@ public class IsvInfoAddParam {
@Length(max = 500)
private String remark;
+ private String notifyUrl;
}
diff --git a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/isv/vo/IsvInfoVO.java b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/isv/vo/IsvInfoVO.java
index eb1b449a..ddb25071 100755
--- a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/isv/vo/IsvInfoVO.java
+++ b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/isv/vo/IsvInfoVO.java
@@ -58,4 +58,9 @@ public class IsvInfoVO {
*/
private String merchantCode;
+ /**
+ * 回调接口
+ */
+ private String notifyUrl;
+
}
diff --git a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/ApiInfoController.java b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/api/ApiInfoController.java
similarity index 93%
rename from sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/ApiInfoController.java
rename to sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/api/ApiInfoController.java
index c256286c..51d4febe 100755
--- a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/ApiInfoController.java
+++ b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/api/ApiInfoController.java
@@ -1,4 +1,4 @@
-package com.gitee.sop.admin.controller.serve;
+package com.gitee.sop.admin.controller.serve.api;
import com.gitee.fastmybatis.core.PageInfo;
import com.gitee.fastmybatis.core.query.Query;
@@ -6,8 +6,8 @@ import com.gitee.sop.admin.common.dto.StatusUpdateDTO;
import com.gitee.sop.admin.common.req.StatusUpdateParam;
import com.gitee.sop.admin.common.resp.Result;
import com.gitee.sop.admin.common.util.CopyUtil;
-import com.gitee.sop.admin.controller.serve.param.ApiInfoPageParam;
-import com.gitee.sop.admin.controller.serve.vo.ApiInfoVO;
+import com.gitee.sop.admin.controller.serve.api.param.ApiInfoPageParam;
+import com.gitee.sop.admin.controller.serve.api.vo.ApiInfoVO;
import com.gitee.sop.admin.dao.entity.ApiInfo;
import com.gitee.sop.admin.service.serve.ApiInfoService;
import org.springframework.beans.factory.annotation.Autowired;
diff --git a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/param/ApiInfoPageParam.java b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/api/param/ApiInfoPageParam.java
similarity index 90%
rename from sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/param/ApiInfoPageParam.java
rename to sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/api/param/ApiInfoPageParam.java
index 793fdd03..813990a9 100755
--- a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/param/ApiInfoPageParam.java
+++ b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/api/param/ApiInfoPageParam.java
@@ -1,4 +1,4 @@
-package com.gitee.sop.admin.controller.serve.param;
+package com.gitee.sop.admin.controller.serve.api.param;
import com.gitee.fastmybatis.core.query.Operator;
import com.gitee.fastmybatis.core.query.annotation.Condition;
diff --git a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/param/ApiInfoParam.java b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/api/param/ApiInfoParam.java
similarity index 84%
rename from sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/param/ApiInfoParam.java
rename to sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/api/param/ApiInfoParam.java
index 14e98ed0..52825f5c 100755
--- a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/param/ApiInfoParam.java
+++ b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/api/param/ApiInfoParam.java
@@ -1,4 +1,4 @@
-package com.gitee.sop.admin.controller.serve.param;
+package com.gitee.sop.admin.controller.serve.api.param;
import com.gitee.fastmybatis.core.query.Operator;
import com.gitee.fastmybatis.core.query.annotation.Condition;
diff --git a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/vo/ApiInfoVO.java b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/api/vo/ApiInfoVO.java
similarity index 96%
rename from sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/vo/ApiInfoVO.java
rename to sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/api/vo/ApiInfoVO.java
index 2a7ce9c9..f265798a 100755
--- a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/vo/ApiInfoVO.java
+++ b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/api/vo/ApiInfoVO.java
@@ -1,4 +1,4 @@
-package com.gitee.sop.admin.controller.serve.vo;
+package com.gitee.sop.admin.controller.serve.api.vo;
import com.gitee.sop.admin.service.jackson.convert.annotation.UserFormat;
import lombok.Data;
diff --git a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/NotifyInfoController.java b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/NotifyInfoController.java
new file mode 100644
index 00000000..c4b0f76d
--- /dev/null
+++ b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/NotifyInfoController.java
@@ -0,0 +1,68 @@
+package com.gitee.sop.admin.controller.serve.notify;
+
+import com.gitee.fastmybatis.core.PageInfo;
+import com.gitee.fastmybatis.core.query.LambdaQuery;
+import com.gitee.sop.admin.common.req.IdParam;
+import com.gitee.sop.admin.common.resp.Result;
+import com.gitee.sop.admin.common.util.CopyUtil;
+import com.gitee.sop.admin.controller.serve.notify.param.NotifyInfoSearchParam;
+import com.gitee.sop.admin.controller.serve.notify.param.NotifyPushParam;
+import com.gitee.sop.admin.controller.serve.notify.vo.NotifyInfoVO;
+import com.gitee.sop.admin.dao.entity.NotifyInfo;
+import com.gitee.sop.admin.service.serve.NotifyInfoService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+
+/**
+ * @author thc
+ */
+@RestController
+@RequestMapping("serve/notify")
+public class NotifyInfoController {
+
+ @Autowired
+ private NotifyInfoService notifyInfoService;
+
+ /**
+ * 分页查询
+ *
+ * @param param 查询参数
+ * @return 返回分页结果
+ */
+ @GetMapping("/page")
+ public Result> page(NotifyInfoSearchParam param) {
+ LambdaQuery query = param.toLambdaQuery(NotifyInfo.class);
+ PageInfo pageInfo = notifyInfoService.doPage(query)
+ .convert(data -> CopyUtil.copyBean(data, NotifyInfoVO::new));
+ return Result.ok(pageInfo);
+ }
+
+ /**
+ * 重新推送
+ *
+ * @param param 表单数据
+ * @return 返回影响行数
+ */
+ @PostMapping("/push")
+ public Result update(@Validated @RequestBody NotifyPushParam param) {
+ return Result.ok(notifyInfoService.push(param.getId(), param.getUrl()));
+ }
+
+ /**
+ * 结束重试
+ *
+ * @param param 表单数据
+ * @return 返回影响行数
+ */
+ @PostMapping("/end")
+ public Result end(@Validated @RequestBody IdParam param) {
+ return Result.ok(notifyInfoService.end(param.getId()));
+ }
+
+}
diff --git a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/param/NotifyInfoAddParam.java b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/param/NotifyInfoAddParam.java
new file mode 100644
index 00000000..3fee3848
--- /dev/null
+++ b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/param/NotifyInfoAddParam.java
@@ -0,0 +1,72 @@
+package com.gitee.sop.admin.controller.serve.notify.param;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+
+/**
+ * 新增表单
+ *
+ * @author thc
+ */
+@Data
+public class NotifyInfoAddParam {
+
+ /**
+ * app_id
+ */
+ private String appId;
+
+ /**
+ * api_name
+ */
+ private String apiName;
+
+ /**
+ * api_version
+ */
+ private String apiVersion;
+
+ /**
+ * 最近一次发送时间
+ */
+ private LocalDateTime lastSendTime;
+
+ /**
+ * 下一次发送时间
+ */
+ private LocalDateTime nextSendTime;
+
+ /**
+ * 最大发送次数
+ */
+ private Integer sendMax;
+
+ /**
+ * 已发送次数
+ */
+ private Integer sendCnt;
+
+ /**
+ * 发送内容
+ */
+ private String content;
+
+ /**
+ * 状态,1-发送成功,2-发送失败,3-重试结束
+ */
+ private Integer notifyStatus;
+
+ /**
+ * 失败原因
+ */
+ private String errorMsg;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+
+}
\ No newline at end of file
diff --git a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/param/NotifyInfoSearchParam.java b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/param/NotifyInfoSearchParam.java
new file mode 100644
index 00000000..70e5b284
--- /dev/null
+++ b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/param/NotifyInfoSearchParam.java
@@ -0,0 +1,106 @@
+package com.gitee.sop.admin.controller.serve.notify.param;
+
+import com.gitee.fastmybatis.core.query.Operator;
+import com.gitee.fastmybatis.core.query.annotation.Condition;
+import com.gitee.fastmybatis.core.query.param.PageParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 查询表单
+ *
+ * @author thc
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class NotifyInfoSearchParam extends PageParam {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * app_id
+ */
+ @Condition(operator = Operator.like)
+ private String appId;
+
+ /**
+ * api_name
+ */
+ @Condition(operator = Operator.like)
+ private String apiName;
+
+ /**
+ * api_version
+ */
+ @Condition(operator = Operator.like)
+ private String apiVersion;
+
+ /**
+ * 最近一次发送时间
+ */
+ @Condition
+ private LocalDateTime lastSendTime;
+
+ /**
+ * 下一次发送时间
+ */
+ @Condition
+ private LocalDateTime nextSendTime;
+
+ /**
+ * 最大发送次数
+ */
+ @Condition
+ private Integer sendMax;
+
+ /**
+ * 已发送次数
+ */
+ @Condition
+ private Integer sendCnt;
+
+ /**
+ * 发送内容
+ */
+ @Condition(operator = Operator.like)
+ private String content;
+
+ /**
+ * 状态,1-发送成功,2-发送失败,3-重试结束
+ */
+ @Condition
+ private Integer notifyStatus;
+
+ /**
+ * 失败原因
+ */
+ @Condition(operator = Operator.like)
+ private String errorMsg;
+
+ /**
+ * 备注
+ */
+ @Condition(operator = Operator.like)
+ private String remark;
+
+ @Condition
+ private LocalDateTime addTime;
+
+ @Condition
+ private LocalDateTime updateTime;
+
+ /**
+ * 创建人id
+ */
+ @Condition
+ private Long addBy;
+
+ /**
+ * 修改人id
+ */
+ @Condition
+ private Long updateBy;
+
+
+}
diff --git a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/param/NotifyInfoUpdateParam.java b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/param/NotifyInfoUpdateParam.java
new file mode 100644
index 00000000..d142b676
--- /dev/null
+++ b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/param/NotifyInfoUpdateParam.java
@@ -0,0 +1,24 @@
+package com.gitee.sop.admin.controller.serve.notify.param;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.validation.constraints.NotNull;
+
+
+/**
+ * 修改表单
+ *
+ * @author thc
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class NotifyInfoUpdateParam extends NotifyInfoAddParam {
+
+ /**
+ * id
+ */
+ @NotNull(message = "id必填")
+ private Long id;
+
+}
\ No newline at end of file
diff --git a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/param/NotifyPushParam.java b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/param/NotifyPushParam.java
new file mode 100644
index 00000000..8bf7a849
--- /dev/null
+++ b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/param/NotifyPushParam.java
@@ -0,0 +1,11 @@
+package com.gitee.sop.admin.controller.serve.notify.param;
+
+import com.gitee.sop.admin.common.req.IdParam;
+import lombok.Data;
+
+@Data
+public class NotifyPushParam extends IdParam {
+
+ private String url;
+
+}
diff --git a/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/vo/NotifyInfoVO.java b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/vo/NotifyInfoVO.java
new file mode 100644
index 00000000..e2184b3f
--- /dev/null
+++ b/sop-admin/sop-admin-backend/admin-web/src/main/java/com/gitee/sop/admin/controller/serve/notify/vo/NotifyInfoVO.java
@@ -0,0 +1,96 @@
+package com.gitee.sop.admin.controller.serve.notify.vo;
+
+import com.gitee.sop.admin.service.jackson.convert.annotation.UserFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+
+/**
+ * 返回结果
+ *
+ * @author thc
+ */
+@Data
+public class NotifyInfoVO {
+
+ private Long id;
+
+ /**
+ * app_id
+ */
+ private String appId;
+
+ /**
+ * api_name
+ */
+ private String apiName;
+
+ /**
+ * api_version
+ */
+ private String apiVersion;
+
+ /**
+ * 最近一次发送时间
+ */
+ private LocalDateTime lastSendTime;
+
+ /**
+ * 下一次发送时间
+ */
+ private LocalDateTime nextSendTime;
+
+ /**
+ * 最大发送次数
+ */
+ private Integer sendMax;
+
+ /**
+ * 已发送次数
+ */
+ private Integer sendCnt;
+
+ /**
+ * 发送内容
+ */
+ private String content;
+
+ /**
+ * 状态,1-发送成功,2-发送失败,3-重试结束
+ */
+ private Integer notifyStatus;
+
+ /**
+ * 失败原因
+ */
+ private String errorMsg;
+
+ /**
+ * 返回结果
+ */
+ private String resultContent;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ private LocalDateTime addTime;
+
+ private LocalDateTime updateTime;
+
+ /**
+ * 创建人id
+ */
+ @UserFormat
+ private Long addBy;
+
+ /**
+ * 修改人id
+ */
+ @UserFormat
+ private Long updateBy;
+
+
+}
diff --git a/sop-admin/sop-admin-frontend/src/api/notifyInfo.ts b/sop-admin/sop-admin-frontend/src/api/notifyInfo.ts
new file mode 100644
index 00000000..f4b9ad9a
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/api/notifyInfo.ts
@@ -0,0 +1,36 @@
+import { createUrl, http } from "@/utils/http";
+import type { PageResult, Result } from "@/model";
+
+// 后端请求接口
+const apiUrl: any = createUrl({
+ page: "/serve/notify/page",
+ push: "/serve/notify/push",
+ end: "/serve/notify/end"
+});
+
+/**
+ * 接口管理
+ */
+export const api: any = {
+ /**
+ * 分页查询
+ * @param params 查询参数
+ */
+ page(params: object): Promise {
+ return http.get(apiUrl.page, { params });
+ },
+ /**
+ * 推送
+ * @param data 表单内容
+ */
+ push(data: object) {
+ return http.post, any>(apiUrl.push, { data });
+ },
+ /**
+ * 关闭
+ * @param data 表单内容
+ */
+ end(data: object) {
+ return http.post, any>(apiUrl.end, { data });
+ }
+};
diff --git a/sop-admin/sop-admin-frontend/src/model/enums/index.ts b/sop-admin/sop-admin-frontend/src/model/enums/index.ts
index c2466c8f..3f30be96 100755
--- a/sop-admin/sop-admin-frontend/src/model/enums/index.ts
+++ b/sop-admin/sop-admin-frontend/src/model/enums/index.ts
@@ -17,3 +17,10 @@ export enum RegSource {
SYS = 1,
CUSTOM = 2
}
+
+export enum NotifyStatusEnum {
+ SUCCESS = 1,
+ FAIL = 2,
+ END = 3,
+ STOP = 4
+}
diff --git a/sop-admin/sop-admin-frontend/src/views/isv/list/index.ts b/sop-admin/sop-admin-frontend/src/views/isv/list/index.ts
index d3dfba48..fd35ede8 100755
--- a/sop-admin/sop-admin-frontend/src/views/isv/list/index.ts
+++ b/sop-admin/sop-admin-frontend/src/views/isv/list/index.ts
@@ -239,7 +239,8 @@ export function useIsvList() {
id: 0,
status: StatusEnum.ENABLE,
keyFormat: KeyFormatEnum.PKCS8,
- remark: ""
+ remark: "",
+ notifyUrl: ""
};
};
const editFormData = ref(editFormDataGen());
@@ -280,6 +281,11 @@ export function useIsvList() {
showWordLimit: true,
autosize: { minRows: 2, maxRows: 4 }
}
+ },
+ {
+ label: "回调接口",
+ prop: "notifyUrl",
+ valueType: "input"
}
];
diff --git a/sop-admin/sop-admin-frontend/src/views/serve/notify/index.ts b/sop-admin/sop-admin-frontend/src/views/serve/notify/index.ts
new file mode 100644
index 00000000..73be7470
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/views/serve/notify/index.ts
@@ -0,0 +1,382 @@
+import { onMounted, ref } from "vue";
+import {
+ type ButtonsCallBackParams,
+ type PageInfo,
+ type PlusColumn,
+ useTable
+} from "plus-pro-components";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { api } from "@/api/notifyInfo";
+import { WarnTriangleFilled } from "@element-plus/icons-vue";
+import { NotifyStatusEnum } from "@/model/enums";
+
+export function useEntNotifyInfo() {
+ const isAdd = ref(false);
+
+ // ========= search form =========
+
+ // 查询表单对象
+ const searchFormData = ref({
+ appId: "",
+ apiName: "",
+ apiVersion: "",
+ notifyStatus: "",
+ pageIndex: 1,
+ pageSize: 10
+ });
+
+ // 查询表单字段定义
+ const searchFormColumns: PlusColumn[] = [
+ {
+ label: "AppId",
+ prop: "appId"
+ },
+ {
+ label: "接口名称",
+ prop: "apiName"
+ },
+ {
+ label: "接口版本",
+ prop: "apiVersion"
+ },
+ {
+ label: "状态",
+ prop: "notifyStatus",
+ width: 80,
+ valueType: "select",
+ options: [
+ {
+ label: "成功",
+ value: NotifyStatusEnum.SUCCESS,
+ color: "green"
+ },
+ {
+ label: "失败",
+ value: NotifyStatusEnum.FAIL,
+ color: "red"
+ },
+ {
+ label: "重试结束",
+ value: NotifyStatusEnum.END,
+ color: "gray"
+ }
+ ]
+ }
+ ];
+
+ // ========= table =========
+
+ // 表格对象
+ const {
+ tableData,
+ total,
+ pageInfo,
+ buttons: actionButtons
+ } = useTable();
+ // 默认每页条数,默认10
+ pageInfo.value.pageSize = 10;
+
+ // 表格字段定义
+ const tableColumns: PlusColumn[] = [
+ {
+ label: "AppId",
+ prop: "appId"
+ },
+ {
+ label: "接口名称",
+ prop: "apiName"
+ },
+ {
+ label: "接口版本",
+ prop: "apiVersion"
+ },
+ {
+ width: 120,
+ label: "最近一次发送时间",
+ prop: "lastSendTime"
+ },
+ {
+ width: 120,
+ label: "下一次发送时间",
+ prop: "nextSendTime"
+ },
+ {
+ label: "已发送次数",
+ prop: "sendCnt"
+ },
+ {
+ label: "发送内容",
+ prop: "content"
+ },
+ {
+ label: "状态",
+ prop: "notifyStatus",
+ width: 100,
+ valueType: "select",
+ options: [
+ {
+ label: "成功",
+ value: NotifyStatusEnum.SUCCESS,
+ color: "green"
+ },
+ {
+ label: "失败",
+ value: NotifyStatusEnum.FAIL,
+ color: "red"
+ },
+ {
+ label: "重试结束",
+ value: NotifyStatusEnum.END,
+ color: "gray"
+ },
+ {
+ label: "重试结束",
+ value: NotifyStatusEnum.STOP,
+ color: "gray"
+ }
+ ]
+ },
+ {
+ label: "失败原因",
+ prop: "errorMsg"
+ },
+ {
+ label: "返回结果",
+ prop: "resultContent",
+ width: 120
+ },
+ {
+ label: "备注",
+ prop: "remark"
+ },
+ {
+ width: 120,
+ label: "添加时间",
+ prop: "addTime"
+ },
+ {
+ width: 120,
+ label: "修改时间",
+ prop: "updateTime"
+ }
+ ];
+ // 表格按钮定义
+ actionButtons.value = [
+ {
+ text: "推送",
+ props: {
+ type: "primary"
+ },
+ onClick(params: ButtonsCallBackParams) {
+ ElMessageBox.prompt("回调URL", "推送", {
+ confirmButtonText: "确定",
+ cancelButtonText: "取消",
+ inputPlaceholder: "【选填】不填使用默认URL"
+ })
+ .then(({ value }) => {
+ api
+ .push({
+ id: params.row.id,
+ url: value
+ })
+ .then(() => {
+ ElMessage.success("操作成功");
+ search();
+ });
+ })
+ .catch(() => {});
+ }
+ },
+ {
+ text: "结束重试",
+ show: (row: any) => row.notifyStatus === 2,
+ props: {
+ type: "danger"
+ },
+ confirm: {
+ // @ts-ignore
+ popconfirmProps: {
+ width: 150,
+ icon: WarnTriangleFilled,
+ iconColor: "red"
+ },
+ message: _ => `确定要结束重试吗?`
+ },
+ onConfirm(params: ButtonsCallBackParams) {
+ api.end(params.row).then(() => {
+ ElMessage.success("操作成功");
+ search();
+ });
+ }
+ }
+ ];
+
+ // ========= dialog form =========
+
+ // 弹窗显示
+ const dlgShow = ref(false);
+ const dlgTitle = ref("");
+ // 表单值
+ const editFormDataGen = () => {
+ return {
+ appId: "",
+ apiName: "",
+ apiVersion: "",
+ lastSendTime: "",
+ nextSendTime: "",
+ sendMax: "",
+ sendCnt: "",
+ content: "",
+ notifyStatus: "",
+ errorMsg: "",
+ remark: ""
+ };
+ };
+ const editFormData = ref(editFormDataGen());
+ const editFormRules = {
+ appId: [{ required: true, message: "请输入app_id" }],
+ apiName: [{ required: true, message: "请输入api_name" }],
+ apiVersion: [{ required: true, message: "请输入api_version" }]
+ };
+
+ // 表单内容
+ const editFormColumns: PlusColumn[] = [
+ {
+ label: "app_id",
+ prop: "appId",
+ valueType: "input"
+ },
+ {
+ label: "api_name",
+ prop: "apiName",
+ valueType: "input"
+ },
+ {
+ label: "api_version",
+ prop: "apiVersion",
+ valueType: "input"
+ },
+ {
+ label: "最近一次发送时间",
+ prop: "lastSendTime",
+ valueType: "input"
+ },
+ {
+ label: "下一次发送时间",
+ prop: "nextSendTime",
+ valueType: "input"
+ },
+ {
+ label: "已发送次数",
+ prop: "sendCnt",
+ valueType: "input"
+ },
+ {
+ label: "发送内容",
+ prop: "content",
+ valueType: "input"
+ },
+ {
+ label: "状态,1-发送成功,2-发送失败,3-重试结束",
+ prop: "notifyStatus",
+ valueType: "input"
+ },
+ {
+ label: "失败原因",
+ prop: "errorMsg",
+ valueType: "input"
+ },
+ {
+ label: "备注",
+ prop: "remark",
+ valueType: "input"
+ }
+ ];
+
+ // ========= event =========
+
+ // 添加按钮事件
+ const handleAdd = () => {
+ isAdd.value = true;
+ editFormData.value = editFormDataGen();
+ dlgTitle.value = "新增";
+ dlgShow.value = true;
+ };
+
+ // 保存按钮事件,校验成功后触发
+ const handleSave = () => {
+ const postData = editFormData.value;
+ const pms = isAdd.value ? api.add(postData) : api.update(postData);
+ pms.then(() => {
+ ElMessage.success("保存成功");
+ dlgShow.value = false;
+ search();
+ });
+ };
+
+ // 点击查询按钮
+ const handleSearch = () => {
+ pageInfo.value.page = 1;
+ search();
+ };
+
+ // 分页事件
+ const handlePaginationChange = (_pageInfo: PageInfo): void => {
+ pageInfo.value = _pageInfo;
+ search();
+ };
+
+ // 查询
+ const search = async () => {
+ try {
+ const { data } = await doSearch();
+ tableData.value = data.list;
+ total.value = data.total;
+ } catch (error) {}
+ };
+ // 请求接口
+ const doSearch = async () => {
+ // 查询参数
+ const data = searchFormData.value;
+ // 添加分页参数
+ data.pageIndex = pageInfo.value.page;
+ data.pageSize = pageInfo.value.pageSize;
+
+ return api.page(data);
+ };
+
+ onMounted(() => {
+ // 页面加载
+ search();
+ });
+
+ function viewContent(row: any) {
+ ElMessageBox.alert(
+ ``,
+ "发送内容",
+ {
+ dangerouslyUseHTMLString: true
+ }
+ );
+ }
+
+ return {
+ actionButtons,
+ dlgShow,
+ dlgTitle,
+ editFormColumns,
+ editFormData,
+ editFormRules,
+ handleAdd,
+ handlePaginationChange,
+ handleSave,
+ handleSearch,
+ pageInfo,
+ searchFormColumns,
+ searchFormData,
+ tableColumns,
+ tableData,
+ total,
+ viewContent
+ };
+}
diff --git a/sop-admin/sop-admin-frontend/src/views/serve/notify/index.vue b/sop-admin/sop-admin-frontend/src/views/serve/notify/index.vue
new file mode 100644
index 00000000..d362368a
--- /dev/null
+++ b/sop-admin/sop-admin-frontend/src/views/serve/notify/index.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+ 查看
+
+
+
+
+
+
diff --git a/sop-example/example-notify/.gitignore b/sop-example/example-notify/.gitignore
new file mode 100755
index 00000000..18ccf36e
--- /dev/null
+++ b/sop-example/example-notify/.gitignore
@@ -0,0 +1,25 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+nbproject/private/
+build/
+nbbuild/
+dist/
+nbdist/
+.nb-gradle/
+/local-config/
diff --git a/sop-example/example-notify/pom.xml b/sop-example/example-notify/pom.xml
new file mode 100755
index 00000000..88075122
--- /dev/null
+++ b/sop-example/example-notify/pom.xml
@@ -0,0 +1,148 @@
+
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.6.15
+
+
+
+ com.gitee.sop
+ example-notify
+ 5.0.0-SNAPSHOT
+ example-notify
+
+
+ 1.8
+
+ 3.2.16
+
+
+
+
+
+ com.gitee.sop
+ sop-notify-api
+ 5.0.0-SNAPSHOT
+
+
+
+
+ com.gitee.sop
+ sop-spring-boot-starter
+ 5.0.0-SNAPSHOT
+
+
+
+ org.apache.dubbo
+ dubbo-nacos-spring-boot-starter
+
+
+
+ org.apache.dubbo
+ dubbo-zookeeper-curator5-spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ com.gitee.sop
+ sdk-java
+ 5.0.0-SNAPSHOT
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.34
+ provided
+
+
+
+
+
+
+ org.apache.dubbo
+ dubbo-bom
+ ${dubbo.version}
+ pom
+ import
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.12.4
+
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ ${java.version}
+ ${java.version}
+ UTF-8
+
+ -parameters
+
+
+
+
+
+ com.ly.smart-doc
+ smart-doc-maven-plugin
+ 3.0.9
+
+
+ ./src/main/resources/smart-doc.json
+
+ ${project.artifactId}
+
+
+
+ com.gitee.sop
+ sop-service-support
+ 5.0.0-SNAPSHOT
+
+
+
+
+
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+
+ maven_central
+ Maven Central
+ https://repo.maven.apache.org/maven2/
+
+
+
+
diff --git a/sop-example/example-notify/push-doc.sh b/sop-example/example-notify/push-doc.sh
new file mode 100644
index 00000000..7068d908
--- /dev/null
+++ b/sop-example/example-notify/push-doc.sh
@@ -0,0 +1 @@
+mvn -Dfile.encoding=UTF-8 -Dcheckstyle.skip=true smart-doc:torna-rpc
diff --git a/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/ExampleNotifyApplication.java b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/ExampleNotifyApplication.java
new file mode 100755
index 00000000..e35044e1
--- /dev/null
+++ b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/ExampleNotifyApplication.java
@@ -0,0 +1,16 @@
+package com.gitee.sop.notifyexample;
+
+import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+@EnableDubbo
+public class ExampleNotifyApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ExampleNotifyApplication.class, args);
+ }
+
+}
+
diff --git a/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/controller/DemoCallbackController.java b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/controller/DemoCallbackController.java
new file mode 100644
index 00000000..7f118584
--- /dev/null
+++ b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/controller/DemoCallbackController.java
@@ -0,0 +1,71 @@
+package com.gitee.sop.notifyexample.controller;
+
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.gitee.sop.sdk.sign.SignUtil;
+import com.gitee.sop.sdk.sign.SopSignException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 处理回调
+ *
+ * @author 六如
+ */
+@RestController
+@Slf4j
+public class DemoCallbackController {
+ // 平台下发的公钥
+ String publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj0CaMfudpfsrzgT7014aIGQPiEHvk5JPMlHH7YI5JYk+yAgePntojJ8/q1nmeHAauJqEYuCZHfqcjxzLM2hVvttrXtiacTMlr/ea9CGJtx4m20ltrsPOIXPXXZUToxXgO7X1FNvgXgeBBPcWLrsmJUgAQbM1KG/bo9QdNp/cFf5tBuo+1fXB9qXlZnSCbvQwrhfDGAF7NmEYkvkoQeys9YkASAl+zeEOXdBkPQjKDd9USyb/tIkrgLmeo0EOp+PytmEOAsMPSeIEdRcwrgg16X9BvMvnPKLTetQxXILG7r6kkkLj1pVA8EGinRDFu0jwp/Wu+wwUvRlpDRvUbyWEOQIDAQAB";
+
+ /**
+ * 模拟客户端处理接口回调
+ * 回调接口:http://127.0.0.1:7074/notify/callback
+ * 返回状态200表示成功收到请求
+ * 返回非200表示处理失败,平台会进行重试,重试机制见:com.gitee.sop.notify.service.NotifyBizService#timeLevel
+ * @param content
+ * @return
+ */
+ @PostMapping("notify/callback")
+ public ResponseEntity callback(@RequestBody String content) {
+ log.info("收到回调通知, content={}", content);
+ JSONObject jsonObject = JSON.parseObject(content);
+ // 签名校验
+ if (!checkSign(jsonObject)) {
+ return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
+ .body("ERR");
+ }
+ log.info("签名验证通过,处理业务逻辑");
+ String method = jsonObject.getString("method");
+ // 判断业务类型,处理不同业务
+ switch (method) {
+ // 处理订单创建回调
+ case "shop.order.create": {
+ JSONObject bizContent = jsonObject.getJSONObject("biz_content");
+ log.info("业务参数,bizContent={}", bizContent);
+ break;
+ }
+ case "shop.order.close": {
+ // 处理订单关闭回调
+ break;
+ }
+ default:{}
+ }
+ // 返回200状态即可
+ return ResponseEntity.ok("success");
+ }
+
+ private boolean checkSign(JSONObject jsonObject) {
+ try {
+ return SignUtil.rsaCheckV2(jsonObject, publicKey, "UTF-8", "RSA2");
+ } catch (SopSignException e) {
+ log.error("签名校验错误, jsonObject={}", jsonObject, e);
+ return false;
+ }
+ }
+}
diff --git a/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/open/OpenNotify.java b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/open/OpenNotify.java
new file mode 100755
index 00000000..0bdc7570
--- /dev/null
+++ b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/open/OpenNotify.java
@@ -0,0 +1,23 @@
+package com.gitee.sop.notifyexample.open;
+
+import com.gitee.sop.notifyexample.open.req.CreateOrderRequest;
+import com.gitee.sop.notifyexample.open.resp.CreateOrderResponse;
+import com.gitee.sop.support.annotation.Open;
+
+/**
+ * 回调接口
+ *
+ * @author 六如
+ */
+public interface OpenNotify {
+
+ /**
+ * 下单-回调
+ *
+ * @apiNote 演示回调
+ */
+ @Open("shop.order.create")
+ CreateOrderResponse createOrder(CreateOrderRequest request);
+
+
+}
diff --git a/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/open/impl/OpenNotifyImpl.java b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/open/impl/OpenNotifyImpl.java
new file mode 100755
index 00000000..59044ac3
--- /dev/null
+++ b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/open/impl/OpenNotifyImpl.java
@@ -0,0 +1,31 @@
+package com.gitee.sop.notifyexample.open.impl;
+
+import com.gitee.sop.notifyexample.open.OpenNotify;
+import com.gitee.sop.notifyexample.open.req.CreateOrderRequest;
+import com.gitee.sop.notifyexample.open.resp.CreateOrderResponse;
+import com.gitee.sop.notifyexample.service.OrderService;
+import com.gitee.sop.support.context.OpenContext;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+
+/**
+ * 开放接口实现
+ *
+ * @author 六如
+ */
+@DubboService(validation = "true")
+public class OpenNotifyImpl implements OpenNotify {
+
+ @Autowired
+ private OrderService orderService;
+
+ @Override
+ public CreateOrderResponse createOrder(CreateOrderRequest request) {
+ OpenContext openContext = OpenContext.current();
+ CreateOrderResponse response = new CreateOrderResponse();
+ String bizNumber = orderService.createOrder(request, openContext);
+ response.setOrderNo(bizNumber);
+ return response;
+ }
+}
diff --git a/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/open/req/CreateOrderRequest.java b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/open/req/CreateOrderRequest.java
new file mode 100755
index 00000000..37788ff6
--- /dev/null
+++ b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/open/req/CreateOrderRequest.java
@@ -0,0 +1,47 @@
+package com.gitee.sop.notifyexample.open.req;
+
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+/**
+ * 创建订单
+ *
+ * @author 六如
+ * https://opendocs.alipay.com/open/29ae8cb6_alipay.trade.wap.pay?pathHash=1ef587fd&ref=api&scene=21
+ */
+@Data
+public class CreateOrderRequest {
+
+ /**
+ * 商户网站唯一订单号
+ *
+ * @mock 70501111111S001111119
+ */
+ @Length(max = 64)
+ @NotBlank(message = "商户网站唯一订单号必填")
+ private String outTradeNo;
+
+ /**
+ * 订单总金额.单位为元,精确到小数点后两位,取值范围:[0.01,100000000]
+ *
+ * @mock 9.00
+ */
+ @NotNull(message = "订单总金额不能为空")
+ private BigDecimal totalAmount;
+
+
+ /**
+ * 订单标题。注意:不可使用特殊字符,如 /,=,& 等。
+ *
+ * @mock 大乐透
+ */
+ @Length(max = 256)
+ @NotBlank(message = "订单标题不能为空")
+ private String subject;
+
+
+}
diff --git a/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/open/resp/CreateOrderResponse.java b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/open/resp/CreateOrderResponse.java
new file mode 100755
index 00000000..35fc33a5
--- /dev/null
+++ b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/open/resp/CreateOrderResponse.java
@@ -0,0 +1,21 @@
+package com.gitee.sop.notifyexample.open.resp;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author 六如
+ */
+@Data
+public class CreateOrderResponse {
+
+ /**
+ * 订单号
+ *
+ * @mock 111
+ */
+ @NotNull
+ private String orderNo;
+
+}
diff --git a/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/service/OrderService.java b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/service/OrderService.java
new file mode 100644
index 00000000..e1570324
--- /dev/null
+++ b/sop-example/example-notify/src/main/java/com/gitee/sop/notifyexample/service/OrderService.java
@@ -0,0 +1,77 @@
+package com.gitee.sop.notifyexample.service;
+
+import com.gitee.sop.notify.api.NotifyService;
+import com.gitee.sop.notify.api.req.NotifyRequest;
+import com.gitee.sop.notify.api.resp.NotifyResponse;
+import com.gitee.sop.notifyexample.open.req.CreateOrderRequest;
+import com.gitee.sop.support.context.OpenContext;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * 订单服务,回调处理
+ *
+ * @author 六如
+ */
+@Service
+@Slf4j
+public class OrderService {
+
+ @DubboReference
+ private NotifyService notifyService;
+
+ /**
+ * 下单成功
+ *
+ * @param request 入参
+ * @param openContext 开放平台上下文
+ * @return 返回订单编号
+ */
+ public String createOrder(CreateOrderRequest request, OpenContext openContext) {
+ // 处理业务,回调客户端
+ // 生成一个业务编号
+ String orderNo = UUID.randomUUID().toString();
+ log.info("生成订单,编号:{}", orderNo);
+ // 模拟业务处理耗时
+ try {
+ TimeUnit.MILLISECONDS.sleep(500);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ // 发送回调
+ this.sendNotifyTask(request, openContext, orderNo);
+ return orderNo;
+ }
+
+ private void sendNotifyTask(CreateOrderRequest request, OpenContext openContext, String orderNo) {
+ // 回调
+ NotifyRequest notifyRequest = new NotifyRequest();
+ notifyRequest.setAppId(openContext.getAppId());
+ notifyRequest.setApiName(openContext.getApiName());
+ notifyRequest.setVersion(openContext.getVersion());
+ notifyRequest.setClientIp(openContext.getClientIp());
+ notifyRequest.setNotifyUrl(openContext.getNotifyUrl());
+ notifyRequest.setCharset(openContext.getCharset());
+ Map bizParams = new HashMap<>();
+ bizParams.put("orderNo", orderNo);
+ bizParams.put("msg", "success");
+ bizParams.put("price", "100");
+ notifyRequest.setBizParams(bizParams);
+ notifyRequest.setRemark("下单回调");
+
+ // 发送回调任务
+ NotifyResponse notifyResponse = notifyService.notify(notifyRequest);
+ log.info("回调返回,notifyResponse={}", notifyResponse);
+ if (notifyResponse.getSuccess()) {
+ log.info("保存notifyId到数据库, notifyId={}", notifyResponse.getNotifyId());
+ }
+ }
+
+}
diff --git a/sop-example/example-notify/src/main/resources/application-dev.properties b/sop-example/example-notify/src/main/resources/application-dev.properties
new file mode 100755
index 00000000..bfead63f
--- /dev/null
+++ b/sop-example/example-notify/src/main/resources/application-dev.properties
@@ -0,0 +1 @@
+dubbo.registry.address=zookeeper://localhost:2181
diff --git a/sop-example/example-notify/src/main/resources/application-test.properties b/sop-example/example-notify/src/main/resources/application-test.properties
new file mode 100755
index 00000000..53e5d0bc
--- /dev/null
+++ b/sop-example/example-notify/src/main/resources/application-test.properties
@@ -0,0 +1,2 @@
+
+dubbo.registry.address=nacos://localhost:8848
diff --git a/sop-example/example-notify/src/main/resources/application.properties b/sop-example/example-notify/src/main/resources/application.properties
new file mode 100755
index 00000000..c350dca2
--- /dev/null
+++ b/sop-example/example-notify/src/main/resources/application.properties
@@ -0,0 +1,10 @@
+spring.profiles.active=dev
+
+server.port=7074
+spring.application.name=example-notify
+
+dubbo.protocol.name=dubbo
+dubbo.protocol.port=-1
+dubbo.application.qos-enable=false
+dubbo.consumer.check=false
+dubbo.registry.address=zookeeper://localhost:2181
diff --git a/sop-example/example-notify/src/main/resources/doc.json b/sop-example/example-notify/src/main/resources/doc.json
new file mode 100755
index 00000000..2e1ba445
--- /dev/null
+++ b/sop-example/example-notify/src/main/resources/doc.json
@@ -0,0 +1,27 @@
+{
+ // 开启推送
+ "enable": true,
+ // 扫描package,多个用;隔开
+ "basePackage": "com.gitee.sop.payment.open",
+ // 推送URL,IP端口对应Torna服务器
+ "url": "http://localhost:7700/api",
+ // 模块token
+ "token": "34ff76952462413982d21219cf099d46",
+ // 推送人
+ "author": "Jim",
+ // 打开调试:true/false
+ "debug": true,
+ // 是否替换文档,true:替换,false:不替换(追加)。默认:true
+ "isReplace": true,
+ // 第三方jar中的class配置
+ "jarClass": {
+ "com.xx.Page": {
+ "records": { "value": "查询数据列表", "example": "" },
+ "total": { "value": "总数", "example": "100" },
+ "size": { "value": "页数", "example": "10" },
+ "current": { "value": "当前页", "example": "1" },
+ "countId": { "hidden": true },
+ "orders": { "hidden": true }
+ }
+ }
+}
diff --git a/sop-example/example-notify/src/main/resources/i18n/isp/bizerror_en.properties b/sop-example/example-notify/src/main/resources/i18n/isp/bizerror_en.properties
new file mode 100755
index 00000000..48d134c8
--- /dev/null
+++ b/sop-example/example-notify/src/main/resources/i18n/isp/bizerror_en.properties
@@ -0,0 +1,10 @@
+# 错误配置
+
+# 系统配置
+isp.error_isv.common-error=The system is busy.
+isp.error_isv.invalid-parameter=Invalid parameter, {0}
+
+# ==== 参数配置 ====
+
+goods.remark.notNull=The goods_remark can not be null
+goods.comment.length=The goods_comment length must >= {0} and <= {1}
\ No newline at end of file
diff --git a/sop-example/example-notify/src/main/resources/i18n/isp/bizerror_zh_CN.properties b/sop-example/example-notify/src/main/resources/i18n/isp/bizerror_zh_CN.properties
new file mode 100755
index 00000000..4ac48428
--- /dev/null
+++ b/sop-example/example-notify/src/main/resources/i18n/isp/bizerror_zh_CN.properties
@@ -0,0 +1,14 @@
+# 错误配置
+
+# 系统繁忙
+isp.error_isv.common-error=\u7cfb\u7edf\u7e41\u5fd9
+# 参数无效
+isp.error_isv.invalid-parameter=\u53c2\u6570\u65e0\u6548, {0}
+
+# ==== 参数配置 ====
+
+# 商品备注不能为空
+goods.remark.notNull=\u5546\u54c1\u5907\u6ce8\u4e0d\u80fd\u4e3a\u7a7a
+# 商品评论长度必须在{0}和{1}之间
+goods.comment.length=\u5546\u54c1\u8bc4\u8bba\u957f\u5ea6\u5fc5\u987b\u5728{0}\u548c{1}\u4e4b\u95f4
+
diff --git a/sop-example/example-notify/src/main/resources/i18n/isp/goods_error_en.properties b/sop-example/example-notify/src/main/resources/i18n/isp/goods_error_en.properties
new file mode 100755
index 00000000..195c7d2b
--- /dev/null
+++ b/sop-example/example-notify/src/main/resources/i18n/isp/goods_error_en.properties
@@ -0,0 +1,2 @@
+isp.goods_error_100=the goods_name can NOT be null
+isp.goods_error_101=the goods_name must bigger than {0}
\ No newline at end of file
diff --git a/sop-example/example-notify/src/main/resources/i18n/isp/goods_error_zh_CN.properties b/sop-example/example-notify/src/main/resources/i18n/isp/goods_error_zh_CN.properties
new file mode 100755
index 00000000..af1a43bc
--- /dev/null
+++ b/sop-example/example-notify/src/main/resources/i18n/isp/goods_error_zh_CN.properties
@@ -0,0 +1,5 @@
+# 商品名字不能为空
+isp.goods_error_100=\u5546\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
+
+# 商品名称太短,不能小于{0}个字
+isp.goods_error_101=\u5546\u54C1\u540D\u79F0\u592A\u77ED\uFF0C\u4E0D\u80FD\u5C0F\u4E8E{0}\u4E2A\u5B57
\ No newline at end of file
diff --git a/sop-example/example-notify/src/main/resources/smart-doc.json b/sop-example/example-notify/src/main/resources/smart-doc.json
new file mode 100644
index 00000000..29e2956a
--- /dev/null
+++ b/sop-example/example-notify/src/main/resources/smart-doc.json
@@ -0,0 +1,13 @@
+{
+ "framework": "sop",
+ "outPath": "target/doc",
+ "projectName": "项目",
+ "packageFilters": "com.gitee.sop.payment.open.*",
+ "openUrl": "http://localhost:7700/api", // torna服务器地址
+ "appToken": "34ff76952462413982d21219cf099d46", // torna应用token
+ "debugEnvName":"本地环境",
+ "debugEnvUrl":"http://127.0.0.1:8081",
+ "tornaDebug": true,
+ "replace": true,
+ "showValidation": false
+}
diff --git a/sop-example/example-notify/src/test/java/com/gitee/sop/notifyexample/DocPushTest.java b/sop-example/example-notify/src/test/java/com/gitee/sop/notifyexample/DocPushTest.java
new file mode 100755
index 00000000..5c9dbc3d
--- /dev/null
+++ b/sop-example/example-notify/src/test/java/com/gitee/sop/notifyexample/DocPushTest.java
@@ -0,0 +1,14 @@
+package com.gitee.sop.notifyexample;
+
+
+import cn.torna.swaggerplugin.SwaggerPlugin;
+
+/**
+ * 推送swagger文档
+ * @author thc
+ */
+public class DocPushTest {
+ public static void main(String[] args) {
+ SwaggerPlugin.pushDoc();
+ }
+}
diff --git a/sop-example/pom.xml b/sop-example/pom.xml
index fd4d2854..d10d430b 100755
--- a/sop-example/pom.xml
+++ b/sop-example/pom.xml
@@ -13,5 +13,6 @@
example-payment
example-product
example-rest
+ example-notify
diff --git a/sop-notify/pom.xml b/sop-notify/pom.xml
new file mode 100644
index 00000000..fd10953d
--- /dev/null
+++ b/sop-notify/pom.xml
@@ -0,0 +1,24 @@
+
+
+ 4.0.0
+
+ com.gitee.sop
+ sop-parent
+ 5.0.0-SNAPSHOT
+
+
+ sop-notify
+ pom
+
+
+ UTF-8
+
+
+
+ sop-notify-api
+ sop-notify-service
+
+
+
diff --git a/sop-notify/sop-notify-api/pom.xml b/sop-notify/sop-notify-api/pom.xml
new file mode 100644
index 00000000..57c81843
--- /dev/null
+++ b/sop-notify/sop-notify-api/pom.xml
@@ -0,0 +1,31 @@
+
+
+ 4.0.0
+ com.gitee.sop
+ sop-notify-api
+ 5.0.0-SNAPSHOT
+
+
+ 8
+ 8
+ UTF-8
+
+
+
+
+ javax.validation
+ validation-api
+ 2.0.1.Final
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.36
+ true
+
+
+
+
diff --git a/sop-notify/sop-notify-api/src/main/java/com/gitee/sop/notify/api/NotifyService.java b/sop-notify/sop-notify-api/src/main/java/com/gitee/sop/notify/api/NotifyService.java
new file mode 100644
index 00000000..13f8cbd3
--- /dev/null
+++ b/sop-notify/sop-notify-api/src/main/java/com/gitee/sop/notify/api/NotifyService.java
@@ -0,0 +1,29 @@
+package com.gitee.sop.notify.api;
+
+import com.gitee.sop.notify.api.req.NotifyRequest;
+import com.gitee.sop.notify.api.resp.NotifyResponse;
+
+/**
+ * 回调接口
+ *
+ * @author 六如
+ */
+public interface NotifyService {
+
+ /**
+ * 回调
+ *
+ * @param request 参数
+ * @return 返回结果
+ */
+ NotifyResponse notify(NotifyRequest request);
+
+ /**
+ * 回调立即发送
+ *
+ * @param notifyId notifyId
+ * @return 返回结果
+ */
+ NotifyResponse notifyImmediately(Long notifyId);
+
+}
diff --git a/sop-notify/sop-notify-api/src/main/java/com/gitee/sop/notify/api/req/NotifyRequest.java b/sop-notify/sop-notify-api/src/main/java/com/gitee/sop/notify/api/req/NotifyRequest.java
new file mode 100644
index 00000000..dcf7c6a4
--- /dev/null
+++ b/sop-notify/sop-notify-api/src/main/java/com/gitee/sop/notify/api/req/NotifyRequest.java
@@ -0,0 +1,66 @@
+package com.gitee.sop.notify.api.req;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * @author 六如
+ */
+@Data
+public class NotifyRequest implements Serializable {
+ private static final long serialVersionUID = -4018307141661725928L;
+
+ /**
+ * appId
+ */
+ @NotBlank(message = "appId必填")
+ private String appId;
+
+ /**
+ * apiName
+ */
+ @NotBlank(message = "apiName必填")
+ private String apiName;
+
+ /**
+ * version
+ */
+ @NotBlank(message = "version必填")
+ private String version;
+
+ /**
+ * 编码
+ */
+ @NotBlank(message = "charset必填")
+ private String charset;
+
+ /**
+ * token,没有返回null
+ */
+ private String appAuthToken;
+
+ /**
+ * 客户端ip
+ */
+ private String clientIp;
+
+ /**
+ * 回调地址
+ */
+ private String notifyUrl;
+
+ /**
+ * 业务参数
+ */
+ @NotNull(message = "bizParams必填")
+ private Map bizParams;
+
+ /**
+ * 备注
+ */
+ private String remark;
+}
diff --git a/sop-notify/sop-notify-api/src/main/java/com/gitee/sop/notify/api/resp/NotifyResponse.java b/sop-notify/sop-notify-api/src/main/java/com/gitee/sop/notify/api/resp/NotifyResponse.java
new file mode 100644
index 00000000..3d7f35ce
--- /dev/null
+++ b/sop-notify/sop-notify-api/src/main/java/com/gitee/sop/notify/api/resp/NotifyResponse.java
@@ -0,0 +1,39 @@
+package com.gitee.sop.notify.api.resp;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+
+/**
+ * @author 六如
+ */
+@Data
+public class NotifyResponse implements Serializable {
+ private static final long serialVersionUID = 5813802354743928430L;
+
+ /**
+ * 返回请求id
+ */
+ private Long notifyId;
+
+ private Boolean success = true;
+
+ private String msg;
+
+ public static NotifyResponse success(Long notifyId) {
+ NotifyResponse notifyResponse = new NotifyResponse();
+ notifyResponse.setNotifyId(notifyId);
+ notifyResponse.setSuccess(true);
+ notifyResponse.setMsg("");
+ return notifyResponse;
+ }
+
+ public static NotifyResponse error(String msg) {
+ NotifyResponse notifyResponse = new NotifyResponse();
+ notifyResponse.setNotifyId(null);
+ notifyResponse.setSuccess(false);
+ notifyResponse.setMsg(msg);
+ return notifyResponse;
+ }
+}
diff --git a/sop-notify/sop-notify-service/pom.xml b/sop-notify/sop-notify-service/pom.xml
new file mode 100644
index 00000000..df65c040
--- /dev/null
+++ b/sop-notify/sop-notify-service/pom.xml
@@ -0,0 +1,154 @@
+
+
+ 4.0.0
+
+ com.gitee.sop
+ sop-notify
+ 5.0.0-SNAPSHOT
+
+
+ sop-notify-service
+ 5.0.0-SNAPSHOT
+
+
+ 1.8
+
+ 3.2.16
+
+
+
+
+ com.gitee.sop
+ sop-notify-api
+ 5.0.0-SNAPSHOT
+
+
+ com.gitee.sop
+ sdk-java
+ 5.0.0-SNAPSHOT
+
+
+ io.gitee.durcframework
+ fastmybatis-spring-boot-starter
+
+
+ net.oschina.durcframework
+ http-helper
+
+
+
+ com.gitee.sop
+ sop-spring-boot-starter
+ 5.0.0-SNAPSHOT
+
+
+
+ org.apache.dubbo
+ dubbo-nacos-spring-boot-starter
+
+
+
+ org.apache.dubbo
+ dubbo-zookeeper-curator5-spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+ mysql
+ mysql-connector-java
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.34
+ provided
+
+
+
+
+
+
+ org.apache.dubbo
+ dubbo-bom
+ ${dubbo.version}
+ pom
+ import
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.12.4
+
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ ${java.version}
+ ${java.version}
+ UTF-8
+
+ -parameters
+
+
+
+
+
+ com.ly.smart-doc
+ smart-doc-maven-plugin
+ 3.0.9
+
+
+ ./src/main/resources/smart-doc.json
+
+ ${project.artifactId}
+
+
+
+ com.gitee.sop
+ sop-service-support
+ 5.0.0-SNAPSHOT
+
+
+
+
+
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+
+ maven_central
+ Maven Central
+ https://repo.maven.apache.org/maven2/
+
+
+
diff --git a/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/SopNotifyApplication.java b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/SopNotifyApplication.java
new file mode 100755
index 00000000..1ed38198
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/SopNotifyApplication.java
@@ -0,0 +1,18 @@
+package com.gitee.sop.notify;
+
+import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@SpringBootApplication
+@EnableDubbo
+@EnableScheduling
+public class SopNotifyApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SopNotifyApplication.class, args);
+ }
+
+}
+
diff --git a/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/dao/entity/NotifyInfo.java b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/dao/entity/NotifyInfo.java
new file mode 100644
index 00000000..3608c99c
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/dao/entity/NotifyInfo.java
@@ -0,0 +1,99 @@
+package com.gitee.sop.notify.dao.entity;
+
+import java.time.LocalDateTime;
+
+import com.gitee.fastmybatis.annotation.Pk;
+import com.gitee.fastmybatis.annotation.PkStrategy;
+import com.gitee.fastmybatis.annotation.Table;
+
+import lombok.Data;
+
+
+/**
+ * 表名:notify_info
+ * 备注:回调信息
+ *
+ * @author 六如
+ */
+@Table(name = "notify_info", pk = @Pk(name = "id", strategy = PkStrategy.INCREMENT))
+@Data
+public class NotifyInfo {
+
+ private Long id;
+
+ /**
+ * app_id
+ */
+ private String appId;
+
+ /**
+ * api_name
+ */
+ private String apiName;
+
+ /**
+ * api_version
+ */
+ private String apiVersion;
+
+ /**
+ * 回调url
+ */
+ private String notifyUrl;
+
+ /**
+ * 最近一次发送时间
+ */
+ private LocalDateTime lastSendTime;
+
+ /**
+ * 下一次发送时间
+ */
+ private LocalDateTime nextSendTime;
+
+ /**
+ * 已发送次数
+ */
+ private Integer sendCnt;
+
+ /**
+ * 发送内容
+ */
+ private String content;
+
+ /**
+ * 状态,1-发送成功,2-发送失败,3-重试结束
+ */
+ private Integer notifyStatus;
+
+ /**
+ * 失败原因
+ */
+ private String errorMsg;
+
+ /**
+ * 返回结果
+ */
+ private String resultContent;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+ private LocalDateTime addTime;
+
+ private LocalDateTime updateTime;
+
+ /**
+ * 创建人id
+ */
+ private Long addBy;
+
+ /**
+ * 修改人id
+ */
+ private Long updateBy;
+
+
+}
diff --git a/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/dao/mapper/IsvMapper.java b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/dao/mapper/IsvMapper.java
new file mode 100755
index 00000000..81ac1c85
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/dao/mapper/IsvMapper.java
@@ -0,0 +1,14 @@
+package com.gitee.sop.notify.dao.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author 六如
+ */
+@Mapper
+public interface IsvMapper {
+
+ String getPrivatePlatformKey(String appId);
+
+ String getNotifyUrl(String appId);
+}
diff --git a/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/dao/mapper/NotifyInfoMapper.java b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/dao/mapper/NotifyInfoMapper.java
new file mode 100644
index 00000000..cce15029
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/dao/mapper/NotifyInfoMapper.java
@@ -0,0 +1,13 @@
+package com.gitee.sop.notify.dao.mapper;
+
+import com.gitee.fastmybatis.core.mapper.BaseMapper;
+import com.gitee.sop.notify.dao.entity.NotifyInfo;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author 六如
+ */
+@Mapper
+public interface NotifyInfoMapper extends BaseMapper {
+
+}
diff --git a/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/dubbo/NotifyServiceImpl.java b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/dubbo/NotifyServiceImpl.java
new file mode 100644
index 00000000..0a4821b6
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/dubbo/NotifyServiceImpl.java
@@ -0,0 +1,48 @@
+package com.gitee.sop.notify.dubbo;
+
+import com.gitee.sop.notify.api.NotifyService;
+import com.gitee.sop.notify.api.req.NotifyRequest;
+import com.gitee.sop.notify.api.resp.NotifyResponse;
+import com.gitee.sop.notify.service.NotifyBizService;
+import com.gitee.sop.notify.service.bo.NotifyBO;
+import com.gitee.sop.sdk.sign.SopSignException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+
+
+/**
+ * @author 六如
+ */
+@DubboService(validation = "true")
+@Slf4j
+public class NotifyServiceImpl implements NotifyService {
+
+ @Autowired
+ private NotifyBizService notifyBizService;
+
+ @Override
+ public NotifyResponse notify(NotifyRequest request) {
+ NotifyBO notifyBO = new NotifyBO();
+ BeanUtils.copyProperties(request, notifyBO);
+ try {
+ Long notifyId = notifyBizService.notify(notifyBO);
+ return NotifyResponse.success(notifyId);
+ } catch (SopSignException e) {
+ log.error("回调异常,服务端签名失败, request={}", request, e);
+ return NotifyResponse.error(e.getErrMsg());
+ }
+ }
+
+ @Override
+ public NotifyResponse notifyImmediately(Long notifyId) {
+ try {
+ notifyBizService.notifyImmediately(notifyId);
+ return NotifyResponse.success(notifyId);
+ } catch (Exception e) {
+ log.error("回调异常, notifyId={}", notifyId, e);
+ return NotifyResponse.error(e.getMessage());
+ }
+ }
+}
diff --git a/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/enums/NotifyStatusEnum.java b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/enums/NotifyStatusEnum.java
new file mode 100644
index 00000000..2c9c81a3
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/enums/NotifyStatusEnum.java
@@ -0,0 +1,22 @@
+package com.gitee.sop.notify.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author 六如
+ */
+@AllArgsConstructor
+@Getter
+public enum NotifyStatusEnum {
+
+ // 状态,1-发送成功,2-发送失败,3-重试结束
+ SEND_SUCCESS(1, "发送成功"),
+ SEND_FAIL(2, "发送失败"),
+ RETRY_OVER(3, "重试结束");
+
+ private final Integer value;
+
+ private final String description;
+
+}
diff --git a/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/schedule/NotifySchedule.java b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/schedule/NotifySchedule.java
new file mode 100644
index 00000000..099b6834
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/schedule/NotifySchedule.java
@@ -0,0 +1,27 @@
+package com.gitee.sop.notify.schedule;
+
+import com.gitee.sop.notify.service.NotifyBizService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author 六如
+ */
+@Component
+public class NotifySchedule {
+
+ @Autowired
+ private NotifyBizService notifyBizService;
+
+ /**
+ * 每分钟执行一次
+ */
+ @Scheduled(cron = "0 0/1 * * * ?")
+ public void run() {
+ notifyBizService.retry(LocalDateTime.now());
+ }
+
+}
diff --git a/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/service/IsvService.java b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/service/IsvService.java
new file mode 100644
index 00000000..96f24c6f
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/service/IsvService.java
@@ -0,0 +1,24 @@
+package com.gitee.sop.notify.service;
+
+import com.gitee.sop.notify.dao.mapper.IsvMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author 六如
+ */
+@Service
+public class IsvService {
+
+ @Autowired
+ private IsvMapper isvMapper;
+
+ public String getPrivatePlatformKey(String appId) {
+ return isvMapper.getPrivatePlatformKey(appId);
+ }
+
+ public String getNotifyUrl(String appId) {
+ return isvMapper.getNotifyUrl(appId);
+ }
+
+}
diff --git a/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/service/NotifyBizService.java b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/service/NotifyBizService.java
new file mode 100644
index 00000000..9916fda7
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/service/NotifyBizService.java
@@ -0,0 +1,243 @@
+package com.gitee.sop.notify.service;
+
+import com.alibaba.fastjson.JSON;
+import com.gitee.fastmybatis.core.support.LambdaService;
+import com.gitee.httphelper.HttpHelper;
+import com.gitee.httphelper.result.ResponseResult;
+import com.gitee.sop.notify.dao.entity.NotifyInfo;
+import com.gitee.sop.notify.dao.mapper.NotifyInfoMapper;
+import com.gitee.sop.notify.enums.NotifyStatusEnum;
+import com.gitee.sop.notify.service.bo.NotifyBO;
+import com.gitee.sop.sdk.sign.SignUtil;
+import com.gitee.sop.sdk.sign.SopSignException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.http.HttpStatus;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+
+/**
+ * 回调业务逻辑处理
+ *
+ * @author 六如
+ */
+@Service
+@Slf4j
+public class NotifyBizService implements LambdaService {
+
+ // 对应第1,2,3...次尝试
+ // 即1分钟后进行第一次尝试,如果失败,5分钟后进行第二次尝试
+ @Value("${sop.notify.time-level:1m,5m,10m,30m,1h,2h,5h}")
+ private String timeLevel;
+
+ @Autowired
+ private IsvService isvService;
+
+ /**
+ * 第一次发送
+ *
+ * @param notifyBO 回调内容
+ * @return 返回回调id
+ * @throws SopSignException 异常处理
+ */
+ public Long notify(NotifyBO notifyBO) throws SopSignException {
+ NotifyInfo notifyInfo = buildRecord(notifyBO);
+ return doNotify(notifyBO, notifyInfo);
+ }
+
+ public void notifyImmediately(Long notifyId) throws SopSignException {
+ NotifyInfo notifyInfo = this.getById(notifyId);
+ String content = notifyInfo.getContent();
+ NotifyBO notifyBO = JSON.parseObject(content, NotifyBO.class);
+ // 发送请求
+ doNotify(notifyBO, notifyInfo);
+ }
+
+
+ /**
+ * 重试
+ *
+ * @param now 当前时间
+ */
+ public void retry(LocalDateTime now) {
+ LocalDateTime nextTime = now.withSecond(0).withNano(0);
+ List list = this.query()
+ .eq(NotifyInfo::getNextSendTime, nextTime)
+ .eq(NotifyInfo::getNotifyStatus, NotifyStatusEnum.SEND_FAIL.getValue())
+ .list();
+ if (list.isEmpty()) {
+ log.info("[notify]无重试记录");
+ return;
+ }
+
+ for (NotifyInfo notifyInfo : list) {
+ retry(notifyInfo);
+ }
+ }
+
+ private void retry(NotifyInfo notifyInfo) {
+ String content = notifyInfo.getContent();
+ NotifyBO notifyBO = JSON.parseObject(content, NotifyBO.class);
+ try {
+ log.info("[notify]开始重试, notifyId={}", notifyInfo.getId());
+ if (Objects.equals(notifyInfo.getNotifyStatus(), NotifyStatusEnum.RETRY_OVER.getValue())) {
+ log.warn("重试次数已用尽, notifyId={}", notifyInfo.getId());
+ return;
+ }
+ // 发送请求
+ doNotify(notifyBO, notifyInfo);
+ } catch (SopSignException e) {
+ log.error("[notify]重试签名错误,notifyId={}", notifyInfo.getId(), e);
+ throw new RuntimeException("重试失败,签名错误");
+ }
+ }
+
+ /**
+ * 构建下一次重试时间
+ *
+ * @param currentSendCnt 当前发送次数
+ * @return 返回null表示重试次数用完
+ */
+ private LocalDateTime buildNextSendTime(Integer currentSendCnt) {
+ String[] split = timeLevel.split(",");
+ if (currentSendCnt >= split.length) {
+ return null;
+ }
+ // 1m
+ String exp = split[currentSendCnt - 1];
+ // 秒,毫秒归零f
+ LocalDateTime time = LocalDateTime.now().withSecond(0).withNano(0);
+ // 最后一个字符,如:m,h,d
+ char ch = exp.charAt(exp.length() - 1);
+ int value = NumberUtils.toInt(exp.substring(0, exp.length() - 1));
+ switch (String.valueOf(ch).toLowerCase()) {
+ case "m":
+ return time.plusMinutes(value);
+ case "h":
+ return time.plusHours(value);
+ case "d":
+ return time.plusDays(value);
+ default:
+ return null;
+ }
+ }
+
+
+ private Long doNotify(NotifyBO notifyBO, NotifyInfo notifyInfo) throws SopSignException {
+ notifyInfo.setSendCnt(notifyInfo.getSendCnt() + 1);
+ notifyInfo.setLastSendTime(LocalDateTime.now());
+ notifyInfo.setNotifyUrl(buildNotifyUrl(notifyBO, notifyInfo));
+
+ String notifyUrl = notifyInfo.getNotifyUrl();
+ // 构建请求参数
+ Map params = buildParams(notifyBO);
+ try {
+ if (StringUtils.isBlank(notifyUrl)) {
+ throw new RuntimeException("回调接口不能为空");
+ }
+
+ String json = JSON.toJSONString(params);
+ log.info("发送回调请求,notifyUrl={}, content={}", notifyUrl, json);
+ ResponseResult responseResult = HttpHelper.postJson(notifyUrl, json)
+ .execute();
+
+ // 这里判断收到200认为请求成功
+ int status = responseResult.getStatus();
+ String resultContent = responseResult.asString();
+ notifyInfo.setResultContent(resultContent);
+ if (status == HttpStatus.SC_OK) {
+ // 更新状态
+ notifyInfo.setNotifyStatus(NotifyStatusEnum.SEND_SUCCESS.getValue());
+ notifyInfo.setErrorMsg("");
+ } else {
+ // 回调失败
+ log.error("回调状态非200:{}, result={}", status, resultContent);
+ throw new RuntimeException(resultContent);
+ }
+ } catch (Exception e) {
+ log.error("回调请求失败, notifyUrl={}, params={}, notifyBO={}", notifyUrl, params, notifyBO, e);
+ notifyInfo.setNotifyStatus(NotifyStatusEnum.SEND_FAIL.getValue());
+ notifyInfo.setErrorMsg(e.getMessage());
+
+ LocalDateTime nextSendTime = buildNextSendTime(notifyInfo.getSendCnt());
+ notifyInfo.setNextSendTime(nextSendTime);
+
+ if (nextSendTime == null) {
+ log.error("回调请求次数达到上线, notifyUrl={}, params={}", notifyUrl, params);
+ notifyInfo.setNotifyStatus(NotifyStatusEnum.RETRY_OVER.getValue());
+ }
+ }
+
+ this.saveOrUpdate(notifyInfo);
+
+ return notifyInfo.getId();
+ }
+
+ private Map buildParams(NotifyBO notifyBO) throws SopSignException {
+ // 公共请求参数
+ Map params = new HashMap<>();
+ String appId = notifyBO.getAppId();
+ params.put("app_id", appId);
+ params.put("method", notifyBO.getApiName());
+ params.put("format", "json");
+ params.put("charset", notifyBO.getCharset());
+ params.put("sign_type", "RSA2");
+ params.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
+ params.put("version", notifyBO.getVersion());
+
+ // 业务参数
+ Map bizContent = notifyBO.getBizParams();
+
+ params.put("biz_content", JSON.toJSONString(bizContent));
+ String content = SignUtil.getSignContent(params);
+
+ String privateKey = isvService.getPrivatePlatformKey(appId);
+ String sign = SignUtil.rsa256Sign(content, privateKey, notifyBO.getCharset());
+ params.put("sign", sign);
+
+ return params;
+ }
+
+ private String buildNotifyUrl(NotifyBO notifyBO, NotifyInfo notifyInfo) {
+ String savedUrl = notifyInfo.getNotifyUrl();
+ if (StringUtils.isNotBlank(savedUrl)) {
+ return savedUrl;
+ }
+ String notifyUrl = notifyBO.getNotifyUrl();
+ if (StringUtils.isBlank(notifyUrl)) {
+ notifyUrl = isvService.getNotifyUrl(notifyBO.getAppId());
+ }
+ return notifyUrl;
+ }
+
+ private NotifyInfo buildRecord(NotifyBO notifyBO) {
+ NotifyInfo notifyInfo = new NotifyInfo();
+ notifyInfo.setAppId(notifyBO.getAppId());
+ notifyInfo.setApiName(notifyBO.getApiName());
+ notifyInfo.setApiVersion(notifyBO.getVersion());
+ notifyInfo.setSendCnt(0);
+ notifyInfo.setContent(JSON.toJSONString(notifyBO));
+ notifyInfo.setNotifyStatus(0);
+ notifyInfo.setErrorMsg("");
+ notifyInfo.setRemark(notifyBO.getRemark());
+ notifyInfo.setAddTime(LocalDateTime.now());
+ notifyInfo.setUpdateTime(LocalDateTime.now());
+ notifyInfo.setAddBy(0L);
+ notifyInfo.setUpdateBy(0L);
+
+ return notifyInfo;
+ }
+
+
+}
diff --git a/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/service/bo/NotifyBO.java b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/service/bo/NotifyBO.java
new file mode 100644
index 00000000..687a9edd
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/java/com/gitee/sop/notify/service/bo/NotifyBO.java
@@ -0,0 +1,64 @@
+package com.gitee.sop.notify.service.bo;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.util.Map;
+
+/**
+ * @author 六如
+ */
+@Data
+public class NotifyBO {
+
+ /**
+ * appId
+ */
+ @NotBlank(message = "appId必填")
+ private String appId;
+
+ /**
+ * apiName
+ */
+ @NotBlank(message = "apiName必填")
+ private String apiName;
+
+ /**
+ * version
+ */
+ @NotBlank(message = "version必填")
+ private String version;
+
+ /**
+ * token,没有返回null
+ */
+ private String appAuthToken;
+
+ /**
+ * 客户端ip
+ */
+ private String clientIp;
+
+ /**
+ * 回调地址
+ */
+ @NotBlank(message = "notifyUrl必填")
+ private String notifyUrl;
+
+ /**
+ * 编码
+ */
+ private String charset;
+
+ /**
+ * 业务参数
+ */
+ @NotBlank(message = "bizParams必填")
+ private Map bizParams;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+}
diff --git a/sop-notify/sop-notify-service/src/main/resources/application-dev.properties b/sop-notify/sop-notify-service/src/main/resources/application-dev.properties
new file mode 100755
index 00000000..1c6bbfdf
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/resources/application-dev.properties
@@ -0,0 +1,7 @@
+mybatis.print-sql=true
+
+# mysql config
+mysql.host=127.0.0.1:3306
+mysql.db=sop
+mysql.username=root
+mysql.password=12345678
diff --git a/sop-notify/sop-notify-service/src/main/resources/application-test.properties b/sop-notify/sop-notify-service/src/main/resources/application-test.properties
new file mode 100755
index 00000000..30f45aa6
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/resources/application-test.properties
@@ -0,0 +1,9 @@
+dubbo.registry.address=nacos://localhost:8848
+
+mybatis.print-sql=true
+
+# mysql config
+mysql.host=127.0.0.1:3306
+mysql.db=sop
+mysql.username=root
+mysql.password=root
diff --git a/sop-notify/sop-notify-service/src/main/resources/application.properties b/sop-notify/sop-notify-service/src/main/resources/application.properties
new file mode 100755
index 00000000..3a5c59e9
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/resources/application.properties
@@ -0,0 +1,47 @@
+server.port=8085
+spring.profiles.active=dev
+
+spring.application.name=sop-notify
+
+dubbo.protocol.name=dubbo
+dubbo.protocol.port=-1
+dubbo.application.qos-enable=false
+dubbo.consumer.check=false
+# ### register config see:https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/registry/overview/
+# ------
+# nacos://localhost:8848 Cluster config:nacos://localhost:8848?backup=localshot:8846,localshot:8847
+# zookeeper://localhost:2181 Cluster config:zookeeper://10.20.153.10:2181?backup=10.20.153.11:2181,10.20.153.12:2181
+# redis://localhost:6379 Cluster config:redis://10.20.153.10:6379?backup=10.20.153.11:6379,10.20.153.12:6379
+# ------
+dubbo.registry.address=zookeeper://localhost:2181
+
+####### mysql config #######
+mysql.host=127.0.0.1:3306
+mysql.db=sop
+mysql.username=
+mysql.password=
+
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+spring.datasource.url=jdbc:mysql://${mysql.host}/${mysql.db}?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
+spring.datasource.username=${mysql.username}
+spring.datasource.password=${mysql.password}
+
+####### mybatis config #######
+mybatis.fill.com.gitee.fastmybatis.core.support.LocalDateTimeFillInsert=add_time
+mybatis.fill.com.gitee.fastmybatis.core.support.LocalDateTimeFillUpdate=update_time
+# mybatis config file
+mybatis.config-location=classpath:mybatis/mybatisConfig.xml
+mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
+
+# log level
+logging.level.com.gitee.sop=info
+# log path
+logging.file.name=logs/sop-notify.log
+# print SQL
+logging.level.com.gitee.sop.notify.dao=error
+logging.level.com.gitee.fastmybatis=info
+mybatis.print-sql=false
+
+# \u5BF9\u5E94\u7B2C1\uFF0C2\uFF0C3...\u6B21\u5C1D\u8BD5
+# \u53731\u5206\u949F\u540E\u8FDB\u884C\u7B2C\u4E00\u6B21\u5C1D\u8BD5\uFF0C\u5982\u679C\u5931\u8D25\uFF0C5\u5206\u949F\u540E\u8FDB\u884C\u7B2C\u4E8C\u6B21\u5C1D\u8BD5
+sop.notify.time-level:1m,5m,10m,30m,1h,2h,5h
\ No newline at end of file
diff --git a/sop-notify/sop-notify-service/src/main/resources/mybatis/mapper/IsvMapper.xml b/sop-notify/sop-notify-service/src/main/resources/mybatis/mapper/IsvMapper.xml
new file mode 100644
index 00000000..3cd6ed82
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/resources/mybatis/mapper/IsvMapper.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/sop-notify/sop-notify-service/src/main/resources/mybatis/mybatisConfig.xml b/sop-notify/sop-notify-service/src/main/resources/mybatis/mybatisConfig.xml
new file mode 100755
index 00000000..3c4b234f
--- /dev/null
+++ b/sop-notify/sop-notify-service/src/main/resources/mybatis/mybatisConfig.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sop-support/sop-service-support/src/main/java/com/gitee/sop/support/dto/NotifyInfo.java b/sop-support/sop-service-support/src/main/java/com/gitee/sop/support/dto/NotifyInfo.java
deleted file mode 100755
index e6d38615..00000000
--- a/sop-support/sop-service-support/src/main/java/com/gitee/sop/support/dto/NotifyInfo.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.gitee.sop.support.dto;
-
-import lombok.Data;
-
-import java.io.Serializable;
-import java.util.Map;
-
-/**
- * 回调信息
- *
- * @author 六如
- */
-@Data
-public class NotifyInfo implements Serializable {
- private static final long serialVersionUID = -2492336644456313771L;
-
- /**
- * 链路id
- */
- private String traceId;
-
- /**
- * 回调接口
- */
- private String url;
-
- /**
- * 跟在url后面的参数
- */
- private Map query;
-
- /**
- * 请求头
- */
- private Map header;
-
- /**
- * 请求体
- */
- private Map body;
-
-}
diff --git a/sop-test/src/test/java/com/gitee/sop/test/NotifyTest.java b/sop-test/src/test/java/com/gitee/sop/test/NotifyTest.java
new file mode 100644
index 00000000..37ddd92c
--- /dev/null
+++ b/sop-test/src/test/java/com/gitee/sop/test/NotifyTest.java
@@ -0,0 +1,65 @@
+package com.gitee.sop.test;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * @author 六如
+ */
+public class NotifyTest extends TestBase {
+
+ String url = "http://localhost:8081/api";
+ String appId = "2019032617262200001";
+ String privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=";
+
+ private final Client client = new Client(url, appId, privateKey, AllInOneTest::assertResult);
+
+ /**
+ * 以get方式提交
+ */
+ public void testGet() {
+ // 参见:com.gitee.sop.notifyexample.open.req.CreateOrderRequest
+ Map bizContent = new LinkedHashMap<>();
+ bizContent.put("outTradeNo", "1111");
+ bizContent.put("totalAmount", "100");
+ bizContent.put("subject", "话费");
+
+ Client.RequestBuilder requestBuilder = new Client.RequestBuilder()
+ .method("shop.order.create")
+ .version("1.0")
+ // 回调地址,见:com.gitee.sop.notifyexample.controller.DemoCallbackController
+ .notifyUrl("http://127.0.0.1:7074/notify/callback")
+ .bizContent(bizContent)
+ .httpMethod(HttpTool.HTTPMethod.GET)
+ .callback((requestInfo, responseData) -> {
+ System.out.println(responseData);
+ });
+
+ client.execute(requestBuilder);
+ }
+
+ /**
+ * 不填notifyUrl
+ */
+ public void testGet2() {
+ // 参见:com.gitee.sop.notifyexample.open.req.CreateOrderRequest
+ Map bizContent = new LinkedHashMap<>();
+ bizContent.put("outTradeNo", "1111");
+ bizContent.put("totalAmount", "100");
+ bizContent.put("subject", "话费");
+
+ Client.RequestBuilder requestBuilder = new Client.RequestBuilder()
+ .method("shop.order.create")
+ .version("1.0")
+ // 回调地址,见:com.gitee.sop.notifyexample.controller.DemoCallbackController
+ //.notifyUrl("http://127.0.0.1:7074/notify/callback")
+ .bizContent(bizContent)
+ .httpMethod(HttpTool.HTTPMethod.POST)
+ .callback((requestInfo, responseData) -> {
+ System.out.println(responseData);
+ });
+
+ client.execute(requestBuilder);
+ }
+
+}
diff --git a/sop.sql b/sop.sql
index f322edea..ea123a8e 100755
--- a/sop.sql
+++ b/sop.sql
@@ -493,7 +493,6 @@ CREATE TABLE `help_doc` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='帮助内容表';
-
-- 2025-08-17
CREATE TABLE `isv_merchant`
(
@@ -507,3 +506,33 @@ CREATE TABLE `isv_merchant`
PRIMARY KEY (`id`),
UNIQUE KEY `uk_appid_merchant` (`app_id`, merchant_code)
) ENGINE=InnoDB COMMENT='isv商户关系表';
+
+-- 2025-11-01
+ALTER TABLE `isv_info`
+ ADD COLUMN `notify_url` varchar(256) NULL COMMENT '回调接口';
+
+CREATE TABLE `notify_info` (
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `app_id` varchar(64) NOT NULL COMMENT 'app_id',
+ `api_name` varchar(64) NOT NULL COMMENT 'api_name',
+ `api_version` varchar(16) NOT NULL COMMENT 'api_version',
+ `notify_url` varchar(255) DEFAULT '' COMMENT '回调url',
+ `last_send_time` datetime DEFAULT NULL COMMENT '最近一次发送时间',
+ `next_send_time` datetime DEFAULT NULL COMMENT '下一次发送时间',
+ `send_max` int(11) DEFAULT NULL COMMENT '最大发送次数',
+ `send_cnt` int(11) DEFAULT NULL COMMENT '已发送次数',
+ `content` text COMMENT '发送内容',
+ `notify_status` tinyint(4) DEFAULT '1' COMMENT '状态,1-发送成功,2-发送失败,3-重试结束',
+ `error_msg` text COMMENT '失败原因',
+ `result_content` text COMMENT '返回结果',
+ `remark` varchar(256) DEFAULT '' COMMENT '备注',
+ `add_time` datetime DEFAULT CURRENT_TIMESTAMP,
+ `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `add_by` bigint(20) DEFAULT '0' COMMENT '创建人id',
+ `update_by` bigint(20) DEFAULT '0' COMMENT '修改人id',
+ PRIMARY KEY (`id`),
+ KEY `idx_app_id` (`app_id`) USING BTREE
+) ENGINE=InnoDB COMMENT='回调信息';
+
+INSERT INTO `sys_resource` ( `menu_type`, `title`, `name`, `path`, `component`, `rank`, `redirect`, `icon`, `extra_icon`, `enter_transition`, `leave_transition`, `active_path`, `auths`, `frame_src`, `frame_loading`, `keep_alive`, `hidden_tag`, `fixed_tag`, `show_link`, `show_parent`, `parent_id`, `is_deleted`, `add_time`, `update_time`, `add_by`, `update_by`) VALUES
+ (0, '回调管理', 'NotifyMgr', '/serve/notify', '', 99, '', 'ri:align-vertically', '', '', '', '', '', '', 0, 0, 0, 0, 1, 0, 1, 0, '2025-11-01 20:37:08', '2025-11-01 20:37:08', 1, 1);
diff --git a/upgrade/sop-20251101.sql b/upgrade/sop-20251101.sql
new file mode 100644
index 00000000..75f7ab73
--- /dev/null
+++ b/upgrade/sop-20251101.sql
@@ -0,0 +1,29 @@
+ALTER TABLE `isv_info`
+ ADD COLUMN `notify_url` varchar(256) NULL COMMENT '回调接口';
+
+CREATE TABLE `notify_info`
+(
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `app_id` varchar(64) NOT NULL COMMENT 'app_id',
+ `api_name` varchar(64) NOT NULL COMMENT 'api_name',
+ `api_version` varchar(16) NOT NULL COMMENT 'api_version',
+ `notify_url` varchar(255) DEFAULT '' COMMENT '回调url',
+ `last_send_time` datetime DEFAULT NULL COMMENT '最近一次发送时间',
+ `next_send_time` datetime DEFAULT NULL COMMENT '下一次发送时间',
+ `send_max` int(11) DEFAULT NULL COMMENT '最大发送次数',
+ `send_cnt` int(11) DEFAULT NULL COMMENT '已发送次数',
+ `content` text COMMENT '发送内容',
+ `notify_status` tinyint(4) DEFAULT '1' COMMENT '状态,1-发送成功,2-发送失败,3-重试结束',
+ `error_msg` text COMMENT '失败原因',
+ `result_content` text COMMENT '返回结果',
+ `remark` varchar(256) DEFAULT '' COMMENT '备注',
+ `add_time` datetime DEFAULT CURRENT_TIMESTAMP,
+ `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `add_by` bigint(20) DEFAULT '0' COMMENT '创建人id',
+ `update_by` bigint(20) DEFAULT '0' COMMENT '修改人id',
+ PRIMARY KEY (`id`),
+ KEY `idx_app_id` (`app_id`) USING BTREE
+) ENGINE=InnoDB COMMENT='回调信息';
+
+INSERT INTO `sys_resource` ( `menu_type`, `title`, `name`, `path`, `component`, `rank`, `redirect`, `icon`, `extra_icon`, `enter_transition`, `leave_transition`, `active_path`, `auths`, `frame_src`, `frame_loading`, `keep_alive`, `hidden_tag`, `fixed_tag`, `show_link`, `show_parent`, `parent_id`, `is_deleted`, `add_time`, `update_time`, `add_by`, `update_by`) VALUES
+ (0, '回调管理', 'NotifyMgr', '/serve/notify', '', 99, '', 'ri:align-vertically', '', '', '', '', '', '', 0, 0, 0, 0, 1, 0, 1, 0, '2025-11-01 20:37:08', '2025-11-01 20:37:08', 1, 1);