修复业务镜像增量同步与营销账单导出 #56
@ -484,27 +484,29 @@ class MarketingAccountBillMirrorRepository
|
||||
$currentMonth = $driver === 'sqlite'
|
||||
? "date('now', 'start of month')"
|
||||
: "DATE_FORMAT(CURRENT_DATE, '%Y-%m-01')";
|
||||
$nextMonth = $driver === 'sqlite'
|
||||
? "date(m.month_start, '+1 month')"
|
||||
: 'DATE_ADD(m.month_start, INTERVAL 1 MONTH)';
|
||||
$lastDay = $driver === 'sqlite'
|
||||
? "date(m.month_start, '+1 month', '-1 day')"
|
||||
: "DATE_FORMAT(LAST_DAY(m.month_start), '%Y-%m-%d')";
|
||||
$table = $this->dataset($datasetKey)->targetTable();
|
||||
|
||||
$groups = $this->connection->table($table . ' AS src')
|
||||
$ranked = $this->connection->table($table . ' AS src')
|
||||
->whereNotNull('src.shopName')
|
||||
->whereNotNull('src.' . $dateField)
|
||||
->whereRaw("src.$dateField < $currentMonth")
|
||||
->when($storeName !== null && $storeName !== '', fn (Builder $query) => $query->where('src.shopName', $storeName))
|
||||
->when($month !== null && preg_match('/^\d{4}-\d{2}$/', $month) === 1, function (Builder $query) use ($dateField, $month): void {
|
||||
$start = $month . '-01';
|
||||
$end = date('Y-m-d', strtotime($start . ' +1 month'));
|
||||
$query->where('src.' . $dateField, '>=', $start)->where('src.' . $dateField, '<', $end);
|
||||
})
|
||||
->selectRaw("src.shopName, $monthStart AS month_start, COALESCE(SUM($incomeExpression), 0) AS $incomeAlias, COALESCE(SUM($expenseExpression), 0) AS $expenseAlias")
|
||||
->groupBy('src.shopName')
|
||||
->groupByRaw($monthStart);
|
||||
->selectRaw("src.shopName, src.$dateField AS entry_time, src.id AS entry_id, src.balance AS entry_balance, $monthStart AS month_start")
|
||||
->selectRaw("$incomeExpression AS income_value, $expenseExpression AS expense_value")
|
||||
->selectRaw("LAG(src.balance) OVER (PARTITION BY src.shopName ORDER BY src.$dateField, src.id) AS opening_balance")
|
||||
->selectRaw("ROW_NUMBER() OVER (PARTITION BY src.shopName, $monthStart ORDER BY src.$dateField, src.id) AS month_first_rank")
|
||||
->selectRaw("ROW_NUMBER() OVER (PARTITION BY src.shopName, $monthStart ORDER BY src.$dateField DESC, src.id DESC) AS month_last_rank");
|
||||
|
||||
$groups = $this->connection->query()->fromSub($ranked, 'ranked')
|
||||
->when($month !== null && preg_match('/^\d{4}-\d{2}$/', $month) === 1, fn (Builder $query) => $query->where('month_start', $month . '-01'))
|
||||
->selectRaw("shopName, month_start, COALESCE(SUM(income_value), 0) AS $incomeAlias, COALESCE(SUM(expense_value), 0) AS $expenseAlias")
|
||||
->selectRaw("MAX(CASE WHEN month_first_rank = 1 THEN opening_balance END) AS $openingAlias")
|
||||
->selectRaw("MAX(CASE WHEN month_last_rank = 1 THEN entry_balance END) AS $closingAlias")
|
||||
->groupBy('shopName')
|
||||
->groupBy('month_start');
|
||||
|
||||
$total = $this->connection->query()->fromSub(clone $groups, 'monthly_count')->count();
|
||||
if ($total === 0) {
|
||||
@ -512,25 +514,7 @@ class MarketingAccountBillMirrorRepository
|
||||
}
|
||||
|
||||
$query = $this->connection->query()->fromSub($groups, 'm')
|
||||
->selectRaw("m.shopName, $lastDay AS balance_date, m.$incomeAlias, m.$expenseAlias")
|
||||
->selectSub(function (Builder $query) use ($table, $dateField): void {
|
||||
$query->from($table . ' AS opening_entry')
|
||||
->select('opening_entry.balance')
|
||||
->whereColumn('opening_entry.shopName', 'm.shopName')
|
||||
->whereColumn('opening_entry.' . $dateField, '<', 'm.month_start')
|
||||
->orderByDesc('opening_entry.' . $dateField)
|
||||
->orderByDesc('opening_entry.id')
|
||||
->limit(1);
|
||||
}, $openingAlias)
|
||||
->selectSub(function (Builder $query) use ($table, $dateField, $nextMonth): void {
|
||||
$query->from($table . ' AS closing_entry')
|
||||
->select('closing_entry.balance')
|
||||
->whereColumn('closing_entry.shopName', 'm.shopName')
|
||||
->whereRaw("closing_entry.$dateField < $nextMonth")
|
||||
->orderByDesc('closing_entry.' . $dateField)
|
||||
->orderByDesc('closing_entry.id')
|
||||
->limit(1);
|
||||
}, $closingAlias)
|
||||
->selectRaw("m.shopName, $lastDay AS balance_date, m.$incomeAlias, m.$expenseAlias, m.$openingAlias, m.$closingAlias")
|
||||
->orderByDesc('m.month_start')
|
||||
->orderBy('m.shopName')
|
||||
->forPage($page, $pageSize);
|
||||
|
||||
@ -114,8 +114,8 @@ class BusinessMirrorSynchronizer
|
||||
if ($mode === 'incremental' && $dataset->watermarkColumn() !== null) {
|
||||
$watermark = $this->quote($dataset->watermarkColumn());
|
||||
$where[] = $watermark . ' >= ? AND ' . $watermark . ' < ?';
|
||||
$bindings[] = $windowStart?->format('Y-m-d H:i:s.u');
|
||||
$bindings[] = $windowEnd?->format('Y-m-d H:i:s.u');
|
||||
$bindings[] = $this->sqlServerDateTime($windowStart);
|
||||
$bindings[] = $this->sqlServerDateTime($windowEnd);
|
||||
} elseif ($mode === 'incremental' && $cursor === null && $highWatermark !== []) {
|
||||
[$predicate, $values] = $this->keyset($dataset->keyColumns(), $highWatermark);
|
||||
$where[] = $predicate;
|
||||
@ -137,6 +137,14 @@ class BusinessMirrorSynchronizer
|
||||
return $this->queries->forSource($dataset->sourceKey())->select($sql, $bindings);
|
||||
}
|
||||
|
||||
private function sqlServerDateTime(?Carbon $value): ?string
|
||||
{
|
||||
// ISO 8601 with millisecond precision is accepted consistently by SQL Server
|
||||
// through FreeTDS. Six-digit fractions were interpreted as an invalid date by
|
||||
// some upstream datetime columns and broke every incremental run.
|
||||
return $value?->format('Y-m-d\TH:i:s.v');
|
||||
}
|
||||
|
||||
/** @return array{0:string,1:list<mixed>} */
|
||||
private function keyset(array $columns, array $cursor): array
|
||||
{
|
||||
|
||||
@ -129,6 +129,10 @@ class BusinessMirrorSynchronizerTest extends TestCase
|
||||
$this->assertStringContainsString('[updateTime] >= ? AND [updateTime] < ?', $this->query->calls[0]['sql']);
|
||||
$this->assertStringContainsString('ORDER BY [updateTime], [Id]', $this->query->calls[0]['sql']);
|
||||
$this->assertCount(2, $this->query->calls[0]['bindings']);
|
||||
foreach ($this->query->calls[0]['bindings'] as $binding) {
|
||||
$this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$/', $binding);
|
||||
$this->assertStringNotContainsString(' ', $binding);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_incremental_sync_refuses_to_guess_before_first_full_sync(): void
|
||||
|
||||
@ -95,12 +95,14 @@ class MarketingAccountBillMirrorRepositoryTest extends TestCase
|
||||
$this->assertSame(50.0, $taobaoPage2['rows'][0]['amount']);
|
||||
$this->assertSame(['镜像店铺'], $taobao->storeOptions());
|
||||
$this->assertSame(30.0, $taobaoSummary['net']);
|
||||
$this->assertSame(100.0, $taobaoMonthly['rows'][0]['opening_balance']);
|
||||
$this->assertSame(130.0, $taobaoMonthly['rows'][0]['closing_balance']);
|
||||
$this->assertSame(0.0, $taobaoMonthly['rows'][0]['balance_difference']);
|
||||
|
||||
$this->assertSame(2, $pddPage2['total']);
|
||||
$this->assertSame(50.0, $pddPage2['rows'][0]['amount']);
|
||||
$this->assertSame(30.0, $pddSummary['net']);
|
||||
$this->assertSame(100.0, $pddMonthly['rows'][0]['opening_balance']);
|
||||
$this->assertSame(130.0, $pddMonthly['rows'][0]['closing_balance']);
|
||||
$this->assertSame(0.0, $pddMonthly['rows'][0]['balance_difference']);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user