>,total:int,ready:bool} */ public function paginate(int $page, int $perPage, array $filters = []): array { $connection = $this->mirror->connection(); $dataset = $this->datasets->get(self::DATASET); $items = $this->datasets->get(self::ITEM_DATASET); if (! $this->ready($dataset, $items, $connection->getName())) { return ['data' => [], 'total' => 0, 'ready' => false]; } $perPage = min(max($perPage, 1), 100); // 主视图按商品行展开,同一安装单会出现多行;主表只保留每个 // SourceGuid 的最新 Id,商品行通过 details() 单独展开。 $latest = $connection->table($dataset->targetTable()) ->selectRaw('MAX(Id) AS id') ->groupBy('SourceGuid'); $this->applyShipmentCutoff($latest); $query = $connection->table($dataset->targetTable() . ' AS v') ->joinSub($latest, 'latest', static fn ($join) => $join->on('latest.id', '=', 'v.Id')) ->select('v.*'); $this->applyFilters($query, $filters, $items->targetTable()); $total = (clone $query)->count(); $rows = $query->orderByDesc('v.Id')->forPage(max($page, 1), $perPage)->get(); return ['data' => $rows->map(fn (object $row): array => $this->map((array) $row))->all(), 'total' => $total, 'ready' => true]; } /** @return array{data:list>,ready:bool} */ public function details(string $guid): array { $dataset = $this->datasets->get(self::DATASET); $items = $this->datasets->get(self::ITEM_DATASET); $connection = $this->mirror->connection(); if (! $this->ready($dataset, $items, $connection->getName())) { return ['data' => [], 'ready' => false]; } $mainExists = $connection->table($dataset->targetTable()) ->where('SourceGuid', $guid); $this->applyShipmentCutoff($mainExists); $mainExists = $mainExists->exists(); if (! $mainExists) { return ['data' => [], 'ready' => true]; } $transactionNo = $connection->table($dataset->targetTable()) ->where('SourceGuid', $guid) ->orderByDesc('Id')->value('OrderNum'); $rows = $connection->table($items->targetTable()) ->where('guid', $guid) ->orderBy('Id') ->limit(101) ->get(); return ['data' => $rows->take(100)->map(fn (object $row): array => $this->mapItem((array) $row, $transactionNo))->all(), 'ready' => true, 'has_more' => $rows->count() > 100]; } private function ready($dataset, $items, string $connection): bool { return $this->mirror->ready($dataset) && $this->mirror->ready($items) && Schema::connection($connection)->hasTable($dataset->targetTable()) && Schema::connection($connection)->hasTable($items->targetTable()); } private function applyShipmentCutoff($query): void { $minimumShipTime = $this->minimumShipTime(); if ($minimumShipTime !== null) { $query->whereNotNull('ShipTIme')->where('ShipTIme', '>=', $minimumShipTime); } } private function minimumShipTime(): ?string { if (! $this->minimumShipTimeResolved) { $this->resolvedMinimumShipTime = $this->datasets->minimumDatetime(self::DATASET)['value'] ?? null; $this->minimumShipTimeResolved = true; } return $this->resolvedMinimumShipTime; } /** @return array{rows:int,requested:float,settled:float,paid:float} */ public function summary(array $filters = []): array { $dataset = $this->datasets->get(self::DATASET); $connection = $this->mirror->connection(); if (! $this->mirror->ready($dataset) || ! Schema::connection($connection->getName())->hasTable($dataset->targetTable())) { return ['rows' => 0, 'requested' => 0.0, 'settled' => 0.0, 'paid' => 0.0]; } $latest = $connection->table($dataset->targetTable()) ->selectRaw('MAX(Id) AS id') ->groupBy('SourceGuid'); $this->applyShipmentCutoff($latest); $query = $connection->table($dataset->targetTable() . ' AS v') ->joinSub($latest, 'latest', static fn ($join) => $join->on('latest.id', '=', 'v.Id')); $this->applyFilters($query, $filters, $this->datasets->get(self::ITEM_DATASET)->targetTable()); $row = $query ->selectRaw('COUNT(*) AS row_count, COALESCE(SUM(v.installcost),0) AS requested, COALESCE(SUM(v.settleAmount),0) AS settled, COALESCE(SUM(v.acceptAmount),0) AS paid') ->first(); return ['rows' => (int) $row->row_count, 'requested' => (float) $row->requested, 'settled' => (float) $row->settled, 'paid' => (float) $row->paid]; } /** * 按客服审核时间汇总安装费;金额取安装明细子表的结算明细金额。 * * @return array{ready:bool,total:float,rows:list} */ public function feeDetails(array $filters = []): array { $dataset = $this->datasets->get(self::DATASET); $items = $this->datasets->get(self::ITEM_DATASET); $connection = $this->mirror->connection(); if (! $this->ready($dataset, $items, $connection->getName())) { return [ 'ready' => false, 'total' => 0.0, 'rows' => [], 'previous_total' => null, 'previous_from' => null, 'previous_to' => null, 'change_rate' => null, ]; } $from = trim((string) ($filters['customer_review_from'] ?? '')); $to = trim((string) ($filters['customer_review_to'] ?? '')); if ($from === '' || $to === '') { return [ 'ready' => true, 'total' => 0.0, 'rows' => [], 'previous_total' => null, 'previous_from' => null, 'previous_to' => null, 'change_rate' => null, ]; } $query = $this->feeDetailsQuery($connection, $dataset, $items, $from, $to); $platformSql = "CASE" . " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '%阿里巴巴%'" . " OR COALESCE(TRIM(install_main.shop_name), '') LIKE '%天猫%'" . " OR COALESCE(TRIM(install_main.shop_name), '') LIKE '%新佰广%'" . " OR COALESCE(TRIM(install_main.shop_name), '') LIKE '%淘工厂%'" . " OR COALESCE(TRIM(install_main.shop_name), '') LIKE '%淘宝%' THEN '淘系'" . " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '抖音-%' THEN '抖音'" . " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '拼多多-%' THEN '拼多多'" . " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '唯品会MP-%' THEN '唯品会'" . " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '得物-%' THEN '得物'" . " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '京东-%' THEN '京东'" . " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '京喜-%' THEN '京喜'" . " ELSE '其他' END"; $rows = $query ->selectRaw($platformSql . ' AS platform') ->selectRaw("COALESCE(NULLIF(TRIM(install_main.shop_name), ''), '未填写店铺') AS store_name") ->selectRaw("COALESCE(NULLIF(TRIM(install_main.installCompanyName), ''), '未填写安装公司') AS installation_company") ->selectRaw("CASE WHEN install_main.costWay IS NULL OR TRIM(install_main.costWay) = '' THEN '包安装' ELSE '不包安装' END AS payment_channel") ->selectRaw('ROUND(SUM(CAST(i.settleItemAmount AS DECIMAL(18,2))), 2) AS amount') ->groupByRaw( $platformSql . ',' . " COALESCE(NULLIF(TRIM(install_main.shop_name), ''), '未填写店铺')," . " COALESCE(NULLIF(TRIM(install_main.installCompanyName), ''), '未填写安装公司')," . " CASE WHEN install_main.costWay IS NULL OR TRIM(install_main.costWay) = '' THEN '包安装' ELSE '不包安装' END" ) ->havingRaw('ABS(SUM(CAST(i.settleItemAmount AS DECIMAL(18,2)))) > 0.004') ->get() ->map(static fn (object $row): array => [ 'platform' => trim((string) ($row->platform ?? '其他')), 'store_name' => trim((string) ($row->store_name ?? '')), 'installation_company' => trim((string) ($row->installation_company ?? '')), 'payment_channel' => (string) ($row->payment_channel ?? '包安装'), 'amount' => round((float) ($row->amount ?? 0), 2), ]) ->sort(static fn (array $left, array $right): int => [abs($right['amount']), $left['platform'], $left['store_name'], $left['installation_company']] <=> [abs($left['amount']), $right['platform'], $right['store_name'], $right['installation_company']] ) ->values() ->all(); $total = round(array_sum(array_column($rows, 'amount')), 2); $previousRange = $this->previousMonthRange($from, $to); $previousTotal = null; $changeRate = null; if ($previousRange !== null) { $previousQuery = $this->feeDetailsQuery( $connection, $dataset, $items, $previousRange['from'], $previousRange['to'], ); $previousTotal = round((float) ($previousQuery ->selectRaw('COALESCE(SUM(CAST(i.settleItemAmount AS DECIMAL(18,2))), 0) AS total') ->first()->total ?? 0), 2); if ($previousTotal > 0) { $changeRate = round((($total - $previousTotal) / $previousTotal) * 100, 1); } } return [ 'ready' => true, 'total' => $total, 'rows' => $rows, 'previous_total' => $previousTotal, 'previous_from' => $previousRange['from'] ?? null, 'previous_to' => $previousRange['to'] ?? null, 'change_rate' => $changeRate, ]; } private function feeDetailsQuery( $connection, $dataset, $items, string $from, string $to, ) { $latest = $connection->table($dataset->targetTable()) ->selectRaw('MAX(Id) AS id') ->groupBy('SourceGuid'); $main = $connection->table($dataset->targetTable() . ' AS v') ->joinSub($latest, 'latest_install', static fn ($join) => $join->on('latest_install.id', '=', 'v.Id')) ->select(['v.SourceGuid', 'v.shop_name', 'v.installCompanyName', 'v.costWay']); $this->applyShipmentCutoff($main); return $connection->table($items->targetTable() . ' AS i') ->joinSub($main, 'install_main', static fn ($join) => $join->on('install_main.SourceGuid', '=', 'i.guid')) ->whereNotNull('i.kfshTime') ->where('i.kfshTime', '>=', $from) ->where('i.kfshTime', '<', \Illuminate\Support\Carbon::parse($to)->addDay()->toDateString()); } /** @return array{from:string,to:string}|null */ private function previousMonthRange(string $from, string $to): ?array { $start = \Illuminate\Support\Carbon::parse($from)->startOfDay(); $end = \Illuminate\Support\Carbon::parse($to)->startOfDay(); if ($end->lt($start)) { return null; } if ($start->day === 1 && $end->isSameDay($end->copy()->endOfMonth())) { $previousStart = $start->copy()->subMonthNoOverflow()->startOfMonth(); $previousEnd = $previousStart->copy()->endOfMonth(); } else { $previousStart = $start->copy()->subMonthNoOverflow(); $previousEnd = $end->copy()->subMonthNoOverflow(); } return [ 'from' => $previousStart->toDateString(), 'to' => $previousEnd->toDateString(), ]; } public static function requestFilters(): array { $rules = []; $rules['time_scope'] = ['nullable', 'in:created,customer_review,finance_review']; $rules['date_from'] = ['nullable', 'date_format:Y-m-d']; $rules['date_to'] = ['nullable', 'date_format:Y-m-d']; if (request()->filled('date_from')) $rules['date_to'][] = 'after_or_equal:date_from'; foreach (['created', 'customer_review', 'finance_review'] as $prefix) { $rules[$prefix . '_from'] = ['nullable', 'date_format:Y-m-d']; $rules[$prefix . '_to'] = ['nullable', 'date_format:Y-m-d']; if (request()->filled($prefix . '_from')) $rules[$prefix . '_to'][] = 'after_or_equal:' . $prefix . '_from'; } foreach (['push_status','order_no','customer_review','finance_review','quote_status','arranged_status','fee_payer','completed_status','shop_name','shop_code','delete_status','installation_company','installation_no','order_type','product_id'] as $key) { $rules[$key] = ['nullable', 'string', 'max:191']; } foreach (['customer_review', 'finance_review', 'arranged_status', 'completed_status', 'delete_status'] as $key) $rules[$key][] = 'in:0,1'; $filters = validator(request()->only(array_keys($rules)), $rules)->validate(); if (! array_key_exists('time_scope', $filters) && ! array_key_exists('date_from', $filters) && ! array_key_exists('date_to', $filters)) { return $filters; } $scope = (string) ($filters['time_scope'] ?? 'customer_review'); $prefix = match ($scope) { 'created' => 'created', 'finance_review' => 'finance_review', default => 'customer_review', }; foreach (['created', 'customer_review', 'finance_review'] as $legacyPrefix) { unset($filters[$legacyPrefix . '_from'], $filters[$legacyPrefix . '_to']); } $filters[$prefix . '_from'] = $filters['date_from'] ?? null; $filters[$prefix . '_to'] = $filters['date_to'] ?? null; return $filters; } private function applyFilters($query, array $filters, string $itemTable): void { foreach (['created'=>'SubmitTime', 'customer_review'=>'kfshTime', 'finance_review'=>'cwshTime'] as $prefix=>$column) { if (!empty($filters[$prefix.'_from'])) $query->where('v.'.$column, '>=', $filters[$prefix.'_from']); if (!empty($filters[$prefix.'_to'])) $query->where('v.'.$column, '<', \Illuminate\Support\Carbon::parse($filters[$prefix.'_to'])->addDay()->toDateString()); } foreach (['push_status'=>'apistatus','order_no'=>'OrderNum','quote_status'=>'BidStatus','fee_payer'=>'costtaker','shop_name'=>'shop_name','shop_code'=>'shopcode','installation_company'=>'installCompanyName','installation_no'=>'ordernum_id','order_type'=>'TypeDto'] as $key=>$column) { if (isset($filters[$key]) && $filters[$key] !== '') $query->where('v.'.$column, $filters[$key]); } foreach (['customer_review'=>'kfsh','finance_review'=>'cwsh','completed_status'=>'installComplete','delete_status'=>'Delete'] as $key=>$column) { if (!isset($filters[$key]) || $filters[$key] === '') continue; if ((string) $filters[$key] === '1') { $query->where('v.'.$column, 1); } else { $query->where(function ($sub) use ($column): void { $sub->whereNull('v.'.$column)->orWhere('v.'.$column, 0); }); } } if (isset($filters['arranged_status']) && $filters['arranged_status'] !== '') { if ($filters['arranged_status'] === '1') { $query->where('v.StatusId', '>', 0); } else { $query->where(function ($sub): void { $sub->whereNull('v.StatusId')->orWhere('v.StatusId', '<=', 0); }); } } if (!empty($filters['product_id'])) { $query->whereExists(function ($sub) use ($itemTable, $filters) { $sub->selectRaw('1')->from($itemTable.' AS filter_item') ->whereColumn('filter_item.guid', 'v.SourceGuid')->where('filter_item.shopproid', $filters['product_id']); }); } } /** @param array $row */ private function map(array $row): array { $value = static function (array $row, string ...$names): mixed { foreach ($names as $name) { foreach ($row as $key => $value) if (strcasecmp((string) $key, $name) === 0) return $value; } return null; }; $mapped = [ 'id' => $value($row, 'detail_id', 'Id'), 'installation_no' => $value($row, 'ordernum_id', 'OrderNum'), 'delete_status' => $value($row, 'Delete'), 'quote_status' => $value($row, 'BidStatus'), 'quote_deadline' => $value($row, 'BidEndTime'), 'installation_company' => $value($row, 'installCompanyName'), 'submitted_at' => $value($row, 'SubmitTime'), 'shipped_at' => $value($row, 'ShipTIme'), 'buyer_nickname' => $value($row, 'BuyersNick'), 'store_name' => $value($row, 'shop_name', 'shopname'), 'installation_type' => $value($row, 'TypeDto'), 'pre_push_verification' => $value($row, 'checkMsg'), 'push_status' => $value($row, 'apistatus'), 'push_result' => $value($row, 'apiresult'), 'arranged_status' => $this->arranged($value($row, 'StatusId')), 'order_no' => $value($row, 'OrderNum'), 'item_quantity' => $value($row, 'Num'), 'buyer_name' => $value($row, 'BuyersName'), 'buyer_phone' => $value($row, 'Mobile'), 'notes' => $value($row, 'Nots'), 'province' => $value($row, 'Province'), 'city' => $value($row, 'City'), 'district' => $value($row, 'Area'), 'address' => $value($row, 'Address'), 'requested_amount' => $value($row, 'installcost'), 'settled_amount' => $value($row, 'settleAmount'), 'image_urls' => $value($row, 'Picture'), 'completed_status' => $this->yesNo($value($row, 'installComplete')), 'customer_service_review' => $this->review($value($row, 'kfsh')), 'customer_service_reviewed_at' => $value($row, 'kfshTime'), 'finance_review' => $this->review($value($row, 'cwsh')), 'finance_reviewed_at' => $value($row, 'cwshTime'), 'actual_install_quantity' => $value($row, 'installNum'), 'actual_install_fee' => $value($row, 'install_Cost'), 'actual_install_product_name' => $value($row, 'installGoodsName'), 'installer_phone' => $value($row, 'installTel'), 'installation_company_notes' => $value($row, 'installNote'), 'installation_fee_payer' => $value($row, 'costtaker'), 'payment_channel' => $value($row, 'costWay'), 'payment_transaction_no' => $value($row, 'actporderNum'), 'payment_voucher' => $value($row, 'costWayVoucher'), 'payment_amount' => $value($row, 'acceptAmount'), 'creator' => $value($row, 'Operation'), 'related_guid' => $value($row, 'detail_guid', 'SourceGuid'), 'product_id' => $value($row, 'shopproid'), 'auxiliary_spu' => $value($row, 'Spu'), 'product_store_code' => $value($row, 'shopskucode'), 'item_no' => $value($row, 'proCode'), 'barcode' => $value($row, 'barCode'), 'product_name' => $value($row, 'skuName'), 'supplier_name' => $value($row, 'scmname'), 'detail_requested_amount' => $value($row, 'applyItemAmount'), 'detail_requested_quantity' => $value($row, 'installcount'), 'detail_settled_amount' => $value($row, 'settleItemAmount'), 'detail_received_amount' => $value($row, 'acceptItemAmount'), 'change_reason' => $value($row, 'remark'), 'source_created_at' => $value($row, 'item_created_at'), 'source_modified_at' => $value($row, 'item_modified_at'), ]; // 页面按模板的完整 61 列渲染;视图暂未提供的字段必须显式为 null, // 不能让 Blade 读取未定义属性。 return array_replace(array_fill_keys(array_keys(\App\Models\Business\InstallationDetail::columnLabels()), null), $mapped); } /** @param array $row */ private function mapItem(array $row, mixed $transactionNo = null): array { $value = static function (array $row, string ...$names): mixed { foreach ($names as $name) { foreach ($row as $key => $value) { if (strcasecmp((string) $key, $name) === 0) { return $value; } } } return null; }; return [ 'transaction_no' => $value($row, 'Sourceno', 'source_no', 'orderNum', 'ordernum') ?? $transactionNo, 'product_id' => $value($row, 'shopproid'), 'id_owner' => $value($row, 'id_owner', 'idOwner'), 'department_owner' => $value($row, 'department_owner', 'departmentOwner'), 'auxiliary_spu' => $value($row, 'Spu'), 'product_store_code' => $value($row, 'shopskucode'), 'item_no' => $value($row, 'proCode'), 'barcode' => $value($row, 'barCode'), 'product_name' => $value($row, 'skuName'), 'supplier_name' => $value($row, 'scmname'), 'detail_requested_amount' => $value($row, 'applyItemAmount'), 'detail_requested_quantity' => $value($row, 'installcount'), 'detail_settled_amount' => $value($row, 'settleItemAmount'), 'detail_received_amount' => $value($row, 'acceptItemAmount'), 'change_reason' => $value($row, 'remark'), 'source_created_at' => $value($row, 'createTime'), 'source_modified_at' => $value($row, 'modifyTime'), ]; } private function yesNo(mixed $value): string { return (int) $value === 1 ? '是' : '否'; } private function review(mixed $value): string { return (int) $value === 1 ? '已审核' : '未审核'; } private function arranged(mixed $value): string { return (int) $value > 0 ? '已安排' : '未安排'; } }