- 按流程状态=已完成同步全部记录(去掉收款人过滤) - 明细表「收款费用明细」JSON 解码并按行展开为多行 - 新增「标签」列:收款人=宁波顾家望业家居有限公司→刘萧代发,其余→1688代发 - 「料号」列改为「商品编码」(值留空) - 仓库改为数据库层分页 + SQL 聚合,避免大数据量拖垮页面 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
208 lines
9.4 KiB
PHP
208 lines
9.4 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Repositories;
|
||
|
||
use Illuminate\Database\ConnectionInterface;
|
||
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();
|
||
|
||
// 计数/分页/合计一律走 SQL,只取当前页的行,避免把整表读入内存(大数据量会拖垮页面)。
|
||
// 状态过滤:镜像在同步阶段按 only_status 只写入指定状态(当前=已完成);状态派生自
|
||
// 表单 JSON、非独立列,无法用 SQL 过滤,因此请求的状态若与同步保留状态不同则结果为空,
|
||
// 否则全部行都符合、无需再过滤。
|
||
$onlyStatus = strtoupper(trim((string) config('upstream_mirrors.ding_online_daifa_price.only_status', '')));
|
||
$statusFilter = strtoupper(trim((string) ($filters['status'] ?? '')));
|
||
$mismatchStatus = $statusFilter !== '' && $onlyStatus !== '' && $statusFilter !== $onlyStatus;
|
||
|
||
$applyFilters = function ($query) use ($filters): void {
|
||
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['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', '<', (string) $filters['created_to'] . ' 99:99:99');
|
||
}
|
||
};
|
||
|
||
$countQuery = $this->connection->table(self::TARGET_TABLE);
|
||
$applyFilters($countQuery);
|
||
$total = $mismatchStatus ? 0 : (int) $countQuery->count();
|
||
|
||
$summary = ['count' => 0, 'matched_count' => 0, 'daifa_amount' => 0.0, 'paid_amount' => 0.0, 'price_diff' => 0.0];
|
||
if (! $mismatchStatus) {
|
||
$summaryQuery = $this->connection->table(self::TARGET_TABLE);
|
||
$applyFilters($summaryQuery);
|
||
$agg = $summaryQuery->selectRaw(
|
||
'COUNT(*) AS c, '
|
||
. "SUM(CASE WHEN product_id IS NOT NULL AND product_id <> '' THEN 1 ELSE 0 END) AS mc, "
|
||
. 'COALESCE(SUM(daifa_amount), 0) AS da, '
|
||
. 'COALESCE(SUM(paid_amount), 0) AS pa, '
|
||
. 'COALESCE(SUM(price_diff), 0) AS pd'
|
||
)->first();
|
||
$summary = [
|
||
'count' => (int) ($agg->c ?? 0),
|
||
'matched_count' => (int) ($agg->mc ?? 0),
|
||
'daifa_amount' => round((float) ($agg->da ?? 0), 2),
|
||
'paid_amount' => round((float) ($agg->pa ?? 0), 2),
|
||
'price_diff' => round((float) ($agg->pd ?? 0), 2),
|
||
];
|
||
}
|
||
|
||
$pageRows = [];
|
||
if (! $mismatchStatus) {
|
||
$pageQuery = $this->connection->table(self::TARGET_TABLE);
|
||
$applyFilters($pageQuery);
|
||
$pageRows = $pageQuery
|
||
->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, []);
|
||
$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];
|
||
$data[] = [
|
||
'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 ?? ''),
|
||
// 标签:收款人为宁波顾家望业家居有限公司 => 刘萧代发;其余 => 1688代发。
|
||
'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 ?? ''),
|
||
];
|
||
}
|
||
|
||
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,
|
||
];
|
||
}
|
||
|
||
/** @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)) {
|
||
$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;
|
||
}
|
||
}
|