微信服务商分账思路剖析、设计流程及源码实现

这篇具有很好参考价值的文章主要介绍了微信服务商分账思路剖析、设计流程及源码实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
🌲文章所在专栏:微信体系
🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
💬 向我询问任何您想要的东西,ID:vnjohn
🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏
😄 代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏

目录


好久不见,文章很长时间没有更新了,一致追求于文章的质量输出,为了避免大家再次遇坑,特此记录一下分享给大家

需求背景

在服务商多商户运营下,需要在业务方「门店」这侧平摊给品牌「分公司或经销商」一定金额入账,以此作为基准,需要多商户底座来支撑需求实现,目前文章是在服务商模式实现分账逻辑

前期准备

1、准备一个服务商账号,申请步骤可以查看网上资料进行阅览,一般公司的运营都会这个操作
2、门店需要进行分账的,都需要给它申请一个商户号,并将商户号绑定到服务商下的特约商户,每个商户号都需要与wxAppId 进行关联「小程序或公众号」
3、登录「小程序或公众号」后台设置页面进行商户号授权操作

新商户号入驻流程
1、申请商户号绑定到服务商特约商户下
2、商户号绑定小程序appId
3、小程序管理员登录后台进行同意商户号绑定

实现步骤

微信接口文档:开发文档-微信支付服务商平台
1、在 PC 后台加一个功能模块,用于设置门店的商户号信息和是否开启分账功能来支撑动态是否进行分账的实现,重要的一个字段为分账比例,该比例字段取值范围在 0~30 之间,最大分账比例不能超过 30%
2、微信 JSAPI 统一下单接口除了下单时所需的参数之外,另外增加一个参数:profit_sharing「是否分账:Y-是,需要分账、N-否,不分账,字母要求大写,不传默认不分账」,该参数值在业务中不是即传即用的,而是根据第一点「后台门店分账配置」来设置参数值,如果该门店未开启分账或未设置分账比例和商户号,即传 N,其他都满足时传递 Y
3、在订单表中增加字段业务类型「businessType」,用于区分当前订单在统一下单时是否进行了分账,以便于在支付回调以后进行订单的判别,如果该订单支持分账,则生成一条分账记录,用于后续定时扫描该分账记录表,调用微信服务商分账接口
4、在支付回调和退款回调接口中,支付回调负责生成分账记录,退款回调用于更新分账记录「记录分账回退信息,考虑到分账回退可能会发生多次退款,所以该订单每个子商品进行退款都需要记录一下回退金额信息,以便于持平付款金额和退款金额」
5、注意:关于支付的商户号信息,subMchId 代表的是服务商下的特约商户ID,如果在当前门店进行下单的话,subMchId 应当取用门店在 PC 后台设置的商户号

extra:在公司业务中,可能会分为两种模式:直连商户和服务商商户模式,在直连商户中不需要设置 subMchId 其他信息,所以它取值字段为 mchId,当然它的支付证书和密钥也应该用门店的,这时候在 PC 后台仅仅设置商户号和比例信息就不能满足需求了,这取决于公司的业务范围;服务商商户中进行下单和分账都需要设置 subMchId,但它的证书和密钥不需要使用门店商户的,服务商下所有特约商户统一使用服务商后台的证书和密钥即可。

流程分析

微信服务商分账思路剖析、设计流程及源码实现
以上流程图涉及到的只是分账发起前的一些前置工作,也是必备工作

微信服务商分账思路剖析、设计流程及源码实现
以上流程图,是如何运用分账记录进行实时的请求分账,以及分账前后的预处理工作流程分解

实现过程中问题点

1、商户号与订单不匹配错误(分账时商户号必须和下单支付商户号一致,统一下单时「服务商模式」子商户号填写的应是门店商户号,不可用品牌商户号)
2、请求分账的交易模式和下单的交易模式不匹配,普通商户的交易只能普通商户发起分账,服务商下单的交易只能服务商发起分账「该问题就是直连商户模式和服务商模式不能混乱使用」
3、分账接收方全称未设置「调用分账接口时,商户接收方全称没设值,微信分账接口返回该错误」
4、证书文件有问题,请核实!「在服务商模式下,证书统一使用的是服务商证书;直连商户模式下,证书使用的是当前门店所配置的证书,目前该流程待后续扩展 TODO」
5、微信回调接口处:调用微信接口查询订单时,子商户号填写错误「微信返回子商户号与订单不匹配」,导致无法形成入账记录「解决:在支付流水表中加一个当前支付商户号字段,最好在订单信息表中也追加一个支付商户号方便后续涉及到的相关业务用到」
6、区分服务商/直连模式,解决:在订单表中追加一个 mode 字段判别其属于那种模式「后台切换支付模式(服务商、直连商品模式)可能会导致之前的分账记录无法请求分账」,该业务属于后续扩展直连商户分账方案提前预备

