From ae5aac26ac575d892952d917fe909035e8edba4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=AF=E4=B9=90=E5=8A=A0=E5=86=B0?= <11869599+addie66@user.noreply.gitee.com> Date: Tue, 13 Oct 2026 10:50:30 +0800 Subject: [PATCH] =?UTF-8?q?```=20feat(shopro=5Fhuifu):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=88=86=E8=B4=A6=E5=AA=92=E4=BD=93=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=91=BD=E4=BB=A4=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=88=86=E8=B4=A6=E9=85=8D=E7=BD=AE=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 UploadSplitMedia 命令用于上传小程序运营媒介截图到汇付 - 在 SyncSplitReceivers 中添加异常处理和更清晰的日志输出 - 优化 HuifuMerchantSplitService 中的分账配置逻辑 - 当分账比例>20%时自动验证并上传运营媒介材料 - 添加相关配置项到数据库升级脚本 - 注册新命令到应用命令列表 ``` --- .../command/SyncSplitReceivers.php | 21 +-- .../shopro_huifu/command/UploadSplitMedia.php | 120 ++++++++++++++++++ .../split/HuifuMerchantSplitService.php | 110 ++++++++++++++++ .../shopro_huifu/upgrade_huifu_split_bind.sql | 12 +- application/command.php | 1 + 5 files changed, 255 insertions(+), 9 deletions(-) create mode 100644 addons/shopro_huifu/command/UploadSplitMedia.php diff --git a/addons/shopro_huifu/command/SyncSplitReceivers.php b/addons/shopro_huifu/command/SyncSplitReceivers.php index 9e32833..2bf10f2 100644 --- a/addons/shopro_huifu/command/SyncSplitReceivers.php +++ b/addons/shopro_huifu/command/SyncSplitReceivers.php @@ -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'])) { diff --git a/addons/shopro_huifu/command/UploadSplitMedia.php b/addons/shopro_huifu/command/UploadSplitMedia.php new file mode 100644 index 0000000..e021dc6 --- /dev/null +++ b/addons/shopro_huifu/command/UploadSplitMedia.php @@ -0,0 +1,120 @@ +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; + } +} diff --git a/addons/shopro_huifu/library/split/HuifuMerchantSplitService.php b/addons/shopro_huifu/library/split/HuifuMerchantSplitService.php index 493107d..8f3af42 100644 --- a/addons/shopro_huifu/library/split/HuifuMerchantSplitService.php +++ b/addons/shopro_huifu/library/split/HuifuMerchantSplitService.php @@ -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|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 */ diff --git a/addons/shopro_huifu/upgrade_huifu_split_bind.sql b/addons/shopro_huifu/upgrade_huifu_split_bind.sql index 7922b40..d2a4248 100644 --- a/addons/shopro_huifu/upgrade_huifu_split_bind.sql +++ b/addons/shopro_huifu/upgrade_huifu_split_bind.sql @@ -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'); diff --git a/application/command.php b/application/command.php index 80da871..9edec18 100644 --- a/application/command.php +++ b/application/command.php @@ -26,6 +26,7 @@ return [ 'addons\shopro_huifu\command\ProbeWechatOnboarding', 'addons\shopro_huifu\command\QuerySplitConfig', 'addons\shopro_huifu\command\SyncSplitReceivers', + 'addons\shopro_huifu\command\UploadSplitMedia', 'addons\shopro_huifu\command\DiagnoseFreezeFlow', 'addons\shopro_huifu\command\OnboardPoll', 'addons\shopro_huifu\command\OnboardAutoSubmit',