feat:聚合码支付

This commit is contained in:
dzc committed 2026-08-06 09:51:33 +08:00
1 parent d19cb1137e
commit 112c8dde3c
57 files changed
+8346 -153

No files matched your search

@@ -5,14 +5,25 @@ import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.feign.annotation.NoToken;
import com.bsd.pjcloud.thirdparty.dto.appplus.ThirdMicropayDto;
import com.bsd.pjcloud.thirdparty.dto.appplus.ThirdMiniRemotePayDto;
import com.bsd.pjcloud.thirdparty.dto.appplus.ThirdAggregateOrderCodePayDto;
import com.bsd.pjcloud.thirdparty.dto.appplus.ThirdOrderCodePayDto;
import com.bsd.pjcloud.thirdparty.dto.appplus.ThirdRemoterRefundDto;
import com.bsd.pjcloud.thirdparty.vo.appplus.pay.ThirdMiniAppPayVo;
import com.bsd.pjcloud.thirdparty.vo.appplus.pay.ThirdAggregateOrderCodePayStatusVo;
import com.bsd.pjcloud.thirdparty.vo.appplus.pay.ThirdAggregateOrderCodePayVo;
import com.bsd.pjcloud.thirdparty.vo.appplus.pay.ThirdOrderCodePayStatusVo;
import com.bsd.pjcloud.thirdparty.vo.appplus.pay.ThirdOrderCodePayVo;
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
import jakarta.validation.Valid;
import org.springframework.cloud.openfeign.FeignClient;
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.RequestParam;
/**
* 第三方支付服务内部 Feign 契约,复用统一内部调用头和服务发现配置。
*/
@FeignClient(contextId = "remoterThirdAppPlusPayService", value = ServiceNameConstants.THIRDPARTY_SERVICE)
public interface RemoterThirdAppPlusPayService {
@@ -30,6 +41,46 @@ public interface RemoterThirdAppPlusPayService {
@PostMapping("/pay-remoter/micropay")
R<String> remoterMicropay(@RequestBody ThirdMicropayDto dto);
/**
* 创建支付宝或微信订单码支付订单。
*
* @param dto 可信内部调用方提供的业务、商户和金额信息
* @return 支付订单号、二维码内容串和服务端过期时间
*/
@NoToken
@PostMapping("/pay-remoter/order-code-pay")
R<ThirdOrderCodePayVo> orderCodePay(@Valid @RequestBody ThirdOrderCodePayDto dto);
/**
* 查询订单码支付的最小状态信息。
*
* @param payNo 第三方支付服务生成的支付订单号
* @return 内部支付状态、成功标识和过期时间
*/
@NoToken
@GetMapping("/pay-remoter/order-code-pay/status")
R<ThirdOrderCodePayStatusVo> getOrderCodePayStatus(@RequestParam("payNo") String payNo);
/**
* 创建支付宝与微信共用的聚合订单二维码。
*
* @param dto 业务订单、商户和金额参数,不指定支付平台
* @return 聚合会话编号、HTTPS 二维码内容和失效时间
*/
@PostMapping("/pay-remoter/aggregate-order-code-pay")
@NoToken
R<ThirdAggregateOrderCodePayVo> aggregateOrderCodePay(@Valid @RequestBody ThirdAggregateOrderCodePayDto dto);
/**
* 按聚合会话编号查询最小支付状态。
*
* @param aggregateNo 聚合支付会话编号
* @return 聚合状态、选定平台和实际第三方子订单号
*/
@GetMapping("/pay-remoter/aggregate-order-code-pay/status")
@NoToken
R<ThirdAggregateOrderCodePayStatusVo> getAggregateOrderCodePayStatus(@RequestParam("aggregateNo") String aggregateNo);
/**
* app支付
*/
@@ -0,0 +1,101 @@
package com.bsd.pjcloud.thirdparty.constant.pay;
import cn.hutool.core.util.ObjectUtil;
import java.util.Arrays;
/**
* 聚合订单码支付会话状态。
*
* <p>状态编码持久化到 {@code third_aggregate_pay_session.status},用于约束扫码选平台、
* 第三方子订单创建以及最终支付结果同步的单向状态流转。</p>
*/
public enum ThirdAggregatePaySessionStatusEnum {
/**
* 聚合码已经创建,尚未有支付宝或微信完成付款人授权。
*/
WAIT_SCAN("10", "等待扫码"),
/**
* 首个扫码平台已经锁定,正在创建对应平台的 JSAPI 子订单。
*/
PLATFORM_CREATING("15", "平台订单创建中"),
/**
* 第三方 JSAPI 子订单创建成功,正在等待付款结果。
*/
PAYING("20", "支付中"),
/**
* 绑定的第三方子订单已经确认支付成功。
*/
PAY_SUCCESS("30", "支付成功"),
/**
* 绑定的第三方子订单已被支付平台关闭或撤销。
*/
PAY_CLOSED("40", "交易已关闭"),
/**
* 聚合会话在未产生成功支付结果前超过服务端有效期。
*/
EXPIRED("50", "已过期"),
/**
* 选定平台后创建第三方子订单失败,当前聚合码不可继续支付。
*/
CREATE_FAILED("60", "创建失败");
/**
* 持久化到聚合支付会话表并通过状态接口返回的协议编码。
*/
private final String type;
/**
* 用于管理端和诊断信息展示的中文状态说明。
*/
private final String description;
/**
* 创建一个聚合支付会话状态枚举值。
*
* @param type 持久化状态编码
* @param description 中文状态说明
*/
ThirdAggregatePaySessionStatusEnum(String type, String description) {
this.type = type;
this.description = description;
}
/**
* 获取持久化状态编码。
*
* @return 聚合支付会话状态编码
*/
public String getType() {
return type;
}
/**
* 获取中文状态说明。
*
* @return 聚合支付会话状态说明
*/
public String getDescription() {
return description;
}
/**
* 按持久化编码解析聚合支付会话状态。
*
* @param type 数据库或接口中的状态编码,允许为空
* @return 匹配状态;编码为空或未知时返回 {@code null}
*/
public static ThirdAggregatePaySessionStatusEnum valueOfByType(String type) {
if (ObjectUtil.isEmpty(type)) {
return null;
}
return Arrays.stream(values()).filter(value -> ObjectUtil.equals(value.type, type)).findFirst().orElse(null);
}
}
@@ -1,32 +1,100 @@
package com.bsd.pjcloud.thirdparty.constant.pay;
import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 第三方支付订单状态
* 第三方支付订单状态。
*
* <p>状态编码会持久化到 {@code third_pay_order.status},不得随意修改已有编码。</p>
*/
@AllArgsConstructor
@Getter
public enum ThirdPayOrderStatusEnum {
/**
* 支付订单已创建,尚未确认买家付款。
*/
PAY_UNPAID("10", "未支付"),
/**
* 支付平台已经确认买家付款成功。
*/
PAY_SUCCESS("20", "支付成功"),
/**
* 创建支付或支付处理明确失败,不能继续使用当前支付订单。
*/
PAY_FAILED("30", "支付失败"),
/**
* 支付平台已关闭交易,包括二维码超时后撤销成功的场景。
*/
PAY_CLOSED("40", "交易已关闭"),
/**
* 退款请求已受理,最终退款结果尚未确认。
*/
REFUNDING("100", "退款中"),
/**
* 退款已经成功完成。
*/
REFUND_SUCCESS("200", "退款成功"),
/**
* 退款处理明确失败。
*/
REFUND_FAILED("300", "退款失败"),
;
/**
* 持久化到支付订单表的状态编码,也是对外状态查询返回的协议值。
*/
private final String type;
/**
* 用于管理端和诊断信息展示的中文状态说明。
*/
private final String description;
/**
* 创建一个支付订单状态枚举值。
*
* @param type 持久化状态编码
* @param description 中文状态说明
*/
ThirdPayOrderStatusEnum(String type, String description) {
this.type = type;
this.description = description;
}
/**
* 获取持久化状态编码。
*
* @return 支付订单状态编码
*/
public String getType() {
return type;
}
/**
* 获取状态的中文说明。
*
* @return 中文状态说明
*/
public String getDescription() {
return description;
}
/**
* 按持久化状态编码解析枚举。
*
* @param type 支付订单状态编码,允许为空
* @return 匹配的状态;编码为空或未知时返回 {@code null}
*/
public static ThirdPayOrderStatusEnum valueOfByType(String type) {
if (ObjectUtil.isEmpty(type)) return null;
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().get();
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().orElse(null);
}
}
@@ -0,0 +1,53 @@
package com.bsd.pjcloud.thirdparty.dto.appplus;
import com.bsd.pjcloud.thirdparty.constant.pay.ThirdPayOrderBizTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
/**
* 支付宝与微信聚合订单码创建请求。
*
* <p>请求不指定支付平台,实际平台由付款人使用支付宝或微信扫码后在服务端原子选定。</p>
*/
@Data
@Schema(description = "支付宝与微信聚合订单码创建请求")
public class ThirdAggregateOrderCodePayDto {
/**
* 支付业务类型,首版必须为 {@link ThirdPayOrderBizTypeEnum#YXFJ_PAY}。
*/
@NotNull(message = "支付业务类型不能为空")
@Schema(description = "支付业务类型", requiredMode = Schema.RequiredMode.REQUIRED)
private ThirdPayOrderBizTypeEnum bizType;
/**
* 调用方业务订单唯一标识,用于同一租户内的支付幂等和跨二维码互斥。
*/
@NotBlank(message = "支付业务id不能为空")
@Schema(description = "支付业务id", requiredMode = Schema.RequiredMode.REQUIRED)
private String bizId;
/**
* 收款商户ID,用于读取商户支付宝授权和微信支付子商户号配置。
*/
@NotNull(message = "商户id不能为空")
@Schema(description = "收款商户ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long mchId;
/**
* 应付金额,单位为人民币分,必须为正整数。
*/
@NotNull(message = "支付金额不能为空")
@Positive(message = "支付金额必须大于0")
@Schema(description = "支付金额,单位为分", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer amount;
/**
* 付款用户ID;匿名消费机订单可不传,服务端统一按 {@code -1L} 保存。
*/
@Schema(description = "付款用户ID,匿名订单可为空")
private Long userId;
}
@@ -0,0 +1,62 @@
package com.bsd.pjcloud.thirdparty.dto.appplus;
import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformTypeEnum;
import com.bsd.pjcloud.thirdparty.constant.pay.ThirdPayOrderBizTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
/**
* 订单码支付创建请求。
*
* <p>接受云消费机支付宝或微信订单码,金额由可信内部调用方按人民币分传入。</p>
*/
@Data
@Schema(description = "订单码支付创建请求")
public class ThirdOrderCodePayDto {
/**
* 支付业务类型,必须为 {@link ThirdPayOrderBizTypeEnum#YXFJ_PAY}。
*/
@NotNull(message = "支付业务类型不能为空")
@Schema(description = "支付业务类型", requiredMode = Schema.RequiredMode.REQUIRED)
private ThirdPayOrderBizTypeEnum bizType;
/**
* 调用方业务订单唯一标识,用于同一租户内的支付幂等控制。
*/
@NotBlank(message = "支付业务id不能为空")
@Schema(description = "支付业务id", requiredMode = Schema.RequiredMode.REQUIRED)
private String bizId;
/**
* 扫码支付平台,必须为 {@link AppPlusPlatformTypeEnum#ALI_PAY} 或
* {@link AppPlusPlatformTypeEnum#WECHAT}。
*/
@NotNull(message = "扫码支付平台不能为空")
@Schema(description = "扫码支付平台", requiredMode = Schema.RequiredMode.REQUIRED)
private AppPlusPlatformTypeEnum miniPlatformType;
/**
* 应付金额,单位为人民币分,必须为正整数。
*/
@NotNull(message = "支付金额不能为空")
@Positive(message = "支付金额必须大于0")
@Schema(description = "支付金额,单位为分", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer amount;
/**
* 付款用户ID;匿名消费机订单可不传,服务端统一按 {@code -1L} 保存。
*/
@Schema(description = "付款用户ID,匿名订单可为空")
private Long userId;
/**
* 收款商户ID,服务端仅按该标识读取商户及其启用的目标平台配置。
*/
@NotNull(message = "商户id不能为空")
@Schema(description = "收款商户ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long mchId;
}
@@ -0,0 +1,162 @@
package com.bsd.pjcloud.thirdparty.entity.pay;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.bsd.pjcloud.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 支付宝与微信聚合订单码会话。
*
* <p>会话只负责跨平台扫码选择、访问令牌和第三方子订单绑定,实际支付报文继续由
* {@code third_pay_order} 保存。</p>
*/
@Data
@TenantTable
@TableName("third_aggregate_pay_session")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "支付宝与微信聚合订单码会话")
public class ThirdAggregatePaySessionEntity extends Model<ThirdAggregatePaySessionEntity> {
/**
* 聚合会话主键,由 MyBatis-Plus 分布式ID生成器写入。
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private Long id;
/**
* 聚合会话唯一编号,供内部创建和状态查询接口使用。
*/
@Schema(description = "聚合会话编号")
private String aggregateNo;
/**
* 收银 URL 随机访问令牌的 SHA-256 十六进制摘要,用于扫码入口等值查询。
*/
@Schema(description = "访问令牌SHA-256摘要")
private String accessTokenHash;
/**
* 收银 URL 随机访问令牌的 Jasypt 密文,仅用于幂等请求恢复原二维码。
*/
@Schema(description = "访问令牌密文")
private String accessTokenCiphertext;
/**
* 支付业务类型编码,首版固定为云消费机支付业务。
*/
@Schema(description = "支付业务类型编码")
private String bizType;
/**
* 调用方业务订单唯一标识,同一租户内用于支付幂等。
*/
@Schema(description = "支付业务id")
private String bizId;
/**
* 付款用户ID;匿名消费机订单保存为 {@code -1L}。
*/
@Schema(description = "付款用户ID")
private Long userId;
/**
* 收款商户ID,对应钱包服务中的商户主键。
*/
@Schema(description = "收款商户ID")
private Long mchId;
/**
* 应付金额,单位为人民币分,和绑定的第三方子订单金额保持一致。
*/
@Schema(description = "支付金额,单位为分")
private Integer price;
/**
* 聚合会话状态编码,对应 {@code ThirdAggregatePaySessionStatusEnum}。
*/
@Schema(description = "聚合会话状态编码")
private String status;
/**
* 首个有效 OAuth 回调原子选定的支付平台编码;尚未扫码时为空。
*/
@Schema(description = "选定支付平台编码")
private String selectedPlatform;
/**
* 绑定的 {@code third_pay_order.pay_no};未创建第三方子订单时为空。
*/
@Schema(description = "第三方支付子订单号")
private String payNo;
/**
* 支付宝用户ID或微信服务商公众号 sp_openid 的 SHA-256 摘要,用于阻止不同付款人并发复用会话。
*/
@Schema(description = "付款人身份SHA-256摘要")
private String payerIdentityHash;
/**
* 聚合二维码和第三方子订单的服务端失效时间,使用服务端本地时区。
*/
@Schema(description = "失效时间")
private LocalDateTime expireTime;
/**
* 聚合会话进入成功、关闭、过期或创建失败终态的服务端时间。
*/
@Schema(description = "终态确认时间")
private LocalDateTime resolvedTime;
/**
* 创建人,由公共字段填充器写入;匿名扫码场景允许为空。
*/
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建人")
private String createBy;
/**
* 会话创建时间,由公共字段填充器写入。
*/
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private LocalDateTime createTime;
/**
* 最近修改人,由公共字段填充器写入;定时任务更新时允许为空。
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "修改人")
private String updateBy;
/**
* 最近状态或绑定信息更新时间,由公共字段填充器写入。
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "修改时间")
private LocalDateTime updateTime;
/**
* 逻辑删除标记,{@code 0} 表示有效,{@code 1} 表示已删除。
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
@Schema(description = "逻辑删除标记")
private String delFlag;
/**
* 会话所属租户ID,由多租户拦截器和公共字段填充逻辑维护。
*/
@Schema(description = "租户ID")
private Long tenantId;
}
@@ -0,0 +1,53 @@
package com.bsd.pjcloud.thirdparty.vo.appplus.pay;
import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 聚合订单码最小支付状态。
*
* <p>不返回访问令牌、付款人标识、商户配置或第三方原始报文。</p>
*/
@Data
@Schema(description = "聚合订单码最小支付状态")
public class ThirdAggregateOrderCodePayStatusVo {
/**
* 聚合支付服务生成的会话编号。
*/
@Schema(description = "聚合支付会话编号")
private String aggregateNo;
/**
* 聚合会话内部状态编码,对应 {@code ThirdAggregatePaySessionStatusEnum}。
*/
@Schema(description = "聚合会话状态编码")
private String status;
/**
* 首个有效扫码后选定的支付平台;尚未扫码时为空。
*/
@Schema(description = "选定支付平台")
private AppPlusPlatformTypeEnum selectedPlatform;
/**
* 实际第三方 JSAPI 子订单号;尚未选定平台时为空。
*/
@Schema(description = "第三方支付子订单号")
private String payNo;
/**
* 是否已经由第三方支付平台确认支付成功。
*/
@Schema(description = "是否支付成功")
private Boolean paySuccess;
/**
* 服务端认定的聚合二维码失效时间,使用服务端本地时区。
*/
@Schema(description = "聚合二维码失效时间")
private LocalDateTime expireTime;
}
@@ -0,0 +1,38 @@
package com.bsd.pjcloud.thirdparty.vo.appplus.pay;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 支付宝与微信聚合订单码创建结果。
*/
@Data
@Schema(description = "支付宝与微信聚合订单码创建结果")
public class ThirdAggregateOrderCodePayVo {
/**
* 聚合支付服务生成的会话编号,用于内部状态查询和问题定位。
*/
@Schema(description = "聚合支付会话编号")
private String aggregateNo;
/**
* 同时供支付宝和微信扫码访问的 HTTPS 收银地址,不是图片或 Base64。
*/
@Schema(description = "聚合二维码内容串")
private String qrCode;
/**
* 二维码协议类型,固定为 {@code AGGREGATE_ORDER_PAY},不得与自有钱包码混用。
*/
@Schema(description = "二维码协议类型")
private String qrCodeType;
/**
* 服务端认定的聚合二维码失效时间,使用服务端本地时区。
*/
@Schema(description = "聚合二维码失效时间")
private LocalDateTime expireTime;
}
@@ -0,0 +1,40 @@
package com.bsd.pjcloud.thirdparty.vo.appplus.pay;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 订单码支付状态查询结果。
*
* <p>仅返回调用方确认付款所需的最小状态,不包含商户配置和第三方平台原始报文。</p>
*/
@Data
@Schema(description = "订单码支付状态查询结果")
public class ThirdOrderCodePayStatusVo {
/**
* 第三方支付服务生成的唯一商户订单号。
*/
@Schema(description = "支付订单号")
private String payNo;
/**
* third_pay_order 中保存的内部支付状态编码。
*/
@Schema(description = "内部支付状态编码")
private String status;
/**
* 是否已经确认支付成功,仅内部状态为支付成功时返回 {@code true}。
*/
@Schema(description = "是否支付成功")
private Boolean paySuccess;
/**
* 服务端认定的二维码失效时间,使用服务端本地时区。
*/
@Schema(description = "二维码失效时间")
private LocalDateTime expireTime;
}
@@ -0,0 +1,34 @@
package com.bsd.pjcloud.thirdparty.vo.appplus.pay;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 订单码支付创建结果。
*
* <p>二维码字段是支付宝 {@code qr_code} 或微信 {@code code_url} 内容串,调用方负责将其渲染为二维码图片。</p>
*/
@Data
@Schema(description = "订单码支付创建结果")
public class ThirdOrderCodePayVo {
/**
* 第三方支付服务生成的唯一商户订单号,用于后续查询支付状态。
*/
@Schema(description = "支付订单号")
private String payNo;
/**
* 支付宝 {@code qr_code} 或微信 {@code code_url} 内容串,不是图片、Base64 或二进制数据。
*/
@Schema(description = "第三方支付平台二维码内容串")
private String qrCode;
/**
* 服务端认定的二维码失效时间,使用服务端本地时区。
*/
@Schema(description = "二维码失效时间")
private LocalDateTime expireTime;
}