回调管理

This commit is contained in:
六如
2025-11-03 07:56:35 +08:00
parent 9d8a04b702
commit 5001809fef
75 changed files with 2828 additions and 56 deletions

View File

@@ -10,7 +10,7 @@
通过简单的配置后,你的项目就具备了和支付宝开放平台的一样的接口提供能力。 通过简单的配置后,你的项目就具备了和支付宝开放平台的一样的接口提供能力。
SOP封装了开放平台大部分功能包括签名验证、统一异常处理、统一返回内容 、业务参数验证JSR-303、秘钥管理等,未来还会实现更多功能 SOP封装了开放平台大部分功能包括签名验证、统一异常处理、统一返回内容 、业务参数验证JSR-303、秘钥管理、接口回调等。
## 项目特点 ## 项目特点

View File

@@ -2,6 +2,7 @@
## 日常更新 ## 日常更新
- 2025-11-01添加回调处理。有升级SQL[sop-20251101.sql](./upgrade/sop-20251101.sql)
- 2025-09-12修复推送文档报找不到@Open注解问题 - 2025-09-12修复推送文档报找不到@Open注解问题
- 2025-08-29smart-doc升级到3.1.1 - 2025-08-29smart-doc升级到3.1.1
- 2025-08-17admin后台可关联商户;fastmybatis升级到3.1.7。有升级SQL[sop-20250817.sql](./upgrade/sop-20250817.sql) - 2025-08-17admin后台可关联商户;fastmybatis升级到3.1.7。有升级SQL[sop-20250817.sql](./upgrade/sop-20250817.sql)

View File

@@ -24,6 +24,7 @@
<module>sop-gateway</module> <module>sop-gateway</module>
<module>sop-registry</module> <module>sop-registry</module>
<module>sop-support</module> <module>sop-support</module>
<module>sop-notify</module>
</modules> </modules>
<properties> <properties>
@@ -127,7 +128,7 @@
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId> <artifactId>transmittable-thread-local</artifactId>
<version>2.14.5</version> <version>2.14.2</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>

View File

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

View File

@@ -38,6 +38,11 @@ public class IsvInfo {
*/ */
private String remark; private String remark;
/**
* 回调接口
*/
private String notifyUrl;
/** /**
* 添加时间 * 添加时间
*/ */

View File

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

View File

@@ -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<NotifyInfo> {
}

View File

@@ -28,6 +28,11 @@
<artifactId>admin-dao</artifactId> <artifactId>admin-dao</artifactId>
<version>5.0.0-SNAPSHOT</version> <version>5.0.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-notify-api</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.dubbo</groupId> <groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId> <artifactId>dubbo</artifactId>

View File

@@ -119,10 +119,7 @@ public class IsvInfoService implements ServiceSupport<IsvInfo, IsvInfoMapper> {
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int update(IsvInfoUpdateDTO isvInfoUpdateDTO) { public int update(IsvInfoUpdateDTO isvInfoUpdateDTO) {
IsvInfo isvInfo = new IsvInfo(); IsvInfo isvInfo = CopyUtil.copyBean(isvInfoUpdateDTO, IsvInfo::new);
isvInfo.setId(isvInfoUpdateDTO.getId());
isvInfo.setStatus(isvInfoUpdateDTO.getStatus());
isvInfo.setRemark(isvInfoUpdateDTO.getRemark());
int cnt = this.update(isvInfo); int cnt = this.update(isvInfo);
sendChangeEvent(isvInfoUpdateDTO.getId()); sendChangeEvent(isvInfoUpdateDTO.getId());
return cnt; return cnt;
@@ -185,4 +182,11 @@ public class IsvInfoService implements ServiceSupport<IsvInfo, IsvInfoMapper> {
sendChangeEvent(isvId); sendChangeEvent(isvId);
return i; return i;
} }
public Integer setNotifyUrl(Long id, String url) {
return this.query()
.eq(IsvInfo::getId, id)
.set(IsvInfo::getNotifyUrl, url)
.update();
}
} }

View File

@@ -24,4 +24,9 @@ public class IsvInfoAddDTO {
@Length(max = 500) @Length(max = 500)
private String remark; private String remark;
/**
* 回调接口
*/
private String notifyUrl;
} }

View File

@@ -58,4 +58,9 @@ public class IsvInfoDTO {
*/ */
private String merchantCode; private String merchantCode;
/**
* 回调接口
*/
private String notifyUrl;
} }

View File

