修复非京喜退款镜像查询与测试

This commit is contained in:
苏木 2026-08-21 16:11:48 +08:00
parent 6f751408e6
commit eebf8b4c2e
6 changed files with 473 additions and 23 deletions

View File

@ -253,10 +253,8 @@ class Pages extends Controller
$request = request();
$defaultTo = now()->toDateString();
$defaultFrom = now()->subDays(6)->toDateString();
$from = trim((string) $request->query('from', $defaultFrom));
$to = trim((string) $request->query('to', $defaultTo));
$from = preg_match('/^\d{4}-\d{2}-\d{2}$/', $from) === 1 ? $from : $defaultFrom;
$to = preg_match('/^\d{4}-\d{2}-\d{2}$/', $to) === 1 ? $to : $defaultTo;
$from = $this->dateFilter($request->query('from', $defaultFrom)) ?: $defaultFrom;
$to = $this->dateFilter($request->query('to', $defaultTo)) ?: $defaultTo;
if ($from > $to) {
[$from, $to] = [$to, $from];
}

View File

@ -72,7 +72,17 @@ return [
'operations_replica.rpt.fys_companyInfo' => ['source_key' => 'operations_replica', 'schema' => 'rpt', 'object' => 'fys_companyInfo', 'target' => 'ops_fys_company_info', 'keys' => ['Id'], 'watermark' => 'updateTIme'],
'operations_replica.rpt.fys_shopInfo' => ['source_key' => 'operations_replica', 'schema' => 'rpt', 'object' => 'fys_shopInfo', 'target' => 'ops_fys_shop_info', 'keys' => ['Id'], 'watermark' => 'updateTime'],
'operations_replica.rpt.fys_TrademarkInfo' => ['source_key' => 'operations_replica', 'schema' => 'rpt', 'object' => 'fys_TrademarkInfo', 'target' => 'ops_fys_trademark_info', 'keys' => ['Id'], 'watermark' => 'updateTime'],
'operations_replica.rpt.jst_Orders' => ['source_key' => 'operations_replica', 'schema' => 'rpt', 'object' => 'jst_Orders', 'target' => 'ops_jst_orders', 'keys' => ['Id'], 'watermark' => 'updateTime'],
'operations_replica.rpt.jst_Orders' => [
'source_key' => 'operations_replica',
'schema' => 'rpt',
'object' => 'jst_Orders',
'target' => 'ops_jst_orders',
'keys' => ['Id'],
'watermark' => 'updateTime',
'indexes' => [
['o_id', 'Id'],
],
],
'operations_replica.rpt.jst_OrderItem' => [
'source_key' => 'operations_replica',
'schema' => 'rpt',
@ -80,7 +90,7 @@ return [
'target' => 'ops_jst_order_item',
'keys' => ['Id'],
'indexes' => [
['o_id', 'outer_oi_id', 'Id'],
['o_id', 'Id'],
],
],
'operations_replica.rpt.jst_sku' => [
@ -122,8 +132,6 @@ return [
'keys' => ['Id'],
'indexes' => [
['as_id', 'Id'],
['sku_id', 'Id'],
['shop_sku_id', 'Id'],
],
],
'operations_replica.rpt.jst_shopInfo' => ['source_key' => 'operations_replica', 'schema' => 'rpt', 'object' => 'jst_shopInfo', 'target' => 'ops_jst_shop_info', 'keys' => ['Id'], 'watermark' => 'updateTime'],

View File

@ -5,7 +5,9 @@ namespace Modules\EcommerceFinance\Repositories;
use Carbon\CarbonInterface;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Contracts\BusinessMirrorDataset;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use RuntimeException;
@ -83,7 +85,7 @@ class NonJingxiRefundMirrorRepository
} elseif ($statuses->contains('running')) {
$code = 'syncing';
} else {
$lastSucceeded = $lastSucceededAt ? now()->parse($lastSucceededAt) : null;
$lastSucceeded = $lastSucceededAt ? Carbon::parse($lastSucceededAt) : null;
$lagMinutes = $lastSucceeded === null
? null
: (int) ceil(max(0, ($at ?? now())->getTimestamp() - $lastSucceeded->getTimestamp()) / 60);
@ -148,27 +150,27 @@ class NonJingxiRefundMirrorRepository
private function applyFilters(Builder $query, array $filters): void
{
$query->where('r.as_date', '>=', $filters['from'] . ' 00:00:00')
->where('r.as_date', '<', now()->parse($filters['to'])->addDay()->format('Y-m-d 00:00:00'));
->where('r.as_date', '<', Carbon::parse($filters['to'])->addDay()->format('Y-m-d 00:00:00'));
if ($filters['order_no'] !== '') {
$needle = '%' . $filters['order_no'] . '%';
$needle = $this->contains($filters['order_no']);
$query->where(function (Builder $order) use ($needle): void {
$order->where('r.o_id', 'like', $needle)
->orWhere('r.so_id', 'like', $needle)
->orWhere('r.outer_as_id', 'like', $needle);
$order->whereRaw("CAST(r.o_id AS CHAR) LIKE ? ESCAPE '!'", [$needle])
->orWhereRaw("r.so_id LIKE ? ESCAPE '!'", [$needle])
->orWhereRaw("r.outer_as_id LIKE ? ESCAPE '!'", [$needle]);
});
}
if ($filters['shop_name'] !== '') {
$query->where('r.shop_name', 'like', '%' . $filters['shop_name'] . '%');
$query->whereRaw("r.shop_name LIKE ? ESCAPE '!'", [$this->contains($filters['shop_name'])]);
}
if ($filters['sku_id'] !== '') {
$needle = '%' . $filters['sku_id'] . '%';
$needle = $this->contains($filters['sku_id']);
$query->where(function (Builder $sku) use ($needle): void {
$sku->where('i.sku_id', 'like', $needle)
->orWhere('i.shop_sku_id', 'like', $needle)
->orWhere('i.i_id', 'like', $needle);
$sku->whereRaw("i.sku_id LIKE ? ESCAPE '!'", [$needle])
->orWhereRaw("i.shop_sku_id LIKE ? ESCAPE '!'", [$needle])
->orWhereRaw("i.i_id LIKE ? ESCAPE '!'", [$needle]);
});
}
}
@ -236,7 +238,12 @@ class NonJingxiRefundMirrorRepository
}
}
private function dataset(string $key)
private function contains(string $value): string
{
return '%' . str_replace(['!', '%', '_'], ['!!', '!%', '!_'], trim($value)) . '%';
}
private function dataset(string $key): BusinessMirrorDataset
{
return $this->datasets->get($key);
}

View File

@ -0,0 +1,214 @@
<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository;
use Tests\Feature\FeatureTestCase;
class NonJingxiRefundPageTest extends FeatureTestCase
{
protected function grantAdminPermission(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
}
public function test_page_renders_mirror_rows_second_page_and_filters(): void
{
$this->grantAdminPermission();
setting()->set('default.list_limit', 25);
$states = [
NonJingxiRefundMirrorRepository::INFO_DATASET => ['status' => 'running', 'initial_completed_at' => '2026-08-15 00:00:00'],
NonJingxiRefundMirrorRepository::ITEM_DATASET => ['status' => 'running', 'initial_completed_at' => '2026-08-15 00:00:00'],
];
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('states')->once()->andReturn($states);
$repository->shouldReceive('syncStatus')->once()->with($states)->andReturn([
'code' => 'syncing',
'last_succeeded_at' => '2026-08-21 12:00:00',
]);
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('paginate')->once()->withArgs(
static fn (array $filters, int $page, int $limit): bool =>
$filters === [
'from' => '2026-08-01',
'to' => '2026-08-21',
'order_no' => 'ORDER-1',
'shop_name' => '测试店',
'sku_id' => 'SKU-1',
] && $page === 2 && $limit === 25
)->andReturn([
'data' => [(object) [
'Id' => 102,
'refundDate' => '2026-08-20 10:00:00',
'as_id' => 2002,
'outer_as_id' => 'AS-2002',
'o_id' => 3002,
'so_id' => 'ORDER-1',
'type' => '退款',
'node' => '完成',
'isZC' => '是',
'shop_name' => '测试店',
'platform' => '天猫',
'responsibleDepartment' => '电商部',
'totalRefundMoney' => 99.5,
'originPostcode' => 'ZIP',
'shop_i_id' => 'ITEM-1',
'shop_sku_id' => 'SHOP-SKU-1',
'i_id' => 'STYLE-1',
'sku_id' => 'SKU-1',
'buyer_paid_amount' => 120,
'qty' => 1,
'unitCostGoods' => 50,
'detailRefundMoney' => 99.5,
'refundType' => '退货退款',
'refundQty' => 1,
'refundCostGoods' => 50,
'RefundMoneyIncome' => 99.5,
'AfSalesMoney' => 99.5,
'createTime' => '2026-08-20 09:00:00',
'status' => '完成',
'shop_status' => '完成',
'created' => '2026-08-20 09:00:00',
'confirm_date' => '2026-08-20 10:00:00',
]],
'total' => 26,
]);
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
$url = route('business.pages.show', [
'company_id' => $this->company->id,
'section' => 'refunds',
'page' => 'non-jingxi-refund-data',
]) . '?' . http_build_query([
'from' => '2026-08-01', 'to' => '2026-08-21', 'order_no' => 'ORDER-1',
'shop_name' => '测试店', 'sku_id' => 'SKU-1', 'page' => 2,
]);
$this->loginAs()->get($url)
->assertOk()
->assertSee('data-filter-contract="non-jingxi-refund-data.filters"', false)
->assertSee('data-sync-state="syncing"', false)
->assertSeeText('业务镜像同步中,当前显示最近成功数据')
->assertSeeText('AS-2002')
->assertSeeText('SKU-1')
->assertSeeText('99.50')
->assertSeeText('共 26 条记录')
->assertSeeText('第 2 / 2 页')
->assertSee('value="2026-08-01"', false)
->assertSee('value="ORDER-1"', false);
}
public function test_page_does_not_report_empty_data_before_initial_sync_finishes(): void
{
$this->grantAdminPermission();
$states = [
NonJingxiRefundMirrorRepository::INFO_DATASET => ['status' => 'running'],
NonJingxiRefundMirrorRepository::ITEM_DATASET => ['status' => 'running'],
];
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('states')->once()->andReturn($states);
$repository->shouldReceive('syncStatus')->once()->with($states)->andReturn([
'code' => 'initializing',
'last_succeeded_at' => null,
]);
$repository->shouldReceive('ready')->once()->andReturnFalse();
$repository->shouldNotReceive('paginate');
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'refunds',
'page' => 'non-jingxi-refund-data',
]))
->assertOk()
->assertSeeText('业务镜像正在执行首次同步')
->assertSeeText('首次同步尚未完成,完成后将自动显示数据。')
->assertDontSeeText('当前筛选范围暂无数据。');
}
public function test_page_shows_a_real_empty_filter_result(): void
{
$this->grantAdminPermission();
$states = [
NonJingxiRefundMirrorRepository::INFO_DATASET => ['status' => 'succeeded', 'initial_completed_at' => '2026-08-15 00:00:00'],
NonJingxiRefundMirrorRepository::ITEM_DATASET => ['status' => 'succeeded', 'initial_completed_at' => '2026-08-15 00:00:00'],
];
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('states')->once()->andReturn($states);
$repository->shouldReceive('syncStatus')->once()->andReturn(['code' => 'healthy', 'last_succeeded_at' => '2026-08-21 12:00:00']);
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('paginate')->once()->andReturn(['data' => [], 'total' => 0]);
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'refunds',
'page' => 'non-jingxi-refund-data',
'sku_id' => 'NOT-FOUND',
]))
->assertOk()
->assertSeeText('当前筛选范围暂无数据。')
->assertSeeText('共 0 条记录');
}
public function test_page_replaces_invalid_dates_with_the_default_range(): void
{
$this->grantAdminPermission();
$this->travelTo('2026-08-21 12:00:00');
$states = [
NonJingxiRefundMirrorRepository::INFO_DATASET => ['status' => 'succeeded', 'initial_completed_at' => '2026-08-15 00:00:00'],
NonJingxiRefundMirrorRepository::ITEM_DATASET => ['status' => 'succeeded', 'initial_completed_at' => '2026-08-15 00:00:00'],
];
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('states')->once()->andReturn($states);
$repository->shouldReceive('syncStatus')->once()->with($states)->andReturn([
'code' => 'healthy',
'last_succeeded_at' => '2026-08-21 12:00:00',
]);
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('paginate')->once()->withArgs(
static fn (array $filters, int $page, int $limit): bool =>
$filters['from'] === '2026-08-15'
&& $filters['to'] === '2026-08-21'
&& $page === 1
&& $limit === 25
)->andReturn(['data' => [], 'total' => 0]);
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'refunds',
'page' => 'non-jingxi-refund-data',
'from' => '2026-02-31',
'to' => 'not-a-date',
]))
->assertOk()
->assertSee('value="2026-08-15"', false)
->assertSee('value="2026-08-21"', false)
->assertDontSee('非京喜退款数据暂时无法读取,请稍后重试。');
}
public function test_page_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.pages.show', [
'section' => 'refunds',
'page' => 'non-jingxi-refund-data',
]))->assertForbidden();
}
}

