CaiWuXiTong/modules/EcommerceFinance/Repositories/DingOnlineDaifaPriceMirrorRepository.php
limeng a9c85b2a9f 线上代发价格:接入钉钉流程(全部已完成)+ 标签 + 商品编码 + 明细展开与SQL分页
- 按流程状态=已完成同步全部记录(去掉收款人过滤)
- 明细表「收款费用明细」JSON 解码并按行展开为多行
- 新增「标签」列:收款人=宁波顾家望业家居有限公司→刘萧代发,其余→1688代发
- 「料号」列改为「商品编码」(值留空)
- 仓库改为数据库层分页 + SQL 聚合,避免大数据量拖垮页面

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 18:38:35 +08:00

208 lines
9.4 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\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;
}
}