feat(shopro_huifu): 添加汇付进件自动提交功能

- 新增自动提交开关配置项,支持定时自动提交汇付进件
- 添加失败重提间隔配置,支持失败后指定时间自动重试
- 创建OnboardAutoSubmit命令类,实现自动提交逻辑
- 在进件页面更新提示信息,说明自动提交流程
- 同步更新数据库配置表结构和升级脚本
```
这个提交包含在:
可乐加冰
2026-10-08 13:40:52 +08:00
父节点 fd14b701c3
当前提交 fa8d57693b
共有 12 个文件被更改,包括 224 次插入 和 5 次删除

查看文件

@@ -46,6 +46,13 @@
<el-form-item label="结算周期"><el-input v-model="form.onboard_settle_cycle" placeholder="D1"></el-input></el-form-item>
<el-form-item label="结算批次号"><el-input v-model="form.onboard_settle_batch_no"></el-input></el-form-item>
<el-form-item label="进件通知URL"><el-input v-model="form.onboard_notify_url" readonly></el-input></el-form-item>
<el-form-item label="自动提交进件">
<el-switch v-model="form.onboard_auto_submit" active-value="1" inactive-value="0"></el-switch>
<span class="text-muted" style="font-size:12px;margin-left:8px;">需配置 crontab: shopro_huifu:onboard-auto-submit</span>
</el-form-item>
<el-form-item label="失败重提间隔(小时)">
<el-input v-model="form.onboard_auto_retry_hours" placeholder="24" style="width:120px"></el-input>
</el-form-item>
<el-divider content-position="left">只读</el-divider>
<el-form-item label="支付通知URL"><el-input v-model="form.notify_url" readonly></el-input></el-form-item>

查看文件

@@ -21,7 +21,7 @@
<div class="panel panel-default panel-intro">
<div class="panel-heading">
汇付个人进件
<span class="text-muted" style="font-size:12px;margin-left:12px;">先审核进件资料,业务审核通过后再提交汇付</span>
<span class="text-muted" style="font-size:12px;margin-left:12px;">业务审核通过后由定时任务自动提交汇付;失败满 24 小时自动重提(可手动「提交汇付/重提」)</span>
</div>
<div class="panel-body">
<div style="margin-bottom:12px;">

查看文件

@@ -0,0 +1,39 @@
<?php
namespace addons\shopro_huifu\command;
use addons\shopro_huifu\library\onboard\HuifuOnboardAutoSubmit;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
class OnboardAutoSubmit extends Command
{
protected function configure()
{
$this->setName('shopro_huifu:onboard-auto-submit')
->addOption('limit', 'l', Option::VALUE_OPTIONAL, '每轮最多处理条数', 30)
->addOption('dry-run', null, Option::VALUE_NONE, '只扫描不调用汇付')
->setDescription('Auto submit Huifu onboard: new drafts immediately, failed retry after cooldown');
}
protected function execute(Input $input, Output $output)
{
$dryRun = (bool) $input->getOption('dry-run');
$limit = intval($input->getOption('limit'));
$output->writeln('=== 汇付进件自动提交 ===' . ($dryRun ? ' [dry-run]' : ''));
$stats = (new HuifuOnboardAutoSubmit())->run($limit, $dryRun);
$output->writeln('扫描: ' . $stats['scanned']);
$output->writeln('首次提交: ' . $stats['submit_now']);
$output->writeln('失败重提: ' . $stats['retry']);
$output->writeln('跳过: ' . $stats['skipped']);
$output->writeln('执行失败: ' . $stats['failed']);
foreach ($stats['details'] as $line) {
$output->writeln(' · ' . $line);
}
return $stats['failed'] > 0 ? 1 : 0;
}
}

查看文件

@@ -105,5 +105,7 @@ INSERT IGNORE INTO `__PREFIX__shopro_huifu_config` (`name`, `value`, `group`, `d
('onboard_cash_type', 'D1', 'onboard', '取现类型'),
('onboard_cash_fix_amt', '0.03', 'onboard', '取现固定手续费'),
('onboard_cash_fee_rate', '0', 'onboard', '取现费率'),
('onboard_skill_source', 'hfps/1.3.5;hfms/1.0.2', 'onboard', '进件SDK skill_source');
('onboard_skill_source', 'hfps/1.3.5;hfms/1.0.2', 'onboard', '进件SDK skill_source'),
('onboard_auto_submit', '1', 'onboard', '定时自动提交汇付进件(0关1开)'),
('onboard_auto_retry_hours', '24', 'onboard', '进件失败后自动重提间隔(小时)');