源码

  <binarywang.version>4.1.0</binarywang.version>
  
 <dependency>
    <groupId>com.github.binarywang</groupId>
     <artifactId>weixin-java-pay</artifactId>
     <version>${binarywang.version}</version>
 </dependency>
/**
 * @Author vnjohn
 * @since 2022/10/24
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class StoreCommissionRecordDTO extends BaseDTO implements Serializable {
    private static final long serialVersionUID = 4170369961010123978L;
    
    /**
     * 品牌ID
     */
    private Long brandId;

    /**
     * 门店ID
     */
    private Long storeId;

    /**
     * 门店商户号
     */
    private String mchId;

    /**
     * 订单编号
     */
    private String orderNo;

    /**
     * 当前分账金额
     */
    private BigDecimal commission;

    /**
     * 状态:0-冻结中、1-已结算/可提现、2-已失效),默认0
     * @see BuCommissionStatusEnum
     */
    private Integer status;

    /**
     * 分账执行次数
     */
    private Integer callNum;

    /**
     * 订单支付时间
     */
    private LocalDateTime orderPayTime;

    /**
     * 支付交易号:更新时传的是退款交易号,新增是支付交易号
     */
    private String outTransactionId;

    /**
     * 部分退款,出现记录多次分账情况
     * 扩展字段:{"outTransactionId":xx,"commission":1}
     * outTransactionId:退款交易号
     * commission:当次退款金额
     */
    private String extInfo;

    /**
     * 分账占比
     */
    private Integer commissionPercent;

    /**
     * 退款金额:单位分
     */
    private Long refundAmount;

    /**
     * 分账后交易后的订单号
     */
    private String profitSharingOrderNo;

}
/**
 * @Author vnjohn
 * @since 2022/10/26
 */
@Data
@Builder
public class PayConfig implements Serializable {
    private static final long serialVersionUID = 1618292038545334525L;

    /**
     * 微信公众号或者小程序APPID
     */
    private String wxAppId;

    /**
     * 根据证书路径解析出的商户证书数据流
     */
    private InputStream sslCertInputStream;

    /**
     * 证书路径
     */
    private String certPath;

    /**
     * 证书密码
     */
    private String certPassword;

    /**
     * 微信商户Id
     */
    private String mchId;

    /**
     * 微信商户密钥
     */
    private String mchKey;

    /**
     * 子商户Id
     */
    private String subMchId;

    /**
     * 子微信id
     */
    private String subAppId;

    /**
     * 微信子商户密钥
     */
    private String subMchKey;

    /**
     * 订单号+分销人Id,分账单号(内部生成)
     */
    private String partnerTradeNo;

    /**
     * 分账接收方类型:个人或商户
     */
    private String type;

    /**
     * 分账接收方微信用户openId
     */
    private String receiveWxOpenId;

    /**
     * 分账接收方商户号ID
     */
    private String receiveMchId;

    /**
     * 分账接收方商户名称
     */
    private String receiveMchName;

    /**
     * 支付描述
     */
    private String desc;

    /**
     * 支付金额
     */
    private Long amount;

    /**
     * 微信统一支付transaction_id
     */
    private String outTransactionId;

    /**
     * 商户支付设置类型
     */
    private Integer payType;

}
/**
 * 业务单元分账结算任务执行器
 * 五分钟执行一次,一次处理 200 条分账
 *
 * @Author vnjohn
 * @since 2022/10/25
 */
@Slf4j
@Component
public class StoreCommissionSettleJobExecutor {
    @Resource
    private StoreCommissionRecordGateway commissionRecordGateway;

    @Resource
    private StoreLedgerRpcService storeLedgerRpcService;

    @Resource
    private AbstractWxPayService profitSharingWxPayService;

    /**
     * redis标记已发放佣金记录key前缀
     */
    private static final String ORDER_COMMISSION_KEY_PREFIX = "LOCK_STORE_COMMISSION_LEDGER";

    /**
     * 分账订单标识前缀
     */
    private static final String PROFIT_SHARING_ORDER_PREFIX = "BU";

    /**
     * 业务单元分账描述
     */
    private static String PROFIT_SHARING_DESC = "门店「%s」分到商户「%s」";