@@ -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<NotifyInfo, NotifyInfoMapper> {
@DubboReference
private NotifyService notifyService;
public PageInfo<NotifyInfo> doPage(LambdaQuery<NotifyInfo> query) {
query.orderByDesc(NotifyInfo::getId);
PageInfo<NotifyInfo> 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();
}
}

View File

@@ -24,4 +24,5 @@ public class IsvInfoAddParam {
@Length(max = 500) @Length(max = 500)
private String remark; private String remark;
private String notifyUrl;
} }

View File

@@ -58,4 +58,9 @@ public class IsvInfoVO {
*/ */
private String merchantCode; private String merchantCode;
/**
* 回调接口
*/
private String notifyUrl;
} }

View File

@@ -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.PageInfo;
import com.gitee.fastmybatis.core.query.Query; 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.req.StatusUpdateParam;
import com.gitee.sop.admin.common.resp.Result; import com.gitee.sop.admin.common.resp.Result;
import com.gitee.sop.admin.common.util.CopyUtil; import com.gitee.sop.admin.common.util.CopyUtil;
import com.gitee.sop.admin.controller.serve.param.ApiInfoPageParam; import com.gitee.sop.admin.controller.serve.api.param.ApiInfoPageParam;
import com.gitee.sop.admin.controller.serve.vo.ApiInfoVO; import com.gitee.sop.admin.controller.serve.api.vo.ApiInfoVO;
import com.gitee.sop.admin.dao.entity.ApiInfo; import com.gitee.sop.admin.dao.entity.ApiInfo;
import com.gitee.sop.admin.service.serve.ApiInfoService; import com.gitee.sop.admin.service.serve.ApiInfoService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -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.Operator;
import com.gitee.fastmybatis.core.query.annotation.Condition; import com.gitee.fastmybatis.core.query.annotation.Condition;

View File

@@ -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.Operator;
import com.gitee.fastmybatis.core.query.annotation.Condition; import com.gitee.fastmybatis.core.query.annotation.Condition;

View File

@@ -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 com.gitee.sop.admin.service.jackson.convert.annotation.UserFormat;
import lombok.Data; import lombok.Data;

View File

@@ -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<PageInfo<NotifyInfoVO>> page(NotifyInfoSearchParam param) {
LambdaQuery<NotifyInfo> query = param.toLambdaQuery(NotifyInfo.class);
PageInfo<NotifyInfoVO> pageInfo = notifyInfoService.doPage(query)
.convert(data -> CopyUtil.copyBean(data, NotifyInfoVO::new));
return Result.ok(pageInfo);
}
/**
* 重新推送
*
* @param param 表单数据
* @return 返回影响行数
*/
@PostMapping("/push")
public Result<Integer> update(@Validated @RequestBody NotifyPushParam param) {
return Result.ok(notifyInfoService.push(param.getId(), param.getUrl()));
}
/**
* 结束重试
*
* @param param 表单数据
* @return 返回影响行数
*/
@PostMapping("/end")
public Result<Integer> end(@Validated @RequestBody IdParam param) {
return Result.ok(notifyInfoService.end(param.getId()));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<PageResult> {
return http.get<PageResult, any>(apiUrl.page, { params });
},
/**
* 推送
* @param data 表单内容
*/
push(data: object) {
return http.post<Result<any>, any>(apiUrl.push, { data });
},
/**
* 关闭
* @param data 表单内容
*/
end(data: object) {
return http.post<Result<any>, any>(apiUrl.end, { data });
}
};

View File

@@ -17,3 +17,10 @@ export enum RegSource {
SYS = 1, SYS = 1,
CUSTOM = 2 CUSTOM = 2
} }
export enum NotifyStatusEnum {
SUCCESS = 1,
FAIL = 2,
END = 3,
STOP = 4
}

View File

@@ -239,7 +239,8 @@ export function useIsvList() {
id: 0, id: 0,
status: StatusEnum.ENABLE, status: StatusEnum.ENABLE,
keyFormat: KeyFormatEnum.PKCS8, keyFormat: KeyFormatEnum.PKCS8,
remark: "" remark: "",
notifyUrl: ""
}; };
}; };
const editFormData = ref<FieldValues>(editFormDataGen()); const editFormData = ref<FieldValues>(editFormDataGen());
@@ -280,6 +281,11 @@ export function useIsvList() {
showWordLimit: true, showWordLimit: true,
autosize: { minRows: 2, maxRows: 4 } autosize: { minRows: 2, maxRows: 4 }
} }
},
{
label: "回调接口",
prop: "notifyUrl",
valueType: "input"
} }
]; ];

