这样设计对接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;

}

作用

保存每一笔订单创建、支付记录

支付接口

回调接口设计

原则

  1. 统一入口和出口
  2. 使用策略模式

实现

统一回调接口

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


}

作用

整套支付对接设计实现后,可以快速地迭代对接新的支付方式,把整个方案告知产品经理后,产品经理大拇指不自觉地竖了起来。

总结

  1. 能配置化尽量配置化解决
  2. 统一回调入口需要兼容不同渠道的回调数据传输方式,统一出口和客户端/H5约定好规则
  3. 出现订单漏发货情况,需要严格根据补单操作进行订单最终一致性确认
  4. 支付链路中可以增加支付告警设计,以及时感知业务运行情况
#从0到1千万级直播项目#
从0-1开发千万级直播项目 文章被收录于专栏

文章内容源自本人所在互联网社交企业实战项目,分享、记录从0-1做一个千万级直播项目,内容包括高并发场景下技术选型、架构设计、业务解决方案等。

全部评论

相关推荐

面试摇了我吧:啊哈哈面试提前五个小时发,点击不能参加就是放弃
点赞 评论 收藏
分享
1 1 评论
分享
牛客网
牛客企业服务