    @XxlJob("storeCommissionSettleJobExecutor")
    public ReturnT<String> execute(String param) {
        StoreCommissionRecordQuery query = new StoreCommissionRecordQuery();
        // 0-冻结、1-结算、2-失效
        query.setStatus(StoreCommissionStatusEnum.FREEZE.getCode());
        query.setPageNo(PageConstant.DEFAULT_PAGE_NO);
        query.setPageSize(PageConstant.DEFAULT_PAGE_SIZE);
        List<StoreCommissionRecordDTO> storeCommissionRecordList = commissionRecordGateway.page(query);
        log.info("分账记录:{}",JsonUtil.toJson(storeCommissionRecordList));
        if (CollectionUtils.isEmpty(storeCommissionRecordList)) {
            return ReturnT.SUCCESS;
        }
        // 筛选出所有品牌和门店 id 后查询出其下支付配置和商户号配置信息
        List<Long> brandIdList = storeCommissionRecordList.stream().map(StoreCommissionRecordDTO::getBrandId).distinct().collect(Collectors.toList());
        List<Long> storeIdList = storeCommissionRecordList.stream().map(StoreCommissionRecordDTO::getStoreId).distinct().collect(Collectors.toList());
        Map<Long, PayConfigDTO> payConfigGroupBrandMap = new HashMap<>(brandIdList.size());
        Map<Long, storeLedgerDTO> storeLedgerConfigByStoreMap = new HashMap<>(storeIdList.size());
        List<StoreCommissionRecordDTO> records = new ArrayList<>();
        // 支付配置、分账记录存入 Map,避免遍历时出现相同的配置去重复查询
        for (StoreCommissionRecordDTO commissionRecord : storeCommissionRecordList) {
            // 品牌-支付配置信息
            PayConfigDTO payConfigDTO = payConfigGroupBrandMap.get(commissionRecord.getBrandId());
            if (null == payConfigDTO) {
                ResultDTO<PayConfigDTO> payConfigByBrandIdResult = storeLedgerRpcService.getPayConfigByBrandId(commissionRecord.getTenantId(), commissionRecord.getBrandId());
                log.info("支付配置信息:{}",JsonUtil.toJson(payConfigByBrandIdResult));
                if (!payConfigByBrandIdResult.isSuccess() || Objects.isNull(payConfigByBrandIdResult.getData())) {
                    continue;
                }
                payConfigDTO = payConfigByBrandIdResult.getData();
                payConfigGroupBrandMap.put(commissionRecord.getBrandId(), payConfigDTO);
            }

            // 门店-分账配置信息
            StoreLedgerDTO storeLedgerDTO = storeLedgerConfigByStoreMap.get(commissionRecord.getStoreId());
            if (null == storeLedgerDTO) {
                ResultDTO<storeLedgerDTO> ledgerConfigResult = storeLedgerRpcService.getOneBuLeader(commissionRecord.getTenantId(), commissionRecord.getStoreId());
                log.info("分账配置信息:{}",JsonUtil.toJson(ledgerConfigResult));
                if (ledgerConfigResult.isSuccess() && Objects.nonNull(ledgerConfigResult.getData().getId())) {
                    storeLedgerDTO = ledgerConfigResult.getData();
                    storeLedgerConfigByStoreMap.put(commissionRecord.getStoreId(), storeLedgerDTO);
                }
            }

            // 返回的是可正常更新的分账记录
            records.add(processOrderLedgerCommission(payConfigDTO, commissionRecord, storeLedgerDTO));
        }
        if (CollectionUtils.isEmpty(records)) {
            log.info("该次任务暂无完成分账数");
            return ReturnT.SUCCESS;
        }
        log.info("该次任务完成分账数:{}", records.size());
        return ReturnT.SUCCESS;
    }

    @RedisLock(keys = {"#commissionRecord.orderNo"}, name = ORDER_COMMISSION_KEY_PREFIX)
    @Transactional(rollbackFor = Exception.class)
    public StoreCommissionRecordDTO processOrderLedgerCommission(PayConfigDTO payConfigDTO, StoreCommissionRecordDTO commissionRecord, storeLedgerDTO storeLedgerDTO) {
        if (commissionRecord.getStatus().equals(StoreCommissionStatusEnum.SETTLE.getCode())) {
            log.warn("该支付订单已分账过,请勿重复执行,{}", commissionRecord.getOrderNo());
            return null;
        }
        if (null == commissionRecord.getCommission() || commissionRecord.getCommission().doubleValue() <= 0) {
            log.warn("分账金额为空,recordId:{}", commissionRecord.getId());
            return null;
        }
        // 构建支付配置参数
        // TODO 此处追加一个 mode 字段,判别其是服务商模式还是自助申请模式
        PayConfig payConfig = buildPayConfig(payConfigDTO, commissionRecord, storeLedgerDTO);
        // 请求模版函数服务发起微信分账请求
        String outOrderNo = profitSharingWxPayService.pay(payConfig);
        if (StringUtils.isEmpty(outOrderNo)) {
            log.error("该笔订单:{},分账失败", commissionRecord.getOrderNo());
            return null;
        }
        commissionRecord.setProfitSharingOrderNo(outOrderNo);
        // 更新分账表记录状态
        commissionRecord.setStatus(StoreCommissionStatusEnum.SETTLE.getCode());
        commissionRecordGateway.batchUpdateSettleStatus(Collections.singletonList(commissionRecord));
        return commissionRecord;
    }

