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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 微信服务商分账思路剖析、设计流程及源码实现

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

需求背景

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

前期准备

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 个人博客



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
小程序 前端开发 测试技术
微信小程序的开发完整流程是什么?
微信小程序的开发完整流程是什么?
115 7
|
29天前
|
人工智能 弹性计算 搜索推荐
打造个性化的微信公众号AI小助手:从人设到工作流程
在数字化时代,一个有个性且功能强大的AI小助手能显著提升用户体验。本文档指导如何在微信公众号上设置AI小助手“小智”,涵盖其人设、功能规划及工作流程设计,旨在打造一个既智能又具吸引力的AI伙伴。
|
21天前
|
小程序 前端开发 算法
|
26天前
|
Java API 开发者
Java如何实现企业微信审批流程
大家好,我是V哥。本文分享如何在企业微信中实现审批流程,通过调用企业微信的开放API完成。主要内容包括获取Access Token、创建审批模板、发起审批流程和查询审批结果。提供了一个Java示例代码,帮助开发者快速上手。希望对你有帮助,关注V哥爱编程,编码路上同行。
|
1月前
|
移动开发 小程序 数据可视化
一招学会DIY官网可视化设计支持导出微擎、UNIAPP、H5、微信小程序源码
一招学会DIY官网可视化设计支持导出微擎、UNIAPP、H5、微信小程序源码
42 2
|
1月前
|
小程序
微信小程序的注册流程
微信小程序的注册流程
78 0
|
3月前
|
JSON 小程序 JavaScript
微信小程序制作 购物商城首页 【内包含源码】
这篇文章提供了一个微信小程序购物商城首页的实现方法和源码,包括页面布局、数据结构、核心代码以及如何配置tabBar和搜索框组件。
微信小程序制作 购物商城首页 【内包含源码】
|
2月前
|
搜索推荐
2024微信个人名片在线生成HTML源码
微信个人名片卡片在线生成,这是一款微信个人名片生成网站源码,无第三方接口,本地直接生成可长期使用。 主要用于生成用户个性化的名片页面,包括头像、姓名、联系方式、个人介绍等信息。 在本地浏览器打开即可,源码是html的,也可上传到服务器上。
50 0
2024微信个人名片在线生成HTML源码
|
3月前
|
前端开发 JavaScript API
微信公众号项目,实现微信支付(具体流程和参数)
微信公众号项目,实现微信支付(具体流程和参数)
|
4月前
|
JavaScript 小程序 Java
基于微信小程序餐厅点餐系统设计与实现(源码+lw+部署文档+讲解等)
基于微信小程序餐厅点餐系统设计与实现(源码+lw+部署文档+讲解等)
242 1
下一篇
无影云桌面