CaiWuXiTong/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php

372 lines
19 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 Modules\EcommerceFinance\Repositories;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
/** 只读安装明细镜像;页面不得回退直连 JianSun。 */
class InstallationDetailMirrorRepository
{
public const DATASET = 'jiansun.dbo.fys_Ticketing_install_view';
public const ITEM_DATASET = 'jiansun.dbo.fys_Ticketing_install_item';
private bool $minimumShipTimeResolved = false;
private ?string $resolvedMinimumShipTime = null;
public function __construct(
private readonly GenericBusinessMirrorRepository $mirror,
private readonly BusinessMirrorDatasetRegistry $datasets,
)
{
}
/** @return array{data:list<array<string,mixed>>,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<array<string,mixed>>,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];
}
/**
* 按客服审核时间直接从安装明细子表汇总,并用 SourceGuid 取得最新主表的安装公司。
* 不能先按主表 kfshTime 过滤,否则主表日期更新滞后时会漏掉已同步的明细。
*
* @return array{ready:bool,data:list<array{installation_company:string,settled_amount:float,received_amount:float}>,total_settled:float,total_received:float}
*/
public function companySummary(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, 'data' => [], 'total_settled' => 0.0, 'total_received' => 0.0];
}
$from = trim((string) ($filters['customer_review_from'] ?? ''));
$to = trim((string) ($filters['customer_review_to'] ?? ''));
if ($from === '' && $to === '') {
return ['ready' => true, 'data' => [], 'total_settled' => 0.0, 'total_received' => 0.0];
}
$query = $connection->table($items->targetTable() . ' AS i')
->whereNotNull('i.kfshTime');
$minimumShipTime = $this->minimumShipTime();
if ($minimumShipTime !== null) {
$mainTable = $dataset->targetTable();
$query->whereExists(function ($sub) use ($mainTable, $minimumShipTime): void {
$sub->selectRaw('1')
->from($mainTable . ' AS retained_main')
->whereColumn('retained_main.SourceGuid', 'i.guid')
->whereNotNull('retained_main.ShipTIme')
->where('retained_main.ShipTIme', '>=', $minimumShipTime);
});
}
if ($from !== '') {
$query->where('i.kfshTime', '>=', $from);
}
if ($to !== '') {
$query->where('i.kfshTime', '<', \Illuminate\Support\Carbon::parse($to)->addDay()->toDateString());
}
$itemTotals = $query
->selectRaw('i.guid AS guid')
->selectRaw('COALESCE(SUM(CAST(i.settleItemAmount AS DECIMAL(18,2))), 0) AS settled_amount')
->selectRaw('COALESCE(SUM(CAST(i.acceptItemAmount AS DECIMAL(18,2))), 0) AS received_amount')
->groupBy('i.guid')
->get();
$companyByGuid = [];
$guids = $itemTotals->pluck('guid')
->filter(static fn (mixed $guid): bool => trim((string) $guid) !== '')
->unique()
->values()
->all();
foreach (array_chunk($guids, 1000) as $guidChunk) {
$companyQuery = $connection->table($dataset->targetTable())
->whereIn('SourceGuid', $guidChunk)
->select(['Id', 'SourceGuid', 'installCompanyName'])
->orderBy('Id');
$this->applyShipmentCutoff($companyQuery);
$companyQuery
->get()
->each(static function (object $row) use (&$companyByGuid): void {
$companyByGuid[(string) $row->SourceGuid] = trim((string) ($row->installCompanyName ?? ''));
});
}
$companyTotals = [];
foreach ($itemTotals as $item) {
$company = $companyByGuid[(string) $item->guid] ?? '';
$company = $company !== '' ? $company : '未填写安装公司';
$companyTotals[$company] ??= ['settled_amount' => 0.0, 'received_amount' => 0.0];
$companyTotals[$company]['settled_amount'] += (float) $item->settled_amount;
$companyTotals[$company]['received_amount'] += (float) $item->received_amount;
}
$rows = [];
foreach ($companyTotals as $company => $amounts) {
$rows[] = [
'installation_company' => $company,
'settled_amount' => round($amounts['settled_amount'], 2),
'received_amount' => round($amounts['received_amount'], 2),
];
}
usort($rows, static fn (array $left, array $right): int =>
[$right['settled_amount'], $left['installation_company']]
<=> [$left['settled_amount'], $right['installation_company']]
);
return [
'ready' => true,
'data' => $rows,
'total_settled' => round(array_sum(array_column($rows, 'settled_amount')), 2),
'total_received' => round(array_sum(array_column($rows, 'received_amount')), 2),
];
}
public static function requestFilters(): array
{
$rules = [];
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','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';
return validator(request()->only(array_keys($rules)), $rules)->validate();
}
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','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<string,mixed> $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<string,mixed> $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 ? '已安排' : '未安排';
}
}