    /**
     * 构建微信分账配置参数
     * 请求分账的交易模式和下单的交易模式不匹配,普通商户的交易只能普通商户发起分账,服务商下单的交易只能服务商发起分账
     *
     * @param payConfigDTO
     * @param storeLedgerDTO
     * @return
     */
    private PayConfig buildPayConfig(PayConfigDTO payConfigDTO, StoreCommissionRecordDTO commissionRecord, StoreLedgerDTO storeLedgerDTO) {
        PayConfig payConfig = PayConfig.builder().build();
        payConfig.setWxAppId(payConfigDTO.getExtAppId());
        payConfig.setMchId(payConfigDTO.getExtMchId());
        payConfig.setMchKey(payConfigDTO.getMchKey());
        payConfig.setCertPath(payConfigDTO.getCertPath());
        // 判断模式是否为服务商模式
        if (payConfigDTO.getMode().equals(PayModeEnum.SERVICE_PROVIDER.getCode())) {
            WxServiceProviderConfig serviceProviderConfig = JsonUtil.toObject(payConfigDTO.getExtData(), WxServiceProviderConfig.class);
            payConfig.setWxAppId(serviceProviderConfig.getAppId());
            payConfig.setSubAppId(payConfigDTO.getExtAppId());
            payConfig.setMchId(serviceProviderConfig.getMchId());
            payConfig.setMchKey(serviceProviderConfig.getMchKey());
            payConfig.setCertPath(serviceProviderConfig.getCertPath());
            payConfig.setSubMchId(Objects.nonNull(storeLedgerDTO.getMchId()) ? storeLedgerDTO.getMchId() : payConfigDTO.getExtMchId());
        }
        payConfig.setReceiveMchId(payConfigDTO.getExtMchId());
        // 「分账接收方商户名称」不能为空
        payConfig.setReceiveMchName(payConfigDTO.getMchName());
        payConfig.setPartnerTradeNo(PROFIT_SHARING_ORDER_PREFIX + commissionRecord.getOrderNo() + commissionRecord.getStoreId());
        payConfig.setAmount(commissionRecord.getCommission().longValue());
        payConfig.setType(ProfitSharingTypeEnum.MERCHANT_ID.name());
        payConfig.setOutTransactionId(commissionRecord.getOutTransactionId());
        payConfig.setDesc(String.format(PROFIT_SHARING_DESC, storeLedgerDTO.getStoreId(), payConfig.getReceiveMchName()));
        log.info("微信分账配置参数:{}", JsonUtil.toJson(payConfig));
        return payConfig;
    }

}
/**
 * @Author vnjohn
 * @since 2022/10/26
 */
@Slf4j
public abstract class AbstractWxPayService {
    public static final String SUCCESS_STRING = "SUCCESS";

    public static final String HMAC_SHA256 = "HMAC-SHA256";

    /**
     * 证书内容缓存redis key前缀
     */
    public final static String FILE_CERT_REDIS_PREFIX = "cert_data_";

    /**
     * 证书文件路径连接符
     */
    public final static String FILE_CERT_URL_JOIN = "/";

    private static volatile WxPayService wxPayService;

    /**
     * 获取支付配置服务实例
     *
     * @param payConfig
     * @param keyContext
     * @return
     */
    protected static WxPayService getWxPayServiceInstance(PayConfig payConfig, byte[] keyContext) {
        if (null == wxPayService) {
            synchronized (WxPayService.class) {
                if (null == wxPayService) {
                    wxPayService = getWxPayService(payConfig, keyContext);
                }
            }
        }
        return wxPayService;
    }