查看文件

@@ -302,6 +302,15 @@ class HuifuOnboardAdminView
return $parts ? implode('、', $parts) : '无进件角色';
}
/**
* 分销/区域公司业务审核是否已通过(status=normal)
*/
public static function isBusinessApproved(array $data)
{
$biz = self::businessStatus($data);
return ($biz['status'] ?? '') === 'normal';
}
protected static function businessStatus(array $data)
{
$roleType = $data['role_type'] ?? '';

查看文件

@@ -0,0 +1,146 @@
<?php
namespace addons\shopro_huifu\library\onboard;
use addons\shopro_huifu\library\HuifuConfig;
use addons\shopro_huifu\model\Onboard;
use think\Log;
/**
* 汇付进件后台自动提交:业务已通过且从未提交汇付的立即提交;失败/驳回满冷却期后重提。
*/
class HuifuOnboardAutoSubmit
{
public function run($limit = 30, $dryRun = false)
{
$stats = [
'scanned' => 0,
'submit_now' => 0,
'retry' => 0,
'skipped' => 0,
'failed' => 0,
'details' => [],
];
if (!HuifuOnboardGate::isOnboardRequired()) {
$stats['details'][] = 'skip: 汇付进件未启用';
return $stats;
}
if (!$this->isAutoSubmitEnabled()) {
$stats['details'][] = 'skip: onboard_auto_submit=0';
return $stats;
}
$limit = max(1, min(100, (int) $limit));
$retryAfter = $this->getRetryAfterSeconds();
$service = new HuifuOnboardService();
$now = time();
$rows = Onboard::where('onboard_status', 'in', [
Onboard::STATUS_DRAFT,
Onboard::STATUS_FAILED,
Onboard::STATUS_REJECTED,
])->order('id asc')->limit($limit * 3)->select();
foreach ($rows as $row) {
if ($stats['submit_now'] + $stats['retry'] >= $limit) {
break;
}
$stats['scanned']++;
$data = $row->toArray();
$action = $this->resolveAction($data, $now, $retryAfter);
if ($action === null) {
$stats['skipped']++;
continue;
}
$payload = json_decode($row['payload'] ?? '', true);
if (!$payload || !is_array($payload)) {
$stats['skipped']++;
$stats['details'][] = 'id=' . $row['id'] . ' skip: 无进件资料 payload';
continue;
}
$line = 'id=' . $row['id'] . ' uid=' . $row['user_id'] . ' ' . $row['role_type']
. ' action=' . $action;
if ($dryRun) {
$stats[$action === 'submit_now' ? 'submit_now' : 'retry']++;
$stats['details'][] = $line . ' (dry-run)';
continue;
}
try {
if ($action === 'submit_now') {
$service->submitFromDraft($row['user_id'], $row['role_type'], $row['role_ref_id']);
$stats['submit_now']++;
} else {
$service->resubmit($row['user_id'], $row['role_type'], $row['role_ref_id'], $payload);
$stats['retry']++;
}
$stats['details'][] = $line . ' ok';
} catch (\Throwable $e) {
$stats['failed']++;
$msg = HuifuOnboardError::translate($e->getMessage());
Log::error('[shopro_huifu] onboard auto-submit fail ' . $line . ' ' . $e->getMessage());
$stats['details'][] = $line . ' fail: ' . $msg;
}
}
return $stats;
}
/**
* @return string|null submit_now|retry
*/
protected function resolveAction(array $data, $now, $retryAfter)
{
if (!HuifuOnboardAdminView::isBusinessApproved($data)) {
return null;
}
$status = (string) ($data['onboard_status'] ?? '');
$updatedAt = intval($data['updatetime'] ?? 0);
if ($status === Onboard::STATUS_DRAFT) {
if ($this->hasSubmittedToHuifu($data)) {
return null;
}
return 'submit_now';
}
if (in_array($status, [Onboard::STATUS_FAILED, Onboard::STATUS_REJECTED], true)) {
if ($updatedAt <= 0 || ($now - $updatedAt) < $retryAfter) {
return null;
}
return 'retry';
}
return null;
}
protected function hasSubmittedToHuifu(array $data)
{
if (!empty($data['basic_req_seq_id']) || !empty($data['busi_req_seq_id'])) {
return true;
}
if (!empty($data['huifu_id'])) {
return true;
}
return false;
}
protected function isAutoSubmitEnabled()
{
return HuifuConfig::getInstance()->get('onboard_auto_submit', '1') !== '0';
}
protected function getRetryAfterSeconds()
{
$hours = (float) HuifuConfig::getInstance()->get('onboard_auto_retry_hours', '24');
if ($hours <= 0) {
$hours = 24;
}
return (int) round($hours * 3600);
}
}

查看文件

@@ -25,7 +25,9 @@ define(['jquery', 'bootstrap', 'backend', 'form'], function ($, undefined, Backe
onboard_settle_cycle: 'D1',
onboard_settle_batch_no: '300',
onboard_settle_fee_rate: '0.05',
onboard_notify_url: ''
onboard_notify_url: '',
onboard_auto_submit: '1',
onboard_auto_retry_hours: '24'
}
},
mounted: function () {

查看文件

@@ -0,0 +1,4 @@
-- 汇付进件自动提交配置(已有站点执行一次)
INSERT IGNORE INTO `__PREFIX__shopro_huifu_config` (`name`, `value`, `group`, `description`) VALUES
('onboard_auto_submit', '1', 'onboard', '定时自动提交汇付进件(0关1开)'),
('onboard_auto_retry_hours', '24', 'onboard', '进件失败后自动重提间隔(小时)');

查看文件

@@ -52,6 +52,13 @@
</div>
</el-form-item>
<el-form-item label="进件通知URL"><el-input v-model="form.onboard_notify_url" readonly></el-input></el-form-item>
<el-form-item label="自动提交进件">
<el-switch v-model="form.onboard_auto_submit" active-value="1" inactive-value="0"></el-switch>
<span class="text-muted" style="font-size:12px;margin-left:8px;">需配置 crontab: shopro_huifu:onboard-auto-submit</span>
</el-form-item>
<el-form-item label="失败重提间隔(小时)">
<el-input v-model="form.onboard_auto_retry_hours" placeholder="24" style="width:120px"></el-input>
</el-form-item>
<el-divider content-position="left">只读</el-divider>
<el-form-item label="支付通知URL"><el-input v-model="form.notify_url" readonly></el-input></el-form-item>

查看文件

@@ -21,7 +21,7 @@
<div class="panel panel-default panel-intro">
<div class="panel-heading">
汇付个人进件
<span class="text-muted" style="font-size:12px;margin-left:12px;">先审核进件资料,业务审核通过后再提交汇付</span>
<span class="text-muted" style="font-size:12px;margin-left:12px;">业务审核通过后由定时任务自动提交汇付;失败满 24 小时自动重提(可手动「提交汇付/重提」)</span>
</div>
<div class="panel-body">
<div style="margin-bottom:12px;">

查看文件

@@ -26,6 +26,7 @@ return [
'addons\shopro_huifu\command\ProbeWechatOnboarding',
'addons\shopro_huifu\command\QuerySplitConfig',
'addons\shopro_huifu\command\OnboardPoll',
'addons\shopro_huifu\command\OnboardAutoSubmit',
'addons\shopro_huifu\command\ImportBankBranch',
'addons\shopro_wechat_trade\command\InitOrderPath',
'addons\shopro_wechat_trade\command\ProbeOrderPath',

查看文件

@@ -24,7 +24,9 @@ define(['jquery', 'bootstrap', 'backend', 'form'], function ($, undefined, Backe
onboard_required: '1',
onboard_settle_cycle: 'D1',
onboard_settle_batch_no: '300',
onboard_notify_url: ''
onboard_notify_url: '',
onboard_auto_submit: '1',
onboard_auto_retry_hours: '24'
}
},
mounted: function () {