chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<?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.xspaceagi</groupId>
|
||||
<artifactId>app-platform-pay</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>app-platform-pay-sdk</artifactId>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.xspaceagi.pay.sdk;
|
||||
|
||||
/** Open API 统一 4 位业务码 */
|
||||
public final class OpenApiCodes {
|
||||
|
||||
private OpenApiCodes() {
|
||||
}
|
||||
|
||||
public static final String SUCCESS = "0000";
|
||||
public static final String BAD_REQUEST = "0400";
|
||||
public static final String UNAUTHORIZED = "0401";
|
||||
public static final String FORBIDDEN = "0403";
|
||||
public static final String INTERNAL_ERROR = "0500";
|
||||
public static final String BUSINESS_ERROR = "9999";
|
||||
|
||||
public static String fromHttpStatus(int httpStatus) {
|
||||
if (httpStatus < 0 || httpStatus > 9999) {
|
||||
return INTERNAL_ERROR;
|
||||
}
|
||||
return String.format("%04d", httpStatus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.OpenApiCodes;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ApiResponse<T> {
|
||||
private String code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return ApiResponse.<T>builder().code(OpenApiCodes.SUCCESS).message("Success").data(data).build();
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> fail(String code, String message) {
|
||||
return fail(code, message, null);
|
||||
}
|
||||
|
||||
/** 业务失败但附带部分 data(如 bootstrap 会话无效时仍返回收银台品牌字段) */
|
||||
public static <T> ApiResponse<T> fail(String code, String message, T data) {
|
||||
return ApiResponse.<T>builder().code(code).message(message).data(data).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ByClientIdRequest {
|
||||
private String clientId;
|
||||
|
||||
private Long timestamp;
|
||||
|
||||
private String nonce;
|
||||
|
||||
private String signature;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** 业务服务端调用:验签通过后创建收银台会话并返回跳转 URL */
|
||||
@Data
|
||||
public class CashierSessionCreateRequest {
|
||||
private String clientId;
|
||||
|
||||
private Long timestamp;
|
||||
|
||||
private String nonce;
|
||||
|
||||
private String signature;
|
||||
|
||||
private String gatewayPaymentOrderNo;
|
||||
|
||||
private Long orderAmount;
|
||||
|
||||
private String subject;
|
||||
|
||||
/** 支付成功回跳,可空 */
|
||||
private String bizRedirectUrl;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CashierSessionCreateResponse {
|
||||
private String cashierUrl;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DeveloperAccountInfoResponse {
|
||||
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
/** 关联 {@code user.id} */
|
||||
private Long userId;
|
||||
private String userName;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String realName;
|
||||
private String idCardNo;
|
||||
/** 身份证正面照片 URL */
|
||||
private String idCardFrontPhotoUrl;
|
||||
/** 身份证反面照片 URL */
|
||||
private String idCardBackPhotoUrl;
|
||||
private String bankName;
|
||||
private String branchName;
|
||||
private String bankCardNo;
|
||||
private Date created;
|
||||
private Date modified;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.MerchantOnboardingStatus;
|
||||
import com.xspaceagi.pay.sdk.enums.MerchantOnboardingType;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class MerchantOnboardingPageRequest {
|
||||
private String clientId;
|
||||
|
||||
private MerchantOnboardingType onboardingType;
|
||||
|
||||
private MerchantOnboardingStatus status;
|
||||
|
||||
private Long timestamp;
|
||||
|
||||
private String nonce;
|
||||
|
||||
private String signature;
|
||||
|
||||
private Integer page;
|
||||
|
||||
private Integer pageSize;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.MerchantOnboardingStatus;
|
||||
import com.xspaceagi.pay.sdk.enums.MerchantOnboardingType;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class MerchantOnboardingResponse {
|
||||
private Long id;
|
||||
private MerchantOnboardingType onboardingType;
|
||||
private String clientId;
|
||||
private Long userId;
|
||||
private MerchantOnboardingStatus status;
|
||||
private String auditRemark;
|
||||
/** 审核时间 */
|
||||
private LocalDateTime auditedAt;
|
||||
|
||||
/** 商户名称 */
|
||||
private String merchantName;
|
||||
/** 商户简称 */
|
||||
private String merchantShortName;
|
||||
/** 统一社会信用代码 */
|
||||
private String creditCode;
|
||||
/** 注册地址 */
|
||||
private String registeredAddress;
|
||||
/** 法人姓名 */
|
||||
private String legalPersonName;
|
||||
/** 法人证件号 */
|
||||
private String legalPersonIdNo;
|
||||
|
||||
/** 经办联系人 */
|
||||
private String contactName;
|
||||
/** 联系人电话 */
|
||||
private String contactPhone;
|
||||
/** 联系人邮箱 */
|
||||
private String contactEmail;
|
||||
|
||||
/** 开户名称 */
|
||||
private String bankName;
|
||||
/** 开户银行名称 */
|
||||
private String bankAccountName;
|
||||
/** 开户银行支行名称 */
|
||||
private String bankBranchName;
|
||||
/** 银行账号 */
|
||||
private String bankAccountNo;
|
||||
/** 银行回单附言备注 */
|
||||
private String bankReceiptRemark;
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
/** 营业执照 */
|
||||
private String orgCertificateUrl;
|
||||
/** 法人身份证正面 */
|
||||
private String legalPersonIdCardFrontUrl;
|
||||
/** 法人身份证背面 */
|
||||
private String legalPersonIdCardBackUrl;
|
||||
/** 财务室照片 */
|
||||
private String photoFinanceRoomUrl;
|
||||
/** 门头照片 */
|
||||
private String photoGateUrl;
|
||||
/** 地标照片 */
|
||||
private String photoLandmarkUrl;
|
||||
/** 银行开户证明 */
|
||||
private String bankAccountProofUrl;
|
||||
|
||||
private LocalDateTime created;
|
||||
private LocalDateTime modified;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class MerchantOnboardingUpdateRequest extends MerchantOnboardingUpsertRequest {
|
||||
private Long id;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.MerchantOnboardingStatus;
|
||||
import com.xspaceagi.pay.sdk.enums.MerchantOnboardingType;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 创建/全量更新进件资料(图片字段均为 URL)。
|
||||
*/
|
||||
@Data
|
||||
public class MerchantOnboardingUpsertRequest {
|
||||
private MerchantOnboardingType onboardingType;
|
||||
|
||||
/** 由服务端按登录租户凭证写入,请求体传入将被忽略 */
|
||||
private String clientId;
|
||||
|
||||
/** 用户进件填写 */
|
||||
private Long userId;
|
||||
|
||||
/** 毫秒级 Unix 时间戳 */
|
||||
private Long timestamp;
|
||||
|
||||
private String nonce;
|
||||
|
||||
private String signature;
|
||||
|
||||
private MerchantOnboardingStatus status;
|
||||
|
||||
private String auditRemark;
|
||||
|
||||
private String merchantName;
|
||||
private String merchantShortName;
|
||||
private String creditCode;
|
||||
private String registeredAddress;
|
||||
private String legalPersonName;
|
||||
private String legalPersonIdNo;
|
||||
|
||||
private String contactName;
|
||||
private String contactPhone;
|
||||
private String contactEmail;
|
||||
|
||||
private String bankAccountName;
|
||||
private String bankName;
|
||||
private String bankBranchName;
|
||||
private String bankAccountNo;
|
||||
private String bankReceiptRemark;
|
||||
private String remark;
|
||||
|
||||
private String orgCertificateUrl;
|
||||
private String legalPersonIdCardUrl;
|
||||
/** 法人身份证人像面/正面图片 URL */
|
||||
private String legalPersonIdCardFrontUrl;
|
||||
/** 法人身份证国徽面/反面图片 URL */
|
||||
private String legalPersonIdCardBackUrl;
|
||||
private String photoFinanceRoomUrl;
|
||||
private String photoGateUrl;
|
||||
private String photoLandmarkUrl;
|
||||
private String bankAccountProofUrl;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class PageResult<T> {
|
||||
private List<T> records;
|
||||
private long total;
|
||||
/** 当前页,从 1 开始 */
|
||||
private long page;
|
||||
private long pageSize;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PayConfigQueryRequest {
|
||||
private String clientId;
|
||||
|
||||
/** 毫秒级 Unix 时间戳 */
|
||||
private Long timestamp;
|
||||
|
||||
private String nonce;
|
||||
|
||||
private String signature;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class PayConfigResponse {
|
||||
private BigDecimal payRate;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayMode;
|
||||
import com.xspaceagi.pay.sdk.enums.PaymentStatus;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class PaymentOrderCreateResponse {
|
||||
private String gatewayPaymentOrderNo;
|
||||
private String bizOrderNo;
|
||||
private PaymentStatus status;
|
||||
private PayMode payMode;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PaymentStatusQueryRequest {
|
||||
private String clientId;
|
||||
|
||||
private Long timestamp;
|
||||
|
||||
private String nonce;
|
||||
|
||||
private String signature;
|
||||
|
||||
private String gatewayPaymentOrderNo;
|
||||
|
||||
/**
|
||||
* 为 true 时向支付渠道发起交易查询(如安心付 /query),成功则落库并调用通知状态更新(/update/notify_status)。
|
||||
* 默认 false,仅读本地库。
|
||||
*/
|
||||
private Boolean syncFromChannel;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ScanOrderAndTransactionCreateRequest {
|
||||
private String clientId;
|
||||
|
||||
private Long timestamp;
|
||||
|
||||
private String nonce;
|
||||
|
||||
private String signature;
|
||||
|
||||
private String bizOrderNo;
|
||||
|
||||
private PayChannel payChannel;
|
||||
|
||||
private Long orderAmount;
|
||||
|
||||
private String subject;
|
||||
|
||||
private String ext;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ScanOrderCreateRequest {
|
||||
private String clientId;
|
||||
|
||||
private Long timestamp;
|
||||
|
||||
private String nonce;
|
||||
|
||||
private String signature;
|
||||
|
||||
private String bizOrderNo;
|
||||
|
||||
private Long orderAmount;
|
||||
|
||||
private String subject;
|
||||
|
||||
private String ext;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayMode;
|
||||
import com.xspaceagi.pay.sdk.enums.PaymentStatus;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ScanOrderCreateResponse {
|
||||
private String gatewayPaymentOrderNo;
|
||||
private String gatewayPaymentTxNo;
|
||||
/** 渠道服务费,单位分 */
|
||||
private Long providerFee;
|
||||
/** 平台服务费,单位分 */
|
||||
private Long platformFee;
|
||||
/** 净额,单位分(订单金额扣减手续费后) */
|
||||
private Long netAmount;
|
||||
private PaymentStatus status;
|
||||
private PayChannel payChannel;
|
||||
private PayMode payMode;
|
||||
private String qrCodeContent;
|
||||
private String expiredAt;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import com.xspaceagi.pay.sdk.enums.PayMode;
|
||||
import com.xspaceagi.pay.sdk.enums.PaymentStatus;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ScanOrderStatusQueryResponse {
|
||||
private String gatewayPaymentOrderNo;
|
||||
private String gatewayPaymentTxNo;
|
||||
private PaymentStatus status;
|
||||
private PaymentStatus transactionStatus;
|
||||
private PayChannel payChannel;
|
||||
private PayMode payMode;
|
||||
private Long orderAmount;
|
||||
private Long providerFee;
|
||||
private Long platformFee;
|
||||
private Long netAmount;
|
||||
private String qrCodeContent;
|
||||
private String paidAt;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.xspaceagi.pay.sdk.dto;
|
||||
|
||||
import com.xspaceagi.pay.sdk.enums.PayChannel;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ScanTransactionCreateRequest {
|
||||
private String clientId;
|
||||
|
||||
private Long timestamp;
|
||||
|
||||
private String nonce;
|
||||
|
||||
private String signature;
|
||||
|
||||
private String gatewayPaymentOrderNo;
|
||||
|
||||
private PayChannel payChannel;
|
||||
|
||||
private String clientIp;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.xspaceagi.pay.sdk.enums;
|
||||
|
||||
public enum MerchantOnboardingStatus {
|
||||
DRAFT("草稿"),
|
||||
PENDING_REVIEW("待审核"),
|
||||
UNDER_REVIEW("审核中"),
|
||||
APPROVED("已通过"),
|
||||
REJECTED("已拒绝");
|
||||
|
||||
private final String name;
|
||||
|
||||
MerchantOnboardingStatus(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/** 管理台/对外展示用中文名 */
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xspaceagi.pay.sdk.enums;
|
||||
|
||||
public enum MerchantOnboardingType {
|
||||
PLATFORM("平台进件"),
|
||||
TENANT("租户进件"),
|
||||
USER("用户进件");
|
||||
|
||||
private final String name;
|
||||
|
||||
MerchantOnboardingType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/** 管理台/对外展示用中文名 */
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.xspaceagi.pay.sdk.enums;
|
||||
|
||||
public enum PayChannel {
|
||||
WxPay("微信支付"),
|
||||
AliPay("支付宝"),
|
||||
UnionPay("银联支付");
|
||||
|
||||
private final String name;
|
||||
|
||||
PayChannel(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/** 管理台/对外展示用中文名 */
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/** API / 管理台入参:枚举名,大小写不敏感 */
|
||||
public static PayChannel parse(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
throw new IllegalArgumentException("payChannel is blank");
|
||||
}
|
||||
String s = raw.trim();
|
||||
for (PayChannel pc : values()) {
|
||||
if (pc.name().equalsIgnoreCase(s)) {
|
||||
return pc;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown pay channel: " + raw);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.xspaceagi.pay.sdk.enums;
|
||||
|
||||
public enum PayMode {
|
||||
scan("主扫");
|
||||
|
||||
private final String name;
|
||||
|
||||
PayMode(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/** 管理台/对外展示用中文名 */
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.xspaceagi.pay.sdk.enums;
|
||||
|
||||
public enum PaymentStatus {
|
||||
INIT("待发起"),
|
||||
PENDING("待支付"),
|
||||
PAID("成功"),
|
||||
FAILED("失败"),
|
||||
CLOSED("关单");
|
||||
|
||||
private final String name;
|
||||
|
||||
PaymentStatus(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/** 管理台/对外展示用中文名 */
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public static PaymentStatus fromPersistedString(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
throw new IllegalArgumentException("payment status blank");
|
||||
}
|
||||
String t = raw.trim();
|
||||
return valueOf(t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.xspaceagi.pay.sdk.service;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.DeveloperAccountInfoResponse;
|
||||
|
||||
public interface IDeveloperAccountInfoRpcService {
|
||||
|
||||
DeveloperAccountInfoResponse getDeveloperAccountInfo(Long tenantId, Long userId);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.xspaceagi.pay.sdk.service;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentOrderCreateResponse;
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderStatusQueryResponse;
|
||||
|
||||
/** 平台内支付 RPC 契约(由 pay-application 实现,供 bill 等外部模块依赖调用)。 */
|
||||
public interface IPaymentRpcService {
|
||||
|
||||
/** 创建扫码支付订单 */
|
||||
PaymentOrderCreateResponse createOrderForScan(ScanOrderCreateRequest request);
|
||||
|
||||
/**
|
||||
* 获取收银台地址;会话有过期时间,过期后需重新调用。
|
||||
*/
|
||||
CashierSessionCreateResponse createCashierSession(CashierSessionCreateRequest request);
|
||||
|
||||
/** 查询支付状态(仅更新 pay_order,不通知 Bill) */
|
||||
ScanOrderStatusQueryResponse queryStatus(PaymentStatusQueryRequest request);
|
||||
|
||||
/**
|
||||
* 结算页主动同步:按业务单号查网关终态并通知 Bill(与支付轮询任务单 tick 一致,幂等)。
|
||||
*
|
||||
* @param bizOrderNo 业务订单号,通常为 Bill 订单 id 字符串
|
||||
* @return 是否已通知到 Bill 终态或此前已完成通知
|
||||
*/
|
||||
boolean syncSettlementForBizOrderNo(String bizOrderNo);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateRequest;
|
||||
|
||||
/** 收银台 Open API 路径常量;{@link #PATH_SESSION} 需 HMAC 验签,其余为免签路径。 */
|
||||
public final class OpenApiCashierSign {
|
||||
|
||||
public static final String BASE = "/open-api/cashier";
|
||||
public static final String PATH_SESSION = BASE + "/session";
|
||||
public static final String PATH_SESSION_BOOTSTRAP = BASE + "/session/bootstrap";
|
||||
public static final String PATH_SCAN_INIT = BASE + "/scan/init";
|
||||
public static final String PATH_SCAN_STATUS = BASE + "/scan/status";
|
||||
|
||||
private OpenApiCashierSign() {}
|
||||
|
||||
public static byte[] signCreateSession(CashierSessionCreateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_SESSION, request, clientSecret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 接入方 Open API 签名通用入口(自动生成 timestamp/nonce 的重载见各 {@code OpenApi*Sign})。
|
||||
* <p>body 哈希仅包含 {@link OpenApiSignPolicies} 中为该 path 配置的业务字段;完整 JSON 仍可带 ext 等非签名字段。</p>
|
||||
*/
|
||||
public final class OpenApiClientSignHelper {
|
||||
|
||||
private OpenApiClientSignHelper() {}
|
||||
|
||||
public static String newNonce() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
public static long nowTimestamp() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public static byte[] buildSignedPostBody(
|
||||
String path, Object requestBody, long timestamp, String nonce, String clientSecret) {
|
||||
return OpenApiSignedRequestSupport.buildSignedPostBody(path, requestBody, timestamp, nonce, clientSecret);
|
||||
}
|
||||
|
||||
public static String buildSignedPostJson(
|
||||
String path, Object requestBody, long timestamp, String nonce, String clientSecret) {
|
||||
return OpenApiSignedRequestSupport.buildSignedPostJson(path, requestBody, timestamp, nonce, clientSecret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.ByClientIdRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.MerchantOnboardingPageRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.MerchantOnboardingUpdateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.MerchantOnboardingUpsertRequest;
|
||||
|
||||
/** 进件 Open API 路径与签名辅助 */
|
||||
public final class OpenApiMerchantOnboardingSign {
|
||||
|
||||
public static final String BASE = "/open-api/merchant-onboarding";
|
||||
public static final String PATH_ADD = BASE + "/add";
|
||||
public static final String PATH_UPDATE = BASE + "/update";
|
||||
public static final String PATH_GET_BY_CLIENT_ID = BASE + "/get-by-client-id";
|
||||
public static final String PATH_LIST_BY_CLIENT_ID = BASE + "/list-by-client-id";
|
||||
public static final String PATH_PAGE = BASE + "/page";
|
||||
|
||||
private OpenApiMerchantOnboardingSign() {}
|
||||
|
||||
public static byte[] signAdd(MerchantOnboardingUpsertRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_ADD, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signUpdate(MerchantOnboardingUpdateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_UPDATE, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signGetByClientId(ByClientIdRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_GET_BY_CLIENT_ID, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signListByClientId(ByClientIdRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_LIST_BY_CLIENT_ID, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signPage(MerchantOnboardingPageRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_PAGE, request, clientSecret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Open API HMAC-SHA256 签名与验签(唯一实现,接入方与服务端共用)。
|
||||
* <p>接入方优先使用各 {@code OpenApi*Sign} 或 {@link OpenApiClientSignHelper};服务端验签使用 {@link #verify}。</p>
|
||||
* <p>body 哈希字段见 {@link OpenApiSignPolicies}。</p>
|
||||
*/
|
||||
public final class OpenApiPayloadSigner {
|
||||
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
|
||||
private OpenApiPayloadSigner() {}
|
||||
|
||||
/**
|
||||
* @param httpBody 可为最终 HTTP body(含 signature)或不含 signature;哈希前均会剔除 signature 字段
|
||||
*/
|
||||
public static String sign(
|
||||
String httpMethod, String path, long timestamp, String nonce, byte[] httpBody, String clientSecret) {
|
||||
if (clientSecret == null) {
|
||||
throw new IllegalArgumentException("clientSecret must not be null");
|
||||
}
|
||||
String canonical = OpenApiSignCanonical.build(httpMethod, path, timestamp, nonce, httpBody);
|
||||
return hmacSha256Hex(clientSecret, canonical);
|
||||
}
|
||||
|
||||
public static boolean verify(
|
||||
String httpMethod,
|
||||
String path,
|
||||
long timestamp,
|
||||
String nonce,
|
||||
byte[] rawBody,
|
||||
String clientSecret,
|
||||
String signature) {
|
||||
if (signature == null || signature.isBlank() || clientSecret == null) {
|
||||
return false;
|
||||
}
|
||||
String expected = sign(httpMethod, path, timestamp, nonce, rawBody, clientSecret);
|
||||
return constantTimeEqualsHex(expected, signature.trim());
|
||||
}
|
||||
|
||||
private static String hmacSha256Hex(String secret, String data) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_SHA256);
|
||||
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256));
|
||||
return Hex.encodeHexString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new IllegalStateException("HMAC-SHA256 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean constantTimeEqualsHex(String expectedHex, String actualHex) {
|
||||
try {
|
||||
byte[] expected = Hex.decodeHex(expectedHex.toCharArray());
|
||||
byte[] actual = Hex.decodeHex(actualHex.toLowerCase(Locale.ROOT).toCharArray());
|
||||
return expected.length == actual.length && MessageDigest.isEqual(expected, actual);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.PayConfigQueryRequest;
|
||||
|
||||
/** 支付配置 Open API 路径与签名辅助 */
|
||||
public final class OpenApiPaymentConfigSign {
|
||||
|
||||
public static final String BASE = "/open-api/payment/config";
|
||||
public static final String PATH_QUERY = BASE + "/query";
|
||||
|
||||
private OpenApiPaymentConfigSign() {}
|
||||
|
||||
public static byte[] signQuery(PayConfigQueryRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_QUERY, request, clientSecret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.xspaceagi.pay.sdk.dto.PaymentStatusQueryRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderAndTransactionCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanOrderCreateRequest;
|
||||
import com.xspaceagi.pay.sdk.dto.ScanTransactionCreateRequest;
|
||||
|
||||
/** 主扫支付 Open API 路径与签名辅助 */
|
||||
public final class OpenApiPaymentScanSign {
|
||||
|
||||
public static final String BASE = "/open-api/payment/scan";
|
||||
public static final String PATH_CREATE_ORDER_AND_TRANSACTION = BASE + "/create-order-and-transaction";
|
||||
public static final String PATH_CREATE_ORDER = BASE + "/create-order";
|
||||
public static final String PATH_CREATE_TRANSACTION = BASE + "/create-transaction";
|
||||
public static final String PATH_STATUS = BASE + "/status";
|
||||
|
||||
private OpenApiPaymentScanSign() {}
|
||||
|
||||
public static byte[] signCreateOrderAndTransaction(
|
||||
ScanOrderAndTransactionCreateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_CREATE_ORDER_AND_TRANSACTION, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signCreateOrder(ScanOrderCreateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_CREATE_ORDER, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signCreateTransaction(ScanTransactionCreateRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_CREATE_TRANSACTION, request, clientSecret);
|
||||
}
|
||||
|
||||
public static byte[] signQueryStatus(PaymentStatusQueryRequest request, String clientSecret) {
|
||||
return OpenApiSignSupport.sign(PATH_STATUS, request, clientSecret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** OpenAPI 待签名字符串:绑定 HTTP 方法、路径、时间戳、nonce 与请求体(不含 {@code signature} 字段)。 */
|
||||
final class OpenApiSignCanonical {
|
||||
|
||||
private OpenApiSignCanonical() {}
|
||||
|
||||
static String bodySha256Hex(byte[] body) {
|
||||
return DigestUtils.sha256Hex(body == null ? new byte[0] : body);
|
||||
}
|
||||
|
||||
static String build(String method, String path, long timestamp, String nonce, byte[] httpBody) {
|
||||
String m = method == null ? "" : method.trim().toUpperCase(Locale.ROOT);
|
||||
String p = OpenApiSignPolicies.normalizePath(path);
|
||||
String n = nonce == null ? "" : nonce.trim();
|
||||
List<String> signFields = OpenApiSignPolicies.signFieldsForPath(p);
|
||||
byte[] bodyForSign = OpenApiSignFieldCanonical.canonicalBytes(
|
||||
OpenApiSignJson.bodyBytesWithoutSignature(httpBody), signFields);
|
||||
return m + "\n" + p + "\n" + timestamp + "\n" + n + "\n" + bodySha256Hex(bodyForSign);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/** 仅提取参与签名的业务字段,按字段名字典序序列化为 JSON 后做 SHA256(避免全量 body 二次序列化漂移)。 */
|
||||
final class OpenApiSignFieldCanonical {
|
||||
|
||||
private OpenApiSignFieldCanonical() {}
|
||||
|
||||
static byte[] canonicalBytes(byte[] httpBody, List<String> signFieldNames) {
|
||||
if (signFieldNames == null || signFieldNames.isEmpty()) {
|
||||
return OpenApiSignJson.bodyBytesWithoutSignature(httpBody);
|
||||
}
|
||||
try {
|
||||
JsonNode root = OpenApiSignJson.mapper().readTree(httpBody == null || httpBody.length == 0 ? "{}".getBytes() : httpBody);
|
||||
if (!root.isObject()) {
|
||||
throw new IllegalArgumentException("OpenAPI sign body must be a JSON object");
|
||||
}
|
||||
ObjectNode out = OpenApiSignJson.mapper().createObjectNode();
|
||||
TreeMap<String, JsonNode> sorted = new TreeMap<>();
|
||||
for (String name : signFieldNames) {
|
||||
if (name == null || name.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
JsonNode value = root.get(name);
|
||||
if (value != null && !value.isNull()) {
|
||||
sorted.put(name, value);
|
||||
}
|
||||
}
|
||||
sorted.forEach(out::set);
|
||||
return OpenApiSignJson.mapper().writeValueAsBytes(out);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Invalid OpenAPI JSON body for field signing", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
/** Open API 签名专用 JSON(不对外作为通用工具暴露) */
|
||||
final class OpenApiSignJson {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper().disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
private OpenApiSignJson() {}
|
||||
|
||||
static ObjectMapper mapper() {
|
||||
return MAPPER;
|
||||
}
|
||||
|
||||
static byte[] bodyBytesWithoutSignature(byte[] httpBody) {
|
||||
if (httpBody == null || httpBody.length == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
try {
|
||||
JsonNode root = MAPPER.readTree(httpBody);
|
||||
if (root.isObject()) {
|
||||
((ObjectNode) root).remove("signature");
|
||||
}
|
||||
return MAPPER.writeValueAsBytes(root);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Invalid OpenAPI JSON body for signing", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 各 Open API 路径参与 body 哈希的字段(不含 {@code timestamp}/{@code nonce}/{@code signature})。
|
||||
* <p>path 与各 {@code OpenApi*Sign#PATH_*} 保持一致,为单一来源。</p>
|
||||
*/
|
||||
public final class OpenApiSignPolicies {
|
||||
|
||||
private OpenApiSignPolicies() {}
|
||||
|
||||
private static final Map<String, List<String>> BY_PATH = Map.ofEntries(
|
||||
Map.entry(
|
||||
OpenApiPaymentScanSign.PATH_CREATE_ORDER_AND_TRANSACTION,
|
||||
List.of("bizOrderNo", "clientId", "orderAmount", "payChannel")),
|
||||
Map.entry(
|
||||
OpenApiPaymentScanSign.PATH_CREATE_ORDER,
|
||||
List.of("bizOrderNo", "clientId", "orderAmount")),
|
||||
Map.entry(
|
||||
OpenApiPaymentScanSign.PATH_CREATE_TRANSACTION,
|
||||
List.of("clientId", "gatewayPaymentOrderNo", "payChannel")),
|
||||
Map.entry(
|
||||
OpenApiPaymentScanSign.PATH_STATUS,
|
||||
List.of("clientId", "gatewayPaymentOrderNo", "syncFromChannel")),
|
||||
Map.entry(OpenApiPaymentConfigSign.PATH_QUERY, List.of("clientId")),
|
||||
Map.entry(
|
||||
OpenApiMerchantOnboardingSign.PATH_ADD,
|
||||
List.of("clientId", "creditCode", "merchantName", "onboardingType", "userId")),
|
||||
Map.entry(
|
||||
OpenApiMerchantOnboardingSign.PATH_UPDATE,
|
||||
List.of("clientId", "creditCode", "id", "merchantName")),
|
||||
Map.entry(OpenApiMerchantOnboardingSign.PATH_GET_BY_CLIENT_ID, List.of("clientId")),
|
||||
Map.entry(OpenApiMerchantOnboardingSign.PATH_LIST_BY_CLIENT_ID, List.of("clientId")),
|
||||
Map.entry(
|
||||
OpenApiMerchantOnboardingSign.PATH_PAGE,
|
||||
List.of("clientId", "onboardingType", "page", "pageSize", "status")),
|
||||
Map.entry(
|
||||
OpenApiCashierSign.PATH_SESSION,
|
||||
List.of("clientId", "gatewayPaymentOrderNo", "orderAmount")));
|
||||
|
||||
public static List<String> signFieldsForPath(String path) {
|
||||
String normalized = normalizePath(path);
|
||||
List<String> fields = BY_PATH.get(normalized);
|
||||
if (fields == null) {
|
||||
throw new IllegalArgumentException("No OpenAPI sign field policy for path: " + normalized);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
static String normalizePath(String path) {
|
||||
if (path == null || path.isBlank()) {
|
||||
return "/";
|
||||
}
|
||||
String p = path.trim();
|
||||
if (!p.startsWith("/")) {
|
||||
p = "/" + p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
/** 各 {@code OpenApi*Sign} 共用的签名流程。 */
|
||||
final class OpenApiSignSupport {
|
||||
|
||||
private OpenApiSignSupport() {}
|
||||
|
||||
static byte[] sign(String path, Object request, String clientSecret) {
|
||||
long timestamp = OpenApiClientSignHelper.nowTimestamp();
|
||||
String nonce = OpenApiClientSignHelper.newNonce();
|
||||
OpenApiSignTimestamps.apply(request, timestamp, nonce);
|
||||
return OpenApiClientSignHelper.buildSignedPostBody(path, request, timestamp, nonce, clientSecret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
/** 将 {@code timestamp}/{@code nonce} 写回请求 POJO(含 spec 模块 DTO,运行时由 Jackson 更新)。 */
|
||||
final class OpenApiSignTimestamps {
|
||||
|
||||
private OpenApiSignTimestamps() {}
|
||||
|
||||
static void apply(Object request, long timestamp, String nonce) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("request must not be null");
|
||||
}
|
||||
try {
|
||||
ObjectNode patch = OpenApiSignJson.mapper().createObjectNode();
|
||||
patch.put("timestamp", timestamp);
|
||||
patch.put("nonce", nonce);
|
||||
OpenApiSignJson.mapper().readerForUpdating(request).readValue(patch);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to apply OpenAPI timestamp/nonce", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.xspaceagi.pay.sdk.sign;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/** 组装带签名的 Open API POST body(按路径策略仅对关键字段参与 body 哈希)。 */
|
||||
final class OpenApiSignedRequestSupport {
|
||||
|
||||
private OpenApiSignedRequestSupport() {}
|
||||
|
||||
static byte[] buildSignedPostBody(
|
||||
String path, Object requestBody, long timestamp, String nonce, String clientSecret) {
|
||||
ObjectNode node = toObjectNode(requestBody);
|
||||
node.remove("signature");
|
||||
node.put("timestamp", timestamp);
|
||||
node.put("nonce", nonce);
|
||||
byte[] bodyForSign = writeBytes(node);
|
||||
String signature =
|
||||
OpenApiPayloadSigner.sign("POST", path, timestamp, nonce, bodyForSign, clientSecret);
|
||||
node.put("signature", signature);
|
||||
return writeBytes(node);
|
||||
}
|
||||
|
||||
static String buildSignedPostJson(
|
||||
String path, Object requestBody, long timestamp, String nonce, String clientSecret) {
|
||||
return new String(buildSignedPostBody(path, requestBody, timestamp, nonce, clientSecret), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static ObjectNode toObjectNode(Object requestBody) {
|
||||
if (requestBody instanceof ObjectNode on) {
|
||||
return on.deepCopy();
|
||||
}
|
||||
return OpenApiSignJson.mapper().valueToTree(requestBody);
|
||||
}
|
||||
|
||||
private static byte[] writeBytes(JsonNode node) {
|
||||
try {
|
||||
return OpenApiSignJson.mapper().writeValueAsBytes(node);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Serialize OpenAPI JSON failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user