    /**
     * 添加配置信息
     *
     * @return
     */
    public static WxPayService getWxPayService(PayConfig payConfig, byte[] keyContext) {
        log.info("添加微信配置信息:{},keyContext:{}", JsonUtil.toJson(payConfig), keyContext);
        WxPayConfig wxPayConfig = new WxPayConfig();
        wxPayConfig.setSignType(HMAC_SHA256);
        wxPayConfig.setKeyContent(keyContext);
        wxPayConfig.setAppId(StringUtils.trimToNull(payConfig.getWxAppId()));
        wxPayConfig.setMchId(StringUtils.trimToNull(payConfig.getMchId()));
        if (StrUtil.isNotBlank(payConfig.getSubMchId())) {
            wxPayConfig.setSubAppId(payConfig.getSubMchId());
            wxPayConfig.setSubMchId(payConfig.getSubMchId());
        }
        wxPayConfig.setMchKey(StringUtils.trimToNull(payConfig.getMchKey()));
        WxPayService wxPayService = new WxPayServiceImpl();
        wxPayService.setConfig(wxPayConfig);
        return wxPayService;
    }

    /**
     * 微信付款入口,返回外部交易的订单号
     *
     * @param payConfig
     * @return
     */
    public String pay(PayConfig payConfig) {
        if (payConfig == null) {
            log.warn("支付配置为空,不发起转账行为");
            return null;
        }
        boolean prePayResult;
        try {
            prePayResult = preparePay(payConfig);
        } catch (Exception e) {
            log.error("微信付款前的准备发生异常:{}", e.getMessage(), e);
            // 暂时返回 null 预处理下一条
            return null;
//            throw new BizException("分账前的准备发生异常");
        }

        String resStr = null;
        if (prePayResult) {
            try {
                resStr = beginPay(payConfig);
            } catch (Exception e) {
                log.error("微信付款发生异常:{}", e.getMessage(), e);
                // 暂时返回 null 预处理下一条
                return null;
//                throw new BizException("微信付款发生异常00000001");
            }

            if (resStr == null) {
                throw new BizException("微信付款发生异常: 响应值为空");
            }
        }
        return resStr;
    }

    /**
     * 分账前置工作
     *
     * @param payConfig
     * @return
     * @throws Exception
     */
    protected abstract boolean preparePay(PayConfig payConfig) throws Exception;

    /**
     * 请求分账
     *
     * @param payConfig
     * @return
     * @throws Exception
     */
    protected abstract String beginPay(PayConfig payConfig) throws Exception;

    /**
     * 分账回退
     *
     * @param payConfig
     * @return
     * @throws Exception
     */
    protected abstract Map<String, String> afterPay(PayConfig payConfig) throws Exception;

    /**
     * 创建支付随机字符串
     *
     * @return
     */
    protected static String getNonceStr() {
        return RandomStringUtils.randomAlphanumeric(32);
    }

    /**
     * 构建sha256参数的签名值
     *
     * @param params
     * @param paternerKey
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String getSha256Sign(Map<String, String> params, String paternerKey) throws UnsupportedEncodingException {
        String stringSignTemp = createSign(params, false) + "&key=" + paternerKey;
        return hmacSHA256(stringSignTemp, paternerKey).toUpperCase();
    }

    /**
     * 构造签名
     *
     * @param params
     * @param encode
     * @return
     * @throws UnsupportedEncodingException
     */
    protected static String createSign(Map<String, String> params, boolean encode) throws UnsupportedEncodingException {
        Set<String> keysSet = params.keySet();
        Object[] keys = keysSet.toArray();
        Arrays.sort(keys);
        StringBuffer temp = new StringBuffer();
        boolean first = true;
        for (Object key : keys) {
            // 参数为空不参与签名
            if (key == null || StringUtils.isEmpty(params.get(key))) {
                continue;
            }
            if (first) {
                first = false;
            } else {
                temp.append("&");
            }
            temp.append(key).append("=");
            Object value = params.get(key);
            String valueStr = "";
            if (null != value) {
                valueStr = value.toString();
            }
            if (encode) {
                temp.append(URLEncoder.encode(valueStr, "UTF-8"));
            } else {
                temp.append(valueStr);
            }
        }
        return temp.toString();
    }

}
/**
 * 商户通过微信分账给个人或商户 service
 *
 * @author vnjohn
 * @since 2022/10/25
 */
@Slf4j
@Service("wxProfitSharingPayService")
public class WxProfitSharingPayServiceImpl extends AbstractWxPayService {
    @Resource
    private FileStorageService fileStorageService;

    @Resource
    private RestTemplate restTemplate;

    private final static String AUTH_TYPE = "WECHATPAY2-SHA256-RSA2048";

    private final static String ADD_RECEIVERS_URL = "https://api.mch.weixin.qq.com/v3/profitsharing/receivers/add";

    private final static String PROFIT_SHARING_URL = "https://api.mch.weixin.qq.com/v3/profitsharing/orders";

