feat(shopro_huifu): 支持汇付分账配置rule_origin双模式

支持汇付分账配置的两种模式:
- rule_origin=01:接口发起模式,不带接收方列表,接收方在confirm时指定
- rule_origin=02:控台配置模式,需带acct_split_bunch_list固定接收方

更新SyncSplitReceivers命令支持--rule-origin选项,调整分账配置提交逻辑,
新增配置项split_config_rule_origin默认为01模式,并完善相关文档说明

BREAKING CHANGE: 分账配置流程调整,需重新配置相关参数
```
这个提交包含在:
可乐加冰
2026-10-13 10:35:02 +08:00
父节点 161d6f7e90
当前提交 b66b74b4f2
共有 4 个文件被更改,包括 97 次插入 和 38 次删除

查看文件

@@ -115,8 +115,8 @@ class QuerySplitConfig extends Command
}
$output->writeln('');
$output->writeln('下一步(汇付实施/工单):');
$output->writeln(' 1) v2/merchant/split/config 提交:rule_origin=01, div_flag=Y, apply_ratio≥业务比例(如30)');
$output->writeln(' 2) 控台/接口绑定分账接收方 6666000245630872(进件通过 ≠ 分账关系已配)');
$output->writeln(' 1) php think shopro_huifu:sync-split-receivers(rule_origin=01 仅开通分账,不带接收方列表)');
$output->writeln(' 2) 进件子户在 confirm 的 acct_split_bunch 中指定;若仍 22000002 再核控台分账关系/实施工单');
$output->writeln(' 3) 生效后再跑本命令,应出现 rule_origin;新订单测分账,勿重试 unconfirm_amt=0 的旧单');
$output->writeln('完整 JSON: php think shopro_huifu:query-split-config --json');
return 2;

查看文件

@@ -16,10 +16,11 @@ class SyncSplitReceivers extends Command
{
$this->setName('shopro_huifu:sync-split-receivers')
->addOption('dry-run', null, Option::VALUE_NONE, '仅预览将提交的接收方与参数,不调汇付')
->addOption('init-only', null, Option::VALUE_NONE, '仅提交主商户分账开关(rule_origin=01),不带接收方列表')
->addOption('init-only', null, Option::VALUE_NONE, '同默认:仅提交 split/config,不带 acct_split_bunch_list')
->addOption('rule-origin', null, Option::VALUE_OPTIONAL, '01=接口发起(默认) 02=控台配置且提交接收方列表')
->addOption('huifu-id', null, Option::VALUE_OPTIONAL, '主商户汇付号,默认 platform_huifu_id 或插件 huifu_id')
->addOption('scene', null, Option::VALUE_OPTIONAL, '分账模式 scene,默认插件 split_config_scene(A5)')
->setDescription('Submit v2/merchant/split/config and bind approved onboard huifu_ids as split receivers');
->setDescription('Submit v2/merchant/split/config (01=open split; 02=with receiver list)');
}
protected function execute(Input $input, Output $output)
@@ -40,24 +41,35 @@ class SyncSplitReceivers extends Command
$scene = $sceneOpt !== '' ? $sceneOpt : $service->resolveSplitConfigScene();
$output->writeln('scene(分账模式): ' . $scene);
$ruleOriginOpt = trim((string) $input->getOption('rule-origin'));
$ruleOrigin = $ruleOriginOpt !== '' ? $ruleOriginOpt : $service->resolveRuleOrigin([]);
$output->writeln('rule_origin: ' . $ruleOrigin . ($ruleOrigin === '01' ? '(接口发起,confirm 传 acct_split_bunch)' : '(控台配置,须带接收方列表)'));
$dryRun = (bool) $input->getOption('dry-run');
$initOnly = (bool) $input->getOption('init-only');
$options = [
'dry_run' => $dryRun,
'main_huifu_id' => $main,
'scene' => $scene,
'rule_origin' => $ruleOrigin,
];
if ($initOnly) {
$output->writeln('模式: init-only(不带 acct_split_bunch_list)');
$result = $service->submitSplitConfig([], $options);
if ($initOnly || $ruleOrigin === '01') {
$output->writeln('模式: 提交分账配置(rule_origin=01 不传 acct_split_bunch_list)');
if ($ruleOrigin === '01') {
$result = $service->syncAllApprovedReceivers($options);
} else {
$result = $service->submitSplitConfig([], $options);
}
} else {
$output->writeln('模式: 全量(远端已绑 + 本地已进件 approved)');
$output->writeln('模式: rule_origin=02,合并远端+本地进件后提交接收方列表');
$result = $service->syncAllApprovedReceivers($options);
}
if (!empty($result['dry_run'])) {
$output->writeln('[DRY-RUN] 将提交接收方 ' . count($result['receivers'] ?? []) . ' 个');
$localCount = count($result['receivers'] ?? []);
$output->writeln('[DRY-RUN] 本地/合并进件 huifu_id 共 ' . $localCount . ' 个'
. ($ruleOrigin === '01' ? '(01 模式不会写入 split/config 请求体)' : ''));
$output->writeln(json_encode($result['data'] ?? [], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
return 0;
}
@@ -66,9 +78,12 @@ class SyncSplitReceivers extends Command
if (isset($result['resp_code'])) {
$output->writeln('resp_code: ' . $result['resp_code']);
}
if (isset($result['submitted'])) {
$output->writeln('提交接收方数量: ' . $result['submitted']
if (isset($result['submitted']) && ($result['rule_origin'] ?? $ruleOrigin) === '02') {
$output->writeln('split/config 提交接收方: ' . $result['submitted']
. '(远端已有 ' . ($result['remote'] ?? 0) . ',本地进件 ' . ($result['local'] ?? 0) . ')');
} elseif (!empty($result['receivers'])) {
$output->writeln('本地已进件子户(confirm 时作为 acct_split_bunch 接收方,非 split/config 明细): '
. count($result['receivers']));
}
if (!empty($result['receivers'])) {
foreach ($result['receivers'] as $hid) {

查看文件

@@ -10,7 +10,10 @@ use think\Db;
use think\Log;
/**
* 主商户分账配置提交 + 进件子户绑定为分账接收方(v2/merchant/split/config)
* 主商户分账配置提交(v2/merchant/split/config)
*
* rule_origin=01(接口发起):不得传 acct_split_bunch_list,接收方在 delaytrans/confirm 的 acct_split_bunch 指定
* rule_origin=02(控台配置):须传 acct_split_bunch_list 固定接收方与比例
*/
class HuifuMerchantSplitService
{
@@ -117,12 +120,14 @@ class HuifuMerchantSplitService
}
$applyRatio = (string) ($options['apply_ratio'] ?? $this->resolveApplyRatio());
$ruleOrigin = trim((string) ($options['rule_origin'] ?? '01'));
if (!in_array($ruleOrigin, ['01', '02'], true)) {
$ruleOrigin = '01';
}
$ruleOrigin = $this->resolveRuleOrigin($options);
$receiverHuifuIds = $this->normalizeReceiverIds($receiverHuifuIds, $mainHuifuId);
if ($ruleOrigin === '01' && $receiverHuifuIds !== [] && empty($options['force_receiver_list'])) {
Log::write('[shopro_huifu] split/config rule_origin=01 忽略 acct_split_bunch_list('
. count($receiverHuifuIds) . ' 个接收方仅在 confirm 时指定)', 'info');
$receiverHuifuIds = [];
}
$scene = trim((string) ($options['scene'] ?? $this->resolveSplitConfigScene()));
if ($scene === '') {
@@ -150,19 +155,21 @@ class HuifuMerchantSplitService
$extend['async_return_url'] = $asyncUrl;
}
if ($receiverHuifuIds !== []) {
if ($ruleOrigin === '02') {
if ($receiverHuifuIds === []) {
throw new \InvalidArgumentException('rule_origin=02 时必须提供分账接收方列表');
}
$extend['acct_split_bunch_list'] = $this->encodeAcctSplitBunchList($receiverHuifuIds, $applyRatio);
} elseif ($ruleOrigin === '02') {
throw new \InvalidArgumentException('rule_origin=02 时必须提供分账接收方列表');
}
if ($dryRun) {
return [
'ok' => true,
'message' => 'dry-run',
'dry_run' => true,
'receivers' => $receiverHuifuIds,
'data' => ['extend' => $extend, 'huifu_id' => $mainHuifuId],
'ok' => true,
'message' => 'dry-run',
'dry_run' => true,
'receivers' => $receiverHuifuIds,
'rule_origin' => $ruleOrigin,
'data' => ['extend' => $extend, 'huifu_id' => $mainHuifuId],
];
}
@@ -176,9 +183,9 @@ class HuifuMerchantSplitService
}
/**
* 全量同步:合并远端 + 全部已进件子户,一次提交 split/config
* 提交分账配置:01 仅开通开关;02 时合并远端+本地进件后带 acct_split_bunch_list
*
* @return array{ok:bool,message:string,submitted:int,receivers:string[],remote:int,local:int}
* @return array{ok:bool,message:string,submitted:int,receivers:string[],remote:int,local:int,rule_origin?:string}
*/
public function syncAllApprovedReceivers(array $options = [])
{
@@ -186,31 +193,40 @@ class HuifuMerchantSplitService
throw new \RuntimeException('汇付插件未启用');
}
$ruleOrigin = $this->resolveRuleOrigin($options);
$main = $this->resolveMainHuifuId();
$local = $this->collectApprovedReceiverHuifuIds();
$remote = $this->fetchRemoteReceiverHuifuIds($main);
$merged = $this->normalizeReceiverIds(array_merge($remote, $local), $main);
$submit = $this->submitSplitConfig($merged, $options);
$toSubmit = $ruleOrigin === '02' ? $merged : [];
$submit = $this->submitSplitConfig($toSubmit, $options);
$ok = !empty($submit['ok']) && empty($submit['dry_run']);
$respCode = is_array($submit['data'] ?? null) ? (string) ($submit['data']['resp_code'] ?? '') : '';
$businessOk = $respCode === '' || $respCode === '00000000';
if ($ok && $businessOk && empty($options['dry_run'])) {
$this->markOnboardBindByHuifuIds($merged, 'ok', $submit['message'] ?? 'submitted');
if ($ruleOrigin === '02') {
$this->markOnboardBindByHuifuIds($merged, 'ok', $submit['message'] ?? 'submitted');
} else {
$this->markOnboardBindByHuifuIds($local, 'ok', 'rule_origin=01 分账配置已提交,接收方在 confirm 指定');
}
} elseif (!$ok && empty($options['dry_run'])) {
$this->markOnboardBindByHuifuIds($local, 'fail', $submit['message'] ?? 'submit failed');
}
return [
'ok' => !empty($submit['ok']) && ($businessOk || !empty($submit['dry_run'])),
'message' => (string) ($submit['message'] ?? ''),
'submitted' => count($merged),
'receivers' => $merged,
'remote' => count($remote),
'local' => count($local),
'resp_code' => $respCode,
'data' => $submit['data'] ?? null,
'ok' => !empty($submit['ok']) && ($businessOk || !empty($submit['dry_run'])),
'message' => (string) ($submit['message'] ?? ''),
'submitted' => $ruleOrigin === '02' ? count($merged) : 0,
'receivers' => $merged,
'remote' => count($remote),
'local' => count($local),
'resp_code' => $respCode,
'rule_origin' => $ruleOrigin,
'dry_run' => !empty($submit['dry_run']),
'data' => $submit['data'] ?? null,
];
}
@@ -240,12 +256,19 @@ class HuifuMerchantSplitService
return ['ok' => false, 'message' => 'split_auto_bind_receiver=0', 'skipped' => true];
}
$ruleOrigin = $this->resolveRuleOrigin($options);
if ($ruleOrigin === '01' && $this->isSplitConfigEffective($main)) {
$this->updateOnboardBindFields($row, 'skip', 'rule_origin=01,分账配置已生效');
return ['ok' => true, 'message' => 'split config already effective', 'skipped' => true];
}
$remote = $this->fetchRemoteReceiverHuifuIds($main);
$local = $this->collectApprovedReceiverHuifuIds();
$merged = $this->normalizeReceiverIds(array_merge($remote, $local, [$hid]), $main);
$toSubmit = $ruleOrigin === '02' ? $merged : [];
try {
$submit = $this->submitSplitConfig($merged, $options);
$submit = $this->submitSplitConfig($toSubmit, $options);
} catch (\Throwable $e) {
$this->updateOnboardBindFields($row, 'fail', $e->getMessage());
Log::error('[shopro_huifu] bind onboard split fail id=' . $row['id'] . ' ' . $e->getMessage());
@@ -284,6 +307,26 @@ class HuifuMerchantSplitService
return $scene !== '' ? $scene : 'A5';
}
public function resolveRuleOrigin(array $options = [])
{
$ruleOrigin = trim((string) ($options['rule_origin'] ?? $this->config->get('split_config_rule_origin', '01')));
if (!in_array($ruleOrigin, ['01', '02'], true)) {
$ruleOrigin = '01';
}
return $ruleOrigin;
}
public function isSplitConfigEffective($mainHuifuId = null)
{
$mainHuifuId = $mainHuifuId ?: $this->resolveMainHuifuId();
$result = $this->client->queryMerchantSplitConfig($mainHuifuId);
if (!$result['ok']) {
return false;
}
$items = HuifuClient::parseSplitBillResult($result['data']['split_bill_result'] ?? null);
return $items !== [];
}
/**
* @return string|null JSON 字符串,供 extend.split_ext_info
*/

查看文件

@@ -7,7 +7,8 @@ ALTER TABLE `__PREFIX__shopro_huifu_onboard`
INSERT IGNORE INTO `__PREFIX__shopro_huifu_config` (`name`, `value`, `group`, `description`) VALUES
('split_auto_bind_receiver', '1', 'split', '进件通过后自动调 v2/merchant/split/config 绑定分账接收方(0关1开)'),
('split_config_async_url', '', 'split', '分账配置异步通知 virgo:// 地址,可选'),
('split_config_scene', 'A5', 'split', '分账模式 scene:A5推广方佣金/02线上其他/00其他,须与汇付控台申请一致'),
('split_config_rule_origin', '01', 'split', '分账规则来源 01接口发起(商城confirm传账) 02控台配置'),
('split_config_scene', 'A5', 'split', '分账模式 scene:A5推广方佣金等,须与汇付申请一致(勿与02线上其他混淆)'),
('split_config_ext_info', '', 'split', 'split_ext_info 整段 JSON,留空则>30%时用内置说明或下方三字段'),
('split_config_busi_instruction', '', 'split', '业务模式说明 busi_instruction'),
('split_config_capital_instruction', '', 'split', '资金流向说明 capital_instruction'),