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

    • 需求背景
    • 前期准备
    • 实现步骤
    • 流程分析
    • 实现过程中问题点
    • 源码
    • 码农的成果
    • 总结

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

需求背景

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

前期准备

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张图片
以上流程图,是如何运用分账记录进行实时的请求分账,以及分账前后的预处理工作流程分解

实现过程中问题点

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

源码

  4.1.0
  
 
    com.github.binarywang
     weixin-java-pay
     ${binarywang.version}
 
/**
 * @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 afterPay(PayConfig payConfig) throws Exception {
//        /**
//         * 封装参数
//         */
//        Map 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 个人博客

你可能感兴趣的:(业务设计,微信,小程序)