CaiWuXiTong/modules/EcommerceFinance/Repositories/GoodsReconciliationRepository.php

371 lines
17 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 Modules\EcommerceFinance\Services\JianSunConnection;
use PDO;
/**
* 货款对账数据仓库
*
* 数据来源:
* - 入库记录:fys_supplierGoods_sttlement_purchase(~228K)+ pm_purchaseIn + pm_averageUnitPrice
* - 代发记录:fys_supplierGoods_sttlement_sale(~2.8M)+ pm_averageUnitPrice
*
* 性能策略:入库/代发分开查询,各自执行分页,避免全量 UNION ALL。
*/
class GoodsReconciliationRepository
{
private ?PDO $pdo = null;
private function db(): PDO
{
return $this->pdo ??= JianSunConnection::get();
}
// ===================================================================
// 汇总统计(轻量:只用 COUNT + SUM,不做复杂 JOIN)
// ===================================================================
public function summary(array $filters = []): array
{
$purchase = $this->purchaseSummary($filters);
$sale = $this->saleSummary($filters);
return [
'totalAmount' => round($purchase['totalAmount'] + $sale['totalAmount'], 2),
'totalGuessAmount' => round($purchase['totalGuessAmount'] + $sale['totalGuessAmount'], 2),
'sentCount' => $purchase['sentCount'] + $sale['sentCount'],
'unsentCount' => $purchase['unsentCount'] + $sale['unsentCount'],
'totalCount' => $purchase['totalCount'] + $sale['totalCount'],
];
}
private function purchaseSummary(array $filters): array
{
$where = $this->buildPurchaseWhere($filters);
$params = $this->buildPurchaseParams($filters);
// 按需 JOIN,减少不必要的全表扫描
$joinA = $joinU = '';
if (strpos($where, 'a.') !== false) {
$joinA = ' LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.scmname = a.supplier';
}
if (strpos($where, 'u.') !== false) {
$joinU = ' LEFT JOIN pm_unOrderDetails u ON s.spo = u.purchase AND s.skuoid = u.sku';
}
$sql = "SELECT
COUNT(*) as totalCount,
ISNULL(SUM(s.settlementPrice), 0) as totalAmount,
0 as totalGuessAmount,
COUNT(*) as sentCount,
0 as unsentCount
FROM fys_supplierGoods_sttlement_purchase s
{$joinA}{$joinU}
WHERE 1=1 {$where}";
$stmt = $this->db()->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return $row ?: ['totalCount' => 0, 'totalAmount' => 0, 'totalGuessAmount' => 0, 'sentCount' => 0, 'unsentCount' => 0];
}
private function saleSummary(array $filters): array
{
$where = $this->buildSaleWhere($filters);
$params = $this->buildSaleParams($filters);
$joinA = '';
if (strpos($where, 'a.') !== false) {
$joinA = ' LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.supplier = a.supplier';
}
$sql = "SELECT
COUNT(*) as totalCount,
ISNULL(SUM(s.settlementPrice), 0) as totalAmount,
0 as totalGuessAmount,
ISNULL(SUM(CASE WHEN s.expressNo IS NOT NULL AND s.expressNo != '' THEN 1 ELSE 0 END), 0) as sentCount,
ISNULL(SUM(CASE WHEN s.expressNo IS NULL OR s.expressNo = '' THEN 1 ELSE 0 END), 0) as unsentCount
FROM fys_supplierGoods_sttlement_sale s
{$joinA}
WHERE 1=1 {$where}";
$stmt = $this->db()->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch();
return $row ?: ['totalCount' => 0, 'totalAmount' => 0, 'totalGuessAmount' => 0, 'sentCount' => 0, 'unsentCount' => 0];
}
// ===================================================================
// 列表查询(核心 — 分别查入库/代发再合并)
// ===================================================================
public function list(array $filters = [], int $page = 1, int $perPage = 25): array
{
$type = $filters['record_type'] ?? 'all';
// 分别查询(同一分页参数,各自内部分页)
$purchaseResult = null;
$saleResult = null;
$totalPurchase = 0;
$totalSale = 0;
if ($type === 'all' || $type === 'purchase') {
$where = $this->buildPurchaseWhere($filters);
$params = $this->buildPurchaseParams($filters);
$totalPurchase = $this->countPurchase($where, $params);
$purchaseResult = $this->fetchPurchase($where, $params, $page, $perPage);
}
if ($type === 'all' || $type === 'sale') {
$where = $this->buildSaleWhere($filters);
$params = $this->buildSaleParams($filters);
$totalSale = $this->countSale($where, $params);
$saleResult = $this->fetchSale($where, $params, $page, $perPage);
}
// 合并并按 createTime 排序,取前 perPage 条
$merged = array_merge($purchaseResult ?? [], $saleResult ?? []);
// 按 createTime DESC 排序
usort($merged, function ($a, $b) {
return strcmp($b['createTime'] ?? '', $a['createTime'] ?? '');
});
// 截取当前页
$merged = array_slice($merged, 0, $perPage);
// 格式化金额
foreach ($merged as &$row) {
$row['amount'] = round((float) $row['amount'], 2);
$row['guessAmount'] = round((float) $row['guessAmount'], 2);
$row['unitPrice'] = round((float) $row['unitPrice'], 2);
$row['guessUnitPrice'] = round((float) ($row['guessUnitPrice'] ?? 0), 2);
}
return [
'data' => $merged,
'total' => $totalPurchase + $totalSale,
];
}
// ===================================================================
// 筛选器选项
// ===================================================================
public function filterOptions(): array
{
$sql = "SELECT DISTINCT scmname FROM fys_supplierGoods_sttlement_purchase WHERE scmname IS NOT NULL AND scmname != ''
UNION
SELECT DISTINCT supplier FROM fys_supplierGoods_sttlement_sale WHERE supplier IS NOT NULL AND supplier != ''
ORDER BY scmname";
$stmt = $this->db()->query($sql);
$suppliers = $stmt->fetchAll(PDO::FETCH_COLUMN);
$sql = "SELECT DISTINCT whname FROM fys_supplierGoods_sttlement_purchase WHERE whname IS NOT NULL AND whname != ''
UNION
SELECT DISTINCT whName FROM fys_supplierGoods_sttlement_sale WHERE whName IS NOT NULL AND whName != ''
ORDER BY whname";
$stmt = $this->db()->query($sql);
$warehouses = $stmt->fetchAll(PDO::FETCH_COLUMN);
return [
'suppliers' => $suppliers,
'warehouses' => $warehouses,
];
}
// ===================================================================
// 入库查询
// ===================================================================
private function countPurchase(string $where, array $params): int
{
// 仅当筛选条件引用关联表字段时才 JOIN,否则纯主表 COUNT
$joins = '';
if (strpos($where, 'p.') !== false) {
$joins .= ' LEFT JOIN pm_purchaseIn p ON s.spo = p.sourceno';
}
if (strpos($where, 'a.') !== false) {
$joins .= ' LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.scmname = a.supplier';
}
if (strpos($where, 'u.') !== false) {
$joins .= ' LEFT JOIN pm_unOrderDetails u ON s.spo = u.purchase AND s.skuoid = u.sku';
}
$sql = "SELECT COUNT(*) as cnt FROM fys_supplierGoods_sttlement_purchase s{$joins} WHERE 1=1 {$where}";
$stmt = $this->db()->prepare($sql);
$stmt->execute($params);
return (int) $stmt->fetch()['cnt'];
}
private function fetchPurchase(string $where, array $params, int $page, int $perPage): array
{
$offset = ($page - 1) * $perPage;
$sql = "SELECT
'purchase' AS recordType,
ISNULL(p.hno, '') AS stockNo,
ISNULL(s.spo, '') AS purchaseNo,
FORMAT(ISNULL(s.created, s.createTime), 'yyyy-MM-dd HH:mm:ss') AS sendTime,
ISNULL(s.scmname, '') AS supplier,
ISNULL(s.skuoid, '') AS skuCode,
ISNULL(s.procode, '') AS productCode,
ISNULL(s.skuname, '') AS skuName,
ISNULL(s.settlementStock, 0) AS qty,
ISNULL(s.singlePrice, 0) AS unitPrice,
ISNULL(s.settlementPrice, 0) AS amount,
ISNULL(a.price, 0) AS guessUnitPrice,
ISNULL(s.settlementStock * ISNULL(a.price, 0), 0) AS guessAmount,
'' AS supplierGoodsCode,
ISNULL(s.remark, '') AS remark,
ISNULL(s.whname, '') AS warehouse,
FORMAT(s.createTime, 'yyyy-MM-dd HH:mm:ss') AS createTime,
'' AS expressName,
'' AS expressNo,
CASE WHEN u.Id IS NOT NULL THEN 1 ELSE 0 END AS isUnsent,
s.id AS sourceId
FROM fys_supplierGoods_sttlement_purchase s
LEFT JOIN pm_purchaseIn p ON s.spo = p.sourceno
LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.scmname = a.supplier
LEFT JOIN pm_unOrderDetails u ON s.spo = u.purchase AND s.skuoid = u.sku
WHERE 1=1 {$where}
ORDER BY s.createTime DESC
OFFSET {$offset} ROWS FETCH NEXT {$perPage} ROWS ONLY";
$stmt = $this->db()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
// ===================================================================
// 代发查询
// ===================================================================
private function countSale(string $where, array $params): int
{
$sql = "SELECT COUNT(*) as cnt FROM fys_supplierGoods_sttlement_sale s
LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.supplier = a.supplier
WHERE 1=1 {$where}";
$stmt = $this->db()->prepare($sql);
$stmt->execute($params);
return (int) $stmt->fetch()['cnt'];
}
private function fetchSale(string $where, array $params, int $page, int $perPage): array
{
$offset = ($page - 1) * $perPage;
$sql = "SELECT
'sale' AS recordType,
'' AS stockNo,
'' AS purchaseNo,
FORMAT(s.sendTime, 'yyyy-MM-dd HH:mm:ss') AS sendTime,
ISNULL(s.supplier, '') AS supplier,
ISNULL(s.skucode, '') AS skuCode,
ISNULL(s.procode, '') AS productCode,
ISNULL(s.skuname, '') AS skuName,
ISNULL(s.settlementStock, 0) AS qty,
ISNULL(s.singlePrice, 0) AS unitPrice,
ISNULL(s.settlementPrice, 0) AS amount,
ISNULL(a.price, 0) AS guessUnitPrice,
ISNULL(s.settlementStock * ISNULL(a.price, 0), 0) AS guessAmount,
'' AS supplierGoodsCode,
ISNULL(s.remark, '') AS remark,
ISNULL(s.whName, '') AS warehouse,
FORMAT(s.createTime, 'yyyy-MM-dd HH:mm:ss') AS createTime,
ISNULL(s.expressName, '') AS expressName,
ISNULL(s.expressNo, '') AS expressNo,
CASE WHEN s.expressNo IS NULL OR s.expressNo = '' THEN 1 ELSE 0 END AS isUnsent,
s.Id AS sourceId
FROM fys_supplierGoods_sttlement_sale s
LEFT JOIN pm_averageUnitPrice a ON s.procode = a.number AND s.supplier = a.supplier
WHERE 1=1 {$where}
ORDER BY s.createTime DESC
OFFSET {$offset} ROWS FETCH NEXT {$perPage} ROWS ONLY";
$stmt = $this->db()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
// ===================================================================
// WHERE 与参数构建
// ===================================================================
private function like(string $v): string
{
return '%' . $v . '%';
}
// --- 入库 WHERE ---
private function buildPurchaseWhere(array $filters): string
{
$c = [];
if (!empty($filters['supplier'])) $c[] = 'AND s.scmname LIKE :supplier';
if (!empty($filters['stock_no'])) $c[] = 'AND p.hno LIKE :stock_no';
if (!empty($filters['purchase_no'])) $c[] = 'AND s.spo LIKE :purchase_no';
if (!empty($filters['product_code'])) $c[] = 'AND s.procode LIKE :product_code';
if (!empty($filters['warehouse'])) $c[] = 'AND s.whname LIKE :warehouse';
if (!empty($filters['send_time_from'])) $c[] = 'AND s.created >= :send_time_from';
if (!empty($filters['send_time_to'])) $c[] = 'AND s.created <= :send_time_to';
if (!empty($filters['create_time_from'])) $c[] = 'AND s.createTime >= :create_time_from';
if (!empty($filters['create_time_to'])) $c[] = 'AND s.createTime <= :create_time_to';
if (!empty($filters['sent_status'])) {
if ($filters['sent_status'] === 'sent') $c[] = 'AND u.Id IS NULL';
if ($filters['sent_status'] === 'unsent') $c[] = 'AND u.Id IS NOT NULL';
}
if (!empty($filters['has_diff'])) {
$c[] = 'AND ABS(ISNULL(s.settlementPrice, 0) - ISNULL(s.settlementStock * ISNULL(a.price, 0), 0)) > 0.01';
}
return $c ? ' ' . implode(' ', $c) : '';
}
private function buildPurchaseParams(array $filters): array
{
$p = [];
if (!empty($filters['supplier'])) $p[':supplier'] = $this->like($filters['supplier']);
if (!empty($filters['stock_no'])) $p[':stock_no'] = $this->like($filters['stock_no']);
if (!empty($filters['purchase_no'])) $p[':purchase_no'] = $this->like($filters['purchase_no']);
if (!empty($filters['product_code'])) $p[':product_code'] = $this->like($filters['product_code']);
if (!empty($filters['warehouse'])) $p[':warehouse'] = $this->like($filters['warehouse']);
if (!empty($filters['send_time_from'])) $p[':send_time_from'] = $filters['send_time_from'];
if (!empty($filters['send_time_to'])) $p[':send_time_to'] = $filters['send_time_to'];
if (!empty($filters['create_time_from'])) $p[':create_time_from'] = $filters['create_time_from'];
if (!empty($filters['create_time_to'])) $p[':create_time_to'] = $filters['create_time_to'];
return $p;
}
// --- 代发 WHERE ---
private function buildSaleWhere(array $filters): string
{
$c = [];
if (!empty($filters['supplier'])) $c[] = 'AND s.supplier LIKE :supplier_s';
if (!empty($filters['product_code'])) $c[] = 'AND s.procode LIKE :product_code_s';
if (!empty($filters['express_no'])) $c[] = 'AND s.expressNo LIKE :express_no';
if (!empty($filters['warehouse'])) $c[] = 'AND s.whName LIKE :warehouse_s';
if (!empty($filters['send_time_from'])) $c[] = 'AND s.sendTime >= :send_time_from_s';
if (!empty($filters['send_time_to'])) $c[] = 'AND s.sendTime <= :send_time_to_s';
if (!empty($filters['create_time_from'])) $c[] = 'AND s.createTime >= :create_time_from_s';
if (!empty($filters['create_time_to'])) $c[] = 'AND s.createTime <= :create_time_to_s';
if (!empty($filters['sent_status'])) {
if ($filters['sent_status'] === 'sent') $c[] = "AND s.expressNo IS NOT NULL AND s.expressNo != ''";
if ($filters['sent_status'] === 'unsent') $c[] = "AND (s.expressNo IS NULL OR s.expressNo = '')";
}
if (!empty($filters['has_diff'])) {
$c[] = 'AND ABS(ISNULL(s.settlementPrice, 0) - ISNULL(s.settlementStock * ISNULL(a.price, 0), 0)) > 0.01';
}
return $c ? ' ' . implode(' ', $c) : '';
}
private function buildSaleParams(array $filters): array
{
$p = [];
if (!empty($filters['supplier'])) $p[':supplier_s'] = $this->like($filters['supplier']);
if (!empty($filters['product_code'])) $p[':product_code_s'] = $this->like($filters['product_code']);
if (!empty($filters['express_no'])) $p[':express_no'] = $this->like($filters['express_no']);
if (!empty($filters['warehouse'])) $p[':warehouse_s'] = $this->like($filters['warehouse']);
if (!empty($filters['send_time_from'])) $p[':send_time_from_s'] = $filters['send_time_from'];
if (!empty($filters['send_time_to'])) $p[':send_time_to_s'] = $filters['send_time_to'];
if (!empty($filters['create_time_from'])) $p[':create_time_from_s'] = $filters['create_time_from'];
if (!empty($filters['create_time_to'])) $p[':create_time_to_s'] = $filters['create_time_to'];
return $p;
}
}