背景
主要用户群体所在地区有 香港、台湾、新加坡、马来西亚,那么对应使用的货币会有台币、港币、新币、马来币,针对此多种类型货币的支付对接,我们会找多种三方支付平台,寻找当地手续费最低化和用户使用最普遍化的方式,因此在技术设计上我们需要做支付方式、手续费、货币汇率、地区、支付挡位切换等配置。
整体对接流程设计
客户端内购
使用场景
一般使用对应商店内购支付,比如苹果支付、谷歌支付,这里提一嘴,手续费大概30%,这么高,那为什么我们还要接呢? 因为你要在它们对应商店上架,算是潜规则,你不接它的东西,他不给你App过审,我理解为交保护费…
支付对接流程
H5第三方支付
使用场景
第三方支付主要就是为了手续费低这一块去考虑的,需要对接多种渠道,不同渠道对应的地区下可能有最优惠政策,有些甚至能谈到5%,对于平台来说这对比内购的30%无疑是诱惑巨大的,这里也有个问题,H5支付的入口,一般我们提包审核的时候都会关闭H5入口,不然分分钟给你拒审,所以这一块得做个开关。
支付对接流程
服务端业务设计
支付规则
配置表
@Data
public class RechargeRule implements Serializable {
/**
* 主键
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 钻石数量
*/
private BigDecimal amount;
/**
* 赠送钻石数量
*/
private BigDecimal giveAmount;
/**
* 支付货币类型
*/
private String currencyType;
/**
* 支付货币数量
*/
private BigDecimal currencyAmount;
/**
* 三方产品Id
*/
private String productId;
/**
* 挡位类型
*
* @see RechargeProductType#getType()
* @see RechargeProductType#getDesc()
*/
private Integer type;
/**
* 平台类型 0:所有 1:安卓 2:IOS 3:H5
*/
private Integer platformType;
/**
* 标签类型
*
* @see RechargeBadgeType#getType()
* @see RechargeBadgeType#getDesc()
*/
private Integer badgeType;
/**
* 创建时间
*/
private Date createTime;
public interface IRechargeRuleService extends IService<RechargeRule> {
/**
* 根据平台类型获取所有充值产品规则
*
* @param platformType 充值平台类型
* @return
*/
List<RechargeRuleVo> getRechargeRuleAllByType(int platformType);
/**
* 根据产品ID查询充值配置
*
* @param productId 订单id
* @return 充值配置
*/
RechargeRule getRechargeRuleByProductId(String productId);
/**
* 根据产品类型获取
*
* @param type
* @return
*/
List<RechargeRuleVo> getByProductType(int type);
}
作用
每一条记录对应一个支付挡位,根据客户端/H5平台类型获取对应的挡位列表展示,挡位类型字段可对应相应的充值活动,标签类型字段可对应相应的支付标记类型
支付渠道
配置表
@Data
public class PayChannel {
/**
* 序列主键
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 渠道类型 {@link PayChannelType}
*/
private int channelType;
/**
* 渠道名称
*/
private String channelName;
/**
* 付款方式ID
*/
private String payWayId;
}
public enum PayChannelType {
/**
* 谷歌支付
*/
CHANNEL_GOOGLE(1, "谷歌支付", "gg"),
/**
* 苹果支付
*/
CHANNEL_APPLE(2, "苹果支付", "ios"),
/**
* Passion三方支付
*/
CHANNEL_PAYSSION(3, "Payssion三方支付", "pa"),
/**
* MyCard三方支付
*/
CHANNEL_MYCARD(4, "MyCard三方支付", "my"),
/**
* Paypal三方支付
*/
CHANNEL_PAYPAL(5, "Paypal三方支付", "pp"),
/**
* 代充
*/
CHANNEL_AGENT(6, "代充", "ag"),
;
@Getter
private int type;
@Getter
private String desc;
@Getter
private String orderPrefix;
public static PayChannelType parse(int channelType) {
return Arrays.stream(PayChannelType.values()).filter(o -> o.getType() == channelType).findAny().orElse(null);
}
}
作用
配置对应渠道下不同的支付方式,相同渠道可能会有同一支付方式,所以用channer_type+pay_way_id为唯一键的方式去做区分,pay_way_id可能有几十上百种
地区-支付渠道
配置表
@Data
public class PayAreaChannel {
/**
* 序列主键
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 支付渠道ID
*/
private Long channelId;
/**
* 区域代码
*/
private String areaCode;
}
@AllArgsConstructor
public enum PayAreaType {
/**
* 香港
*/
HONG_KONG("HK", "香港", CurrencyType.HKD),
/**
* 台湾
*/
TAIWAN("TW", "台湾", CurrencyType.TWD),
/**
* 新加坡
*/
SINGAPORE("SG", "新加坡", CurrencyType.SGD),
/**
* 马来西亚
*/
MALAYSIA("MYS", "马来西亚", CurrencyType.MYR),
;
@Getter
private String code;
@Getter
private String desc;
@Getter
private CurrencyType currencyType;
public static PayAreaType parse(String areaCode) {
return Arrays.stream(PayAreaType.values()).filter(o -> o.getCode().equals(areaCode)).findFirst().orElse(null);
}
}
@AllArgsConstructor
@Getter
public enum CurrencyType {
/**
* 人民币
*/
CNY(1, "CNY"),
/**
* 台币
*/
TWD(2, "台币"),
/**
* 港币
*/
HKD(3, "港币"),
/**
* 马来币
*/
MYR(4, "马来币"),
/**
* 新加坡币
*/
SGD(5, "新加坡币"),
/**
* 美元
*/
USD(6, "美元"),
;
@Getter
private int type;
@Getter
private String desc;
public static CurrencyType parse(int type) {
return Arrays.stream(CurrencyType.values()).filter(o -> o.getType() == type).findAny().orElse(null);
}
}
作用
把对应地区-支付渠道ID配置对应上,可以用来区分每个地区下对应的支付渠道,支付方式,货币类型是哪些。
汇率
配置表
public class ExchangeRateConfig {
/**
* 实际的主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 货币
*/
@TableField("currency")
private String currency;
/**
* 货币种类 {@link com.miyo.common.core.enums.CurrencyType}
*/
private int currencyType;
/**
* 汇率种类
* @see com.miyo.user.entity.configuration.enums.RateType
*/
private int rateType;
/**
* 汇率
*/
@TableField("exchange_rate")
private BigDecimal exchangeRate;
/**
* 手续费
*/
@TableField("service_charge")
private BigDecimal serviceCharge;
}
作用
配置对应货币的汇率以进行货币转换和支付结算
订单信息
@Data
public class PayOrder {
/**
* 序列主键
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 渠道
*
* @see PayChannelType#getType()
* @see PayChannelType#getDesc()
*/
private Integer channelType;
/**
* 用户id
*/
private Long userId;
/**
* 支付状态 0待支付1已支付2支付取消
*/
private Integer status;
/**
* 三方单号/渠道订单号
*/
private String channelOrderId;
/**
* 系统内部订单ID
*/
/**
* 支付凭证
*/
private String purchaseToken;
/**
* 包名
*/
private String packageName;
/**
* 充值规则的产品id
*/
private String productId;
/**
* 支付货币类型
*
* @see CurrencyType#name()
*/
private String currencyType;
/**
* 支付货币金额
*/
private BigDecimal currencyAmount;
/**
* 到账金币数量
*/
private BigDecimal amount;
/**
* 赠送钻石数量
*/
private BigDecimal giveAmount;
/**
* 支付时间
*/
private Date payTime;
/**
* 创建时间
*/
private Date createTime;
/**
* 額外參數(JSON定义)
*/
private String extra;
/**
* 对应货币手续费
*/
private BigDecimal currencyServiceCharge;
/**
* 支付金额(人民币,后台数据统计用)
*/
private BigDecimal cnyAmount;
/**
* 手续费(人民币,后台数据统计用)
*/
private BigDecimal cnyServiceCharge;
}
作用
保存每一笔订单创建、支付记录
支付接口
回调接口设计
原则
- 统一入口和出口
- 使用策略模式
实现
统一回调接口
@PostMapping(value = "pay")
public RedirectView pay(@RequestBody PayBo bo) {
log.info("支付通知 | {}", GsonUtil.GsonString(bo));
PayRes payRes = PayCallbackFactory.getInstance().getStrategy(bo.getPayType()).verifyOrder(bo);
return new RedirectView(payRes.getRedirectUrl()); // 设置重定向到客户端的URL
}
回调工厂
public class PayCallbackFactory {
private PayCallbackFactory() {
}
private static class Builder {
private static final PayCallbackFactory factory = new PayCallbackFactory();
}
public static PayCallbackFactory getInstance() {
return Builder.factory;
}
//策略包路径
private static final String STRATEGY_PACKAGE = "com.xxx.xxx.service.strategy.pay";
private static final Map<Integer, Class> STRATEGY_MAP = new HashMap<>();
// 获取所有策略
static {
Reflections reflections = new Reflections(STRATEGY_PACKAGE);
Set<Class<?>> classSet = reflections.getTypesAnnotatedWith(PayEvent.class);
classSet.forEach(aClass -> {
PayEvent payEvent = aClass.getAnnotation(PayEvent.class);
STRATEGY_MAP.put(payEvent.type(), aClass);
});
}
public PayHandler getStrategy(Integer eventType) {
Class clazz = STRATEGY_MAP.get(eventType);
if (StringUtils.isEmpty(clazz)) {
return null;
}
return (PayHandler) SpringContextUtil.getBean(clazz);
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PayEvent {
int type();
}
支付处理
public interface PayHandler {
/**
* 订单校验
*
* @return
*/
PayRes verifyOrder(Object param);
/**
* 补单操作
*
* @param param
*/
default void repairOrder(Object param) {
}
/**
* 订单检查
*
* @param param
* @return
*/
default String checkOrder(Object param) {
return null;
}
/**
* 获取支付类型
*
* @return
*/
int getPayType();
/**
* 獲取額外參數
*
* @param param
* @return
*/
default String getPayOrderExtraParam(Object param, SdkOrderVerifyResult sdkOrderVerifyResult) {
return null;
}
/**
* 确认订单并发货
*
* @param payOrder
*/
default void confirmOrderAndDelivery(PayOrder payOrder) {
}
}
作用
整套支付对接设计实现后,可以快速地迭代对接新的支付方式,把整个方案告知产品经理后,产品经理大拇指不自觉地竖了起来。
总结
- 能配置化尽量配置化解决
- 统一回调入口需要兼容不同渠道的回调数据传输方式,统一出口和客户端/H5约定好规则
- 出现订单漏发货情况,需要严格根据补单操作进行订单最终一致性确认
- 支付链路中可以增加支付告警设计,以及时感知业务运行情况