View File

@@ -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<any[]>();
// 默认每页条数,默认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<any>(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(
`<textarea style="width: 400px;height: 300px;" readonly>${row.content}</textarea>`,
"发送内容",
{
dangerouslyUseHTMLString: true
}
);
}
return {
actionButtons,
dlgShow,
dlgTitle,
editFormColumns,
editFormData,
editFormRules,
handleAdd,
handlePaginationChange,
handleSave,
handleSearch,
pageInfo,
searchFormColumns,
searchFormData,
tableColumns,
tableData,
total,
viewContent
};
}

View File

@@ -0,0 +1,73 @@
<script setup lang="ts">
import { useEntNotifyInfo } from "./index";
const {
actionButtons,
dlgShow,
dlgTitle,
editFormColumns,
editFormData,
editFormRules,
handleAdd,
handlePaginationChange,
handleSave,
handleSearch,
pageInfo,
searchFormColumns,
searchFormData,
tableColumns,
tableData,
total,
viewContent
} = useEntNotifyInfo();
</script>
<template>
<el-card shadow="never">
<template #header>
<PlusSearch
v-model="searchFormData"
:columns="searchFormColumns"
:show-number="3"
label-position="right"
:has-reset="true"
@reset="handleSearch"
@search="handleSearch"
/>
</template>
<PlusTable
:columns="tableColumns"
:table-data="tableData"
:action-bar="{ buttons: actionButtons, width: 130 }"
:pagination="{
total,
modelValue: pageInfo,
pageSizeList: [10, 20, 50, 100],
align: 'right'
}"
showOverflowTooltip
adaptive
@paginationChange="handlePaginationChange"
>
<template #title>
<el-button type="primary" @click="handleAdd">新增</el-button>
</template>
<template #plus-cell-content="scoped">
<el-link type="primary" @click="viewContent(scoped.row)">
查看
</el-link>
</template>
</PlusTable>
<PlusDialogForm
v-model:visible="dlgShow"
v-model="editFormData"
:dialog="{ title: dlgTitle }"
:form="{
columns: editFormColumns,
rules: editFormRules,
labelWidth: '100px',
labelPosition: 'right'
}"
:hasErrorTip="false"
@confirm="handleSave"
/>
</el-card>
</template>

25
sop-example/example-notify/.gitignore vendored Executable file
View File

@@ -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/

View File

@@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.15</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.gitee.sop</groupId>
<artifactId>example-notify</artifactId>
<version>5.0.0-SNAPSHOT</version>
<name>example-notify</name>
<properties>
<java.version>1.8</java.version>
<!-- dubbo版本 -->
<dubbo.version>3.2.16</dubbo.version>
</properties>
<dependencies>
<!-- 回调处理 -->
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-notify-api</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<!-- sop接入依赖 -->
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-spring-boot-starter</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<!-- nacos注册中心 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-nacos-spring-boot-starter</artifactId>
</dependency>
<!-- zookeeper注册中心 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-zookeeper-curator5-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sdk-java</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<!-- 打包时跳过测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<!-- 文档推送 -->
<plugin>
<groupId>com.ly.smart-doc</groupId>
<artifactId>smart-doc-maven-plugin</artifactId>
<version>3.0.9</version>
<configuration>
<!--指定生成文档的使用的配置文件-->
<configFile>./src/main/resources/smart-doc.json</configFile>
<!--指定项目名称-->
<projectName>${project.artifactId}</projectName>
</configuration>
<dependencies>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-service-support</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>maven_central</id>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
</repositories>
</project>

View File

@@ -0,0 +1 @@
mvn -Dfile.encoding=UTF-8 -Dcheckstyle.skip=true smart-doc:torna-rpc

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
dubbo.registry.address=zookeeper://localhost:2181

View File

@@ -0,0 +1,2 @@
dubbo.registry.address=nacos://localhost:8848

View File

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

View File

@@ -0,0 +1,27 @@
{
// 开启推送
"enable": true,
// 扫描package多个用;隔开
"basePackage": "com.gitee.sop.payment.open",
// 推送URLIP端口对应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 }
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,5 +13,6 @@
<module>example-payment</module> <module>example-payment</module>
<module>example-product</module> <module>example-product</module>
<module>example-rest</module> <module>example-rest</module>
<module>example-notify</module>
</modules> </modules>
</project> </project>

24
sop-notify/pom.xml Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-parent</artifactId>
<version>5.0.0-SNAPSHOT</version>
</parent>
<artifactId>sop-notify</artifactId>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<modules>
<module>sop-notify-api</module>
<module>sop-notify-service</module>
</modules>
</project>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-notify-api</artifactId>
<version>5.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

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

View File

@@ -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<String, Object> bizParams;
/**
* 备注
*/
private String remark;
}

