CaiWuXiTong/modules/EcommerceFinance/Repositories/DingOnlineDaifaPriceMirrorRepository.php

237 lines
9.9 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();
// The synchronized dataset normally contains one configured status, so
// count, totals, and paging can stay in SQL instead of loading all rows.
$onlyStatus = strtoupper(trim((string) config('upstream_mirrors.ding_online_daifa_price.only_status', '')));
$statusFilter = strtoupper(trim((string) ($filters['status'] ?? '')));
$statusMismatch = $statusFilter !== '' && $onlyStatus !== '' && $statusFilter !== $onlyStatus;
$total = $statusMismatch ? 0 : (int) $this->filteredQuery($filters)->count();
$summary = [
'count' => 0,
'matched_count' => 0,
'daifa_amount' => 0.0,
'paid_amount' => 0.0,
'price_diff' => 0.0,
];
$pageRows = [];
if (! $statusMismatch) {
$aggregate = $this->filteredQuery($filters)
->selectRaw(
'COUNT(*) AS row_count, '
. "SUM(CASE WHEN product_id IS NOT NULL AND product_id <> '' AND product_name IS NOT NULL AND product_name <> '' THEN 1 ELSE 0 END) AS matched_count, "
. 'COALESCE(SUM(daifa_amount), 0) AS daifa_amount, '
. 'COALESCE(SUM(paid_amount), 0) AS paid_amount, '
. 'COALESCE(SUM(price_diff), 0) AS price_diff'
)
->first();
$summary = [
'count' => (int) ($aggregate->row_count ?? 0),
'matched_count' => (int) ($aggregate->matched_count ?? 0),
'daifa_amount' => round((float) ($aggregate->daifa_amount ?? 0), 2),
'paid_amount' => round((float) ($aggregate->paid_amount ?? 0), 2),
'price_diff' => round((float) ($aggregate->price_diff ?? 0), 2),
];
$pageRows = $this->filteredQuery($filters)
->orderByDesc('workflow_created_at')
->orderByDesc('process_instance_id')
->orderByDesc('detail_index')
->forPage($page, $perPage)
->get();
}
$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' => $summary,
];
}
/** @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 ?? ''),
'tag' => ((string) ($row->payee_name ?? '')) === '宁波顾家望业家居有限公司'
? '刘萧代发'
: '1688代发',
'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)) . '%';
}
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;
}
}