    private final static String SERIAL_NO = "3815DE178035C04BD26DEE2C1CD0E4DDE6FD0347";

    /**
     * 获取证书文件流信息,提供给具体实现读取证书用的
     *
     * @param certFilePath
     * @return
     */
    public byte[] getCertBytes(String mchId, String certFilePath) {
        if (StrUtil.isBlank(certFilePath)) {
            return null;
        }
        certFilePath = StrUtil.trimToNull(certFilePath);
        RedisUtil redisUtil = RedisUtil.getInstance();
        String key = FILE_CERT_REDIS_PREFIX + mchId + certFilePath;
        // 看下是否存在redis里面。
        String fileVal = redisUtil.get(key);
        if (fileVal != null) {
            try {
                return Base64.getDecoder().decode(fileVal.getBytes());
            }catch(Exception e){
                log.error(e.toString());
                return getCertificateFromCloud(certFilePath, redisUtil, key);
            }
        } else {
            return getCertificateFromCloud(certFilePath, redisUtil, key);
        }
    }

    private byte[] getCertificateFromCloud(String certFilePath, RedisUtil redisUtil, String key) {
        String[] bucketAndKey = certFilePath.split(FILE_CERT_URL_JOIN);
        InputStream in = fileStorageService.getFileStream(BucketEnum.CERT,bucketAndKey[1]);
        try {
            byte[] fileBytes = IOUtils.toByteArray(in);
            //缓存一天
            String findContents = Base64.getEncoder().encodeToString(fileBytes);
            redisUtil.set(key, findContents, 86400);
            return fileBytes;
        } catch (IOException e) {
            log.error(e.toString());
        }
        return null;
    }

    /**
     * 添加分账接收方
     *
     * @param payConfig
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preparePay(PayConfig payConfig) throws Exception {
        if (StringUtils.isEmpty(payConfig.getOutTransactionId())) {
            log.error("无法进行前置分账,微信订单号为空");
            return false;
        }
        ProfitSharingService profitSharingService = getWxPayServiceInstance(payConfig, getCertBytes(payConfig.getMchId(), payConfig.getCertPath())).getProfitSharingService();
        // 封装微信请求参数
        ProfitSharingReceiverRequest receiverRequest = BeanUtil.copy(buildBaseParam(payConfig), ProfitSharingReceiverRequest.class);
        ProfitSharingReceiver profitSharingReceiver = ProfitSharingReceiver.builder()
                                                                           .account(payConfig.getReceiveMchId())
                                                                           .type(payConfig.getType())
                                                                           .name(payConfig.getReceiveMchName())
                                                                           .build();
        profitSharingReceiver.setRelationType();
        receiverRequest.setReceiver(JsonUtil.toJson(profitSharingReceiver));
        // 这里会取出所有参数封装为 Map 返回
        Map<String, String> params = receiverRequest.getSignParams();
        String sha256Sign = getSha256Sign(params, payConfig.getMchKey());
        receiverRequest.setSign(sha256Sign);
        log.info("prePay addReceiver params:{},receiverRequest:{}", JsonUtil.toJson(params), JsonUtil.toJson(receiverRequest));
        ProfitSharingReceiverResult sharingReceiverResult = profitSharingService.addReceiver(receiverRequest);
        if (null != sharingReceiverResult && sharingReceiverResult.getResultCode().equals(SUCCESS_STRING)) {
            log.info("添加分账接收方成功,响应信息:{}", JsonUtil.toJson(sharingReceiverResult));
            return true;
        }
        log.error("添加分账接收方,响应异常:{}", JsonUtil.toJson(sharingReceiverResult));
        return false;
    }

    /**
     * 请求单次分账
     *
     * @param payConfig
     * @return
     * @throws Exception
     */
    @Override
    protected String beginPay(PayConfig payConfig) throws Exception {
        // 封装参数
        ProfitSharingService profitSharingService = getWxPayServiceInstance(payConfig, getCertBytes(payConfig.getMchId(), payConfig.getCertPath())).getProfitSharingService();
        // 基本参数
        BaseWxPayRequest wxPayRequest = buildBaseParam(payConfig);
        // 单次分账参数
        ProfitSharingRequest request = BeanUtil.copy(wxPayRequest, ProfitSharingRequest.class);
        request.setTransactionId(payConfig.getOutTransactionId());
        request.setOutOrderNo(payConfig.getPartnerTradeNo());
        // 分账接收方列表
        ProfitSharingReceiver profitSharingReceiver = ProfitSharingReceiver.builder()
                                                                           .account(payConfig.getReceiveMchId())
                                                                           .type(payConfig.getType())
                                                                           .amount(payConfig.getAmount())
                                                                           .name(payConfig.getReceiveMchName())
                                                                           .description(payConfig.getDesc())
                                                                           .build();
                                                                           
        profitSharingReceiver.setRelationType();
        List<ProfitSharingReceiver> receivers = Collections.singletonList(profitSharingReceiver);
        request.setReceivers(JsonUtil.toJson(receivers));
        // 生成签名且设值
        Map<String, String> signParams = request.getSignParams();
        String sha256Sign = getSha256Sign(signParams, payConfig.getMchKey());
        request.setSign(sha256Sign);
        // 发出请求
        ProfitSharingResult sharingResult = profitSharingService.profitSharing(request);
        log.info("beginPay profitSharing params:{},receiverRequest:{}", JsonUtil.toJson(signParams), JsonUtil.toJson(request));
        if (null != sharingResult && sharingResult.getResultCode().equals(SUCCESS_STRING)) {
            log.info("请求单次分账成功,响应信息:{}", JsonUtil.toJson(sharingResult));
            return sharingResult.getOutOrderNo();
        }
        log.error("请求单次分账失败,响应异常:{}", JsonUtil.toJson(sharingResult));
        return null;
    }

