feat: 每日同步聚水潭标签并刷新种菜金额 #896
322
app/Business/JstOrderLabelDailySynchronizer.php
Normal file
322
app/Business/JstOrderLabelDailySynchronizer.php
Normal file
@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
namespace App\Business;
|
||||
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Support\Carbon;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 聚水潭订单标签明细的每日同步。
|
||||
*
|
||||
* 先核对历史日期是否逐日覆盖,再补齐缺口,最后替换刷新最近数日以吸收上游迟到修改。
|
||||
*/
|
||||
class JstOrderLabelDailySynchronizer
|
||||
{
|
||||
public const SOURCE_DATASET = 'operations_replica.rpt.jst_Orders';
|
||||
|
||||
public function __construct(
|
||||
private readonly DatabaseManager $databases,
|
||||
private readonly JstOrderLabelImporter $importer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function sync(
|
||||
string $from,
|
||||
string $to,
|
||||
int $companyId,
|
||||
int $refreshDays = 7,
|
||||
bool $dryRun = false,
|
||||
): array {
|
||||
[$fromDate, $toDate] = $this->dateRange($from, $to);
|
||||
if ($companyId <= 0) {
|
||||
throw new InvalidArgumentException('公司 ID 无效。');
|
||||
}
|
||||
if ($refreshDays < 1 || $refreshDays > 31) {
|
||||
throw new InvalidArgumentException('刷新天数必须在 1 至 31 之间。');
|
||||
}
|
||||
|
||||
$this->assertSourceReady();
|
||||
|
||||
$refreshStart = $fromDate->copy();
|
||||
$candidateRefreshStart = $toDate->copy()->subDays($refreshDays - 1);
|
||||
if ($candidateRefreshStart->gt($refreshStart)) {
|
||||
$refreshStart = $candidateRefreshStart;
|
||||
}
|
||||
|
||||
$historyEnd = $refreshStart->copy()->subDay();
|
||||
$historyCoverage = $historyEnd->lt($fromDate)
|
||||
? []
|
||||
: $this->coverage($fromDate, $historyEnd, $companyId);
|
||||
$historyIssues = array_values(array_filter(
|
||||
$historyCoverage,
|
||||
static fn (array $row): bool => $row['status'] !== 'ok',
|
||||
));
|
||||
|
||||
foreach ($historyIssues as $issue) {
|
||||
if ($issue['status'] === 'source_empty') {
|
||||
throw new RuntimeException(sprintf(
|
||||
'历史数据检查未通过:源订单镜像在 %s 没有订单,已停止拉取新数据。',
|
||||
$issue['date'],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$backfills = $this->contiguousRanges($historyIssues);
|
||||
if ($dryRun) {
|
||||
return [
|
||||
'status' => $historyIssues === [] ? 'would_refresh' : 'would_backfill',
|
||||
'from' => $fromDate->toDateString(),
|
||||
'to' => $toDate->toDateString(),
|
||||
'checked_days' => (int) $fromDate->diffInDays($toDate) + 1,
|
||||
'history_issues' => $historyIssues,
|
||||
'backfills' => array_map(static fn (array $range): array => [
|
||||
'from' => $range['from'],
|
||||
'to' => $range['to'],
|
||||
], $backfills),
|
||||
'refresh' => [
|
||||
'from' => $refreshStart->toDateString(),
|
||||
'to' => $toDate->toDateString(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$backfillResults = [];
|
||||
foreach ($backfills as $range) {
|
||||
$backfillResults[] = $this->importer->import(
|
||||
$range['from'],
|
||||
$range['to'],
|
||||
$companyId,
|
||||
true,
|
||||
'jst-order-label-daily-sync',
|
||||
);
|
||||
}
|
||||
|
||||
if ($historyIssues !== []) {
|
||||
$remainingHistory = $this->coverage($fromDate, $historyEnd, $companyId);
|
||||
$remainingIssues = array_values(array_filter(
|
||||
$remainingHistory,
|
||||
static fn (array $row): bool => $row['status'] !== 'ok',
|
||||
));
|
||||
if ($remainingIssues !== []) {
|
||||
throw new RuntimeException('历史缺口补导后仍未通过覆盖检查:' . $this->issueSummary($remainingIssues));
|
||||
}
|
||||
}
|
||||
|
||||
$refreshResult = $this->importer->import(
|
||||
$refreshStart->toDateString(),
|
||||
$toDate->toDateString(),
|
||||
$companyId,
|
||||
true,
|
||||
'jst-order-label-daily-sync',
|
||||
);
|
||||
|
||||
$finalCoverage = $this->coverage($fromDate, $toDate, $companyId);
|
||||
$finalIssues = array_values(array_filter(
|
||||
$finalCoverage,
|
||||
static fn (array $row): bool => $row['status'] !== 'ok',
|
||||
));
|
||||
if ($finalIssues !== []) {
|
||||
throw new RuntimeException('同步后覆盖检查未通过:' . $this->issueSummary($finalIssues));
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'from' => $fromDate->toDateString(),
|
||||
'to' => $toDate->toDateString(),
|
||||
'checked_days' => count($finalCoverage),
|
||||
'backfilled_ranges' => array_map(static fn (array $range): array => [
|
||||
'from' => $range['from'],
|
||||
'to' => $range['to'],
|
||||
], $backfills),
|
||||
'backfill_results' => $backfillResults,
|
||||
'refresh' => $refreshResult,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 只读确认指定日期都已覆盖;任何缺日或订单数不一致都会阻止下游重算。
|
||||
*
|
||||
* @return list<array{date:string,source_orders:int,target_orders:int,target_rows:int,status:string}>
|
||||
*/
|
||||
public function assertCovered(string $from, string $to, int $companyId): array
|
||||
{
|
||||
[$fromDate, $toDate] = $this->dateRange($from, $to);
|
||||
if ($companyId <= 0) {
|
||||
throw new InvalidArgumentException('公司 ID 无效。');
|
||||
}
|
||||
|
||||
$this->assertSourceReady();
|
||||
$coverage = $this->coverage($fromDate, $toDate, $companyId);
|
||||
$issues = array_values(array_filter(
|
||||
$coverage,
|
||||
static fn (array $row): bool => $row['status'] !== 'ok',
|
||||
));
|
||||
if ($issues !== []) {
|
||||
throw new RuntimeException('历史数据检查未通过:' . $this->issueSummary($issues));
|
||||
}
|
||||
|
||||
return $coverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 逐日比较源订单号数与标签明细订单号数。
|
||||
*
|
||||
* 标签拆行后同一订单会出现多行,因此不能用总行数直接比较订单完整性。
|
||||
*
|
||||
* @return list<array{
|
||||
* date:string,source_orders:int,target_orders:int,target_rows:int,status:string
|
||||
* }>
|
||||
*/
|
||||
public function coverage(Carbon $from, Carbon $to, int $companyId): array
|
||||
{
|
||||
$start = $from->copy()->startOfDay()->toDateTimeString();
|
||||
$end = $to->copy()->addDay()->startOfDay()->toDateTimeString();
|
||||
$mirror = $this->mirror();
|
||||
$target = $this->databases->connection();
|
||||
|
||||
$sourceRows = $mirror->table('ops_jst_orders')
|
||||
->where('order_date', '>=', $start)
|
||||
->where('order_date', '<', $end)
|
||||
->selectRaw('DATE(order_date) AS day, COUNT(DISTINCT so_id) AS order_count')
|
||||
->groupByRaw('DATE(order_date)')
|
||||
->get()
|
||||
->keyBy('day');
|
||||
|
||||
$targetRows = $target->table('jst_order_labels')
|
||||
->where('company_id', $companyId)
|
||||
->where('ordered_at', '>=', $start)
|
||||
->where('ordered_at', '<', $end)
|
||||
->selectRaw('DATE(ordered_at) AS day, COUNT(*) AS row_count, COUNT(DISTINCT order_no) AS order_count')
|
||||
->groupByRaw('DATE(ordered_at)')
|
||||
->get()
|
||||
->keyBy('day');
|
||||
|
||||
$coverage = [];
|
||||
for ($day = $from->copy()->startOfDay(); $day->lte($to); $day->addDay()) {
|
||||
$date = $day->toDateString();
|
||||
$sourceOrders = (int) ($sourceRows[$date]->order_count ?? 0);
|
||||
$targetOrderCount = (int) ($targetRows[$date]->order_count ?? 0);
|
||||
$targetRowsCount = (int) ($targetRows[$date]->row_count ?? 0);
|
||||
$status = match (true) {
|
||||
$sourceOrders === 0 => 'source_empty',
|
||||
$targetOrderCount === 0 => 'missing',
|
||||
$targetOrderCount < $sourceOrders => 'incomplete',
|
||||
$targetOrderCount > $sourceOrders => 'extra_orders',
|
||||
default => 'ok',
|
||||
};
|
||||
|
||||
$coverage[] = [
|
||||
'date' => $date,
|
||||
'source_orders' => $sourceOrders,
|
||||
'target_orders' => $targetOrderCount,
|
||||
'target_rows' => $targetRowsCount,
|
||||
'status' => $status,
|
||||
];
|
||||
}
|
||||
|
||||
return $coverage;
|
||||
}
|
||||
|
||||
private function assertSourceReady(): void
|
||||
{
|
||||
$mirror = $this->mirror();
|
||||
$schema = $mirror->getSchemaBuilder();
|
||||
if (! $schema->hasTable('ops_jst_orders')) {
|
||||
throw new RuntimeException('聚水潭订单镜像尚未建立(ops_jst_orders)。');
|
||||
}
|
||||
if (! $schema->hasTable('business_mirror_table_states')) {
|
||||
throw new RuntimeException('聚水潭订单镜像同步状态表尚未建立。');
|
||||
}
|
||||
|
||||
$state = $mirror->table('business_mirror_table_states')
|
||||
->where('dataset_key', self::SOURCE_DATASET)
|
||||
->first(['status', 'initial_completed_at']);
|
||||
if ($state === null || $state->initial_completed_at === null) {
|
||||
throw new RuntimeException('聚水潭订单镜像尚未完成首次同步。');
|
||||
}
|
||||
if ((string) $state->status !== 'succeeded') {
|
||||
throw new RuntimeException('聚水潭订单镜像最近同步未成功,已停止标签明细同步。');
|
||||
}
|
||||
}
|
||||
|
||||
private function mirror(): \Illuminate\Database\ConnectionInterface
|
||||
{
|
||||
return $this->databases->connection(
|
||||
(string) config('upstream_mirrors.connection', 'business_mirror'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{date:string}> $issues
|
||||
* @return list<array{from:string,to:string}>
|
||||
*/
|
||||
private function contiguousRanges(array $issues): array
|
||||
{
|
||||
usort($issues, static fn (array $left, array $right): int => $left['date'] <=> $right['date']);
|
||||
$ranges = [];
|
||||
$start = null;
|
||||
$previous = null;
|
||||
|
||||
foreach ($issues as $issue) {
|
||||
$day = Carbon::createFromFormat('!Y-m-d', $issue['date'], 'Asia/Shanghai');
|
||||
if ($start === null) {
|
||||
$start = $day;
|
||||
$previous = $day;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $day->isSameDay($previous->copy()->addDay())) {
|
||||
$ranges[] = ['from' => $start->toDateString(), 'to' => $previous->toDateString()];
|
||||
$start = $day;
|
||||
}
|
||||
$previous = $day;
|
||||
}
|
||||
|
||||
if ($start !== null && $previous !== null) {
|
||||
$ranges[] = ['from' => $start->toDateString(), 'to' => $previous->toDateString()];
|
||||
}
|
||||
|
||||
return $ranges;
|
||||
}
|
||||
|
||||
/** @param list<array{date:string,status:string}> $issues */
|
||||
private function issueSummary(array $issues): string
|
||||
{
|
||||
return implode(',', array_map(
|
||||
static fn (array $issue): string => $issue['date'] . '=' . $issue['status'],
|
||||
$issues,
|
||||
));
|
||||
}
|
||||
|
||||
/** @return array{0:Carbon,1:Carbon} */
|
||||
private function dateRange(string $from, string $to): array
|
||||
{
|
||||
$fromDate = $this->date($from);
|
||||
$toDate = $this->date($to);
|
||||
if ($fromDate === null || $toDate === null || $fromDate->gt($toDate)) {
|
||||
throw new InvalidArgumentException('日期范围无效,应为 from <= to,格式 YYYY-MM-DD。');
|
||||
}
|
||||
|
||||
return [$fromDate, $toDate];
|
||||
}
|
||||
|
||||
private function date(string $value): ?Carbon
|
||||
{
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$date = Carbon::createFromFormat('!Y-m-d', $value, 'Asia/Shanghai');
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $date->format('Y-m-d') === $value ? $date : null;
|
||||
}
|
||||
}
|
||||
165
app/Business/StoreReceiptPlantingDailySynchronizer.php
Normal file
165
app/Business/StoreReceiptPlantingDailySynchronizer.php
Normal file
@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Business;
|
||||
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Support\Carbon;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 每日编排:先核对标签覆盖,再同步种菜报销工单,最后重算业绩明细种菜金额。
|
||||
*/
|
||||
class StoreReceiptPlantingDailySynchronizer
|
||||
{
|
||||
public const WORK_ORDER_DATASET = 'jiansun.dbo.jst_workOrderInfo';
|
||||
|
||||
public function __construct(
|
||||
private readonly DatabaseManager $databases,
|
||||
private readonly JstOrderLabelDailySynchronizer $labelCoverage,
|
||||
private readonly PlantingWorkOrderSynchronizer $plantingWorkOrders,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param list<string> $months
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function sync(
|
||||
string $from,
|
||||
string $to,
|
||||
int $companyId,
|
||||
array $months = [],
|
||||
bool $dryRun = false,
|
||||
): array {
|
||||
[$fromDate, $toDate] = $this->dateRange($from, $to);
|
||||
if ($companyId <= 0) {
|
||||
throw new InvalidArgumentException('公司 ID 无效。');
|
||||
}
|
||||
|
||||
$coverage = $this->labelCoverage->assertCovered(
|
||||
$fromDate->toDateString(),
|
||||
$toDate->toDateString(),
|
||||
$companyId,
|
||||
);
|
||||
$months = $this->months($months, $toDate);
|
||||
$this->assertWorkOrderSourceReady();
|
||||
|
||||
if ($dryRun) {
|
||||
return [
|
||||
'status' => 'ready',
|
||||
'from' => $fromDate->toDateString(),
|
||||
'to' => $toDate->toDateString(),
|
||||
'checked_days' => count($coverage),
|
||||
'months' => $months,
|
||||
'dry_run' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$plantingSync = [];
|
||||
foreach ($months as $month) {
|
||||
$plantingSync[$month] = $this->plantingWorkOrders->sync(
|
||||
$month . '-01',
|
||||
$this->monthEnd($month),
|
||||
$companyId,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ready_to_refresh',
|
||||
'from' => $fromDate->toDateString(),
|
||||
'to' => $toDate->toDateString(),
|
||||
'checked_days' => count($coverage),
|
||||
'months' => $months,
|
||||
'planting_sync' => $plantingSync,
|
||||
'dry_run' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function months(array $requested, Carbon $through): array
|
||||
{
|
||||
if ($requested === []) {
|
||||
return [
|
||||
$through->copy()->subMonthNoOverflow()->format('Y-m'),
|
||||
$through->format('Y-m'),
|
||||
];
|
||||
}
|
||||
|
||||
$months = [];
|
||||
foreach ($requested as $month) {
|
||||
$month = trim((string) $month);
|
||||
$normalized = null;
|
||||
if (preg_match('/^\d{4}-\d{2}$/', $month) === 1) {
|
||||
try {
|
||||
$normalized = Carbon::createFromFormat('!Y-m', $month, 'Asia/Shanghai')->format('Y-m');
|
||||
} catch (Throwable) {
|
||||
$normalized = null;
|
||||
}
|
||||
}
|
||||
if ($normalized !== $month) {
|
||||
throw new InvalidArgumentException('月份格式无效,应为 YYYY-MM:' . $month);
|
||||
}
|
||||
$months[$month] = true;
|
||||
}
|
||||
$months = array_keys($months);
|
||||
sort($months);
|
||||
|
||||
return $months;
|
||||
}
|
||||
|
||||
private function assertWorkOrderSourceReady(): void
|
||||
{
|
||||
$mirror = $this->databases->connection(
|
||||
(string) config('upstream_mirrors.connection', 'business_mirror'),
|
||||
);
|
||||
$schema = $mirror->getSchemaBuilder();
|
||||
if (! $schema->hasTable('business_mirror_table_states')) {
|
||||
throw new RuntimeException('聚水潭协同工单镜像同步状态表尚未建立。');
|
||||
}
|
||||
|
||||
$state = $mirror->table('business_mirror_table_states')
|
||||
->where('dataset_key', self::WORK_ORDER_DATASET)
|
||||
->first(['status', 'initial_completed_at']);
|
||||
if ($state === null || $state->initial_completed_at === null) {
|
||||
throw new RuntimeException('聚水潭协同工单镜像尚未完成首次同步。');
|
||||
}
|
||||
if ((string) $state->status !== 'succeeded') {
|
||||
throw new RuntimeException('聚水潭协同工单镜像最近同步未成功。');
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array{0:Carbon,1:Carbon} */
|
||||
private function dateRange(string $from, string $to): array
|
||||
{
|
||||
$fromDate = $this->date($from);
|
||||
$toDate = $this->date($to);
|
||||
if ($fromDate === null || $toDate === null || $fromDate->gt($toDate)) {
|
||||
throw new InvalidArgumentException('日期范围无效,应为 from <= to,格式 YYYY-MM-DD。');
|
||||
}
|
||||
|
||||
return [$fromDate, $toDate];
|
||||
}
|
||||
|
||||
private function date(string $value): ?Carbon
|
||||
{
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$date = Carbon::createFromFormat('!Y-m-d', $value, 'Asia/Shanghai');
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $date->format('Y-m-d') === $value ? $date : null;
|
||||
}
|
||||
|
||||
private function monthEnd(string $month): string
|
||||
{
|
||||
return Carbon::createFromFormat('!Y-m', $month, 'Asia/Shanghai')
|
||||
->endOfMonth()
|
||||
->toDateString();
|
||||
}
|
||||
}
|
||||
84
app/Console/Commands/SyncJstOrderLabelsDaily.php
Normal file
84
app/Console/Commands/SyncJstOrderLabelsDaily.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Business\JstOrderLabelDailySynchronizer;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Throwable;
|
||||
|
||||
class SyncJstOrderLabelsDaily extends Command
|
||||
{
|
||||
protected $signature = 'jst-order-labels:sync-daily
|
||||
{--from=2026-10-01 : 覆盖核查起始日期 YYYY-MM-DD}
|
||||
{--to= : 截止日期 YYYY-MM-DD,默认昨日}
|
||||
{--company=1 : 公司 ID}
|
||||
{--refresh-days=7 : 每日替换刷新最近天数}
|
||||
{--dry-run : 只检查并输出计划,不写库}
|
||||
{--json : 输出 JSON}';
|
||||
|
||||
protected $description = '先核对聚水潭订单标签历史覆盖,补齐缺口后刷新最近数据';
|
||||
|
||||
public function handle(JstOrderLabelDailySynchronizer $synchronizer): int
|
||||
{
|
||||
$from = trim((string) $this->option('from'));
|
||||
$to = trim((string) $this->option('to'));
|
||||
$to = $to !== '' ? $to : Carbon::now('Asia/Shanghai')->subDay()->toDateString();
|
||||
|
||||
try {
|
||||
$result = $synchronizer->sync(
|
||||
$from,
|
||||
$to,
|
||||
(int) $this->option('company'),
|
||||
(int) $this->option('refresh-days'),
|
||||
(bool) $this->option('dry-run'),
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
if ($this->option('json')) {
|
||||
$this->line(json_encode([
|
||||
'status' => 'failed',
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'error' => $exception->getMessage(),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
} else {
|
||||
$this->error($exception->getMessage());
|
||||
}
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($this->option('json')) {
|
||||
$this->line(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info(sprintf(
|
||||
'聚水潭订单标签明细同步完成:%s ~ %s,覆盖 %d 天。',
|
||||
$result['from'],
|
||||
$result['to'],
|
||||
(int) ($result['checked_days'] ?? 0),
|
||||
));
|
||||
foreach ($result['backfill_results'] ?? [] as $backfill) {
|
||||
$this->line(sprintf(
|
||||
' 补导 %s ~ %s:%d 单,%d 行。',
|
||||
$backfill['from'],
|
||||
$backfill['to'],
|
||||
(int) $backfill['orders'],
|
||||
(int) $backfill['rows'],
|
||||
));
|
||||
}
|
||||
$refresh = $result['refresh'] ?? null;
|
||||
if (is_array($refresh)) {
|
||||
$this->line(sprintf(
|
||||
' 刷新 %s ~ %s:%d 单,%d 行。',
|
||||
$refresh['from'],
|
||||
$refresh['to'],
|
||||
(int) $refresh['orders'],
|
||||
(int) $refresh['rows'],
|
||||
));
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
113
app/Console/Commands/SyncStoreReceiptPlantingDaily.php
Normal file
113
app/Console/Commands/SyncStoreReceiptPlantingDaily.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Business\StoreReceiptPlantingDailySynchronizer;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Throwable;
|
||||
|
||||
class SyncStoreReceiptPlantingDaily extends Command
|
||||
{
|
||||
protected $signature = 'store-receipt:planting-daily
|
||||
{--from=2026-10-01 : 标签明细覆盖核查起始日期 YYYY-MM-DD}
|
||||
{--to= : 覆盖核查截止日期,默认昨日}
|
||||
{--company=1 : 公司 ID}
|
||||
{--month=* : 重算月份 YYYY-MM,可重复;默认当前月和上月}
|
||||
{--dry-run : 只核查,不写库}
|
||||
{--json : 输出 JSON}';
|
||||
|
||||
protected $description = '核查标签覆盖并刷新业绩明细的种菜金额和标签';
|
||||
|
||||
public function handle(StoreReceiptPlantingDailySynchronizer $synchronizer): int
|
||||
{
|
||||
$from = trim((string) $this->option('from'));
|
||||
$to = trim((string) $this->option('to'));
|
||||
$to = $to !== '' ? $to : Carbon::now('Asia/Shanghai')->subDay()->toDateString();
|
||||
$companyId = (int) $this->option('company');
|
||||
$months = array_values(array_filter(
|
||||
array_map('strval', (array) $this->option('month')),
|
||||
static fn (string $month): bool => trim($month) !== '',
|
||||
));
|
||||
|
||||
try {
|
||||
$result = $synchronizer->sync(
|
||||
$from,
|
||||
$to,
|
||||
$companyId,
|
||||
$months,
|
||||
(bool) $this->option('dry-run'),
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
$this->outputResult([
|
||||
'status' => 'failed',
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ((bool) $this->option('dry-run')) {
|
||||
$this->outputResult($result);
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach ($result['months'] as $month) {
|
||||
$exitCode = $this->call('store-receipt:order-match:sync', [
|
||||
'month' => $month,
|
||||
'--company' => $companyId,
|
||||
'--planting-only' => true,
|
||||
]);
|
||||
$results[$month] = ['exit_code' => $exitCode];
|
||||
if ($exitCode !== self::SUCCESS) {
|
||||
$this->outputResult([
|
||||
'status' => 'failed',
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'months' => $result['months'],
|
||||
'results' => $results,
|
||||
'error' => $month . ' 种菜金额重算失败。',
|
||||
]);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
$result['status'] = 'ok';
|
||||
$result['results'] = $results;
|
||||
$this->outputResult($result);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function outputResult(array $result): void
|
||||
{
|
||||
if ((bool) $this->option('json')) {
|
||||
$this->line(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
return;
|
||||
}
|
||||
|
||||
if (($result['status'] ?? '') === 'failed') {
|
||||
$this->error((string) ($result['error'] ?? '种菜金额同步失败。'));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info(sprintf(
|
||||
'业绩明细种菜金额已刷新:核查 %s ~ %s,月份 %s。',
|
||||
$result['from'],
|
||||
$result['to'],
|
||||
implode('、', $result['months']),
|
||||
));
|
||||
foreach ($result['planting_sync'] ?? [] as $month => $sync) {
|
||||
$this->line(sprintf(
|
||||
' %s 种菜报销明细:%d 单,写入 %d 行。',
|
||||
$month,
|
||||
(int) ($sync['orders'] ?? 0),
|
||||
(int) ($sync['inserted'] ?? 0),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -215,6 +215,10 @@ CRON_TZ=Asia/Shanghai
|
||||
# 镜像价格观测独立保存,按索引分批续传,不替代正式操作历史。
|
||||
3,13,23,33,43,53 * * * * root /usr/bin/flock -n /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-jushuitan-cost-price-history --source=snapshot --page-size=500 --max-pages=20 --quiet >/dev/null 2>&1
|
||||
0 5 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-purchase-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables operations_replica.rpt.jst_Orders --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-ding-online-daifa-price --quiet' >/dev/null 2>&1
|
||||
# 聚水潭订单标签明细:先核对 2026-10-01 起逐日覆盖,补历史缺口,再刷新最近 7 天。
|
||||
10 6 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh jst-order-labels:sync-daily --from=2026-10-01 --company=1 --quiet >/dev/null 2>&1
|
||||
# 业绩明细种菜金额:每天 09:30 核查标签覆盖,同步本月/上月种菜报销并重算种菜金额。
|
||||
30 9 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh store-receipt:planting-daily --from=2026-10-01 --company=1 --quiet >/dev/null 2>&1
|
||||
# 商品月加权与成本:先算月加权单价,再同步商品成本,避免成本先于价格生成。
|
||||
0 18 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh finance:monthly-weighted-product-cost:auto >/dev/null 2>&1
|
||||
# 店铺违规信息预警:每天 05:30 把副本库抓取结果追平到统一表(本地补充字段不覆盖)。
|
||||
|
||||
@ -219,6 +219,10 @@ CRON_TZ=Asia/Shanghai
|
||||
# 镜像价格观测独立保存,按索引分批续传,不替代正式操作历史。
|
||||
3,13,23,33,43,53 * * * * root /usr/bin/flock -n /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-jushuitan-cost-price-history --source=snapshot --page-size=500 --max-pages=20 --quiet >/dev/null 2>&1
|
||||
0 5 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-purchase-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables operations_replica.rpt.jst_Orders --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-ding-online-daifa-price --quiet' >/dev/null 2>&1
|
||||
# 聚水潭订单标签明细:先核对 2026-10-01 起逐日覆盖,补历史缺口,再刷新最近 7 天。
|
||||
10 6 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh jst-order-labels:sync-daily --from=2026-10-01 --company=1 --quiet >/dev/null 2>&1
|
||||
# 业绩明细种菜金额:每天 09:30 核查标签覆盖,同步本月/上月种菜报销并重算种菜金额。
|
||||
30 9 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh store-receipt:planting-daily --from=2026-10-01 --company=1 --quiet >/dev/null 2>&1
|
||||
# 商品月加权与成本:先算月加权单价,再同步商品成本,避免成本先于价格生成。
|
||||
0 18 * * * root /bin/sh /opt/caiwuxitong/run-finance-batch.sh finance:monthly-weighted-product-cost:auto >/dev/null 2>&1
|
||||
# 店铺违规信息预警:每天 05:30 把副本库抓取结果追平到统一表(本地补充字段不覆盖)。
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
"business-mirror:import-jd-wallet",
|
||||
"jst-order-labels:import-planting-rules",
|
||||
"jst-order-labels:import",
|
||||
"jst-order-labels:sync-daily",
|
||||
"business:import-jushuitan-shop-balances",
|
||||
"pinduoduo-bills:import-authoritative",
|
||||
"planting-details:sync-work-orders",
|
||||
|
||||
@ -8,8 +8,8 @@ is_import=0
|
||||
CAIWU_BATCH_CALLER_DIR="$PWD"
|
||||
export CAIWU_BATCH_CALLER_DIR
|
||||
case "${1:-}" in
|
||||
*:import|*:import-*|*:import:*|product:monthly-weighted-average:base|planting-details:sync-work-orders) is_import=1 ;;
|
||||
jushuitan:sync-shipping|store-receipt:order-match:sync|store-receipt:product-cost:sync|store-receipt:auto-resync|order-data:warm-shop-candidates|business-mirror:sync-tables|business-mirror:sync-ding-process-details|business-mirror:process-ding-refresh-requests|business-mirror:index-work-order-waybills|business-mirror:refresh-paid-sales|account-bills:exports:run|finance-ledger:recalculate|product:monthly-weighted-average:auto|product:monthly-weighted-average:compute|product:monthly-weighted-average:report|finance:monthly-weighted-product-cost:auto) ;;
|
||||
*:import|*:import-*|*:import:*|product:monthly-weighted-average:base|planting-details:sync-work-orders|jst-order-labels:sync-daily) is_import=1 ;;
|
||||
jushuitan:sync-shipping|store-receipt:order-match:sync|store-receipt:planting-daily|store-receipt:product-cost:sync|store-receipt:auto-resync|order-data:warm-shop-candidates|business-mirror:sync-tables|business-mirror:sync-ding-process-details|business-mirror:process-ding-refresh-requests|business-mirror:index-work-order-waybills|business-mirror:refresh-paid-sales|account-bills:exports:run|finance-ledger:recalculate|product:monthly-weighted-average:auto|product:monthly-weighted-average:compute|product:monthly-weighted-average:report|finance:monthly-weighted-product-cost:auto) ;;
|
||||
*) echo 'Unsupported finance batch command' >&2; exit 64 ;;
|
||||
esac
|
||||
case "${1:-}" in
|
||||
|
||||
@ -45,6 +45,24 @@ docker exec -u www-data caiwu_app php artisan business-mirror:verify-erp-daily-c
|
||||
|
||||
命令会逐个月份检查期间起止范围、有效版本行数和登记行数;缺月、只导到月中或行数不一致都会返回失败。
|
||||
|
||||
## 聚水潭订单标签明细
|
||||
|
||||
聚水潭订单标签明细复用已同步的 `ops_jst_orders`,不经过浏览器导出。生产宿主机每天 06:10 在订单镜像 05:00 同步后执行:
|
||||
|
||||
```bash
|
||||
sh /opt/caiwuxitong/run-finance-batch.sh jst-order-labels:sync-daily --from=2026-10-01 --company=1
|
||||
```
|
||||
|
||||
任务会先逐日比较源订单与标签明细的订单覆盖。历史缺口按连续日期补导;历史检查未通过时不继续拉取新数据。通过后替换刷新最近 7 天,并再次复核整段覆盖。手动预览可加 `--dry-run --json`。
|
||||
|
||||
业绩明细的种菜金额每天 09:30 单独刷新:
|
||||
|
||||
```bash
|
||||
sh /opt/caiwuxitong/run-finance-batch.sh store-receipt:planting-daily --from=2026-10-01 --company=1
|
||||
```
|
||||
|
||||
任务先确认标签明细覆盖完整,再同步当前月和上月的种菜报销工单,最后对这两个月执行 `store-receipt:order-match:sync --planting-only`。种菜订单号取“种菜报销明细订单号”和“聚水潭是否种菜订单=是”的并集,命中订单的种菜金额取该业绩行收入金额。
|
||||
|
||||
## 组成
|
||||
|
||||
| 文件 | 作用 |
|
||||
|
||||
162
tests/Feature/Console/SyncJstOrderLabelsDailyTest.php
Normal file
162
tests/Feature/Console/SyncJstOrderLabelsDailyTest.php
Normal file
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Console;
|
||||
|
||||
use App\Business\JstOrderLabelImporter;
|
||||
use App\Models\Business\JstOrderLabel;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class SyncJstOrderLabelsDailyTest extends FeatureTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Carbon::setTestNow(Carbon::parse('2026-10-04 10:00:00', 'Asia/Shanghai'));
|
||||
config(['database.connections.business_mirror' => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
]]);
|
||||
DB::purge('business_mirror');
|
||||
|
||||
Schema::connection('business_mirror')->create('business_mirror_table_states', function ($table): void {
|
||||
$table->string('dataset_key')->primary();
|
||||
$table->string('status')->nullable();
|
||||
$table->dateTime('initial_completed_at')->nullable();
|
||||
$table->dateTime('last_succeeded_at')->nullable();
|
||||
});
|
||||
Schema::connection('business_mirror')->create('ops_jst_orders', function ($table): void {
|
||||
$table->integer('Id')->primary();
|
||||
$table->bigInteger('o_id')->nullable();
|
||||
$table->string('so_id')->nullable();
|
||||
$table->string('shop_name')->nullable();
|
||||
$table->dateTime('order_date')->nullable();
|
||||
$table->text('labels')->nullable();
|
||||
$table->text('remark')->nullable();
|
||||
});
|
||||
Schema::connection('business_mirror')->create('ops_jst_order_item', function ($table): void {
|
||||
$table->integer('Id')->primary();
|
||||
$table->bigInteger('o_id')->nullable();
|
||||
$table->string('sku_id')->nullable();
|
||||
$table->string('item_status')->nullable();
|
||||
});
|
||||
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
|
||||
'dataset_key' => 'operations_replica.rpt.jst_Orders',
|
||||
'status' => 'succeeded',
|
||||
'initial_completed_at' => '2026-10-01 00:00:00',
|
||||
'last_succeeded_at' => '2026-10-04 05:00:00',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Carbon::setTestNow();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_backfills_missing_history_then_refreshes_recent_days(): void
|
||||
{
|
||||
$this->orders([
|
||||
['o_id' => 1, 'so_id' => 'A', 'order_date' => '2026-10-01 10:00:00', 'labels' => '标签甲,标签乙'],
|
||||
['o_id' => 2, 'so_id' => 'B', 'order_date' => '2026-10-02 10:00:00', 'labels' => ''],
|
||||
['o_id' => 3, 'so_id' => 'C', 'order_date' => '2026-10-03 10:00:00', 'labels' => '标签丙'],
|
||||
]);
|
||||
|
||||
$this->artisan('jst-order-labels:sync-daily', [
|
||||
'--from' => '2026-10-01',
|
||||
'--to' => '2026-10-03',
|
||||
'--company' => $this->company->id,
|
||||
'--refresh-days' => 1,
|
||||
'--json' => true,
|
||||
])->assertSuccessful();
|
||||
|
||||
$rows = JstOrderLabel::query()->where('company_id', $this->company->id)->orderBy('id')->get();
|
||||
$this->assertCount(4, $rows);
|
||||
$this->assertSame(['标签甲', '标签乙', '(无标签)', '标签丙'], $rows->pluck('label')->all());
|
||||
$this->assertSame(2, $rows->where('order_no', 'A')->count());
|
||||
}
|
||||
|
||||
public function test_does_not_pull_new_data_when_history_source_is_missing(): void
|
||||
{
|
||||
$this->orders([
|
||||
['o_id' => 3, 'so_id' => 'C', 'order_date' => '2026-10-03 10:00:00', 'labels' => '标签丙'],
|
||||
]);
|
||||
|
||||
$this->artisan('jst-order-labels:sync-daily', [
|
||||
'--from' => '2026-10-01',
|
||||
'--to' => '2026-10-03',
|
||||
'--company' => $this->company->id,
|
||||
'--refresh-days' => 1,
|
||||
])->assertFailed();
|
||||
|
||||
$this->assertSame(0, JstOrderLabel::query()->where('company_id', $this->company->id)->count());
|
||||
}
|
||||
|
||||
public function test_second_run_skips_complete_history_and_only_refreshes_latest_day(): void
|
||||
{
|
||||
$this->orders([
|
||||
['o_id' => 1, 'so_id' => 'A', 'order_date' => '2026-10-01 10:00:00', 'labels' => '标签甲'],
|
||||
['o_id' => 2, 'so_id' => 'B', 'order_date' => '2026-10-02 10:00:00', 'labels' => '标签乙'],
|
||||
]);
|
||||
|
||||
app(JstOrderLabelImporter::class)->import(
|
||||
'2026-10-01',
|
||||
'2026-10-01',
|
||||
$this->company->id,
|
||||
true,
|
||||
'test',
|
||||
);
|
||||
$oldCreatedAt = JstOrderLabel::query()->where('order_no', 'A')->value('created_at')?->toDateTimeString();
|
||||
|
||||
$this->artisan('jst-order-labels:sync-daily', [
|
||||
'--from' => '2026-10-01',
|
||||
'--to' => '2026-10-02',
|
||||
'--company' => $this->company->id,
|
||||
'--refresh-days' => 1,
|
||||
'--json' => true,
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->assertSame(
|
||||
$oldCreatedAt,
|
||||
JstOrderLabel::query()->where('order_no', 'A')->value('created_at')?->toDateTimeString(),
|
||||
);
|
||||
$this->assertSame(1, JstOrderLabel::query()->where('order_no', 'B')->count());
|
||||
}
|
||||
|
||||
public function test_refuses_to_run_when_source_mirror_is_not_ready(): void
|
||||
{
|
||||
DB::connection('business_mirror')->table('business_mirror_table_states')
|
||||
->where('dataset_key', 'operations_replica.rpt.jst_Orders')
|
||||
->update(['status' => 'failed']);
|
||||
$this->orders([
|
||||
['o_id' => 1, 'so_id' => 'A', 'order_date' => '2026-10-01 10:00:00', 'labels' => '标签甲'],
|
||||
]);
|
||||
|
||||
$this->artisan('jst-order-labels:sync-daily', [
|
||||
'--from' => '2026-10-01',
|
||||
'--to' => '2026-10-01',
|
||||
'--company' => $this->company->id,
|
||||
])->assertFailed();
|
||||
|
||||
$this->assertSame(0, JstOrderLabel::query()->where('company_id', $this->company->id)->count());
|
||||
}
|
||||
|
||||
private function orders(array $rows): void
|
||||
{
|
||||
foreach ($rows as $index => $row) {
|
||||
DB::connection('business_mirror')->table('ops_jst_orders')->insert([
|
||||
'Id' => $index + 1,
|
||||
'o_id' => $row['o_id'],
|
||||
'so_id' => $row['so_id'],
|
||||
'shop_name' => '测试店铺',
|
||||
'order_date' => $row['order_date'],
|
||||
'labels' => $row['labels'],
|
||||
'remark' => '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Business;
|
||||
|
||||
use App\Business\JstOrderLabelDailySynchronizer;
|
||||
use App\Business\PlantingWorkOrderSynchronizer;
|
||||
use App\Business\StoreReceiptPlantingDailySynchronizer;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StoreReceiptPlantingDailySynchronizerTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
config(['database.connections.business_mirror' => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
]]);
|
||||
DB::purge('business_mirror');
|
||||
Schema::connection('business_mirror')->create('business_mirror_table_states', function ($table): void {
|
||||
$table->string('dataset_key')->primary();
|
||||
$table->string('status')->nullable();
|
||||
$table->dateTime('initial_completed_at')->nullable();
|
||||
});
|
||||
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
|
||||
'dataset_key' => 'jiansun.dbo.jst_workOrderInfo',
|
||||
'status' => 'succeeded',
|
||||
'initial_completed_at' => '2026-10-01 00:00:00',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_checks_label_coverage_then_syncs_requested_months(): void
|
||||
{
|
||||
$coverage = Mockery::mock(JstOrderLabelDailySynchronizer::class);
|
||||
$coverage->shouldReceive('assertCovered')
|
||||
->once()
|
||||
->with('2026-10-01', '2026-10-31', 1)
|
||||
->andReturn(array_fill(0, 31, ['status' => 'ok']));
|
||||
$workOrders = Mockery::mock(PlantingWorkOrderSynchronizer::class);
|
||||
$workOrders->shouldReceive('sync')
|
||||
->once()
|
||||
->with('2026-10-01', '2026-10-31', 1)
|
||||
->andReturn(['orders' => 10, 'inserted' => 10]);
|
||||
|
||||
$synchronizer = new StoreReceiptPlantingDailySynchronizer(
|
||||
app(DatabaseManager::class),
|
||||
$coverage,
|
||||
$workOrders,
|
||||
);
|
||||
|
||||
$result = $synchronizer->sync('2026-10-01', '2026-10-31', 1, ['2026-10']);
|
||||
|
||||
$this->assertSame('ready_to_refresh', $result['status']);
|
||||
$this->assertSame(['2026-10'], $result['months']);
|
||||
$this->assertSame(31, $result['checked_days']);
|
||||
$this->assertSame(10, $result['planting_sync']['2026-10']['orders']);
|
||||
}
|
||||
|
||||
public function test_defaults_to_current_and_previous_month(): void
|
||||
{
|
||||
$coverage = Mockery::mock(JstOrderLabelDailySynchronizer::class);
|
||||
$coverage->shouldReceive('assertCovered')->once()->andReturn([['status' => 'ok']]);
|
||||
$workOrders = Mockery::mock(PlantingWorkOrderSynchronizer::class);
|
||||
$workOrders->shouldReceive('sync')
|
||||
->once()
|
||||
->with('2026-10-01', '2026-10-31', 1)
|
||||
->andReturn([]);
|
||||
$workOrders->shouldReceive('sync')
|
||||
->once()
|
||||
->with('2026-11-01', '2026-11-30', 1)
|
||||
->andReturn([]);
|
||||
|
||||
$synchronizer = new StoreReceiptPlantingDailySynchronizer(
|
||||
app(DatabaseManager::class),
|
||||
$coverage,
|
||||
$workOrders,
|
||||
);
|
||||
|
||||
$result = $synchronizer->sync('2026-10-01', '2026-11-04', 1, ['2026-11', '2026-10']);
|
||||
|
||||
$this->assertSame(['2026-10', '2026-11'], $result['months']);
|
||||
}
|
||||
|
||||
public function test_blocks_recalculation_when_label_coverage_is_not_ready(): void
|
||||
{
|
||||
$coverage = Mockery::mock(JstOrderLabelDailySynchronizer::class);
|
||||
$coverage->shouldReceive('assertCovered')
|
||||
->once()
|
||||
->andThrow(new \RuntimeException('历史数据检查未通过'));
|
||||
$workOrders = Mockery::mock(PlantingWorkOrderSynchronizer::class);
|
||||
$workOrders->shouldNotReceive('sync');
|
||||
|
||||
$synchronizer = new StoreReceiptPlantingDailySynchronizer(
|
||||
app(DatabaseManager::class),
|
||||
$coverage,
|
||||
$workOrders,
|
||||
);
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('历史数据检查未通过');
|
||||
$synchronizer->sync('2026-10-01', '2026-10-31', 1, ['2026-10']);
|
||||
}
|
||||
}
|
||||
@ -138,6 +138,60 @@ class FinanceBatchIsolationTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function test_jst_label_daily_sync_uses_the_isolated_import_entry(): void
|
||||
{
|
||||
$directory = sys_get_temp_dir() . '/caiwu-jst-label-import-' . bin2hex(random_bytes(6));
|
||||
mkdir($directory);
|
||||
file_put_contents($directory . '/run-finance-import.py', '');
|
||||
file_put_contents(
|
||||
$directory . '/python3',
|
||||
'#!/bin/sh' . "\n" . 'printf "%s\n" "$@" > "$CAIWU_CAPTURE"' . "\n",
|
||||
);
|
||||
chmod($directory . '/python3', 0700);
|
||||
|
||||
try {
|
||||
$process = $this->runBatch($directory, [
|
||||
'jst-order-labels:sync-daily',
|
||||
'--from=2026-10-01',
|
||||
]);
|
||||
|
||||
$this->assertTrue($process->isSuccessful(), $process->getErrorOutput());
|
||||
$arguments = file($directory . '/called', FILE_IGNORE_NEW_LINES);
|
||||
$this->assertContains($directory . '/run-finance-import.py', $arguments);
|
||||
$this->assertContains('jst-order-labels:sync-daily', $arguments);
|
||||
} finally {
|
||||
foreach (glob($directory . '/*') as $path) {
|
||||
unlink($path);
|
||||
}
|
||||
rmdir($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_planting_daily_uses_the_isolated_batch_profile(): void
|
||||
{
|
||||
$directory = sys_get_temp_dir() . '/caiwu-planting-daily-' . bin2hex(random_bytes(6));
|
||||
mkdir($directory);
|
||||
try {
|
||||
$arguments = [
|
||||
'store-receipt:planting-daily',
|
||||
'--from=2026-10-01',
|
||||
'--company=1',
|
||||
];
|
||||
$process = $this->runBatch($directory, $arguments);
|
||||
|
||||
$this->assertTrue($process->isSuccessful(), $process->getErrorOutput());
|
||||
$called = file($directory . '/called', FILE_IGNORE_NEW_LINES);
|
||||
$this->assertContains('batch', $called);
|
||||
$this->assertContains('--no-deps', $called);
|
||||
$this->assertSame($arguments, array_slice($called, -count($arguments)));
|
||||
} finally {
|
||||
foreach (glob($directory . '/*') as $path) {
|
||||
unlink($path);
|
||||
}
|
||||
rmdir($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_business_mirror_history_backfill_runs_in_the_isolated_batch_profile(): void
|
||||
{
|
||||
$directory = sys_get_temp_dir() . '/caiwu-mirror-batch-' . bin2hex(random_bytes(6));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user