fix: 去除退款待确认重复商品明细 (#932)

This commit is contained in:
renshen 2026-11-06 12:20:28 +08:00
parent 440483e32d
commit 0eff89d4c5
3 changed files with 103 additions and 13 deletions

View File

@ -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']]],

View File

@ -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,
]);
}

View File

@ -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();