View File

@ -4,6 +4,7 @@ namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Modules\EcommerceFinance\Models\AlipayBill;
use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository;
use Tests\Feature\FeatureTestCase;
class PagesTest extends FeatureTestCase
@ -150,6 +151,14 @@ class PagesTest extends FeatureTestCase
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnFalse();
$repository->shouldNotReceive('states');
$repository->shouldNotReceive('syncStatus');
$repository->shouldNotReceive('ready');
$repository->shouldNotReceive('paginate');
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'refunds',
@ -177,7 +186,8 @@ class PagesTest extends FeatureTestCase
->assertOk()
->assertSeeText('非京喜退款数据')
->assertSee('aria-label="非京喜退款数据"', false)
->assertSeeText('暂无数据')
->assertSeeText('共享业务镜像尚未建立')
->assertSeeText('数据暂时无法读取')
->assertSee('class="upstream-data-table refund-data-table refund-data-table--wide"', false)
->assertSee('data-header-row="labels"', false)
->assertSee('data-header-row="fields"', false)
@ -219,10 +229,15 @@ class PagesTest extends FeatureTestCase
'AfSalesMoney', 'createTime', 'status', 'shop_status', 'created', 'confirm_date',
], $headers->get('fields'));
$this->withExceptionHandling()->get(route('business.pages.show', [
$this->withExceptionHandling()->loginAs()->get(route('business.pages.show', [
'company_id' => $this->company->id,
'section' => 'refunds',
'page' => 'unknown-refund-page',
]))->assertNotFound();
]))->assertRedirect(route('business.pages.show', [
'company_id' => $this->company->id,
'section' => 'refunds',
'page' => 'non-jingxi-refund-data',
]));
}
public function testPaperMaterialPagesRenderAsSeparateTables()

