129 lines
4.8 KiB
PHP
129 lines
4.8 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Services;
|
||
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Symfony\Component\Process\Process;
|
||
use Throwable;
|
||
|
||
/**
|
||
* 科目映射表变更后,把受影响的「业绩明细」平台×月份放进后台重跑队列。
|
||
*
|
||
* 业绩明细的优惠/冲减/退款金额是同步时按映射表算好落库的,改映射必须重跑对应
|
||
* 平台×月份才会更新;这里把重跑做成后台自动执行,页面保存后无需人工执行命令。
|
||
*/
|
||
class AccountMappingResyncDispatcher
|
||
{
|
||
public const JOBS_TABLE = 'ec_account_mapping_resync_jobs';
|
||
|
||
/**
|
||
* 科目映射表的平台名 → [同步命令 platform 键, 业绩明细平台名]。
|
||
* 只保留已接入业绩同步的平台;其它平台(如唯品会JX)没有同步任务。
|
||
*/
|
||
private const PLATFORM_MAP = [
|
||
'淘宝' => [['taobao-alipay', '淘系支付宝'], ['taobao-wechat', '淘系微信']],
|
||
'天猫' => [['taobao-alipay', '淘系支付宝'], ['taobao-wechat', '淘系微信']],
|
||
'阿里巴巴' => [['taobao-alipay', '淘系支付宝']],
|
||
'拼多多' => [['pinduoduo', '拼多多']],
|
||
'京喜' => [['jingxi', '京喜账单']],
|
||
'京东' => [['jd-transactions', '京东账单'], ['jd-wallet', '京东网银钱包']],
|
||
'抖音' => [['douyin', '抖音']],
|
||
'得物' => [['dewu', '得物']],
|
||
];
|
||
|
||
/**
|
||
* @param list<string> $mappingPlatforms 科目映射表里的平台名
|
||
* @return array{jobs:int}
|
||
*/
|
||
public function dispatchFor(array $mappingPlatforms): array
|
||
{
|
||
$targets = [];
|
||
foreach ($mappingPlatforms as $platform) {
|
||
foreach (self::PLATFORM_MAP[trim((string) $platform)] ?? [] as [$key, $label]) {
|
||
$targets[$key] = $label;
|
||
}
|
||
}
|
||
if ($targets === []) {
|
||
return ['jobs' => 0];
|
||
}
|
||
|
||
$mirror = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
||
$periods = $mirror->table('store_receipt_order_match_rows')
|
||
->whereIn('platform', array_values($targets))
|
||
->select(['platform', 'report_month'])
|
||
->distinct()
|
||
->orderBy('report_month')
|
||
->get()->groupBy('platform');
|
||
if ($periods->isEmpty()) {
|
||
return ['jobs' => 0];
|
||
}
|
||
|
||
$now = now();
|
||
$jobs = 0;
|
||
foreach ($targets as $key => $label) {
|
||
$months = $periods->get($label, collect())->map(fn ($row) => substr((string) $row->report_month, 0, 7))->unique();
|
||
foreach ($months as $month) {
|
||
// running 时再次保存映射,会把状态改回 pending;当前进程完成时发现状态已变,
|
||
// 不会覆盖成 done,于是会紧接着再跑一遍,避免漏掉刚保存的新规则。
|
||
$affected = DB::table(self::JOBS_TABLE)->updateOrInsert(
|
||
['platform' => (string) $key, 'month' => (string) $month],
|
||
[
|
||
'status' => 'pending',
|
||
'message' => null,
|
||
'updated_at' => $now,
|
||
'created_at' => $now,
|
||
]
|
||
);
|
||
if ($affected) {
|
||
$jobs++;
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($jobs > 0) {
|
||
$this->spawnWorker();
|
||
}
|
||
|
||
return ['jobs' => $jobs];
|
||
}
|
||
|
||
/** @return array{jobs:int} 所有已接入平台一起重跑(用于「恢复初始 112 条」) */
|
||
public function dispatchAll(): array
|
||
{
|
||
return $this->dispatchFor(array_keys(self::PLATFORM_MAP));
|
||
}
|
||
|
||
/**
|
||
* 起一个后台进程处理队列。
|
||
*
|
||
* 已有进程在跑时新进程会拿不到锁直接退出;正在跑的进程处理完当前任务后会继续取新任务。
|
||
*/
|
||
public function spawnWorker(): void
|
||
{
|
||
// Production cron consumes the durable queue in the resource-limited batch profile.
|
||
if (config('upstream_pipeline.batch_worker_enabled', false)) {
|
||
return;
|
||
}
|
||
|
||
$this->launchWorker();
|
||
}
|
||
|
||
protected function launchWorker(): void
|
||
{
|
||
try {
|
||
$php = escapeshellarg(PHP_BINARY ?: 'php');
|
||
$command = 'cd ' . escapeshellarg(base_path())
|
||
. ' && nohup ' . $php . ' artisan store-receipt:auto-resync >/dev/null 2>&1 &';
|
||
|
||
Process::fromShellCommandline($command, base_path())
|
||
->disableOutput()
|
||
->setTimeout(10)
|
||
->run();
|
||
} catch (Throwable $exception) {
|
||
// 后台进程起不来时任务仍留在队列里,可手动执行 store-receipt:auto-resync 补跑。
|
||
Log::warning('科目映射表自动重跑进程启动失败:' . $exception->getMessage());
|
||
}
|
||
}
|
||
}
|