feat: 订单利润支持支付发货时间筛选 (#983)

订单利润支持支付时间、发货时间、订单时间筛选;先分页订单键再汇总当前页,补充日期索引与性能优化。
This commit is contained in:
renshen 2026-11-07 16:21:24 +08:00
parent 46964454fd
commit 31de7272e3
10 changed files with 420 additions and 41 deletions

View File

@ -25,7 +25,10 @@ return new class extends Migration
if (! $schema->hasTable(GoodsProfitAfsLookupSchema::TABLE)) return;
$changes = [];
foreach (array_keys(GoodsProfitAfsLookupSchema::INDEXES) as $index) {
foreach ([
'idx_afs_order_time_id',
'idx_afs_shop_order_time_id',
] as $index) {
if ($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, $index)) {
$changes[] = 'DROP INDEX `' . $index . '`';
}

View File

@ -0,0 +1,63 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Services\GoodsProfitAfsLookupSchema;
return new class extends Migration
{
public function up(): void
{
if (config('system_database.mode', 'bootstrap') === 'bootstrap') {
return;
}
app(GoodsProfitAfsLookupSchema::class)->ensure(
DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')),
);
}
public function down(): void
{
if (config('system_database.mode', 'bootstrap') === 'bootstrap') {
return;
}
$db = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
if (! in_array($db->getDriverName(), ['mysql', 'mariadb'], true)) {
return;
}
$schema = Schema::connection($db->getName());
if (! $schema->hasTable(GoodsProfitAfsLookupSchema::TABLE)) {
return;
}
$changes = [];
foreach ([
'idx_afs_pay_time_id',
'idx_afs_shop_pay_time_id',
'idx_afs_send_time_id',
'idx_afs_shop_send_time_id',
] as $index) {
if ($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, $index)) {
$changes[] = 'DROP INDEX `' . $index . '`';
}
}
if ($changes !== []) {
$db->statement('ALTER TABLE `' . GoodsProfitAfsLookupSchema::TABLE . '` '
. implode(', ', $changes) . ', ALGORITHM=INPLACE, LOCK=NONE');
}
foreach ([
GoodsProfitAfsLookupSchema::PAY_DATE_COLUMN,
GoodsProfitAfsLookupSchema::SEND_DATE_COLUMN,
] as $column) {
if ($schema->hasColumn(GoodsProfitAfsLookupSchema::TABLE, $column)) {
$db->statement('ALTER TABLE `' . GoodsProfitAfsLookupSchema::TABLE . '` DROP COLUMN `'
. $column . '`, ALGORITHM=INPLACE, LOCK=NONE');
}
}
}
};

View File

@ -697,9 +697,9 @@ class GoodsProfitAfsRepository
// until the matching additive lookup has been installed.
if ($column === 'undefined7') {
$this->hasDateLookup ??= Schema::connection($this->connectionName())
->hasColumn(self::MAIN_TABLE, GoodsProfitAfsLookupSchema::DATE_COLUMN);
->hasColumn(self::MAIN_TABLE, GoodsProfitAfsLookupSchema::ORDER_DATE_COLUMN);
if ($this->hasDateLookup) {
return '`' . GoodsProfitAfsLookupSchema::DATE_COLUMN . '`';
return '`' . GoodsProfitAfsLookupSchema::ORDER_DATE_COLUMN . '`';
}
}

View File

@ -69,7 +69,7 @@ class OrderProfitRepository
private const ITEM_FIELDS = [
'internal_order_no', 'online_order_no', 'sub_order_no', 'after_sales_no',
'order_time', 'shop_name', 'product_code', 'style_code',
'pay_time', 'send_time', 'order_time', 'shop_name', 'product_code', 'style_code',
'shop_product_code', 'shop_style_code', 'product_name', 'color_spec',
'product_quantity', 'product_amount', 'product_cost',
'actual_shipped_quantity', 'actual_shipped_amount', 'actual_shipped_cost',
@ -133,10 +133,17 @@ class OrderProfitRepository
return ['rows' => [], 'has_more' => false];
}
}
$dateField = $this->dateField($filters);
if ($this->hasDateRange($filters)
&& $this->queryDateColumn($columns, $dateField) === null) {
return ['rows' => [], 'has_more' => false];
}
$page = max($page, 1);
$limit = min(max($limit, 1), 100);
$orderValues = self::batchValues((string) ($filters['order_no'] ?? ''));
$hasMore = null;
$selectedGroups = null;
if ($orderValues !== []) {
$ids = $this->orderNumberIds($orderValues, $columns);
if ($ids === []) {
@ -149,7 +156,20 @@ class OrderProfitRepository
$columns,
);
} else {
$filtered = $this->filtered($this->connection()->table(self::TABLE), $filters, $columns);
$groups = $this->pageOrderGroups($filtered, $columns, $dateField, $page, $limit);
if ($groups === []) {
return ['rows' => [], 'has_more' => false];
}
$hasMore = count($groups) > $limit;
$groups = array_slice($groups, 0, $limit);
$query = $this->filtered($this->connection()->table(self::TABLE), $filters, $columns);
$this->constrainToOrderGroups($query, $groups, $columns);
$selectedGroups = [];
foreach ($groups as $group) {
$selectedGroups[$group['shop_name'] . "\0" . $group['order_key']] = true;
}
}
$shop = $this->quote($columns['shop_name']);
$online = $this->quote($columns['online_order_no']);
@ -158,6 +178,9 @@ class OrderProfitRepository
$refundType = $this->quote($columns['refund_type']);
$categoryOrder = $this->quote($columns['classification_order']);
$orderKey = "COALESCE(NULLIF(TRIM({$online}), ''), TRIM({$internal}))";
$sortDate = $this->quote(
$this->queryDateColumn($columns, $dateField) ?? $columns['order_time'],
);
$query
->selectRaw("{$shop} AS shop_name")
@ -173,10 +196,18 @@ class OrderProfitRepository
}
$records = $query
->groupByRaw("{$shop}, {$orderKey}")
->orderByRaw("MAX({$orderTime}) DESC")
->orderByDesc($columns['internal_order_no'])
->forPage($page, $limit + 1)
->get();
->orderByRaw("MAX({$sortDate}) DESC")
->orderByRaw("MAX({$internal}) DESC");
if ($selectedGroups === null) {
$records = $records->forPage($page, $limit + 1)->get();
} else {
$records = $records->get()->filter(static function (object $record) use ($selectedGroups): bool {
return isset($selectedGroups[
trim((string) ($record->shop_name ?? '')) . "\0"
. trim((string) ($record->order_key ?? ''))
]);
})->values();
}
$rows = $records->map(static function (object $record): array {
$row = [
@ -200,7 +231,9 @@ class OrderProfitRepository
return $row;
})->all();
$hasMore = count($rows) > $limit;
if ($hasMore === null) {
$hasMore = count($rows) > $limit;
}
$rows = array_slice($rows, 0, $limit);
$this->hydrateItems($rows, $columns);
@ -284,13 +317,85 @@ class OrderProfitRepository
return array_keys($ids);
}
/**
* 先按日期/筛选条件只取当前页的订单键,再对少量订单做宽表汇总。
* 这样近 7 天未指定店铺时不会对整段宽表逐行汇总后再丢弃分页外数据。
*
* @param array<string,string> $columns
* @return list<array{shop_name:string,online_order_no:string,internal_order_no:string,order_key:string}>
*/
private function pageOrderGroups(
Builder $query,
array $columns,
string $dateField,
int $page,
int $pageSize,
): array {
$shop = $this->quote($columns['shop_name']);
$online = $this->quote($columns['online_order_no']);
$internal = $this->quote($columns['internal_order_no']);
$orderKey = "COALESCE(NULLIF(TRIM({$online}), ''), TRIM({$internal}))";
$sortDate = $this->quote(
$this->queryDateColumn($columns, $dateField) ?? $columns['order_time'],
);
return $query
->selectRaw("{$shop} AS shop_name")
->selectRaw("{$online} AS online_order_no")
->selectRaw("MAX({$internal}) AS internal_order_no")
->selectRaw("{$orderKey} AS order_key")
->groupByRaw("{$shop}, {$orderKey}")
->orderByRaw("MAX({$sortDate}) DESC")
->orderByRaw("MAX({$internal}) DESC")
->offset(($page - 1) * $pageSize)
->limit($pageSize + 1)
->get()
->map(static fn (object $row): array => [
'shop_name' => trim((string) ($row->shop_name ?? '')),
'online_order_no' => trim((string) ($row->online_order_no ?? '')),
'internal_order_no' => trim((string) ($row->internal_order_no ?? '')),
'order_key' => trim((string) ($row->order_key ?? '')),
])
->all();
}
/**
* @param list<array{shop_name:string,online_order_no:string,internal_order_no:string,order_key:string}> $groups
* @param array<string,string> $columns
*/
private function constrainToOrderGroups(Builder $query, array $groups, array $columns): void
{
$onlineOrders = [];
$internalOrders = [];
$shops = [];
foreach ($groups as $group) {
$shops[$group['shop_name']] = true;
if ($group['online_order_no'] !== '') {
$onlineOrders[$group['online_order_no']] = true;
} elseif ($group['internal_order_no'] !== '') {
$internalOrders[$group['internal_order_no']] = true;
}
}
$query->where(function (Builder $orders) use ($columns, $onlineOrders, $internalOrders): void {
if ($onlineOrders !== []) {
$orders->whereIn($columns['online_order_no'], array_keys($onlineOrders));
}
if ($internalOrders !== []) {
$method = $onlineOrders === [] ? 'whereIn' : 'orWhereIn';
$orders->{$method}($columns['internal_order_no'], array_keys($internalOrders));
}
});
$query->whereIn($columns['shop_name'], array_keys($shops));
}
/**
* @param array<string,mixed> $filters
* @param array<string,string> $columns
*/
private function filtered(Builder $query, array $filters, array $columns): Builder
{
$dateColumn = $this->queryDateColumn($columns);
$dateColumn = $this->queryDateColumn($columns, $this->dateField($filters));
$from = trim((string) ($filters['from'] ?? ''));
$to = trim((string) ($filters['to'] ?? ''));
@ -401,14 +506,19 @@ class OrderProfitRepository
return $parts === [] ? '0' : 'SUM(' . implode(' + ', $parts) . ')';
}
private function queryDateColumn(array $columns): ?string
private function queryDateColumn(array $columns, string $dateField): ?string
{
return Schema::connection($this->connectionName())->hasColumn(
self::TABLE,
GoodsProfitAfsLookupSchema::DATE_COLUMN,
)
? GoodsProfitAfsLookupSchema::DATE_COLUMN
: ($columns['order_time'] ?? null);
$definition = GoodsProfitAfsLookupSchema::DATE_COLUMNS[$dateField] ?? null;
if ($definition === null) {
return null;
}
if (($columns[$dateField] ?? null) === $definition['source']
&& Schema::connection($this->connectionName())->hasColumn(self::TABLE, $definition['lookup'])) {
return $definition['lookup'];
}
return $columns[$dateField] ?? null;
}
private function connection(): ConnectionInterface
@ -442,6 +552,23 @@ class OrderProfitRepository
return false;
}
/** @param array<string,mixed> $filters */
private function dateField(array $filters): string
{
$dateField = trim((string) ($filters['date_field'] ?? 'pay_time'));
return isset(GoodsProfitAfsLookupSchema::DATE_COLUMNS[$dateField])
? $dateField
: 'pay_time';
}
/** @param array<string,mixed> $filters */
private function hasDateRange(array $filters): bool
{
return trim((string) ($filters['from'] ?? '')) !== ''
|| trim((string) ($filters['to'] ?? '')) !== '';
}
/** @return list<string> */
private static function batchValues(string $value): array
{

View File

@ -10,11 +10,38 @@ class GoodsProfitAfsLookupSchema
{
public const TABLE = 'ops_jst_goods_profit_kpi_items_afs';
public const DATE_COLUMN = '_mirror_afs_order_time';
public const ORDER_DATE_COLUMN = '_mirror_afs_order_time';
public const PAY_DATE_COLUMN = '_mirror_afs_pay_time';
public const SEND_DATE_COLUMN = '_mirror_afs_send_time';
/** 页面时间字段 => 上游原列 / 只读查询用的派生日期列。 */
public const DATE_COLUMNS = [
'order_time' => [
'source' => 'undefined7',
'lookup' => self::ORDER_DATE_COLUMN,
],
'pay_time' => [
'source' => 'undefined5',
'lookup' => self::PAY_DATE_COLUMN,
],
'send_time' => [
'source' => 'undefined6',
'lookup' => self::SEND_DATE_COLUMN,
],
];
/** @deprecated Use ORDER_DATE_COLUMN. */
public const DATE_COLUMN = self::ORDER_DATE_COLUMN;
public const INDEXES = [
'idx_afs_order_time_id' => ['_mirror_afs_order_time', 'id'],
'idx_afs_shop_order_time_id' => ['undefined8', '_mirror_afs_order_time', 'id'],
'idx_afs_order_time_id' => [self::ORDER_DATE_COLUMN, 'id'],
'idx_afs_shop_order_time_id' => ['undefined8', self::ORDER_DATE_COLUMN, 'id'],
'idx_afs_pay_time_id' => [self::PAY_DATE_COLUMN, 'id'],
'idx_afs_shop_pay_time_id' => ['undefined8', self::PAY_DATE_COLUMN, 'id'],
'idx_afs_send_time_id' => [self::SEND_DATE_COLUMN, 'id'],
'idx_afs_shop_send_time_id' => ['undefined8', self::SEND_DATE_COLUMN, 'id'],
];
public function ensure(ConnectionInterface $db): void
@ -25,21 +52,29 @@ class GoodsProfitAfsLookupSchema
$schema = Schema::connection($db->getName());
if (! $schema->hasTable(self::TABLE)
|| ! $schema->hasColumns(self::TABLE, ['id', 'undefined7', 'undefined8'])) {
|| ! $schema->hasColumn(self::TABLE, 'id')) {
return;
}
$statements = [];
if (! $schema->hasColumn(self::TABLE, self::DATE_COLUMN)) {
$expression = $this->dateExpression();
// MariaDB requires the virtual column and its indexes in separate
foreach (self::DATE_COLUMNS as $definition) {
$source = $definition['source'];
$lookup = $definition['lookup'];
if (! $schema->hasColumn(self::TABLE, $source)
|| $schema->hasColumn(self::TABLE, $lookup)) {
continue;
}
// MariaDB requires virtual columns and their indexes in separate
// ALTER statements to allow concurrent writes with LOCK=NONE.
$statements[] = 'ADD COLUMN `' . self::DATE_COLUMN
. '` VARCHAR(19) CHARACTER SET ascii COLLATE ascii_bin GENERATED ALWAYS AS (' . $expression . ') VIRTUAL';
$statements[] = 'ADD COLUMN `' . $lookup
. '` VARCHAR(19) CHARACTER SET ascii COLLATE ascii_bin GENERATED ALWAYS AS ('
. $this->dateExpression($source) . ') VIRTUAL';
}
$indexes = [];
foreach (self::INDEXES as $name => $columns) {
if (! $schema->hasIndex(self::TABLE, $name)) {
if (! $schema->hasIndex(self::TABLE, $name)
&& $schema->hasColumns(self::TABLE, $columns)) {
$indexes[] = 'ADD INDEX `' . $name . '` (`' . implode('`, `', $columns) . '`)';
}
}
@ -59,9 +94,20 @@ class GoodsProfitAfsLookupSchema
}
}
public function dateExpression(): string
public function dateExpression(string $column = 'undefined7'): string
{
$raw = "REPLACE(TRIM(`undefined7`), '/', '-')";
$source = null;
foreach (self::DATE_COLUMNS as $definition) {
if ($definition['source'] === $column) {
$source = $column;
break;
}
}
if ($source === null) {
throw new \InvalidArgumentException('订单利润日期字段不安全。');
}
$raw = "REPLACE(TRIM(`{$source}`), '/', '-')";
$pattern = '^[0-9]{1,4}-(0?[1-9]|1[0-2])-(0?[1-9]|[12][0-9]|3[01])'
. '([[:space:]]+([01]?[0-9]|2[0-3])(:[0-5]?[0-9]){0,2}([.][0-9]+)?)?$';

View File

@ -9,6 +9,12 @@ use Throwable;
class OrderProfitPageData
{
private const DATE_FIELDS = [
'pay_time' => '支付时间',
'send_time' => '发货时间',
'order_time' => '订单时间',
];
public function __construct(private readonly OrderProfitRepository $repository)
{
}
@ -56,7 +62,7 @@ class OrderProfitPageData
];
}
/** @return array{from:string,to:string,order_no:string,shop_name:string,shop_names:array<int,string>,sku_id:string} */
/** @return array{date_field:string,from:string,to:string,order_no:string,shop_name:string,shop_names:array<int,string>,sku_id:string} */
private function filters(?array $input): array
{
$input ??= request()->query();
@ -79,6 +85,10 @@ class OrderProfitPageData
$reset = ! empty($input['reset']);
$from = $this->date($input['from'] ?? null);
$to = $this->date($input['to'] ?? null);
$dateField = trim((string) ($input['date_field'] ?? 'pay_time'));
if (! isset(self::DATE_FIELDS[$dateField])) {
$dateField = 'pay_time';
}
if (! $datesWereSubmitted && ! $reset && ! $hasOtherFilters) {
$from = now()->subDays(6)->toDateString();
@ -86,6 +96,7 @@ class OrderProfitPageData
}
return [
'date_field' => $dateField,
'from' => $from,
'to' => $to,
'order_no' => $orderNo,

View File

@ -3,6 +3,12 @@
$filters = $orderProfitFilters ?? [];
$status = $orderProfitStatus ?? [];
$hasFilters = (bool) ($orderProfitHasFilters ?? false);
$dateField = $filters['date_field'] ?? 'pay_time';
$dateFieldLabels = [
'pay_time' => '支付时间',
'send_time' => '发货时间',
'order_time' => '订单时间',
];
$columns = [
'shop_name' => '店铺名称',
'online_order_no' => '线上单号',
@ -115,11 +121,19 @@
<form method="GET" action="{{ request()->url() }}" class="upstream-data-toolbar" aria-label="订单利润筛选">
<div class="upstream-data-field">
<label for="order-profit-from">订单开始日期</label>
<label for="order-profit-date-field">时间类型</label>
<select id="order-profit-date-field" name="date_field">
@foreach ($dateFieldLabels as $value => $label)
<option value="{{ $value }}" @selected($dateField === $value)>{{ $label }}</option>
@endforeach
</select>
</div>
<div class="upstream-data-field">
<label for="order-profit-from">开始日期</label>
<input id="order-profit-from" name="from" type="date" value="{{ $filters['from'] ?? '' }}">
</div>
<div class="upstream-data-field">
<label for="order-profit-to">订单结束日期</label>
<label for="order-profit-to">结束日期</label>
<input id="order-profit-to" name="to" type="date" value="{{ $filters['to'] ?? '' }}">
</div>
<div class="upstream-data-field">
@ -159,7 +173,7 @@
<div class="order-profit-note">
<span class="material-icons-outlined" aria-hidden="true">info</span>
<span>商品成本、退款、退货类型和退平台补贴来自聚水潭订单利润共享镜像;包材费、快递费、账单费用属于聚水潭页面自动匹配结果,当前源表未包含匹配结果,暂显示“—”。</span>
<span>商品成本、退款、退货类型和退平台补贴来自聚水潭订单利润共享镜像;包材费、快递费、账单费用是聚胜算订单利润页面的自动匹配结果,当前商品利润明细镜像不含该结果,不能从现有字段推算,未取得页面结果时显示“—”。</span>
</div>
@if ($orderProfitError ?? null)

View File

@ -21,7 +21,8 @@ class OrderProfitPageTest extends FeatureTestCase
$repository->shouldReceive('paginate')->once()
->with(
Mockery::on(static fn (array $filters): bool => $filters['from'] === now()->subDays(6)->toDateString()
&& $filters['to'] === now()->toDateString()),
&& $filters['to'] === now()->toDateString()
&& $filters['date_field'] === 'pay_time'),
1,
25,
)
@ -61,7 +62,15 @@ class OrderProfitPageTest extends FeatureTestCase
'last_error_code' => null,
]);
$repository->shouldReceive('shopNames')->once()->andReturn(['唯品会MP-卓禾家具特卖旗舰店']);
$repository->shouldReceive('paginate')->once()->andReturn([
$repository->shouldReceive('paginate')->once()
->with(
Mockery::on(static fn (array $filters): bool =>
$filters['date_field'] === 'send_time'
&& $filters['order_no'] === '26092142570188'),
1,
25,
)
->andReturn([
'has_more' => false,
'rows' => [[
'shop_name' => '唯品会MP-卓禾家具特卖旗舰店',
@ -107,16 +116,18 @@ class OrderProfitPageTest extends FeatureTestCase
'section' => 'order-profit',
'page' => 'order-profit-list',
'order_no' => '26092142570188',
'date_field' => 'send_time',
]))
->assertOk()
->assertSeeText('订单利润')
->assertSeeTextInOrder(['时间类型', '支付时间', '发货时间', '订单时间', '开始日期', '结束日期'])
->assertSeeText('唯品会MP-卓禾家具特卖旗舰店')
->assertSeeText('26092142570188')
->assertSeeText('9470588')
->assertSeeText('75.00')
->assertSeeText('20.00')
->assertSeeText('仅退款')
->assertSeeText('包材费、快递费、账单费用属于聚水潭页面自动匹配结果')
->assertSeeText('包材费、快递费、账单费用是聚胜算订单利润页面的自动匹配结果')
->assertSeeText('743208893')
->assertSeeTextInOrder(['内部单号', '明细', '订单时间'])
->assertSee('data-finance-fixed="true"', false)

View File

@ -29,7 +29,10 @@ class GoodsProfitAfsLookupTest extends TestCase
$schema->dropIfExists(GoodsProfitAfsLookupSchema::TABLE);
$schema->create(GoodsProfitAfsLookupSchema::TABLE, function (Blueprint $table): void {
$table->char('_mirror_key', 64)->primary();
$table->integer('id'); $table->string('undefined7', 50)->nullable();
$table->integer('id');
$table->string('undefined5', 50)->nullable();
$table->string('undefined6', 50)->nullable();
$table->string('undefined7', 50)->nullable();
$table->string('undefined8', 50)->nullable(); $table->string('amount', 50)->nullable();
});
try {
@ -40,14 +43,20 @@ class GoodsProfitAfsLookupTest extends TestCase
foreach ($rawDates as $id => $raw) {
$db->table(GoodsProfitAfsLookupSchema::TABLE)->insert([
'_mirror_key' => hash('sha256', (string) $id), 'id' => $id, 'undefined7' => $raw,
'undefined5' => $raw, 'undefined6' => $raw,
'undefined8' => 'fixture shop', 'amount' => '-31.20',
]);
}
$before = $db->table(GoodsProfitAfsLookupSchema::TABLE)->orderBy('id')->get()->toJson();
$migration = require base_path('database/migrations/2026_10_22_150000_index_goods_profit_afs_order_time.php');
$paySendMigration = require base_path('database/migrations/2026_11_07_160000_index_goods_profit_afs_pay_send_time.php');
$migration->up();
$paySendMigration->up();
$migration->up();
$paySendMigration->up();
$this->assertTrue($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, 'idx_afs_order_time_id'));
$this->assertTrue($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, 'idx_afs_pay_time_id'));
$this->assertTrue($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, 'idx_afs_send_time_id'));
$rows = $db->select('SELECT id, `_mirror_afs_order_time` AS indexed_date, '
. "CAST(STR_TO_DATE(REPLACE(`undefined7`, '/', '-'), '%Y-%m-%d %H:%i:%s') AS CHAR(19)) AS original_date "
. 'FROM `' . GoodsProfitAfsLookupSchema::TABLE . '` ORDER BY id');
@ -59,6 +68,7 @@ class GoodsProfitAfsLookupTest extends TestCase
$db->table(GoodsProfitAfsLookupSchema::TABLE)->where('id', 0)->update(['undefined7' => 'bad timestamp']);
$this->assertNull($db->table(GoodsProfitAfsLookupSchema::TABLE)->where('id', 0)->value('_mirror_afs_order_time'));
$db->table(GoodsProfitAfsLookupSchema::TABLE)->where('id', 0)->update(['undefined7' => $rawDates[0]]);
$paySendMigration->down();
$migration->down();
$this->assertSame($before, $db->table(GoodsProfitAfsLookupSchema::TABLE)->orderBy('id')->get()->toJson());
@ -69,7 +79,13 @@ class GoodsProfitAfsLookupTest extends TestCase
'source_key' => 'fixture', 'schema' => 'rpt', 'object' => 'afs',
'target' => GoodsProfitAfsLookupSchema::TABLE, 'keys' => ['id'], 'watermark' => null,
]]]);
foreach (['id' => 'int', 'undefined7' => 'nvarchar', 'undefined8' => 'nvarchar'] as $column => $type) {
foreach ([
'id' => 'int',
'undefined5' => 'nvarchar',
'undefined6' => 'nvarchar',
'undefined7' => 'nvarchar',
'undefined8' => 'nvarchar',
] as $column => $type) {
DB::table('upstream_schema_columns')->insert([
'source_key' => 'fixture', 'schema_name' => 'rpt', 'object_name' => 'afs', 'object_type' => 'table',
'column_name' => $column, 'ordinal_position' => $column === 'id' ? 1 : (int) substr($column, 9),
@ -81,6 +97,8 @@ class GoodsProfitAfsLookupTest extends TestCase
app(BusinessMirrorSchemaManager::class)->ensure($dataset);
app(BusinessMirrorSchemaManager::class)->ensure($dataset);
$this->assertTrue($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, 'idx_afs_shop_order_time_id'));
$this->assertTrue($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, 'idx_afs_shop_pay_time_id'));
$this->assertTrue($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, 'idx_afs_shop_send_time_id'));
} finally {
$schema->dropIfExists(GoodsProfitAfsLookupSchema::TABLE);
DB::purge('afs_lookup_fixture');

View File

@ -29,7 +29,8 @@ class OrderProfitRepositoryTest extends TestCase
$table->integer('id')->primary();
$table->string('_mirror_key')->unique();
foreach ([
'undefined1', 'undefined2', 'undefined3', 'undefined4', 'undefined7',
'undefined1', 'undefined2', 'undefined3', 'undefined4', 'undefined5',
'undefined6', 'undefined7',
'undefined8', 'undefined10', 'undefined11', 'undefined12', 'undefined13',
'undefined18', 'undefined20', 'undefined21', 'undefined22', 'undefined43',
'undefined45', 'undefined46', 'undefined47', 'undefined48', 'undefined49',
@ -39,6 +40,13 @@ class OrderProfitRepositoryTest extends TestCase
] as $column) {
$table->string($column)->nullable();
}
foreach ([
GoodsProfitAfsLookupSchema::ORDER_DATE_COLUMN,
GoodsProfitAfsLookupSchema::PAY_DATE_COLUMN,
GoodsProfitAfsLookupSchema::SEND_DATE_COLUMN,
] as $column) {
$table->string($column, 19)->nullable();
}
});
$schema->create('ops_jst_goods_profit_kpi_items_afs_schemas', function (Blueprint $table): void {
$table->integer('id');
@ -64,6 +72,8 @@ class OrderProfitRepositoryTest extends TestCase
'undefined2' => '线上订单号',
'undefined3' => '子订单号',
'undefined4' => '售后单号',
'undefined5' => '支付时间',
'undefined6' => '发货时间',
'undefined7' => '订单时间',
'undefined8' => '店铺名称',
'undefined10' => '商品编码',
@ -104,7 +114,12 @@ class OrderProfitRepositoryTest extends TestCase
$base = [
'undefined2' => '26092142570188',
'undefined3' => '26092142570188_SKU-1E73E04C8008B3AF',
'undefined5' => '2026/9/21 9:41:00',
'undefined6' => '2026/9/22 10:00:00',
'undefined7' => '2026/9/21 9:40:36',
'_mirror_afs_order_time' => '2026-09-21 09:40:36',
'_mirror_afs_pay_time' => '2026-09-21 09:41:00',
'_mirror_afs_send_time' => '2026-09-22 10:00:00',
'undefined8' => '唯品会MP-卓禾家具特卖旗舰店',
'undefined10' => 'BJ018706N68V01',
'undefined11' => 'BJ0187',
@ -166,8 +181,9 @@ class OrderProfitRepositoryTest extends TestCase
{
$repository = app(OrderProfitRepository::class);
$result = $repository->paginate([
'from' => '2026-10-01',
'to' => '2026-10-31',
'date_field' => 'pay_time',
'from' => '2026-09-01',
'to' => '2026-09-30',
'order_no' => '26092142570188',
'shop_name' => '',
'sku_id' => '',
@ -184,6 +200,76 @@ class OrderProfitRepositoryTest extends TestCase
$this->assertEquals(0.0, $row['post_return_amount']);
$this->assertSame('仅退款', $row['refund_type']);
$this->assertCount(2, $row['items']);
$this->assertSame('2026/9/21 9:41:00', $row['items'][0]['pay_time']);
$this->assertSame('2026/9/22 10:00:00', $row['items'][0]['send_time']);
$this->assertSame(['唯品会MP-卓禾家具特卖旗舰店'], $repository->shopNames());
}
public function test_send_time_filter_uses_its_own_indexed_date_column(): void
{
$repository = app(OrderProfitRepository::class);
$inside = $repository->paginate([
'date_field' => 'send_time',
'from' => '2026-09-22',
'to' => '2026-09-22',
'order_no' => '',
'shop_name' => '',
'sku_id' => '',
], 1, 25);
$outside = $repository->paginate([
'date_field' => 'send_time',
'from' => '2026-09-23',
'to' => '2026-09-23',
'order_no' => '',
'shop_name' => '',
'sku_id' => '',
], 1, 25);
$this->assertCount(1, $inside['rows']);
$this->assertSame('26092142570188', $inside['rows'][0]['online_order_no']);
$this->assertSame([], $outside['rows']);
}
public function test_order_groups_are_paginated_before_wide_sums_are_calculated(): void
{
DB::connection('business_mirror')->table(GoodsProfitAfsLookupSchema::TABLE)->insert([
'id' => 3,
'_mirror_key' => 'row-3',
'undefined1' => '9999',
'undefined2' => 'ORDER-NEW',
'undefined5' => '2026/9/23 8:00:00',
'undefined6' => '2026/9/23 10:00:00',
'undefined7' => '2026/9/23 7:59:00',
'_mirror_afs_order_time' => '2026-09-23 07:59:00',
'_mirror_afs_pay_time' => '2026-09-23 08:00:00',
'_mirror_afs_send_time' => '2026-09-23 10:00:00',
'undefined8' => '唯品会MP-卓禾家具特卖旗舰店',
'undefined10' => 'SKU-NEW',
'undefined20' => '1',
'undefined21' => '10',
'undefined22' => '5',
'undefined52' => '0',
'undefined54' => '0',
'undefined56' => '',
'undefined59' => '否',
]);
$repository = app(OrderProfitRepository::class);
$filters = [
'date_field' => 'send_time',
'from' => '2026-09-22',
'to' => '2026-09-23',
'order_no' => '',
'shop_name' => '',
'sku_id' => '',
];
$first = $repository->paginate($filters, 1, 1);
$second = $repository->paginate($filters, 2, 1);
$this->assertSame('ORDER-NEW', $first['rows'][0]['online_order_no']);
$this->assertTrue($first['has_more']);
$this->assertSame('26092142570188', $second['rows'][0]['online_order_no']);
$this->assertFalse($second['has_more']);
}
}