View File

@ -0,0 +1,208 @@
<?php
namespace Tests\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use Tests\TestCase;
class NonJingxiRefundMirrorRepositoryTest extends TestCase
{
private NonJingxiRefundMirrorRepository $repository;
protected function setUp(): void
{
parent::setUp();
config([
'database.connections.business_mirror' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
],
'upstream_mirrors.connection' => 'business_mirror',
'upstream_mirrors.datasets' => $this->definitions(),
]);
DB::purge('business_mirror');
$this->createTables();
$this->markReady();
$generic = new GenericBusinessMirrorRepository(app('db'));
$this->repository = new NonJingxiRefundMirrorRepository(
new BusinessMirrorDatasetRegistry(),
$generic,
);
}
public function test_ready_mirror_supports_second_page_filters_and_current_page_enrichment(): void
{
$db = DB::connection('business_mirror');
$db->table('ops_jst_return_refund_info')->insert([
$this->refundInfo(1, 101, '2026-08-19 10:00:00', '天猫', '一号店'),
$this->refundInfo(2, 102, '2026-08-20 10:00:00', '抖音', '二号店'),
$this->refundInfo(3, 103, '2026-08-21 10:00:00', 'jingxi', '京喜店'),
]);
$db->table('ops_jst_return_refund_item')->insert([
$this->refundItem(11, 101, 'SKU-1', 'SUB-1'),
$this->refundItem(12, 102, 'SKU-2', 'SUB-2'),
$this->refundItem(13, 103, 'SKU-JX', 'SUB-JX'),
]);
$db->table('ops_jst_orders')->insert([
['Id' => 1, 'o_id' => 8901, 'receiver_zip' => 'ZIP-1'],
['Id' => 2, 'o_id' => 8902, 'receiver_zip' => 'ZIP-2'],
]);
$db->table('ops_jst_order_item')->insert([
['Id' => 1, 'o_id' => 8901, 'outer_oi_id' => 'SUB-1', 'shop_i_id' => 'ITEM-1', 'shop_sku_id' => 'SHOP-SKU-1', 'i_id' => 'STYLE-1', 'sku_id' => 'SKU-1', 'buyer_paid_amount' => 88],
['Id' => 2, 'o_id' => 8902, 'outer_oi_id' => 'SUB-2', 'shop_i_id' => 'ITEM-2', 'shop_sku_id' => 'SHOP-SKU-2', 'i_id' => 'STYLE-2', 'sku_id' => 'SKU-2', 'buyer_paid_amount' => 99],
]);
$db->table('ops_jst_sku')->insert([
['autoid' => 1, 'sku_id' => 'SKU-1', 'sku_type' => 'normal', 'cost_price' => 20],
['autoid' => 2, 'sku_id' => 'SKU-2', 'sku_type' => 'normal', 'cost_price' => 30],
]);
$db->table('ops_fys_shop_info')->insert([
['Id' => 1, 'erpShopName' => '一号店', 'responsibleDepartment' => '部门甲'],
['Id' => 2, 'erpShopName' => '二号店', 'responsibleDepartment' => '部门乙'],
]);
$filters = ['from' => '2026-08-18', 'to' => '2026-08-21', 'order_no' => '', 'shop_name' => '', 'sku_id' => ''];
$page2 = $this->repository->paginate($filters, 2, 1);
$this->assertSame(2, $page2['total']);
$this->assertSame(11, $page2['data'][0]->Id);
$this->assertSame('ZIP-1', $page2['data'][0]->originPostcode);
$this->assertSame('ITEM-1', $page2['data'][0]->shop_i_id);
$this->assertSame(88, $page2['data'][0]->buyer_paid_amount);
$this->assertSame(20.0, $page2['data'][0]->unitCostGoods);
$this->assertSame(40.0, $page2['data'][0]->refundCostGoods);
$this->assertSame('部门甲', $page2['data'][0]->responsibleDepartment);
$filters['shop_name'] = '二号';
$this->assertSame(1, $this->repository->paginate($filters, 1, 25)['total']);
$filters['shop_name'] = '';
$filters['shop_name'] = '%';
$this->assertSame(0, $this->repository->paginate($filters, 1, 25)['total']);
$filters['shop_name'] = '';
$filters['sku_id'] = 'SKU-1';
$this->assertSame(1, $this->repository->paginate($filters, 1, 25)['total']);
$filters['sku_id'] = '';
$filters['order_no'] = '8902';
$this->assertSame(1, $this->repository->paginate($filters, 1, 25)['total']);
}
public function test_sync_status_distinguishes_rotation_from_initial_sync(): void
{
$states = $this->repository->states();
foreach ($states as &$state) {
$state['status'] = 'running';
$state['last_succeeded_at'] = '2026-08-21 12:00:00';
}
unset($state);
$this->assertSame('syncing', $this->repository->syncStatus(
$states,
Carbon::parse('2026-08-21 12:05:00'),
)['code']);
$states[NonJingxiRefundMirrorRepository::INFO_DATASET]['initial_completed_at'] = null;
$this->assertSame('initializing', $this->repository->syncStatus($states)['code']);
}
/** @return array<string,array<string,mixed>> */
private function definitions(): array
{
return [
NonJingxiRefundMirrorRepository::INFO_DATASET => $this->definition('jst_returnAndRefundInfo', 'ops_jst_return_refund_info'),
NonJingxiRefundMirrorRepository::ITEM_DATASET => $this->definition('jst_returnAndRefundItem', 'ops_jst_return_refund_item'),
'operations_replica.rpt.jst_Orders' => $this->definition('jst_Orders', 'ops_jst_orders'),
'operations_replica.rpt.jst_OrderItem' => $this->definition('jst_OrderItem', 'ops_jst_order_item'),
'operations_replica.rpt.jst_sku' => $this->definition('jst_sku', 'ops_jst_sku'),
'operations_replica.rpt.fys_shopInfo' => $this->definition('fys_shopInfo', 'ops_fys_shop_info'),
];
}
/** @return array<string,mixed> */
private function definition(string $object, string $target): array
{
return ['source_key' => 'operations_replica', 'schema' => 'rpt', 'object' => $object, 'target' => $target, 'keys' => ['Id']];
}
private function createTables(): void
{
$schema = Schema::connection('business_mirror');
$schema->create('business_mirror_table_states', function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->string('status')->default('succeeded');
$table->dateTime('initial_completed_at')->nullable();
$table->dateTime('last_succeeded_at')->nullable();
});
$schema->create('ops_jst_return_refund_info', function (Blueprint $table): void {
$table->integer('Id'); $table->integer('as_id'); $table->dateTime('as_date'); $table->string('outer_as_id')->nullable();
$table->integer('o_id'); $table->string('so_id')->nullable(); $table->string('type')->nullable(); $table->string('node')->nullable();
$table->string('shop_type')->nullable(); $table->string('shop_name')->nullable(); $table->string('shop_site')->nullable();
$table->decimal('refund', 18, 2)->nullable(); $table->decimal('receive_refund', 18, 2)->nullable();
$table->decimal('buyer_apply_refund', 18, 2)->nullable(); $table->dateTime('createTime')->nullable();
$table->string('status')->nullable(); $table->string('shop_status')->nullable(); $table->dateTime('created')->nullable();
$table->dateTime('confirm_date')->nullable();
});
$schema->create('ops_jst_return_refund_item', function (Blueprint $table): void {
$table->integer('Id'); $table->integer('as_id'); $table->string('outer_oi_id')->nullable();
$table->string('shop_sku_id')->nullable(); $table->string('i_id')->nullable(); $table->string('sku_id')->nullable();
$table->integer('qty')->nullable(); $table->decimal('shop_amount', 18, 2)->nullable();
$table->string('type')->nullable(); $table->integer('r_qty')->nullable();
});
$schema->create('ops_jst_orders', function (Blueprint $table): void {
$table->integer('Id'); $table->integer('o_id'); $table->string('receiver_zip')->nullable();
});
$schema->create('ops_jst_order_item', function (Blueprint $table): void {
$table->integer('Id'); $table->integer('o_id'); $table->string('outer_oi_id');
$table->string('shop_i_id')->nullable(); $table->string('shop_sku_id')->nullable(); $table->string('i_id')->nullable();
$table->string('sku_id')->nullable(); $table->decimal('buyer_paid_amount', 18, 2)->nullable();
});
$schema->create('ops_jst_sku', function (Blueprint $table): void {
$table->integer('autoid'); $table->string('sku_id'); $table->string('sku_type'); $table->decimal('cost_price', 18, 2)->nullable();
});
$schema->create('ops_fys_shop_info', function (Blueprint $table): void {
$table->integer('Id'); $table->string('erpShopName'); $table->string('responsibleDepartment')->nullable();
});
}
private function markReady(): void
{
foreach (array_keys($this->definitions()) as $key) {
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
'dataset_key' => $key,
'status' => 'succeeded',
'initial_completed_at' => '2026-08-15 00:00:00',
'last_succeeded_at' => '2026-08-21 12:00:00',
]);
}
}
/** @return array<string,mixed> */
private function refundInfo(int $id, int $asId, string $date, string $platform, string $shopName): array
{
return [
'Id' => $id, 'as_id' => $asId, 'as_date' => $date, 'outer_as_id' => 'AS-' . $asId,
'o_id' => 8900 + $id, 'so_id' => 'SO-' . $id, 'type' => '退款', 'node' => '完成',
'shop_type' => '自营', 'shop_name' => $shopName, 'shop_site' => $platform,
'refund' => 100, 'receive_refund' => 80, 'buyer_apply_refund' => 100,
'createTime' => $date, 'status' => '完成', 'shop_status' => '完成',
'created' => $date, 'confirm_date' => $date,
];
}
/** @return array<string,mixed> */
private function refundItem(int $id, int $asId, string $skuId, string $outerOrderItemId): array
{
return [
'Id' => $id, 'as_id' => $asId, 'outer_oi_id' => $outerOrderItemId,
'shop_sku_id' => 'SHOP-' . $skuId, 'i_id' => 'STYLE-' . $id, 'sku_id' => $skuId,
'qty' => 2, 'shop_amount' => 80, 'type' => '退货退款', 'r_qty' => 2,
];
}
}