feat(shopro_huifu): 添加分账扩展材料上传功能并优化图片处理

- 新增 UploadSplitExtendedMaterials 命令用于上传 F469/F330 等分账补充材料
- 优化 ProbeSplitConfigDeps 命令中扩展材料配置的检查逻辑
- 支持 PDF 文件上传,增加文件大小限制(PDF 5MB,图片 2MB)
- 修复 MCC 编码可作为 online_busi_type 备选值的问题

```
这个提交包含在:
可乐加冰
2026-10-16 09:59:32 +08:00
父节点 7767298a43
当前提交 420367200c
共有 4 个文件被更改,包括 175 次插入 和 11 次删除

查看文件

@@ -67,8 +67,21 @@ class ProbeSplitConfigDeps extends Command
$output->writeln('');
$extMat = trim((string) $cfg->get('split_config_extended_material_json', ''));
$output->writeln('--- extended_material_list (按编码表可选/必填) ---');
$output->writeln('split_config_extended_material_json: ' . ($extMat !== '' ? '已配置' : '(空,若下一报错要求补充材料再填)'));
$output->writeln('--- extended_material_list (H5921L 等常需 F469/F330) ---');
if ($extMat === '') {
$output->writeln('split_config_extended_material_json: (空)');
$output->writeln('[FAIL] 酒类分账常见缺 F469 协议 + F330 食品证 → upload-split-ext-materials');
} else {
$output->writeln('split_config_extended_material_json: 已配置');
$decoded = json_decode($extMat, true);
if (is_array($decoded)) {
$types = array_column($decoded, 'file_type');
foreach (['F469', 'F330'] as $need) {
$ok = in_array($need, $types, true);
$output->writeln(' ' . ($ok ? '[OK]' : '[FAIL]') . ' ' . $need);
}
}
}
$client = new HuifuClient();
$basic = $client->queryMerchantBasicdata($main, true);

查看文件

@@ -0,0 +1,119 @@
<?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 补充材料 extended_material_list(如 H5921L 要求的 F469/F330)
*/
class UploadSplitExtendedMaterials extends Command
{
protected function configure()
{
$this->setName('shopro_huifu:upload-split-ext-materials')
->addOption('f469-url', null, Option::VALUE_OPTIONAL, 'F469 分账配置-服务佣金相关协议(pdf/jpg/png URL)')
->addOption('f330-url', null, Option::VALUE_OPTIONAL, 'F330 食品经营许可证(pdf/jpg/png URL)')
->addOption('file-type', null, Option::VALUE_OPTIONAL, '自定义:格式 F469=https://... 可多次传')
->addOption('online-busi-type', null, Option::VALUE_OPTIONAL, '一并写入 split_config_online_busi_type,如 H5921L')
->addOption('save-config', null, Option::VALUE_NONE, '写入 split_config_extended_material_json')
->setDescription('Upload F469/F330 etc. for split/config extended_material_list');
}
protected function execute(Input $input, Output $output)
{
if (!Bridge::isEnabled()) {
$output->writeln('[FAIL] 汇付插件未启用');
return 1;
}
$tasks = [];
$f469 = trim((string) $input->getOption('f469-url'));
$f330 = trim((string) $input->getOption('f330-url'));
if ($f469 !== '') {
$tasks['F469'] = $f469;
}
if ($f330 !== '') {
$tasks['F330'] = $f330;
}
$custom = trim((string) $input->getOption('file-type'));
if ($custom !== '' && strpos($custom, '=') !== false) {
list($ft, $url) = explode('=', $custom, 2);
$ft = trim($ft);
$url = trim($url);
if ($ft !== '' && $url !== '') {
$tasks[$ft] = $url;
}
}
if ($tasks === []) {
$output->writeln('[FAIL] 至少提供 --f469-url 与 --f330-url(酒类 H5921L 分账常见必填)');
$output->writeln('示例:');
$output->writeln(' php think shopro_huifu:upload-split-ext-materials \\');
$output->writeln(' --f469-url=https://域名/多方分账协议.pdf \\');
$output->writeln(' --f330-url=https://域名/食品经营许可证.pdf \\');
$output->writeln(' --online-busi-type=H5921L --save-config');
return 1;
}
$picture = new HuifuOnboardPicture();
$list = [];
foreach ($tasks as $fileType => $url) {
$output->writeln('上传 ' . $fileType . ' ...');
$fileId = $picture->uploadByUrl($fileType, $url);
$output->writeln(' file_id: ' . $fileId);
$list[] = [
'file_id' => $fileId,
'file_type' => $fileType,
];
}
$json = json_encode($list, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$output->writeln('');
$output->writeln('split_config_extended_material_json:');
$output->writeln($json);
if ($input->getOption('save-config')) {
$this->upsertConfig('split_config_extended_material_json', $json, 'split/config extended_material_list JSON');
HuifuConfig::reload();
$output->writeln('[OK] 已写入 split_config_extended_material_json');
}
$busiType = trim((string) $input->getOption('online-busi-type'));
if ($busiType !== '') {
if ($input->getOption('save-config')) {
$this->upsertConfig('split_config_online_busi_type', $busiType, '线上业务类型编码');
HuifuConfig::reload();
$output->writeln('[OK] 已写入 split_config_online_busi_type=' . $busiType);
} else {
$output->writeln('[提示] 未 --save-config,online_busi_type 未写入库');
}
}
$output->writeln('');
$output->writeln('下一步: php think shopro_huifu:sync-split-receivers');
return 0;
}
protected function upsertConfig($name, $value, $description)
{
$row = Db::name('shopro_huifu_config')->where('name', $name)->find();
if ($row) {
Db::name('shopro_huifu_config')->where('name', $name)->update(['value' => $value]);
} else {
Db::name('shopro_huifu_config')->insert([
'name' => $name,
'value' => $value,
'group' => 'split',
'description' => $description,
]);
}
}
}

查看文件

@@ -29,10 +29,14 @@ class HuifuOnboardPicture
$request->setFileType($fileType);
list($sourcePath, $sourceTemp) = $this->resolveUploadFile($imageUrl);
list($uploadPath, $uploadTemp) = $this->normalizeImageForHuifu($sourcePath, $sourceTemp);
if ($this->isPdfFile($sourcePath)) {
list($uploadPath, $uploadTemp) = [$sourcePath, $sourceTemp];
} else {
list($uploadPath, $uploadTemp) = $this->normalizeImageForHuifu($sourcePath, $sourceTemp);
}
$filename = $this->buildUploadFilename($fileType, $uploadPath);
$mime = $this->detectMime($uploadPath);
$mime = $this->isPdfFile($uploadPath) ? 'application/pdf' : $this->detectMime($uploadPath);
$merchantHuifuId = $this->sdk->getPictureMerchantHuifuId();
$extend = $this->sdk->mergeSkillHuifuId([
@@ -166,9 +170,11 @@ class HuifuOnboardPicture
'F12' => 'bank_card',
'F13' => 'bank_card',
];
$base = $map[$fileType] ?? 'upload';
$base = $map[$fileType] ?? strtolower($fileType);
$ext = strtolower(pathinfo($localPath, PATHINFO_EXTENSION));
if (!in_array($ext, ['jpg', 'jpeg', 'png'], true)) {
if ($this->isPdfFile($localPath)) {
$ext = 'pdf';
} elseif (!in_array($ext, ['jpg', 'jpeg', 'png'], true)) {
$ext = 'jpg';
}
if ($ext === 'jpeg') {
@@ -177,6 +183,15 @@ class HuifuOnboardPicture
return substr($base, 0, 20) . '.' . $ext;
}
protected function isPdfFile($localPath)
{
if (!is_file($localPath)) {
return false;
}
$head = @file_get_contents($localPath, false, null, 0, 5);
return is_string($head) && strncmp($head, '%PDF-', 5) === 0;
}
protected function detectMime($localPath)
{
if (function_exists('mime_content_type')) {
@@ -293,6 +308,12 @@ class HuifuOnboardPicture
{
$localPath = $this->resolveLocalPath($imageUrl);
if ($localPath && is_file($localPath)) {
$isPdf = $this->isPdfFile($localPath);
$maxBytes = $isPdf ? (5 * 1024 * 1024) : (2 * 1024 * 1024);
$size = filesize($localPath);
if ($size !== false && $size > $maxBytes) {
throw new \RuntimeException($isPdf ? 'PDF 超过 5MB,请压缩后重新上传' : '图片超过 2MB,请压缩后重新上传');
}
return [$localPath, false];
}
$absoluteUrl = $this->resolveAbsoluteUrl($imageUrl);
@@ -300,10 +321,12 @@ class HuifuOnboardPicture
if ($content === '') {
throw new \RuntimeException('无法读取图片:' . $absoluteUrl);
}
if (strlen($content) > 2 * 1024 * 1024) {
throw new \RuntimeException('图片超过 2MB,请压缩后重新上传');
$isPdf = strncmp($content, '%PDF-', 5) === 0;
$maxBytes = $isPdf ? (5 * 1024 * 1024) : (2 * 1024 * 1024);
if (strlen($content) > $maxBytes) {
throw new \RuntimeException($isPdf ? 'PDF 超过 5MB,请压缩后重新上传' : '图片超过 2MB,请压缩后重新上传');
}
$ext = $this->guessImageExt($content, $absoluteUrl);
$ext = $isPdf ? 'pdf' : $this->guessImageExt($content, $absoluteUrl);
$tmp = tempnam(sys_get_temp_dir(), 'hf_pic_');
if ($tmp === false) {
throw new \RuntimeException('创建临时图片文件失败');
@@ -345,7 +368,10 @@ class HuifuOnboardPicture
}
$path = parse_url($url, PHP_URL_PATH);
$ext = strtolower(pathinfo((string) $path, PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png'], true)) {
if (strncmp($binary, '%PDF-', 5) === 0) {
return 'pdf';
}
if (in_array($ext, ['jpg', 'jpeg', 'png', 'pdf'], true)) {
return $ext === 'jpeg' ? 'jpg' : $ext;
}
return 'jpg';

查看文件

@@ -448,8 +448,14 @@ class HuifuMerchantSplitService
try {
$basic = $this->client->queryMerchantBasicdata($mainHuifuId);
if (!empty($basic['ok']) && is_array($basic['data'] ?? null)) {
$found = HuifuClient::collectNestedKeys($basic['data'], ['online_busi_type']);
$found = HuifuClient::collectNestedKeys($basic['data'], ['online_busi_type', 'mcc']);
$v = trim((string) ($found['online_busi_type'] ?? ''));
if ($v === '') {
$mcc = trim((string) ($found['mcc'] ?? ''));
if ($mcc !== '' && preg_match('/^H[A-Z0-9]{4,}$/i', $mcc)) {
$v = strtoupper($mcc);
}
}
if ($v !== '') {
return $v;
}