这样设计对接N种海外支付 | 产品经理也得竖大拇指
背景
主要用户群体所在地区有 香港、台湾、新加坡、马来西亚,那么对应使用的货币会有台币、港币、新币、马来币,针对此多种类型货币的支付对接,我们会找多种三方支付平台,寻找当地手续费最低化和用户使用最普遍化的方式,因此在技术设计上我们需要做支付方式、手续费、货币汇率、地区、支付挡位切换等配置。
整体对接流程设计
客户端内购
使用场景
一般使用对应商店内购支付,比如苹果支付、谷歌支付,这里提一嘴,手续费大概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约定好规则
- 出现订单漏发货情况,需要严格根据补单操作进行订单最终一致性确认
- 支付链路中可以增加支付告警设计,以及时感知业务运行情况
文章内容源自本人所在互联网社交企业实战项目,分享、记录从0-1做一个千万级直播项目,内容包括高并发场景下技术选型、架构设计、业务解决方案等。