    @Override
    protected Map<String, String> afterPay(PayConfig payConfig) throws Exception {
        return null;
    }

    /**
     * 构建分账请求前基本参数信息
     *
     * @param payConfig
     * @return
     */
    public static BaseWxPayRequest buildBaseParam(PayConfig payConfig) {
        BaseWxPayRequest wxPayRequest = new BaseWxPayRequest() {
            @Override
            protected void checkConstraints() {

            }

            @Override
            protected void storeMap(Map<String, String> map) {

            }
        };
        wxPayRequest.setAppid(payConfig.getWxAppId());
        wxPayRequest.setMchId(payConfig.getMchId());
        wxPayRequest.setSubMchId(payConfig.getSubMchId());
        wxPayRequest.setSubAppId(payConfig.getSubAppId());
        wxPayRequest.setNonceStr(getNonceStr());
        wxPayRequest.setSignType(HMAC_SHA256);
        return wxPayRequest;
    }

//    /**
//     * 分账回退
//     * @param payConfig
//     * @return
//     * @throws Exception
//     */
//    @Override
//    protected Map<String, String> afterPay(PayConfig payConfig) throws Exception {
//        /**
//         * 封装参数
//         */
//        Map<String, String> parm = new LinkedHashMap<>();
//        buildBaseParam(payConfig, parm);
//
//        parm.put("transaction_id", payConfig.getOutTransactionId()); //微信支付订单号
//        parm.put("out_order_no", payConfig.getPartnerTradeNo()); //商户系统内部的分账单号,使用orderNo+distributorId
//        parm.put("description", "分账已完成");
//
//        parm.put("sign", getSha256Sign(parm, payConfig.getMchKey()));
//
//        String xmlStrParam = XmlUtil.xmlFormat(parm, false);
//
//        log.info("beginPay finish-profitSharing url:{}, xmlStrParam:{}, payConfig:{}",
//                FINISH_PROFITSHARING_PAY_URL, xmlStrParam, payConfig.toString());
//        String respXml = HttpUtil.post(FINISH_PROFITSHARING_PAY_URL, xmlStrParam, payConfig);
//
//        log.info("beginPay finish-profitSharing respXml:{}", respXml);
//        return XmlUtil.xmlParse(respXml);
//    }

}

源码部分分享到此处,其他代码由于涉及隐私问题,无法贴出,有问题可以私信或留言
代码不重要,重要是流程和思路能够梳理清楚,代码至简,书写好的风格相信每个人都不一样,TODO

码农的成果

微信服务商分账思路剖析、设计流程及源码实现

总结

遇到事情不要慌,一点一点去攻破,到头来你就会感谢自己的付出没有白费,从一开始什么都没有,到一点一点去研究和分析,才能够最终实现该需求,路上的坑都踩过了,欢迎大家询问和指教!

网上资料也很多,该业务实现,杂七杂八的涉及也很多,也感谢各位博主的付出,贴出此文方案和自身的理解也是为了让各位大佬能够更快的熟悉和切入,避免将时间花费在无效的用处上

博文放在 微信体系 专栏里,欢迎订阅,会持续更新!

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!文章来源地址https://www.toymoban.com/news/detail-413771.html

