普通商品数据来源列与筛选结果导出 #76

Merged
limeng merged 1 commits from feature/limeng-product-source-export into main 2026-08-24 14:48:33 +08:00
6 changed files with 384 additions and 134 deletions

View File

@ -26,6 +26,7 @@ use Modules\EcommerceFinance\Repositories\RawBoardPayablesRepository;
use Modules\EcommerceFinance\Repositories\SupplierPayableReportRepository;
use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository;
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Throwable;
@ -823,31 +824,7 @@ class Pages extends Controller
protected function contentForNormalProducts(): array
{
$filters = [
'i_id' => mb_substr(trim((string) request('i_id', '')), 0, 50),
'sku_id' => mb_substr(trim((string) request('sku_id', '')), 0, 50),
'keyword' => mb_substr(trim((string) request('keyword', '')), 0, 100),
'brand' => mb_substr(trim((string) request('brand', '')), 0, 100),
'category' => mb_substr(trim((string) request('category', '')), 0, 100),
'supplier_name' => mb_substr(trim((string) request('supplier_name', '')), 0, 191),
'enabled' => in_array((string) request('enabled', ''), ['-1', '0', '1'], true)
? (string) request('enabled')
: '',
'stock_disabled' => in_array((string) request('stock_disabled', ''), ['0', '1'], true)
? (string) request('stock_disabled')
: '',
'sale_price_min' => $this->decimalFilter(request('sale_price_min')),
'sale_price_max' => $this->decimalFilter(request('sale_price_max')),
];
if ($filters['sale_price_min'] !== null
&& $filters['sale_price_max'] !== null
&& (float) $filters['sale_price_min'] > (float) $filters['sale_price_max']) {
[$filters['sale_price_min'], $filters['sale_price_max']] = [
$filters['sale_price_max'],
$filters['sale_price_min'],
];
}
$filters = $this->normalProductFilters(request());
$page = max((int) request('page', 1), 1);
$limit = min(max((int) setting('default.list_limit', 25), 1), 50);
@ -895,8 +872,6 @@ class Pages extends Controller
'productCatalogError' => null,
'productCatalogState' => $state,
'productCatalogSyncStatus' => $syncStatus,
'productCatalogProductPriceSource' => ProductCatalogMirrorRepository::PRODUCT_SOURCE_LABEL,
'productCatalogPurchasePriceSource' => ProductCatalogMirrorRepository::PURCHASE_SOURCE_LABEL,
'productCatalogPurchasePriceSyncStatus' => $result['purchase_price_sync_status'] ?? [
'code' => 'unavailable',
'initialized' => false,
@ -917,6 +892,104 @@ class Pages extends Controller
}
}
public function exportNormalProducts(Request $request): StreamedResponse
{
abort_unless($this->productCatalog->available() && $this->productCatalog->ready(), 503);
$filters = $this->normalProductFilters($request);
return response()->streamDownload(function () use ($filters): void {
$csv = fopen('php://output', 'w');
if ($csv === false) {
throw new \RuntimeException('无法创建普通商品资料导出流。');
}
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($csv, [
'ID', '款式编码', '商品编码', '商品名称', '商品简称', '规格 / 颜色',
'品牌', '类目', '供应商', '销售价', '成本价', '普通商品采购价',
'1688代发单价', '数据来源', '市场价', '商品状态', '库存同步', '业务更新时间',
]);
foreach ($this->productCatalog->exportRows($filters) as $row) {
$specification = collect([$row->properties_value, $row->color])
->filter(static fn (mixed $value): bool => $value !== null && $value !== '')
->unique()
->implode(' / ');
$enabledLabel = match ((int) $row->enabled) {
1 => '已启用',
-1 => '已删除',
default => '已停用',
};
fputcsv($csv, [
$row->autoid,
$this->csvText($row->i_id),
$this->csvText($row->sku_id),
$this->csvText($row->name),
$this->csvText($row->short_name),
$this->csvText($specification),
$this->csvText($row->brand),
$this->csvText($row->category),
$this->csvText($row->supplier_name),
$row->sale_price,
$row->cost_price,
$row->purchase_price,
$row->purchase_order_unit_price,
implode(';', $row->data_sources),
$row->market_price,
$enabledLabel,
(int) $row->stock_disabled === 0 ? '正常同步' : '停止同步',
$row->modified,
]);
}
fclose($csv);
}, '普通商品资料_' . now()->format('Ymd_His') . '.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
'Cache-Control' => 'private, no-store',
]);
}
/** @return array<string,mixed> */
private function normalProductFilters(Request $request): array
{
$filters = [
'i_id' => mb_substr(trim((string) $request->query('i_id', '')), 0, 50),
'sku_id' => mb_substr(trim((string) $request->query('sku_id', '')), 0, 50),
'keyword' => mb_substr(trim((string) $request->query('keyword', '')), 0, 100),
'brand' => mb_substr(trim((string) $request->query('brand', '')), 0, 100),
'category' => mb_substr(trim((string) $request->query('category', '')), 0, 100),
'supplier_name' => mb_substr(trim((string) $request->query('supplier_name', '')), 0, 191),
'enabled' => in_array((string) $request->query('enabled', ''), ['-1', '0', '1'], true)
? (string) $request->query('enabled')
: '',
'stock_disabled' => in_array((string) $request->query('stock_disabled', ''), ['0', '1'], true)
? (string) $request->query('stock_disabled')
: '',
'sale_price_min' => $this->decimalFilter($request->query('sale_price_min')),
'sale_price_max' => $this->decimalFilter($request->query('sale_price_max')),
];
if ($filters['sale_price_min'] !== null
&& $filters['sale_price_max'] !== null
&& (float) $filters['sale_price_min'] > (float) $filters['sale_price_max']) {
[$filters['sale_price_min'], $filters['sale_price_max']] = [
$filters['sale_price_max'],
$filters['sale_price_min'],
];
}
return $filters;
}
private function csvText(mixed $value): string
{
$text = trim((string) ($value ?? ''));
return preg_match('/^[=+\-@]/u', $text) === 1 ? "'" . $text : $text;
}
private function emptyProductCatalogContent(
array $filters,
int $limit,
@ -940,8 +1013,6 @@ class Pages extends Controller
'productCatalogError' => $error,
'productCatalogState' => $state,
'productCatalogSyncStatus' => $syncStatus,
'productCatalogProductPriceSource' => ProductCatalogMirrorRepository::PRODUCT_SOURCE_LABEL,
'productCatalogPurchasePriceSource' => ProductCatalogMirrorRepository::PURCHASE_SOURCE_LABEL,
'productCatalogPurchasePriceSyncStatus' => [
'code' => 'unavailable',
'initialized' => false,

View File

@ -3,7 +3,9 @@
namespace Modules\EcommerceFinance\Repositories;
use Carbon\CarbonInterface;
use Generator;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Contracts\BusinessMirrorDataset;
@ -17,7 +19,7 @@ class ProductCatalogMirrorRepository
public const PRODUCT_SOURCE_LABEL = '商品及库存管理(普通商品资料)';
public const PURCHASE_SOURCE_LABEL = '采购单管理-1688代发单价';
public const PURCHASE_SOURCE_LABEL = '1688代发单价';
private readonly ConnectionInterface $connection;
@ -91,6 +93,129 @@ class ProductCatalogMirrorRepository
$page = max($page, 1);
$perPage = min(max($perPage, 1), 50);
$query = $this->filteredQuery($filters);
$total = (clone $query)->count();
$rows = $query->orderBy('autoid')
->forPage($page, $perPage)
->get()
->all();
$purchasePriceSyncStatus = $this->purchasePriceSyncStatus();
$this->attachPurchasePrices($rows, $purchasePriceSyncStatus['initialized']);
return [
'data' => $rows,
'total' => $total,
'purchase_price_sync_status' => $purchasePriceSyncStatus,
];
}
/**
* @param array<string,mixed> $filters
* @return Generator<int,object>
*/
public function exportRows(array $filters, int $batchSize = 1000): Generator
{
if (! $this->ready()) {
throw new RuntimeException('business_mirror_dataset_not_ready');
}
$batchSize = min(max($batchSize, 1), 5000);
$purchasePriceSyncStatus = $this->purchasePriceSyncStatus();
$lastAutoid = null;
while (true) {
$query = $this->filteredQuery($filters);
if ($lastAutoid !== null) {
$query->where('autoid', '>', $lastAutoid);
}
$rows = $query->orderBy('autoid')
->limit($batchSize)
->get()
->all();
if ($rows === []) {
return;
}
$this->attachPurchasePrices($rows, $purchasePriceSyncStatus['initialized']);
foreach ($rows as $row) {
yield $row;
}
$lastAutoid = (int) $rows[array_key_last($rows)]->autoid;
if (count($rows) < $batchSize) {
return;
}
}
}
/** @param list<object> $rows */
private function attachPurchasePrices(array $rows, bool $initialized): void
{
foreach ($rows as $row) {
$row->purchase_order_unit_price = null;
$row->purchase_order_finished_at = null;
$row->purchase_order_id = null;
$row->data_sources = $row->purchase_price === null
? []
: [self::PRODUCT_SOURCE_LABEL];
}
if ($rows === []) {
return;
}
if (! $initialized) {
return;
}
$skuIds = collect($rows)
->pluck('sku_id')
->filter(static fn (mixed $value): bool => is_string($value) && trim($value) !== '')
->map(static fn (string $value): string => trim($value))
->unique()
->values()
->all();
if ($skuIds === []) {
return;
}
$candidates = $this->connection->table('product_purchase_price_mirror_rows')
->where('document_status', 'Finished')
->where('labels', 'like', '%1688一键代发%')
->whereNotNull('unit_price')
->whereIn('sku_id', $skuIds)
->orderBy('sku_id')
->orderByDesc('finished_at')
->orderByDesc('purchase_order_id')
->orderByDesc('item_id')
->get([
'sku_id',
'unit_price',
'finished_at',
'purchase_order_id',
])
->unique('sku_id')
->keyBy('sku_id');
foreach ($rows as $row) {
$purchase = $candidates->get(trim((string) ($row->sku_id ?? '')));
if ($purchase === null) {
continue;
}
$row->purchase_order_unit_price = $purchase->unit_price;
$row->purchase_order_finished_at = $purchase->finished_at;
$row->purchase_order_id = $purchase->purchase_order_id;
$row->data_sources[] = self::PURCHASE_SOURCE_LABEL;
}
}
/** @param array<string,mixed> $filters */
private function filteredQuery(array $filters): Builder
{
$query = $this->connection->table($this->dataset()->targetTable())
->where('sku_type', 'normal')
->select([
@ -149,80 +274,7 @@ class ProductCatalogMirrorRepository
$query->where('sale_price', '<=', $filters['sale_price_max']);
}
$total = (clone $query)->count();
$rows = $query->orderBy('autoid')
->forPage($page, $perPage)
->get()
->all();
$purchasePriceSyncStatus = $this->purchasePriceSyncStatus();
$this->attachPurchasePrices($rows, $purchasePriceSyncStatus['initialized']);
return [
'data' => $rows,
'total' => $total,
'purchase_price_sync_status' => $purchasePriceSyncStatus,
];
}
/** @param list<object> $rows */
private function attachPurchasePrices(array $rows, bool $initialized): void
{
foreach ($rows as $row) {
$row->product_purchase_price_source = self::PRODUCT_SOURCE_LABEL;
$row->purchase_order_unit_price = null;
$row->purchase_order_finished_at = null;
$row->purchase_order_id = null;
$row->purchase_order_price_source = self::PURCHASE_SOURCE_LABEL;
}
if ($rows === []) {
return;
}
if (! $initialized) {
return;
}
$skuIds = collect($rows)
->pluck('sku_id')
->filter(static fn (mixed $value): bool => is_string($value) && trim($value) !== '')
->map(static fn (string $value): string => trim($value))
->unique()
->values()
->all();
if ($skuIds === []) {
return;
}
$candidates = $this->connection->table('product_purchase_price_mirror_rows')
->where('document_status', 'Finished')
->where('labels', 'like', '%1688一键代发%')
->whereNotNull('unit_price')
->whereIn('sku_id', $skuIds)
->orderBy('sku_id')
->orderByDesc('finished_at')
->orderByDesc('purchase_order_id')
->orderByDesc('item_id')
->get([
'sku_id',
'unit_price',
'finished_at',
'purchase_order_id',
])
->unique('sku_id')
->keyBy('sku_id');
foreach ($rows as $row) {
$purchase = $candidates->get(trim((string) ($row->sku_id ?? '')));
if ($purchase === null) {
continue;
}
$row->purchase_order_unit_price = $purchase->unit_price;
$row->purchase_order_finished_at = $purchase->finished_at;
$row->purchase_order_id = $purchase->purchase_order_id;
}
return $query;
}
/** @return array{code:string,initialized:bool,last_succeeded_at:?string} */

View File

@ -4,8 +4,6 @@
$productCatalogError = $content['productCatalogError'] ?? null;
$productCatalogState = $content['productCatalogState'] ?? null;
$productCatalogSyncStatus = $content['productCatalogSyncStatus'] ?? ['code' => 'unavailable', 'lag_minutes' => null];
$productCatalogProductPriceSource = $content['productCatalogProductPriceSource'] ?? '商品及库存管理(普通商品资料)';
$productCatalogPurchasePriceSource = $content['productCatalogPurchasePriceSource'] ?? '采购单管理-1688代发单价';
$productCatalogPurchasePriceSyncStatus = $content['productCatalogPurchasePriceSyncStatus'] ?? [
'code' => 'unavailable',
'initialized' => false,
@ -260,19 +258,21 @@
white-space: nowrap;
}
.product-catalog-price-cell {
min-width: 168px;
.product-catalog-source-list {
min-width: 210px;
color: var(--catalog-muted);
line-height: 1.4;
}
.product-catalog-price-source {
.product-catalog-source-item {
display: block;
margin-top: 3px;
color: var(--catalog-muted);
font-size: 0.6875rem;
line-height: 1.25;
white-space: nowrap;
}
.product-catalog-source-item + .product-catalog-source-item {
margin-top: 2px;
}
.product-catalog-table .is-code {
font-variant-numeric: tabular-nums;
white-space: nowrap;
@ -470,6 +470,15 @@
<span class="material-icons-outlined text-base" aria-hidden="true">search</span>
查询
</button>
<a
class="product-catalog-button"
href="{{ route('business.product-inventory.normal-products.export', array_merge(['company_id' => company_id()], request()->except('page'))) }}"
aria-label="导出当前筛选结果"
title="导出当前筛选结果"
>
<span class="material-icons-outlined text-base" aria-hidden="true">download</span>
导出
</a>
<a
class="product-catalog-button"
href="{{ route('business.pages.show', ['company_id' => company_id(), 'section' => $section['slug'], 'page' => $page['slug']]) }}"
@ -496,11 +505,11 @@
<span class="material-icons-outlined text-base" aria-hidden="true">schedule</span>
<span>
@if (($productCatalogPurchasePriceSyncStatus['code'] ?? '') === 'initializing')
1688 代发单价正在执行首次同步,完成前当前仅显示普通商品采购价。
1688代发单价正在执行首次同步,完成前当前仅显示普通商品采购价。
@elseif (($productCatalogPurchasePriceSyncStatus['code'] ?? '') === 'initial_failed')
1688 代发单价首次同步失败,当前仅显示普通商品采购价。
1688代发单价首次同步失败,当前仅显示普通商品采购价。
@else
1688 代发单价尚未完成首次同步,当前仅显示普通商品采购价。
1688代发单价尚未完成首次同步,当前仅显示普通商品采购价。
@endif
</span>
</div>
@ -522,7 +531,8 @@
<th class="is-numeric">销售价</th>
<th class="is-numeric">成本价</th>
<th class="is-numeric">普通商品采购价</th>
<th class="is-numeric">1688 代发单价</th>
<th class="is-numeric">1688代发单价</th>
<th>数据来源</th>
<th class="is-numeric">市场价</th>
<th>商品状态</th>
<th>库存同步</th>
@ -555,13 +565,14 @@
@foreach (['sale_price', 'cost_price'] as $priceField)
<td class="is-numeric">{{ isset($row->{$priceField}) && $row->{$priceField} !== null ? number_format((float) $row->{$priceField}, 2) : '-' }}</td>
@endforeach
<td class="is-numeric product-catalog-price-cell">
{{ isset($row->purchase_price) && $row->purchase_price !== null ? number_format((float) $row->purchase_price, 2) : '-' }}
<span class="product-catalog-price-source">{{ $row->product_purchase_price_source ?? $productCatalogProductPriceSource }}</span>
</td>
<td class="is-numeric product-catalog-price-cell" title="{{ $row->purchase_order_finished_at ?? '' }}">
{{ isset($row->purchase_order_unit_price) && $row->purchase_order_unit_price !== null ? number_format((float) $row->purchase_order_unit_price, 2) : '-' }}
<span class="product-catalog-price-source">{{ $row->purchase_order_price_source ?? $productCatalogPurchasePriceSource }}</span>
<td class="is-numeric">{{ isset($row->purchase_price) && $row->purchase_price !== null ? number_format((float) $row->purchase_price, 2) : '-' }}</td>
<td class="is-numeric" title="{{ $row->purchase_order_finished_at ?? '' }}">{{ isset($row->purchase_order_unit_price) && $row->purchase_order_unit_price !== null ? number_format((float) $row->purchase_order_unit_price, 2) : '-' }}</td>
<td class="product-catalog-source-list">
@forelse ($row->data_sources ?? [] as $source)
<span class="product-catalog-source-item">{{ $source }}</span>
@empty
-
@endforelse
</td>
<td class="is-numeric">{{ isset($row->market_price) && $row->market_price !== null ? number_format((float) $row->market_price, 2) : '-' }}</td>
<td>
@ -578,7 +589,7 @@
</tr>
@empty
<tr>
<td colspan="17" class="product-catalog-empty">
<td colspan="18" class="product-catalog-empty">
<span class="material-icons-outlined text-3xl block mb-2" aria-hidden="true">inventory_2</span>
@if ($productCatalogError)
数据暂时不可用。

View File

@ -88,6 +88,10 @@ Route::prefix('workspace/courier-payables/quote-management/signatures')
Route::get('{quoteSignature}/files/{kind}', [CourierQuoteSignatures::class, 'download'])->name('download');
});
Route::get('workspace/product-inventory/normal-products/export', 'Business\Pages@exportNormalProducts')
->middleware('business.page:product-inventory,normal-products')
->name('business.product-inventory.normal-products.export');
Route::get('workspace/{section}/{page}/{tab}/export', 'Business\Pages@export')
->name('business.pages.export');

View File

@ -58,11 +58,10 @@ class ProductCatalogPageTest extends FeatureTestCase
'sale_price' => 899,
'cost_price' => 500,
'purchase_price' => 480,
'product_purchase_price_source' => '商品及库存管理(普通商品资料)',
'purchase_order_unit_price' => 65,
'purchase_order_finished_at' => '2026-08-20 11:03:05',
'purchase_order_id' => 827380,
'purchase_order_price_source' => '采购单管理-1688代发单价',
'data_sources' => ['商品及库存管理(普通商品资料)', '1688代发单价'],
'market_price' => 1099,
'brand' => '北木',
'category' => '住宅家具',
@ -106,15 +105,73 @@ class ProductCatalogPageTest extends FeatureTestCase
->assertSeeText('岩板餐桌')
->assertSeeText('899.00')
->assertSeeText('普通商品采购价')
->assertSeeText('1688 代发单价')
->assertSeeText('1688代发单价')
->assertSeeText('数据来源')
->assertSeeText('商品及库存管理(普通商品资料)')
->assertSeeText('采购单管理-1688代发单价')
->assertSeeText('1688代发单价')
->assertSeeText('65.00')
->assertDontSeeText('1688 代发单价尚未完成首次同步')
->assertSeeText('导出')
->assertDontSeeText('1688代发单价尚未完成首次同步')
->assertSeeText('正常同步')
->assertDontSee('pic_url');
}
public function test_normal_products_export_uses_all_filters_and_includes_data_source_column(): void
{
$this->grantAdminPermission();
$repository = \Mockery::mock(ProductCatalogMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('exportRows')
->once()
->withArgs(static function (array $filters): bool {
return $filters['sku_id'] === 'SKU-001'
&& $filters['supplier_name'] === '新北供应商'
&& $filters['sale_price_min'] === '800'
&& $filters['sale_price_max'] === '900';
})
->andReturn((static function (): \Generator {
yield (object) [
'autoid' => 1001,
'i_id' => 'STYLE-001',
'sku_id' => 'SKU-001',
'name' => '岩板餐桌',
'short_name' => '餐桌',
'properties_value' => '1.4米',
'color' => '胡桃色',
'brand' => '北木',
'category' => '住宅家具',
'supplier_name' => '新北供应商',
'sale_price' => 899,
'cost_price' => 500,
'purchase_price' => 480,
'purchase_order_unit_price' => 65,
'data_sources' => ['商品及库存管理(普通商品资料)', '1688代发单价'],
'market_price' => 1099,
'enabled' => 1,
'stock_disabled' => 0,
'modified' => '2026-08-15 10:00:00',
];
})());
$this->app->instance(ProductCatalogMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.product-inventory.normal-products.export', [
'sku_id' => 'SKU-001',
'supplier_name' => '新北供应商',
'sale_price_min' => '900',
'sale_price_max' => '800',
]));
$response->assertOk();
$this->assertStringContainsString('attachment;', (string) $response->headers->get('content-disposition'));
$content = $response->streamedContent();
$this->assertStringStartsWith(chr(0xEF) . chr(0xBB) . chr(0xBF), $content);
$this->assertStringContainsString('数据来源', $content);
$this->assertStringContainsString('商品及库存管理(普通商品资料);1688代发单价', $content);
$this->assertStringContainsString('SKU-001', $content);
}
public function test_normal_products_page_marks_purchase_prices_as_not_yet_synchronized(): void
{
$this->grantAdminPermission();
@ -150,7 +207,7 @@ class ProductCatalogPageTest extends FeatureTestCase
]))
->assertOk()
->assertSee('data-purchase-price-sync-state="pending"', false)
->assertSeeText('1688 代发单价尚未完成首次同步')
->assertSeeText('1688代发单价尚未完成首次同步')
->assertSeeText('当前仅显示普通商品采购价');
}
@ -254,6 +311,39 @@ class ProductCatalogPageTest extends FeatureTestCase
->assertSeeText('最近成功同步的商品');
}
public function test_normal_products_export_reports_an_unavailable_mirror(): void
{
$this->grantAdminPermission();
$repository = \Mockery::mock(ProductCatalogMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnFalse();
$repository->shouldNotReceive('ready');
$repository->shouldNotReceive('exportRows');
$this->app->instance(ProductCatalogMirrorRepository::class, $repository);
$this->withExceptionHandling()
->loginAs()
->get(route('business.product-inventory.normal-products.export'))
->assertServiceUnavailable();
}
public function test_normal_products_export_rejects_users_without_sidebar_permission(): void
{
$subject = 'user_' . $this->user->id;
setting()->set('system.sidebar_permissions', json_encode([
'configured' => [$subject],
'subjects' => [
$subject => ['company-management/basic-information'],
],
], JSON_UNESCAPED_UNICODE));
$this->withExceptionHandling()
->loginAs()
->get(route('business.product-inventory.normal-products.export'))
->assertForbidden();
}
public function test_normal_products_page_rejects_users_without_sidebar_permission(): void
{
$subject = 'user_' . $this->user->id;

View File

@ -151,8 +151,10 @@ class ProductCatalogMirrorRepositoryTest extends TestCase
$this->assertSame('55', (string) $row->purchase_price);
$this->assertSame('65', (string) $row->purchase_order_unit_price);
$this->assertSame(1011, (int) $row->purchase_order_id);
$this->assertSame('采购单管理-1688代发单价', $row->purchase_order_price_source);
$this->assertSame('商品及库存管理(普通商品资料)', $row->product_purchase_price_source);
$this->assertSame(
['商品及库存管理(普通商品资料)', '1688代发单价'],
$row->data_sources,
);
$this->assertTrue($result['purchase_price_sync_status']['initialized']);
}
@ -167,11 +169,31 @@ class ProductCatalogMirrorRepositoryTest extends TestCase
$this->assertSame('SKU-PENDING', $row->sku_id);
$this->assertNull($row->purchase_order_unit_price);
$this->assertSame('采购单管理-1688代发单价', $row->purchase_order_price_source);
$this->assertSame(['商品及库存管理(普通商品资料)'], $row->data_sources);
$this->assertFalse($result['purchase_price_sync_status']['initialized']);
$this->assertSame('pending', $result['purchase_price_sync_status']['code']);
}
public function test_it_exports_all_filtered_rows_in_stable_batches_with_data_sources(): void
{
$this->markReady(3);
$this->insertProduct(30, ['sku_id' => 'SKU-30', 'brand' => '保留品牌', 'purchase_price' => null]);
$this->insertProduct(10, ['sku_id' => 'SKU-10', 'brand' => '保留品牌', 'purchase_price' => 55]);
$this->insertProduct(20, ['sku_id' => 'SKU-20', 'brand' => '排除品牌']);
$this->createPurchasePriceTables(true);
$this->insertPurchasePrice(30, ['sku_id' => 'SKU-30', 'unit_price' => 66]);
$rows = iterator_to_array($this->repository->exportRows(
$this->filters(['brand' => '保留品牌']),
1,
));
$this->assertSame([10, 30], array_map(static fn (object $row): int => $row->autoid, $rows));
$this->assertSame(['商品及库存管理(普通商品资料)'], $rows[0]->data_sources);
$this->assertSame(['1688代发单价'], $rows[1]->data_sources);
$this->assertSame('66', (string) $rows[1]->purchase_order_unit_price);
}
public function test_it_refuses_queries_before_the_initial_sync_completes(): void
{
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([