feat: 订单利润支持支付发货时间筛选 (#983)
订单利润支持支付时间、发货时间、订单时间筛选;先分页订单键再汇总当前页,补充日期索引与性能优化。
This commit is contained in:
parent
46964454fd
commit
31de7272e3
@ -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 . '`';
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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 . '`';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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]+)?)?$';
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user