CaiWuXiTong/tests/Unit/EcommerceFinance/StoreAccountMirrorRepositoryTest.php
renshen 401dfb089a 店铺账号密码:上游新增字段自动跟随(镜像补列 + 页面自动多出这一列)
新增 business-mirror:follow-upstream-columns:先把数据字典刷新到最新,再把上游
视图里新增、当前数据集还没同步的字段追加进镜像定义并立刻建列,下一次常规同步
就会带回数据。只做新增,不删除/不改写已有字段;字段名带 password/secret/token/
cipher/credential 等敏感关键字的一律跳过。已登记到发布 cron(每天 06:40)。

页面侧:config/upstream_mirrors.php 的 auto_follow_columns 用「页面 => 数据集」
登记,FollowedUpstreamColumns 服务把 followed_columns 变成表格列;这类列只展示、
不生成查询条件(后端还没有对应筛选口径)。

数据集自带的数据变动仍然由每分钟的 business-mirror:sync-tables 增量同步。
2026-10-08 13:19:53 +08:00

169 lines
8.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreAccountMirrorRepository;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use Tests\TestCase;
class StoreAccountMirrorRepositoryTest extends TestCase
{
private StoreAccountMirrorRepository $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' => [
StoreAccountMirrorRepository::DATASET => [
'source_key' => 'database_4',
'schema' => 'external_store_accounts',
'object' => 'accounts_v1',
'target' => 'store_accounts',
'keys' => ['id'],
// 模拟 business-mirror:follow-upstream-columns 记录下来的跟随字段。
'followed_columns' => ['platform'],
],
],
]);
DB::purge('business_mirror');
Schema::connection('business_mirror')->create('business_mirror_table_states', function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->dateTime('initial_completed_at')->nullable();
});
Schema::connection('business_mirror')->create('store_accounts', function (Blueprint $table): void {
$table->string('id')->primary();
foreach (['storeName', 'operationOwner', 'operationDepartmentHead', 'operationDirector', 'profitCenter', 'accountType', 'account', 'boundPhone', 'verificationContact', 'accountGroupName', 'createdAt', 'updatedAt'] as $column) {
$table->string($column)->nullable();
}
$table->boolean('passwordConfigured')->nullable();
// 自动跟随上游补进来的字段。
$table->string('platform')->nullable();
});
// 授权状态 / 店铺状态来自这两张镜像表(按店铺名关联)。
Schema::connection('business_mirror')->create('ops_fys_shop_info', function (Blueprint $table): void {
$table->integer('Id');
$table->string('erpShopName')->nullable();
$table->integer('shopState')->nullable();
});
Schema::connection('business_mirror')->create('ops_jst_shop_info', function (Blueprint $table): void {
$table->integer('Id');
$table->string('shop_name')->nullable();
$table->dateTime('session_expired')->nullable();
});
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
'dataset_key' => StoreAccountMirrorRepository::DATASET,
'initial_completed_at' => '2026-09-04 10:00:00',
]);
$this->repository = new StoreAccountMirrorRepository(
new BusinessMirrorDatasetRegistry(),
new GenericBusinessMirrorRepository(app('db')),
);
}
public function test_it_maps_account_type_codes_to_business_labels(): void
{
foreach (['MAIN' => '主账号', 'SUB' => '子账号', '/' => null] as $type => $expected) {
DB::connection('business_mirror')->table('store_accounts')->insert([
'id' => 'account-' . md5($type),
'storeName' => ['MAIN' => 'B店铺', 'SUB' => 'A店铺', '/' => 'C店铺'][$type],
'accountType' => $type,
'account' => 'test-account',
'passwordConfigured' => $type === 'MAIN' ? true : ($type === 'SUB' ? false : null),
]);
}
$rows = collect($this->repository->paginate(1, 50)['data'])->keyBy('id');
$this->assertSame('主账号', $rows['account-' . md5('MAIN')]['account_type']);
$this->assertSame('子账号', $rows['account-' . md5('SUB')]['account_type']);
$this->assertNull($rows['account-' . md5('/')]['account_type']);
$this->assertTrue($rows['account-' . md5('MAIN')]['password_configured']);
$this->assertFalse($rows['account-' . md5('SUB')]['password_configured']);
$this->assertNull($rows['account-' . md5('/')]['password_configured']);
$sorted = $this->repository->paginate(1, 50, ['_sort' => 'store_name', '_direction' => 'asc']);
$this->assertSame(['A店铺', 'B店铺', 'C店铺'], array_column($sorted['data'], 'store_name'));
}
public function test_it_exposes_followed_upstream_columns_to_the_page(): void
{
DB::connection('business_mirror')->table('store_accounts')->insert([
'id' => 'account-followed',
'storeName' => 'A店铺',
'account' => 'acct-A',
'platform' => '拼多多',
]);
$this->assertSame(['platform'], $this->repository->followedColumns());
$this->assertSame([
['name' => 'store_name', 'label' => '店铺名称'],
['name' => 'platform', 'label' => 'platform', 'followed' => true],
], $this->repository->withFollowedFields([['name' => 'store_name', 'label' => '店铺名称']]));
$rows = collect($this->repository->paginate(1, 10)['data'])->keyBy('id');
$this->assertSame('拼多多', $rows['account-followed']['platform']);
}
public function test_it_attaches_authorization_and_store_status_and_filters_them(): void
{
$db = DB::connection('business_mirror');
foreach (['A店铺' => '主账号', 'B店铺' => '主账号', 'C店铺' => '子账号', 'D店铺' => '子账号'] as $store => $type) {
$db->table('store_accounts')->insert([
'id' => 'account-' . md5($store),
'storeName' => $store,
'accountType' => $type === '主账号' ? 'MAIN' : 'SUB',
'account' => 'acct-' . $store,
]);
}
// 店铺档案:A 正常、B 已关闭;聚水潭授权:A 未过期、B 已过期、C 无授权时间,
// D 两条记录都没有。
$db->table('ops_fys_shop_info')->insert([
['Id' => 1, 'erpShopName' => 'A店铺', 'shopState' => 0],
['Id' => 2, 'erpShopName' => 'B店铺', 'shopState' => 6],
]);
$db->table('ops_jst_shop_info')->insert([
['Id' => 1, 'shop_name' => 'A店铺', 'session_expired' => now()->addYear()->toDateTimeString()],
['Id' => 2, 'shop_name' => 'B店铺', 'session_expired' => now()->subDay()->toDateTimeString()],
['Id' => 3, 'shop_name' => 'C店铺', 'session_expired' => null],
]);
$rows = collect($this->repository->paginate(1, 50)['data'])->keyBy('store_name');
$this->assertSame('已授权', $rows['A店铺']['authorization_status']);
$this->assertSame('正常', $rows['A店铺']['store_status']);
$this->assertSame('已过期', $rows['B店铺']['authorization_status']);
$this->assertSame('已关闭', $rows['B店铺']['store_status']);
$this->assertSame('未授权', $rows['C店铺']['authorization_status']);
$this->assertSame('', $rows['C店铺']['store_status']);
$this->assertSame('', $rows['D店铺']['authorization_status']);
$this->assertSame('', $rows['D店铺']['store_status']);
$authorized = $this->repository->paginate(1, 50, ['authorization_status' => '已授权', 'store_status' => '正常']);
$this->assertSame(1, $authorized['total']);
$this->assertSame(['A店铺'], array_column($authorized['data'], 'store_name'));
$missing = $this->repository->paginate(1, 50, ['authorization_status' => '未授权']);
$this->assertSame(['C店铺'], array_column($missing['data'], 'store_name'));
// 关联出来的列也能排序:正常 -> 已关闭 -> 无档案。
$sorted = $this->repository->paginate(1, 50, ['_sort' => 'store_status', '_direction' => 'asc']);
$this->assertSame(['A店铺', 'B店铺'], array_slice(array_column($sorted['data'], 'store_name'), 0, 2));
$sortedDesc = $this->repository->paginate(1, 50, ['_sort' => 'store_status', '_direction' => 'desc']);
$this->assertSame(['B店铺', 'A店铺'], array_slice(array_column($sortedDesc['data'], 'store_name'), 2));
}
}