fix: 快递对账分页索引与重复汇总优化 #143

Merged
WuYongJie merged 1 commits from fix/WuYongJie-courier-query-indexes into main 2026-09-09 15:26:42 +08:00
4 changed files with 104 additions and 3 deletions

View File

@ -410,7 +410,7 @@ return [
'jiansun.dbo.jst_order_express_details' => [
'source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'jst_order_express_details',
'target' => 'js_jst_order_express_details', 'keys' => ['id'], 'watermark' => 'updatedAt',
'indexes' => [['send_time', 'id'], ['inner_order_id', 'id']],
'indexes' => [['send_time', 'id'], ['inner_order_id', 'id'], ['id', 'logistic_id']],
'columns' => [
'id', 'updatedAt', 'inner_order_id', 'so_id', 'logistic_id', 'logistic_company_name',
'logistic_status_name', 'platform_name', 'shop_name', 'warehouse_name', 'send_time',

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private const TABLE = 'js_jst_order_express_details';
private const INDEX = 'idx_courier_detail_id_waybill';
public function up(): void
{
if (config('system_database.mode', 'bootstrap') === 'bootstrap') return;
$connection = (string) config('upstream_mirrors.connection', 'business_mirror');
$schema = Schema::connection($connection);
if (! $schema->hasTable(self::TABLE) || $schema->hasIndex(self::TABLE, self::INDEX)) return;
$db = DB::connection($connection);
$length = $db->getDriverName() === 'sqlite' ? 'length' : 'char_length';
if ($db->table(self::TABLE)->whereRaw($length . '(logistic_id) > 100')->exists()) {
throw new RuntimeException('快递单号超过上游 varchar(100) 定义,已停止变更。');
}
$schema->table(self::TABLE, static function (Blueprint $table): void {
$table->string('logistic_id', 100)->nullable()->change();
$table->index(['id', 'logistic_id'], self::INDEX);
});
}
public function down(): void
{
if (config('system_database.mode', 'bootstrap') === 'bootstrap') return;
$schema = Schema::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
if (! $schema->hasTable(self::TABLE) || ! $schema->hasIndex(self::TABLE, self::INDEX)) return;
$schema->table(self::TABLE, static function (Blueprint $table): void {
$table->dropIndex(self::INDEX);
$table->longText('logistic_id')->nullable()->change();
});
}
};

View File

@ -165,10 +165,12 @@ class CourierFreightReconciliationMirrorRepository
$page = max($page, 1);
$perPage = min(max($perPage, 1), 100);
$query = $this->filteredQuery($filters);
$total = (clone $query)->count('d.id');
// Both joins select at most one latest row, so this count is also the
// paginator total. Avoid traversing the entire joined history twice.
$summary = (clone $query)->selectRaw(
'COUNT(DISTINCT d.id) AS aggregate_count, COALESCE(SUM(o.freight), 0) AS freight',
'COUNT(d.id) AS aggregate_count, COALESCE(SUM(o.freight), 0) AS freight',
)->first();
$total = (int) ($summary->aggregate_count ?? 0);
$rows = (clone $query)->select($this->selectColumns())
->orderByDesc('d.id')
->forPage($page, $perPage)

View File

@ -0,0 +1,59 @@
<?php
namespace Tests\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
use Tests\TestCase;
class CourierDetailPaginationMigrationTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config([
'system_database.mode' => 'connected',
'upstream_mirrors.connection' => 'courier_migration_test',
'database.connections.courier_migration_test' => [
'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '',
],
]);
DB::purge('courier_migration_test');
Schema::connection('courier_migration_test')->create('js_jst_order_express_details', function (Blueprint $table): void {
$table->integer('id');
$table->longText('logistic_id')->nullable();
});
}
public function test_migration_and_rollback_preserve_waybills_and_nulls(): void
{
$table = DB::connection('courier_migration_test')->table('js_jst_order_express_details');
$rows = [['id' => 1, 'logistic_id' => '001234567890123456789'], ['id' => 2, 'logistic_id' => null]];
$table->insert($rows);
$migration = require base_path('database/migrations/2026_09_09_160000_index_courier_detail_pagination.php');
$migration->up();
$migration->up();
$this->assertTrue(Schema::connection('courier_migration_test')->hasIndex('js_jst_order_express_details', 'idx_courier_detail_id_waybill'));
$this->assertSame($rows, $table->orderBy('id')->get()->map(fn ($row) => (array) $row)->all());
$migration->down();
$this->assertFalse(Schema::connection('courier_migration_test')->hasIndex('js_jst_order_express_details', 'idx_courier_detail_id_waybill'));
$this->assertSame($rows, $table->orderBy('id')->get()->map(fn ($row) => (array) $row)->all());
}
public function test_oversize_waybill_stops_migration_without_truncation(): void
{
$value = str_repeat('A', 101);
$table = DB::connection('courier_migration_test')->table('js_jst_order_express_details');
$table->insert(['id' => 1, 'logistic_id' => $value]);
$migration = require base_path('database/migrations/2026_09_09_160000_index_courier_detail_pagination.php');
try {
$migration->up();
$this->fail('Oversize source data must stop migration.');
} catch (RuntimeException $exception) {
$this->assertSame($value, $table->value('logistic_id'));
$this->assertFalse(Schema::connection('courier_migration_test')->hasIndex('js_jst_order_express_details', 'idx_courier_detail_id_waybill'));
}
}
}