到了这里,关于微信服务商分账思路剖析、设计流程及源码实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux内核源码分析 (B.4) 深度剖析 Linux 伙伴系统的设计与实现

    Linux内核源码分析 (B.4) 深度剖析 Linux 伙伴系统的设计与实现 在上篇文章 《深入理解 Linux 物理内存分配全链路实现》 中,笔者为大家详细介绍了 Linux 内存分配在内核中的整个链路实现: image.png 但是当内核执行到 get_page_from_freelist 函数,准备进入伙伴系统执行具体内存分配

    2024年02月07日
    浏览(34)
  • gRPC源码剖析-Server启动流程

    创建一个gRPC Server代码很简单就这么两行,我们可以运行起来单步调试来学习一下gRPC Server启动流程。 Server server = ServerBuilder.forPort(50051)                .addService(new OrderServiceImpl())                .build()                .start(); server.awaitTermination(); 绑定端口 调用NettyServer

    2024年02月07日
    浏览(30)
  • 【spring源码系列-02】通过refresh方法剖析IOC的整体流程

    Spring源码系列整体栏目 内容 链接地址 【一】spring源码整体概述 https://blog.csdn.net/zhenghuishengq/article/details/130940885 【二】通过refresh方法剖析IOC的整体流程 https://blog.csdn.net/zhenghuishengq/article/details/131003428 【三】xml配置文件启动spring时refresh的前置工作 https://blog.csdn.net/zhenghuishen

    2024年02月08日
    浏览(33)
  • python上海运动健身服务商家数据可视化系统设计与实现(django框架)

     博主介绍 :黄菊华老师《Vue.js入门与商城开发实战》《微信小程序商城开发》图书作者,CSDN博客专家,在线教育专家,CSDN钻石讲师;专注大学生毕业设计教育和辅导。 所有项目都配有从入门到精通的基础知识视频课程,免费 项目配有对应开发文档、开题报告、任务书、

    2024年02月04日
    浏览(25)
  • Zookeeper设计理念与源码剖析

    Follower server 可以直接处理读请求,但不能直接处理写请求。写请求只能转发给 leader server 进行处理。 最终所有的写请求在 leader server 端串行执行。(因为分布式环境下永远无法精确地确认不同服务器不同事件发生的先后顺序) ZooKeeper 集群中的所有节点的数据状态通过 ZAB 协

    2024年01月16日
    浏览(30)
  • Vue3设计思想及响应式源码剖析

    对TypeScript支持不友好(所有属性都放在了this对象上,难以推倒组件的数据类型) 大量的API挂载在Vue对象的原型上,难以实现TreeShaking。 架构层面对跨平台dom渲染开发支持不友好,vue3允许自定义渲染器,扩展能力强。 CompositionAPI。受ReactHook启发 对虚拟DOM进行了重写、对模板的

    2024年02月05日
    浏览(35)
  • zookeeper分布式协调系统的架构设计与源码剖析

    目录 001_我们一般到底用ZooKeeper来干什么事儿? 002_有哪些开源的分布式系统中使用了ZooKeeper? 003_为什么我们在分布式系统架构中需要使用ZooKeeper集群? 004_ZooKeeper为了满足分布式系统的需求要有哪些特点 005_为了满足分布式系统的需求,ZooKeeper的架构设计有哪些特点? 006_

    2024年02月03日
    浏览(35)
  • 设计模式学习笔记 - 开源实战一(下):通过剖析JDK源码学习灵活应用设计模式

    上篇文章我们讲解了工厂模式、建造者模式、适配器模式适配器模式在 JDK 中的应用,其中 Calendar 类用到了工厂模式和建造者模式, Collections 类用到了装饰器模式和适配器模式。学习的重点是让你了解,在真实的项目中模式的实现和应用更加灵活、多变,会根据具体的场景做

    2024年04月28日
    浏览(31)
  • 【小程序】实现登录的流程与封装思路

    基本介绍 为什么需要用户登录? 增加用户的粘性和产品的停留时间; 如何识别同一个小程序用户身份? 认识小程序登录流程 openid和unionid 获取code 换取authToken 用户身份多平台共享 账号绑定 手机号绑定 基本演练 第一步操作, 获取到code 第二步将code发送到后端服务器 判断t

    2024年02月11日
    浏览(19)
  • HBase 的功能原理、设计思路、架构设计及源码的解析

    作者:禅与计算机程序设计艺术 1.1 HBase 是什么? HBase 是 Apache 基金会下开源的 NoSQL 数据存储系统。它可以运行于 Hadoop 的环境中,并提供高可靠性、高性能的数据读写服务。HBase 具备列族灵活的结构,支持海量数据的随机查询,适用于各种非关系型数据分析场景。 从 2007 年

    2024年02月08日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包