fix: 去除退款待确认重复商品明细 (#932)
This commit is contained in:
parent
440483e32d
commit
0eff89d4c5
@ -497,6 +497,7 @@ return [
|
||||
'columns' => ['as_id', 'status', 'shop_status', 'confirm_date', 'modified', 'as_date'],
|
||||
'indexes' => [
|
||||
['as_id'],
|
||||
['status(20)', 'shop_status(20)', 'as_id'],
|
||||
],
|
||||
],
|
||||
'operations_replica.rpt.jst_shopInfo' => ['source_key' => 'operations_replica', 'schema' => 'rpt', 'object' => 'jst_shopInfo', 'target' => 'ops_jst_shop_info', 'keys' => ['Id'], 'watermark' => 'updateTime', 'indexes' => [['shop_id', 'Id']]],
|
||||
|
||||
@ -42,6 +42,7 @@ class NonJingxiRefundMirrorRepository
|
||||
self::SUPPLEMENT_DATASET => [['as_id'], ['as_date', 'as_id'], ['so_id'], ['outer_as_id'], ['status', 'shop_status', 'as_date', 'as_id']],
|
||||
self::SUPPLEMENT_ITEM_DATASET => [['as_id', 'asi_id']],
|
||||
self::SHOP_DATASET => [['shop_id', 'Id']],
|
||||
self::STATUS_DATASET => [['as_id'], ['status', 'shop_status', 'as_id']],
|
||||
'operations_replica.rpt.jst_Orders' => [['o_id', 'Id']],
|
||||
'operations_replica.rpt.jst_OrderItem' => [['o_id', 'Id']],
|
||||
];
|
||||
@ -209,14 +210,25 @@ class NonJingxiRefundMirrorRepository
|
||||
|
||||
$page = max($page, 1);
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
$total = $this->sourceQuery($filters, false)->count()
|
||||
+ $this->sourceQuery($filters, true)->count();
|
||||
$total = $this->detailRowCount($filters, false)
|
||||
+ $this->detailRowCount($filters, true);
|
||||
$keys = $this->orderedKeys($filters, null, $page * $perPage)->forPage($page, $perPage)->get();
|
||||
$rows = $this->loadRows($keys);
|
||||
|
||||
return ['data' => $rows->all(), 'total' => $total];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $filters */
|
||||
private function detailRowCount(array $filters, bool $supplement): int
|
||||
{
|
||||
$groupColumns = $this->detailGroupColumns($supplement);
|
||||
$grouped = $this->sourceQuery($filters, $supplement)
|
||||
->select($groupColumns)
|
||||
->groupBy($groupColumns);
|
||||
|
||||
return $this->connection->query()->fromSub($grouped, 'deduplicated_detail_rows')->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 售后状态为待确认、且线上退款已成功的记录。
|
||||
*
|
||||
@ -469,8 +481,12 @@ class NonJingxiRefundMirrorRepository
|
||||
private function sourceQuery(array $filters, bool $supplement, bool $withItems = true): Builder
|
||||
{
|
||||
$table = $supplement ? 'ops_jst_qmcust_refunds' : 'ops_jst_return_refund_info';
|
||||
$query = $this->connection->table($table . ' as r');
|
||||
if ($supplement && $filters['order_no'] === '' && $this->supplementDateIndex !== null
|
||||
$statusAuthoritative = (bool) ($filters['_status_authoritative'] ?? false);
|
||||
$query = $statusAuthoritative
|
||||
? $this->connection->table('ops_jst_refund_status as current_refund')
|
||||
->join($table . ' as r', 'r.as_id', '=', 'current_refund.as_id')
|
||||
: $this->connection->table($table . ' as r');
|
||||
if (! $statusAuthoritative && $supplement && $filters['order_no'] === '' && $this->supplementDateIndex !== null
|
||||
&& $this->connection->getDriverName() === 'mysql') {
|
||||
// The legacy text payload makes a full table scan very expensive;
|
||||
// keep date windows on the verified shared-mirror lookup index.
|
||||
@ -532,10 +548,12 @@ class NonJingxiRefundMirrorRepository
|
||||
if (trim((string) ($filters['status'] ?? '')) !== ''
|
||||
|| trim((string) ($filters['shop_status'] ?? '')) !== '') {
|
||||
$statusAlias = 'current_refund';
|
||||
$query->leftJoin('ops_jst_refund_status as current_refund', 'current_refund.as_id', '=', 'r.as_id');
|
||||
if (! $statusAuthoritative) {
|
||||
$query->leftJoin('ops_jst_refund_status as current_refund', 'current_refund.as_id', '=', 'r.as_id');
|
||||
}
|
||||
}
|
||||
$this->applyPlatformFilter($query, $filters, $supplement);
|
||||
$this->applyFilters($query, $filters, $statusAlias);
|
||||
$this->applyFilters($query, $filters, $statusAlias, $statusAuthoritative);
|
||||
if ($filters['sku_id'] !== '') {
|
||||
if ($withItems) {
|
||||
$this->applySkuFilter($query, $filters['sku_id']);
|
||||
@ -572,9 +590,12 @@ class NonJingxiRefundMirrorRepository
|
||||
$head->where($headColumn, '<', $headValue)
|
||||
->orWhere(function (Builder $remaining) use ($headColumn, $headValue, $cursor, $supplement, $filters): void {
|
||||
$remaining->where($headColumn, $headValue)->whereExists(function (Builder $items) use ($cursor, $supplement, $filters): void {
|
||||
$items->selectRaw('1')->from(($supplement ? 'ops_jst_qmcust_refund_items' : 'ops_jst_return_refund_item') . ' as i')
|
||||
$itemKey = $supplement ? 'i.asi_id' : 'i.Id';
|
||||
$items->select($this->itemGroupColumns())
|
||||
->from(($supplement ? 'ops_jst_qmcust_refund_items' : 'ops_jst_return_refund_item') . ' as i')
|
||||
->whereColumn('i.as_id', 'r.as_id')
|
||||
->where($supplement ? 'i.asi_id' : 'i.Id', '<', $cursor->_item_id);
|
||||
->groupBy($this->itemGroupColumns())
|
||||
->havingRaw("MAX({$itemKey}) < ?", [$cursor->_item_id]);
|
||||
if ($filters['sku_id'] !== '') {
|
||||
$this->applySkuFilter($items, $filters['sku_id']);
|
||||
}
|
||||
@ -595,8 +616,9 @@ class NonJingxiRefundMirrorRepository
|
||||
$this->applySkuFilter($source, $filters['sku_id']);
|
||||
}
|
||||
$source->selectRaw($supplement
|
||||
? 'r.as_date AS refundDate, 1 AS _source_rank, 0 AS _head_id, COALESCE(i.asi_id, 0) AS _item_id, r.as_id AS _source_id'
|
||||
: "r.as_date AS refundDate, 0 AS _source_rank, r.Id AS _head_id, COALESCE(i.Id, 0) AS _item_id, '' AS _source_id");
|
||||
? 'r.as_date AS refundDate, 1 AS _source_rank, 0 AS _head_id, MAX(COALESCE(i.asi_id, 0)) AS _item_id, r.as_id AS _source_id'
|
||||
: "r.as_date AS refundDate, 0 AS _source_rank, r.Id AS _head_id, MAX(COALESCE(i.Id, 0)) AS _item_id, '' AS _source_id")
|
||||
->groupBy($this->detailGroupColumns($supplement));
|
||||
$branch = $this->connection->query()->fromSub($source, 'source_keys');
|
||||
if ($cursor !== null) {
|
||||
$branch->where(function (Builder $after) use ($cursor): void {
|
||||
@ -624,6 +646,27 @@ class NonJingxiRefundMirrorRepository
|
||||
return $query;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function detailGroupColumns(bool $supplement): array
|
||||
{
|
||||
return array_merge([$supplement ? 'r.as_id' : 'r.Id'], $this->itemGroupColumns());
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function itemGroupColumns(): array
|
||||
{
|
||||
return [
|
||||
'i.outer_oi_id',
|
||||
'i.shop_sku_id',
|
||||
'i.i_id',
|
||||
'i.sku_id',
|
||||
'i.qty',
|
||||
'i.shop_amount',
|
||||
'i.r_qty',
|
||||
'i.type',
|
||||
];
|
||||
}
|
||||
|
||||
private function loadRows($keys)
|
||||
{
|
||||
$loaded = collect();
|
||||
@ -692,7 +735,12 @@ class NonJingxiRefundMirrorRepository
|
||||
}
|
||||
|
||||
/** @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,platform?:string} $filters */
|
||||
private function applyFilters(Builder $query, array $filters, ?string $statusAlias = null): void
|
||||
private function applyFilters(
|
||||
Builder $query,
|
||||
array $filters,
|
||||
?string $statusAlias = null,
|
||||
bool $statusAuthoritative = false,
|
||||
): void
|
||||
{
|
||||
$query->where('r.as_date', '>=', $filters['from'] . ' 00:00:00')
|
||||
->where('r.as_date', '<', Carbon::parse($filters['to'])->addDay()->format('Y-m-d 00:00:00'));
|
||||
@ -701,6 +749,8 @@ class NonJingxiRefundMirrorRepository
|
||||
if ($status !== '') {
|
||||
if ($statusAlias === null) {
|
||||
$query->where('r.status', $status);
|
||||
} elseif ($statusAuthoritative) {
|
||||
$query->where("{$statusAlias}.status", $status);
|
||||
} else {
|
||||
$query->whereRaw("COALESCE({$statusAlias}.status, r.status) = ?", [$status]);
|
||||
}
|
||||
@ -710,6 +760,8 @@ class NonJingxiRefundMirrorRepository
|
||||
if ($shopStatus !== '') {
|
||||
if ($statusAlias === null) {
|
||||
$query->where('r.shop_status', $shopStatus);
|
||||
} elseif ($statusAuthoritative) {
|
||||
$query->where("{$statusAlias}.shop_status", $shopStatus);
|
||||
} else {
|
||||
$query->whereRaw("COALESCE({$statusAlias}.shop_status, r.shop_status) = ?", [$shopStatus]);
|
||||
}
|
||||
@ -809,6 +861,7 @@ class NonJingxiRefundMirrorRepository
|
||||
return array_replace($filters, [
|
||||
'status' => self::PENDING_CONFIRMATION_STATUS,
|
||||
'shop_status' => self::PENDING_CONFIRMATION_ONLINE_STATUS,
|
||||
'_status_authoritative' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -224,6 +224,41 @@ class NonJingxiRefundMirrorRepositoryTest extends TestCase
|
||||
));
|
||||
}
|
||||
|
||||
public function test_pending_confirmation_collapses_exact_duplicate_item_rows(): void
|
||||
{
|
||||
$db = DB::connection('business_mirror');
|
||||
$db->table('ops_jst_return_refund_info')->insert([
|
||||
array_replace($this->refundInfo(1, 101, '2026-10-31 16:57:49', '京东商城', '京东测试店'), [
|
||||
'status' => 'WaitConfirm',
|
||||
'shop_status' => 'SUCCESS',
|
||||
]),
|
||||
]);
|
||||
$first = $this->refundItem(11, 101, 'SKU-DUP', 'ITEM-DUP');
|
||||
$duplicate = array_replace($first, ['Id' => 12]);
|
||||
$db->table('ops_jst_return_refund_item')->insert([$first, $duplicate]);
|
||||
$db->table('ops_jst_refund_status')->insert([
|
||||
['as_id' => '101', 'status' => 'WaitConfirm', 'shop_status' => 'SUCCESS'],
|
||||
]);
|
||||
|
||||
$filters = [
|
||||
'from' => '2026-10-01',
|
||||
'to' => '2026-10-31',
|
||||
'order_no' => '',
|
||||
'waybill_no' => '',
|
||||
'shop_name' => '',
|
||||
'sku_id' => '',
|
||||
];
|
||||
$page = $this->repository->paginatePendingConfirmation($filters, 1, 25);
|
||||
$this->assertSame(1, $page['total']);
|
||||
$this->assertCount(1, $page['data']);
|
||||
$this->assertSame(12, (int) $page['data'][0]->Id);
|
||||
$this->assertSame('SKU-DUP', $page['data'][0]->sku_id);
|
||||
|
||||
$export = iterator_to_array($this->repository->exportPendingConfirmationRows($filters, 1));
|
||||
$this->assertCount(1, $export);
|
||||
$this->assertSame(12, (int) $export[0]->Id);
|
||||
}
|
||||
|
||||
public function test_pending_confirmation_monthly_summary_combines_unique_source_counts(): void
|
||||
{
|
||||
$db = DB::connection('business_mirror');
|
||||
@ -409,7 +444,7 @@ class NonJingxiRefundMirrorRepositoryTest extends TestCase
|
||||
['asi_id' => 502, 'as_id' => '202', 'sku_id' => 'MATCH'],
|
||||
]);
|
||||
$filters = ['from' => '2026-07-01', 'to' => '2026-07-31', 'order_no' => '', 'waybill_no' => '', 'shop_name' => '', 'sku_id' => ''];
|
||||
$expected = ['202:502', '202:501', '201:201', '102:14', '102:13', '102:12', '101:11'];
|
||||
$expected = ['202:502', '201:201', '102:14', '102:13', '102:12', '101:11'];
|
||||
$identity = fn ($r) => $r->as_id . ':' . $r->Id;
|
||||
$this->assertSame($expected, array_map($identity, iterator_to_array($this->repository->exportRows($filters, 1))));
|
||||
$paged = [];
|
||||
@ -418,7 +453,7 @@ class NonJingxiRefundMirrorRepositoryTest extends TestCase
|
||||
}
|
||||
$this->assertSame($expected, array_map($identity, $paged));
|
||||
$filters['sku_id'] = 'MATCH';
|
||||
$this->assertSame(['202:502', '202:501', '102:14', '102:12', '101:11'],
|
||||
$this->assertSame(['202:502', '102:14', '102:12', '101:11'],
|
||||
array_map($identity, iterator_to_array($this->repository->exportRows($filters, 1))));
|
||||
}
|
||||
|
||||
@ -552,6 +587,7 @@ class NonJingxiRefundMirrorRepositoryTest extends TestCase
|
||||
$table->string('as_id'); $table->string('status')->nullable(); $table->string('shop_status')->nullable();
|
||||
$table->dateTime('confirm_date')->nullable(); $table->dateTime('modified')->nullable(); $table->dateTime('as_date')->nullable();
|
||||
$table->index(['as_id']);
|
||||
$table->index(['status', 'shop_status', 'as_id']);
|
||||
});
|
||||
$schema->create('ops_jst_qmcust_refunds', function (Blueprint $table): void {
|
||||
$table->string('as_id'); $table->dateTime('as_date'); $table->string('outer_as_id')->nullable();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user