CaiWuXiTong/modules/EcommerceFinance/Repositories/DingOnlineDaifaPriceMirrorRepository.php

234 lines
9.2 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\Database\ConnectionInterface;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Services\DingPayableWorkflowStatus;
use RuntimeException;
/**
* 线上代发业务报销流程的共享镜像读取与按商品ID关联。
*
* 数据由 DingPayableWorkflowMirrorSynchronizer(ding_online_daifa_price 配置)
* 从钉钉 OA 审批同步到 ding_online_daifa_price_mirror 宽表。本仓库只在镜像库
* 读取,并依据 商品ID(product_id)关联共享业务镜像的商品目录,页面不直连上游。
*/
class DingOnlineDaifaPriceMirrorRepository
{
public const DATASET_KEY = 'operations_replica.rpt.ding_online_daifa_price';
public const TARGET_TABLE = 'ding_online_daifa_price_mirror';
private readonly ConnectionInterface $connection;
public function __construct(GenericBusinessMirrorRepository $mirror)
{
$this->connection = $mirror->connection();
}
public function ready(): bool
{
$schema = Schema::connection($this->connection->getName());
if (! $schema->hasTable(self::TARGET_TABLE) || ! $schema->hasTable('business_mirror_table_states')) {
return false;
}
$state = $this->connection->table('business_mirror_table_states')
->where('dataset_key', self::DATASET_KEY)->first();
return $state !== null && $state->initial_completed_at !== null;
}
/** @return array{data:list<array<string,mixed>>,total:int,status:string,last_synced_at:?string,summary:array<string,mixed>} */
public function paginate(array $filters, int $page, int $perPage): array
{
if (! $this->ready()) {
throw new RuntimeException('business_mirror_dataset_not_ready');
}
$page = max($page, 1);
$perPage = min(max($perPage, 1), 100);
$state = $this->connection->table('business_mirror_table_states')->where('dataset_key', self::DATASET_KEY)->first();
$rows = $this->filteredQuery($filters)
->orderByDesc('workflow_created_at')
->orderByDesc('process_instance_id')
->orderByDesc('detail_index')
->get();
$statusFilter = strtoupper(trim((string) ($filters['status'] ?? '')));
$matched = [];
foreach ($rows as $row) {
$workflow = $this->decode((string) $row->workflow_payload, []);
if (! $this->matchesStatus($workflow, $statusFilter)) {
continue;
}
$matched[] = $row;
}
$total = count($matched);
$pageRows = array_slice($matched, ($page - 1) * $perPage, $perPage);
$data = [];
foreach ($pageRows as $row) {
$workflow = $this->decode((string) $row->workflow_payload, []);
$data[] = $this->presentRow($row, $workflow);
}
return [
'data' => $data,
'total' => $total,
'status' => (string) ($state->status ?? 'unknown'),
'last_synced_at' => $state->last_succeeded_at ? (string) $state->last_succeeded_at : null,
'summary' => $this->summary($matched),
];
}
/** @return \Generator<int,array<string,mixed>> */
public function exportRows(array $filters): \Generator
{
if (! $this->ready()) {
throw new RuntimeException('business_mirror_dataset_not_ready');
}
$statusFilter = strtoupper(trim((string) ($filters['status'] ?? '')));
$rows = $this->filteredQuery($filters)
->orderByDesc('workflow_created_at')
->orderByDesc('process_instance_id')
->orderByDesc('detail_index')
->cursor();
foreach ($rows as $row) {
$workflow = $this->decode((string) $row->workflow_payload, []);
if (! $this->matchesStatus($workflow, $statusFilter)) {
continue;
}
yield $this->presentRow($row, $workflow);
}
}
private function filteredQuery(array $filters): Builder
{
$query = $this->connection->table(self::TARGET_TABLE);
if (($filters['product_id'] ?? '') !== '') {
$query->where('product_id', (string) $filters['product_id']);
}
if (($filters['shop_name'] ?? '') !== '') {
$query->where('shop_name', (string) $filters['shop_name']);
}
if (($filters['supplier_name'] ?? '') !== '') {
$query->whereRaw("supplier_name LIKE ? ESCAPE '!'", [$this->contains((string) $filters['supplier_name'])]);
}
if (($filters['material_no'] ?? '') !== '') {
$query->where('material_no', (string) $filters['material_no']);
}
if (($filters['keyword'] ?? '') !== '') {
$keyword = (string) $filters['keyword'];
$query->where(function ($q) use ($keyword): void {
$q->where('process_instance_id', 'like', '%' . $keyword . '%')
->orWhere('daifa_trade_no', 'like', '%' . $keyword . '%')
->orWhere('shop_trade_no', 'like', '%' . $keyword . '%');
});
}
if (($filters['created_from'] ?? '') !== '') {
$query->where('workflow_created_at', '>=', (string) $filters['created_from']);
}
if (($filters['created_to'] ?? '') !== '') {
$query->where('workflow_created_at', '<', \Illuminate\Support\Carbon::parse((string) $filters['created_to'])->addDay()->startOfDay()->format('Y-m-d H:i:s'));
}
return $query;
}
private function matchesStatus(array $workflow, string $statusFilter): bool
{
return $statusFilter === '' || DingPayableWorkflowStatus::key($workflow) === $statusFilter;
}
/** @return array<string,mixed> */
private function presentRow(object $row, array $workflow): array
{
$productId = (string) ($row->product_id ?? '');
// 商品名称已在同步阶段按 商品ID 关联商品目录并写入镜像宽表 product_name 列,
// 页面直接读取,避免对 2500 万行的 ops_jst_sku_map 做临时 JOIN 导致超时。
$resolvedName = $this->cleanName($row->product_name ?? null);
$product = $productId !== '' && $resolvedName !== null
? ['matched' => true, 'name' => $resolvedName, 'sku_id' => null, 'i_id' => null]
: ['matched' => false, 'name' => null, 'sku_id' => null, 'i_id' => null];
return [
'process_instance_id' => (string) $row->process_instance_id,
'detail_index' => (int) $row->detail_index,
'product_id' => $productId,
'daifa_trade_no' => (string) ($row->daifa_trade_no ?? ''),
'shop_trade_no' => (string) ($row->shop_trade_no ?? ''),
'shop_name' => (string) ($row->shop_name ?? ''),
'payee_name' => (string) ($row->payee_name ?? ''),
'supplier_name' => (string) ($row->supplier_name ?? ''),
'material_no' => (string) ($row->material_no ?? ''),
'ship_qty' => $row->ship_qty === null ? null : (int) $row->ship_qty,
'paid_amount' => $row->paid_amount === null ? null : (float) $row->paid_amount,
'daifa_amount' => $row->daifa_amount === null ? null : (float) $row->daifa_amount,
'price_diff' => $row->price_diff === null ? null : (float) $row->price_diff,
'workflow' => $workflow,
'scheme' => $this->decode((string) ($row->scheme_payload ?? '[]'), []),
'tasks' => $this->decode((string) ($row->task_payload ?? '[]'), []),
'status_key' => DingPayableWorkflowStatus::key($workflow),
'status_label' => DingPayableWorkflowStatus::label($workflow),
'matched_product' => $product,
'workflow_created_at' => (string) ($row->workflow_created_at ?? ''),
];
}
private function contains(string $value): string
{
return '%' . str_replace(['!', '%', '_'], ['!!', '!%', '!_'], trim($value)) . '%';
}
/** @param list<object> $rows */
private function summary(array $rows): array
{
$daifa = 0.0;
$paid = 0.0;
$diff = 0.0;
$matched = 0;
foreach ($rows as $row) {
$daifa += (float) ($row->daifa_amount ?? 0);
$paid += (float) ($row->paid_amount ?? 0);
$diff += (float) ($row->price_diff ?? 0);
if (! empty($row->product_id) && $this->cleanName($row->product_name ?? null) !== null) {
$matched++;
}
}
return [
'count' => count($rows),
'matched_count' => $matched,
'daifa_amount' => round($daifa, 2),
'paid_amount' => round($paid, 2),
'price_diff' => round($diff, 2),
];
}
private function cleanName(mixed $value): ?string
{
if ($value === null) {
return null;
}
$name = trim((string) $value);
return $name === '' ? null : $name;
}
private function decode(string $json, mixed $fallback): mixed
{
if ($json === '') {
return $fallback;
}
$decoded = json_decode($json, true);
return is_array($decoded) ? $decoded : $fallback;
}
}