```
feat(shopro_huifu): 添加分账媒体文件上传命令并优化分账配置流程 - 新增 UploadSplitMedia 命令用于上传小程序运营媒介截图到汇付 - 在 SyncSplitReceivers 中添加异常处理和更清晰的日志输出 - 优化 HuifuMerchantSplitService 中的分账配置逻辑 - 当分账比例>20%时自动验证并上传运营媒介材料 - 添加相关配置项到数据库升级脚本 - 注册新命令到应用命令列表 ```
这个提交包含在:
@@ -54,16 +54,21 @@ class SyncSplitReceivers extends Command
|
||||
'rule_origin' => $ruleOrigin,
|
||||
];
|
||||
|
||||
if ($initOnly || $ruleOrigin === '01') {
|
||||
$output->writeln('模式: 提交分账配置(rule_origin=01 不传 acct_split_bunch_list)');
|
||||
if ($ruleOrigin === '01') {
|
||||
$result = $service->syncAllApprovedReceivers($options);
|
||||
try {
|
||||
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 {
|
||||
$result = $service->submitSplitConfig([], $options);
|
||||
$output->writeln('模式: rule_origin=02,合并远端+本地进件后提交接收方列表');
|
||||
$result = $service->syncAllApprovedReceivers($options);
|
||||
}
|
||||
} else {
|
||||
$output->writeln('模式: rule_origin=02,合并远端+本地进件后提交接收方列表');
|
||||
$result = $service->syncAllApprovedReceivers($options);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$output->writeln('[FAIL] ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!empty($result['dry_run'])) {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro_huifu\command;
|
||||
|
||||
use addons\shopro_huifu\library\Bridge;
|
||||
use addons\shopro_huifu\library\HuifuConfig;
|
||||
use addons\shopro_huifu\library\onboard\HuifuOnboardPicture;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 上传小程序运营媒介截图到汇付,生成 split/config 所需的 online_media_info_list JSON
|
||||
*/
|
||||
class UploadSplitMedia extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('shopro_huifu:upload-split-media')
|
||||
->addOption('home-url', null, Option::VALUE_REQUIRED, '首页截图 HTTPS 地址')
|
||||
->addOption('goods-url', null, Option::VALUE_REQUIRED, '商品/服务明细页截图')
|
||||
->addOption('order-url', null, Option::VALUE_REQUIRED, '下单场景页截图')
|
||||
->addOption('pay-url', null, Option::VALUE_REQUIRED, '支付页截图')
|
||||
->addOption('media-name', null, Option::VALUE_OPTIONAL, '小程序名称,默认 雄戈小酒')
|
||||
->addOption('media-type', null, Option::VALUE_OPTIONAL, '运营媒介类型,小程序默认 S3')
|
||||
->addOption('file-type', null, Option::VALUE_OPTIONAL, '汇付图片上传 file_type,默认 F24')
|
||||
->addOption('save-config', null, Option::VALUE_NONE, '写入 shopro_huifu_config.split_config_online_media_json')
|
||||
->setDescription('Upload miniprogram media screenshots for split/config (apply_ratio>20%)');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
if (!Bridge::isEnabled()) {
|
||||
$output->writeln('[FAIL] 汇付插件未启用');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$urls = [
|
||||
'media_front_page' => trim((string) $input->getOption('home-url')),
|
||||
'media_service_page' => trim((string) $input->getOption('goods-url')),
|
||||
'media_order_page' => trim((string) $input->getOption('order-url')),
|
||||
'media_pay_page' => trim((string) $input->getOption('pay-url')),
|
||||
];
|
||||
$labels = [
|
||||
'media_front_page' => 'home-url',
|
||||
'media_service_page' => 'goods-url',
|
||||
'media_order_page' => 'order-url',
|
||||
'media_pay_page' => 'pay-url',
|
||||
];
|
||||
foreach ($urls as $key => $url) {
|
||||
if ($url === '') {
|
||||
$output->writeln('[FAIL] 缺少 --' . $labels[$key]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
$fileType = trim((string) $input->getOption('file-type'));
|
||||
if ($fileType === '') {
|
||||
$fileType = 'F24';
|
||||
}
|
||||
$mediaName = trim((string) $input->getOption('media-name'));
|
||||
if ($mediaName === '') {
|
||||
$mediaName = '雄戈小酒';
|
||||
}
|
||||
$mediaType = trim((string) $input->getOption('media-type'));
|
||||
if ($mediaType === '') {
|
||||
$mediaType = 'S3';
|
||||
}
|
||||
|
||||
$picture = new HuifuOnboardPicture();
|
||||
$fileIds = [];
|
||||
foreach ($urls as $field => $url) {
|
||||
$output->writeln('上传 ' . $field . ' ...');
|
||||
$fileIds[$field] = $picture->uploadByUrl($fileType, $url);
|
||||
$output->writeln(' file_id: ' . $fileIds[$field]);
|
||||
}
|
||||
|
||||
$item = [
|
||||
'media_type' => $mediaType,
|
||||
'media_name' => $mediaName,
|
||||
'media_mer_common_flag' => 'Y',
|
||||
'media_front_page' => $fileIds['media_front_page'],
|
||||
'media_service_page' => $fileIds['media_service_page'],
|
||||
'media_order_page' => $fileIds['media_order_page'],
|
||||
'media_pay_page' => $fileIds['media_pay_page'],
|
||||
];
|
||||
$appId = trim((string) Bridge::getMiniProgramAppId());
|
||||
if ($appId !== '') {
|
||||
$item['app_id'] = $appId;
|
||||
}
|
||||
|
||||
$json = json_encode([$item], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$output->writeln('');
|
||||
$output->writeln('split_config_online_media_json(可复制到 fa_shopro_huifu_config):');
|
||||
$output->writeln($json);
|
||||
|
||||
if ($input->getOption('save-config')) {
|
||||
$row = Db::name('shopro_huifu_config')->where('name', 'split_config_online_media_json')->find();
|
||||
if ($row) {
|
||||
Db::name('shopro_huifu_config')->where('name', 'split_config_online_media_json')->update(['value' => $json]);
|
||||
} else {
|
||||
Db::name('shopro_huifu_config')->insert([
|
||||
'name' => 'split_config_online_media_json',
|
||||
'value' => $json,
|
||||
'group' => 'split',
|
||||
'description' => 'split/config online_media_info_list JSON',
|
||||
]);
|
||||
}
|
||||
HuifuConfig::reload();
|
||||
$output->writeln('[OK] 已写入 split_config_online_media_json');
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('下一步: php think shopro_huifu:sync-split-receivers');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -150,6 +150,17 @@ class HuifuMerchantSplitService
|
||||
$extend['split_ext_info'] = $splitExtInfo;
|
||||
}
|
||||
|
||||
$ratioFloat = floatval($applyRatio);
|
||||
$mediaList = $this->buildOnlineMediaInfoListJson($options, $ratioFloat);
|
||||
if ($mediaList !== null && $mediaList !== '') {
|
||||
$extend['online_media_info_list'] = $mediaList;
|
||||
} elseif ($this->needsOnlineMediaInfo($ratioFloat) && empty($options['skip_media_check'])) {
|
||||
throw new \InvalidArgumentException(
|
||||
'分账比例>20%须提交运营媒介 online_media_info_list(首页/商品/下单/支付截图的汇付 file_id)。'
|
||||
. '请执行 php think shopro_huifu:upload-split-media 或配置 split_config_online_media_json,见 docs/汇付延时分账测试指南.md'
|
||||
);
|
||||
}
|
||||
|
||||
$asyncUrl = trim((string) ($options['async_return_url'] ?? $this->config->get('split_config_async_url', '')));
|
||||
if ($asyncUrl !== '') {
|
||||
$extend['async_return_url'] = $asyncUrl;
|
||||
@@ -381,6 +392,105 @@ class HuifuMerchantSplitService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业商户分账比例 >20% 时汇付要求运营媒介材料(与业务开通一致)
|
||||
*/
|
||||
public function needsOnlineMediaInfo($applyRatio)
|
||||
{
|
||||
return floatval($applyRatio) > 20.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null JSON 字符串(数组),供 extend.online_media_info_list
|
||||
*/
|
||||
public function buildOnlineMediaInfoListJson(array $options = [], $applyRatio = 0.0)
|
||||
{
|
||||
if (isset($options['online_media_info_list'])) {
|
||||
$raw = $options['online_media_info_list'];
|
||||
if (is_array($raw)) {
|
||||
return json_encode($raw, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
$raw = trim((string) $raw);
|
||||
if ($raw !== '') {
|
||||
return $raw;
|
||||
}
|
||||
}
|
||||
|
||||
$cfgJson = trim((string) $this->config->get('split_config_online_media_json', ''));
|
||||
if ($cfgJson !== '') {
|
||||
$decoded = json_decode($cfgJson, true);
|
||||
if (!is_array($decoded)) {
|
||||
throw new \InvalidArgumentException('split_config_online_media_json 不是合法 JSON 数组');
|
||||
}
|
||||
return json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
$item = $this->buildOnlineMediaItemFromConfig();
|
||||
if ($item !== null) {
|
||||
return json_encode([$item], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
if ($this->needsOnlineMediaInfo($applyRatio)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>|null
|
||||
*/
|
||||
public function buildOnlineMediaItemFromConfig()
|
||||
{
|
||||
$front = trim((string) $this->config->get('split_config_media_front_page', ''));
|
||||
$service = trim((string) $this->config->get('split_config_media_service_page', ''));
|
||||
$order = trim((string) $this->config->get('split_config_media_order_page', ''));
|
||||
$pay = trim((string) $this->config->get('split_config_media_pay_page', ''));
|
||||
if ($front === '' || $service === '' || $order === '' || $pay === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mediaType = trim((string) $this->config->get('split_config_media_type', 'S3'));
|
||||
if ($mediaType === '') {
|
||||
$mediaType = 'S3';
|
||||
}
|
||||
$mediaName = trim((string) $this->config->get('split_config_media_name', ''));
|
||||
if ($mediaName === '') {
|
||||
$mediaName = '雄戈小酒';
|
||||
}
|
||||
$common = trim((string) $this->config->get('split_config_media_mer_common_flag', 'Y'));
|
||||
if ($common === '') {
|
||||
$common = 'Y';
|
||||
}
|
||||
|
||||
$item = [
|
||||
'media_type' => $mediaType,
|
||||
'media_name' => $mediaName,
|
||||
'media_mer_common_flag' => $common,
|
||||
'media_front_page' => $front,
|
||||
'media_service_page' => $service,
|
||||
'media_order_page' => $order,
|
||||
'media_pay_page' => $pay,
|
||||
];
|
||||
|
||||
$appId = trim((string) $this->config->get('split_config_media_app_id', ''));
|
||||
if ($appId === '' && class_exists(Bridge::class)) {
|
||||
$appId = trim((string) Bridge::getMiniProgramAppId());
|
||||
}
|
||||
if ($appId !== '' && in_array($mediaType, ['S2', 'S3', 'S4'], true)) {
|
||||
$item['app_id'] = $appId;
|
||||
}
|
||||
|
||||
if ($common === 'N') {
|
||||
$auth = trim((string) $this->config->get('split_config_media_authorize_materials', ''));
|
||||
if ($auth !== '') {
|
||||
$item['authorize_materials'] = $auth;
|
||||
}
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $huifuIds
|
||||
*/
|
||||
|
||||
@@ -12,4 +12,14 @@ INSERT IGNORE INTO `__PREFIX__shopro_huifu_config` (`name`, `value`, `group`, `d
|
||||
('split_config_ext_info', '', 'split', 'split_ext_info 整段 JSON,留空则>30%时用内置说明或下方三字段'),
|
||||
('split_config_busi_instruction', '', 'split', '业务模式说明 busi_instruction'),
|
||||
('split_config_capital_instruction', '', 'split', '资金流向说明 capital_instruction'),
|
||||
('split_config_function_instruction', '', 'split', '功能开通用途 function_instruction');
|
||||
('split_config_function_instruction', '', 'split', '功能开通用途 function_instruction'),
|
||||
('split_config_online_media_json', '', 'split', 'online_media_info_list 整段 JSON 数组(40%分账必填)'),
|
||||
('split_config_media_type', 'S3', 'split', '运营媒介类型 S3=小程序'),
|
||||
('split_config_media_name', '雄戈小酒', 'split', '媒介名称(小程序名)'),
|
||||
('split_config_media_mer_common_flag', 'Y', 'split', '媒介主体与商户主体是否一致 Y/N'),
|
||||
('split_config_media_app_id', '', 'split', '小程序 app_id,留空则读商城配置'),
|
||||
('split_config_media_front_page', '', 'split', '首页截图汇付 file_id'),
|
||||
('split_config_media_service_page', '', 'split', '商品明细页 file_id'),
|
||||
('split_config_media_order_page', '', 'split', '下单页 file_id'),
|
||||
('split_config_media_pay_page', '', 'split', '支付页 file_id'),
|
||||
('split_config_media_authorize_materials', '', 'split', '主体不一致时的授权材料 file_id');
|
||||
|
||||
在新工单中引用
屏蔽一个用户