371 lines
17 KiB
PHP
371 lines
17 KiB
PHP
<?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;
|
||
}
|
||
}
|