CaiWuXiTong/modules/EcommerceFinance/Repositories/GoodsReconciliationRepository.php
WuYongJie e5c068036b feat: 供应商应付 - 货款对账 + 5个子功能页面
新增功能页面:
- 货款对账 (payment-reconciliation): 入库+代发合并对账,228K+2.8M条记录
- 退货对账 (return-reconciliation): finance_purchaseReturn, 36791行
- 发票扣款 (invoice-deductions): finance_invoiceDeduction, 195行
- 赔偿明细 (compensation-details): 纸板厂+压板厂 UNION, 98行
- 借支明细 (advance-details): finance_advanceLoans, 4行
- 样品扣款 (sample-deductions): finance_sampleDeduction, 12行

后端:
- JianSunConnection: PDO_ODBC 直连 SQL Server
- SupplierPayableRepository: 5个子页面的数据查询+分页
- GoodsReconciliationRepository: 货款对账数据查询
- 15个新API路由 (supplier-payables/*)
- 12个JianSun Eloquent模型

前端:
- 6个Blade视图 + 6个Vue组件
- webpack.mix.js 新增5个编译入口
- show.blade.php 动态视图解析
2026-07-21 18:40:24 +08:00

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;
public function __construct()
{
$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->pdo->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->pdo->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->pdo->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->pdo->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->pdo->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 goodsNo,
ISNULL(s.procode, '') AS productCode,
ISNULL(s.skuname, '') AS specName,
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 warehouseName,
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->pdo->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->pdo->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 goodsNo,
ISNULL(s.procode, '') AS productCode,
ISNULL(s.skuname, '') AS specName,
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 warehouseName,
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->pdo->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;
}
}