View File

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

View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-notify</artifactId>
<version>5.0.0-SNAPSHOT</version>
</parent>
<artifactId>sop-notify-service</artifactId>
<version>5.0.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<!-- dubbo版本 -->
<dubbo.version>3.2.16</dubbo.version>
</properties>
<dependencies>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-notify-api</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sdk-java</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.gitee.durcframework</groupId>
<artifactId>fastmybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>net.oschina.durcframework</groupId>
<artifactId>http-helper</artifactId>
</dependency>
<!-- sop接入依赖 -->
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-spring-boot-starter</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<!-- nacos注册中心 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-nacos-spring-boot-starter</artifactId>
</dependency>
<!-- zookeeper注册中心 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-zookeeper-curator5-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<!-- 打包时跳过测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<!-- 文档推送 -->
<plugin>
<groupId>com.ly.smart-doc</groupId>
<artifactId>smart-doc-maven-plugin</artifactId>
<version>3.0.9</version>
<configuration>
<!--指定生成文档的使用的配置文件-->
<configFile>./src/main/resources/smart-doc.json</configFile>
<!--指定项目名称-->
<projectName>${project.artifactId}</projectName>
</configuration>
<dependencies>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-service-support</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>maven_central</id>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
</repositories>
</project>

View File

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

View File

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

View File

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

View File

@@ -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<NotifyInfo> {
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<NotifyInfo, NotifyInfoMapper> {
// 对应第123...次尝试
// 即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<NotifyInfo> 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<String, String> 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<String, String> buildParams(NotifyBO notifyBO) throws SopSignException {
// 公共请求参数
Map<String, String> 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<String, Object> 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;
}
}

View File

@@ -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<String, Object> bizParams;
/**
* 备注
*/
private String remark;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.gitee.sop.notify.dao.mapper.IsvMapper">
<select id="getPrivatePlatformKey" resultType="String">
select t2.private_key_platform
from isv_info t inner join isv_keys t2 on t.id = t2.isv_id
where app_id=#{appId}
limit 1
</select>
<select id="getNotifyUrl" resultType="String">
select t.notify_url
from isv_info t
where app_id=#{appId}
limit 1
</select>
</mapper>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 全局映射器启用缓存 -->
<setting name="cacheEnabled" value="true" />
<!-- 查询时,关闭关联对象即时加载以提高性能 -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 对于未知的SQL查询允许返回不同的结果集以达到通用的效果 -->
<setting name="multipleResultSetsEnabled" value="true" />
<!-- 允许使用列标签代替列名 -->
<setting name="useColumnLabel" value="true" />
<!-- 允许使用自定义的主键值(比如由程序生成的UUID 32位编码作为键值)数据表的PK生成策略将被覆盖 -->
<setting name="useGeneratedKeys" value="false" />
<!-- 对于批量更新操作缓存SQL以提高性能:BATCH -->
<setting name="defaultExecutorType" value="SIMPLE" />
<!-- 超时设置 -->
<setting name="defaultStatementTimeout" value="25000" />
</settings>
<plugins>
<plugin interceptor="com.gitee.fastmybatis.core.support.plugin.SqlFormatterPlugin">
</plugin>
</plugins>
</configuration>

View File

@@ -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<String, Object> query;
/**
* 请求头
*/
private Map<String, Object> header;
/**
* 请求体
*/
private Map<String, Object> body;
}

View File

@@ -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<String, Object> 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<String, Object> 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);
}
}

31
sop.sql
View File

@@ -493,7 +493,6 @@ CREATE TABLE `help_doc` (
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='帮助内容表'; ) ENGINE=InnoDB COMMENT='帮助内容表';
-- 2025-08-17 -- 2025-08-17
CREATE TABLE `isv_merchant` CREATE TABLE `isv_merchant`
( (
@@ -507,3 +506,33 @@ CREATE TABLE `isv_merchant`
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uk_appid_merchant` (`app_id`, merchant_code) UNIQUE KEY `uk_appid_merchant` (`app_id`, merchant_code)
) ENGINE=InnoDB COMMENT='isv商户关系表'; ) 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);

29
upgrade/sop-20251101.sql Normal file
View File

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