Просмотр исходного кода

Merge branch 'master' of git.huaml.com:zhh/huahuibao

shizhongqi 3 месяцев назад
Родитель
Сommit
ba65459e0d

+ 2 - 0
app-ghs/controllers/OrderClearController.php

@@ -4,6 +4,7 @@ namespace ghs\controllers;
 
 use biz\ghs\classes\GhsClass;
 use biz\wx\classes\WxMessageClass;
+use bizGhs\clear\classes\OrderCgClearClass;
 use bizGhs\custom\classes\CustomClass;
 use bizGhs\order\classes\OrderClearClass;
 use bizGhs\shop\classes\ShopClass;
@@ -67,6 +68,7 @@ class OrderClearController extends BaseController
                 //只要新建账单,老账单就取消掉
                 $item->status = PurchaseClearClass::STATUS_EXPIRE;
                 $item->save();
+                OrderCgClearClass::markExpireByClearId($item->id);
             }
         }
 

+ 2 - 0
app-hd/controllers/PurchaseClearController.php

@@ -3,6 +3,7 @@
 namespace hd\controllers;
 
 use biz\ghs\classes\GhsClass;
+use bizHd\purchase\classes\OrderCgClearClass;
 use bizHd\purchase\classes\PurchaseClearClass;
 use bizHd\shop\classes\ShopClass;
 use bizHd\wx\classes\WxOpenClass;
@@ -44,6 +45,7 @@ class PurchaseClearController extends BaseController
             foreach ($list as $item) {
                 $item->status = PurchaseClearClass::STATUS_EXPIRE;
                 $item->save();
+                OrderCgClearClass::markExpireByClearId($item->id);
 
                 if (!empty($ghsShop)) {
                     $orderSn = $item->orderSn ?? '';

+ 361 - 0
biz-ghs/clear/classes/OrderCgClearClass.php

@@ -0,0 +1,361 @@
+<?php
+
+namespace bizGhs\clear\classes;
+
+use bizGhs\base\classes\BaseClass;
+use bizGhs\order\classes\OrderClass;
+use bizHd\purchase\classes\PurchaseClass;
+use bizHd\purchase\classes\PurchaseClearClass;
+use common\components\dict;
+use Yii;
+
+class OrderCgClearClass extends BaseClass
+{
+
+    public static $baseFile = '\bizGhs\clear\models\OrderCgClear';
+
+    const STATUS_AWAIT_PAY = 1;
+    const STATUS_HAS_PAY = 2;
+    const STATUS_EXPIRE = 3;
+
+    public static function buildPairsFromOrders($orderList)
+    {
+        if (empty($orderList)) {
+            return [];
+        }
+        $purchaseIds = [];
+        foreach ($orderList as $order) {
+            $purchaseId = is_array($order) ? ($order['purchaseId'] ?? 0) : ($order->purchaseId ?? 0);
+            if (!empty($purchaseId)) {
+                $purchaseIds[] = $purchaseId;
+            }
+        }
+        $cgMap = [];
+        if (!empty($purchaseIds)) {
+            $cgList = PurchaseClass::getByIds(array_unique($purchaseIds));
+            if (!empty($cgList)) {
+                $cgMap = array_column($cgList, null, 'id');
+            }
+        }
+        $pairs = [];
+        foreach ($orderList as $order) {
+            $orderId = is_array($order) ? ($order['id'] ?? 0) : ($order->id ?? 0);
+            $orderSn = is_array($order) ? ($order['orderSn'] ?? '') : ($order->orderSn ?? '');
+            $amount = self::resolveDebtAmount($order);
+            $cgId = intval(is_array($order) ? ($order['purchaseId'] ?? 0) : ($order->purchaseId ?? 0));
+            $cgSn = '';
+            if ($cgId > 0 && isset($cgMap[$cgId])) {
+                $cgSn = $cgMap[$cgId]['orderSn'] ?? '';
+            }
+            $pairs[] = [
+                'orderId' => intval($orderId),
+                'orderSn' => $orderSn,
+                'cgId' => $cgId,
+                'cgSn' => $cgSn,
+                'amount' => $amount,
+            ];
+        }
+        return $pairs;
+    }
+
+    public static function buildPairsFromPurchases($purchaseList)
+    {
+        if (empty($purchaseList)) {
+            return [];
+        }
+        $saleIds = [];
+        foreach ($purchaseList as $purchase) {
+            $saleId = is_array($purchase) ? ($purchase['saleId'] ?? 0) : ($purchase->saleId ?? 0);
+            if (!empty($saleId)) {
+                $saleIds[] = $saleId;
+            }
+        }
+        $orderMap = [];
+        if (!empty($saleIds)) {
+            $orderList = OrderClass::getByIds(array_unique($saleIds));
+            if (!empty($orderList)) {
+                $orderMap = array_column($orderList, null, 'id');
+            }
+        }
+        $pairs = [];
+        foreach ($purchaseList as $purchase) {
+            $cgId = is_array($purchase) ? ($purchase['id'] ?? 0) : ($purchase->id ?? 0);
+            $cgSn = is_array($purchase) ? ($purchase['orderSn'] ?? '') : ($purchase->orderSn ?? '');
+            $amount = self::resolveDebtAmount($purchase);
+            $orderId = intval(is_array($purchase) ? ($purchase['saleId'] ?? 0) : ($purchase->saleId ?? 0));
+            $orderSn = '';
+            if ($orderId > 0 && isset($orderMap[$orderId])) {
+                $orderSn = $orderMap[$orderId]['orderSn'] ?? '';
+            }
+            $pairs[] = [
+                'orderId' => $orderId,
+                'orderSn' => $orderSn,
+                'cgId' => intval($cgId),
+                'cgSn' => $cgSn,
+                'amount' => $amount,
+            ];
+        }
+        return $pairs;
+    }
+
+    public static function bindRelations($clear, $pairs, $status = self::STATUS_AWAIT_PAY)
+    {
+        $clearId = is_object($clear) ? ($clear->id ?? 0) : ($clear['id'] ?? 0);
+        $clearSn = is_object($clear) ? ($clear->orderSn ?? '') : ($clear['orderSn'] ?? '');
+        if (empty($clearId) || empty($pairs)) {
+            return;
+        }
+        foreach ($pairs as $pair) {
+            $orderId = intval($pair['orderId'] ?? 0);
+            $cgId = intval($pair['cgId'] ?? 0);
+            if ($orderId <= 0 && $cgId <= 0) {
+                continue;
+            }
+            $exists = self::getByCondition([
+                'clearId' => $clearId,
+                'orderId' => $orderId,
+                'cgId' => $cgId,
+            ], true);
+            $newAmount = self::normalizeAmount($pair['amount'] ?? 0);
+            if (!empty($exists)) {
+                self::patchAmountIfEmpty($exists, $newAmount);
+                continue;
+            }
+            self::add([
+                'clearId' => $clearId,
+                'clearSn' => $clearSn,
+                'orderId' => $orderId,
+                'orderSn' => $pair['orderSn'] ?? '',
+                'cgId' => $cgId,
+                'cgSn' => $pair['cgSn'] ?? '',
+                'amount' => $newAmount,
+                'status' => $status,
+            ]);
+        }
+    }
+
+    public static function markDoneByClearId($clearId)
+    {
+        self::markStatusByClearId($clearId, self::STATUS_HAS_PAY);
+    }
+
+    public static function markExpireByClearId($clearId)
+    {
+        self::markStatusByClearId($clearId, self::STATUS_EXPIRE);
+    }
+
+    public static function markStatusByClearId($clearId, $status)
+    {
+        $clearId = intval($clearId);
+        $status = intval($status);
+        if ($clearId <= 0) {
+            return;
+        }
+        $list = self::getAllByCondition(['clearId' => $clearId], null, '*', null, true);
+        if (empty($list)) {
+            return;
+        }
+        foreach ($list as $item) {
+            $item->status = $status;
+            $item->save();
+        }
+    }
+
+    public static function getOrderIdsByClearId($clearId, $fallbackSaleIds = '')
+    {
+        $clearId = intval($clearId);
+        if ($clearId <= 0) {
+            return self::parseIds($fallbackSaleIds);
+        }
+        $list = self::getAllByCondition(['clearId' => $clearId, 'orderId>' => 0], null, 'orderId', null);
+        $ids = array_values(array_unique(array_filter(array_map('intval', array_column($list, 'orderId')))));
+        if (!empty($ids)) {
+            return $ids;
+        }
+        return self::parseIds($fallbackSaleIds);
+    }
+
+    public static function getCgIdsByClearId($clearId, $fallbackPurchaseIds = '')
+    {
+        $clearId = intval($clearId);
+        if ($clearId <= 0) {
+            return self::parseIds($fallbackPurchaseIds);
+        }
+        $list = self::getAllByCondition(['clearId' => $clearId, 'cgId>' => 0], null, 'cgId', null);
+        $ids = array_values(array_unique(array_filter(array_map('intval', array_column($list, 'cgId')))));
+        if (!empty($ids)) {
+            return $ids;
+        }
+        return self::parseIds($fallbackPurchaseIds);
+    }
+
+    public static function getPendingClearIdByOrderId($orderId, $customId)
+    {
+        $orderId = intval($orderId);
+        $customId = intval($customId);
+        if ($orderId <= 0) {
+            return 0;
+        }
+        $list = self::getAllByCondition([
+            'orderId' => $orderId,
+            'status' => self::STATUS_AWAIT_PAY,
+        ], null, 'clearId', null);
+        if (empty($list)) {
+            return 0;
+        }
+        $clearIds = array_unique(array_map('intval', array_column($list, 'clearId')));
+        $now = date("Y-m-d H:i:s");
+        foreach ($clearIds as $clearId) {
+            $clear = ClearClass::getById($clearId, true);
+            if (empty($clear)) {
+                continue;
+            }
+            if (($clear->status ?? 0) != PurchaseClearClass::STATUS_AWAIT_PAY) {
+                continue;
+            }
+            if ($customId > 0 && ($clear->customId ?? 0) != $customId) {
+                continue;
+            }
+            $deadline = $clear->deadline ?? '';
+            if (!empty($deadline) && $deadline < $now) {
+                continue;
+            }
+            return intval($clearId);
+        }
+        return 0;
+    }
+
+    public static function backfillFromClear($clear, $skipExistsCheck = false)
+    {
+        $clearId = is_object($clear) ? ($clear->id ?? 0) : ($clear['id'] ?? 0);
+        if (empty($clearId)) {
+            return 0;
+        }
+        if (!$skipExistsCheck) {
+            $exists = self::getByCondition(['clearId' => $clearId], true);
+            if (!empty($exists)) {
+                return 0;
+            }
+        }
+        $clearStyle = is_object($clear) ? ($clear->clearStyle ?? 0) : ($clear['clearStyle'] ?? 0);
+        $hd2Gys = dict::getDict('clearStyle', 'hd2Gys');
+        $gys2Hd = dict::getDict('clearStyle', 'gys2Hd');
+        if (!in_array(intval($clearStyle), [intval($hd2Gys), intval($gys2Hd)])) {
+            return 0;
+        }
+        $saleIds = is_object($clear) ? ($clear->saleIds ?? '') : ($clear['saleIds'] ?? '');
+        $purchaseIds = is_object($clear) ? ($clear->purchaseIds ?? '') : ($clear['purchaseIds'] ?? '');
+        $pairs = [];
+        $saleArr = self::parseIds($saleIds);
+        if (!empty($saleArr)) {
+            $orderList = OrderClass::getByIds($saleArr);
+            if (!empty($orderList)) {
+                $pairs = self::buildPairsFromOrders($orderList);
+            }
+        } elseif (!empty(self::parseIds($purchaseIds))) {
+            $cgArr = self::parseIds($purchaseIds);
+            $purchaseList = PurchaseClass::getAllByCondition(['id' => ['in', $cgArr]], null, '*', null, true);
+            if (!empty($purchaseList)) {
+                $pairs = self::buildPairsFromPurchases($purchaseList);
+            }
+        }
+        if (empty($pairs)) {
+            return 0;
+        }
+        $status = intval(is_object($clear) ? ($clear->status ?? 0) : ($clear['status'] ?? 0));
+        if (!in_array($status, [self::STATUS_AWAIT_PAY, self::STATUS_HAS_PAY, self::STATUS_EXPIRE], true)) {
+            $status = self::STATUS_AWAIT_PAY;
+        }
+        self::bindRelations($clear, $pairs, $status);
+        return count($pairs);
+    }
+
+    /**
+     * 批量查询关系表已存在的 clearId(避免逐条 SELECT)
+     * @param int[] $clearIds
+     * @return int[]
+     */
+    public static function getExistingClearIds(array $clearIds)
+    {
+        $clearIds = array_values(array_unique(array_filter(array_map('intval', $clearIds))));
+        if (empty($clearIds)) {
+            return [];
+        }
+        $list = self::getAllByCondition(['clearId' => ['in', $clearIds]], null, 'clearId', null);
+        if (empty($list)) {
+            return [];
+        }
+        return array_values(array_unique(array_map('intval', array_column($list, 'clearId'))));
+    }
+
+    /**
+     * 关系行已存在但 amount 为空/0 的 clearId(用于回填补金额)
+     * @param int[] $clearIds
+     * @return int[]
+     */
+    public static function getClearIdsWithEmptyAmount(array $clearIds)
+    {
+        $clearIds = array_values(array_unique(array_filter(array_map('intval', $clearIds))));
+        if (empty($clearIds)) {
+            return [];
+        }
+        $table = self::$baseFile::tableName();
+        $rows = Yii::$app->db->createCommand(
+            'SELECT DISTINCT clearId FROM ' . $table
+            . ' WHERE clearId IN (' . implode(',', $clearIds) . ')'
+            . ' AND (amount IS NULL OR amount = 0 OR amount = \'0\' OR amount = \'0.00\')'
+        )->queryColumn();
+        if (empty($rows)) {
+            return [];
+        }
+        return array_values(array_unique(array_map('intval', $rows)));
+    }
+
+    /**
+     * 关系行 amount 取订单/采购单 actPrice
+     */
+    protected static function resolveDebtAmount($entity)
+    {
+        return self::normalizeAmount(self::readField($entity, 'actPrice'));
+    }
+
+    protected static function readField($entity, $key)
+    {
+        return is_array($entity) ? ($entity[$key] ?? 0) : ($entity->$key ?? 0);
+    }
+
+    protected static function normalizeAmount($amount)
+    {
+        if ($amount === '' || $amount === null) {
+            return '0.00';
+        }
+        return bcadd((string)$amount, '0', 2);
+    }
+
+    protected static function patchAmountIfEmpty($row, $amount)
+    {
+        $amount = self::normalizeAmount($amount);
+        if (bccomp($amount, '0', 2) <= 0) {
+            return false;
+        }
+        $current = self::normalizeAmount(is_object($row) ? ($row->amount ?? 0) : ($row['amount'] ?? 0));
+        if (bccomp($current, '0', 2) > 0) {
+            return false;
+        }
+        if (is_object($row)) {
+            $row->amount = $amount;
+            return $row->save(false);
+        }
+        return false;
+    }
+
+    protected static function parseIds($idStr)
+    {
+        if ($idStr === '' || $idStr === null) {
+            return [];
+        }
+        $arr = explode(',', (string)$idStr);
+        return array_values(array_unique(array_filter(array_map('intval', $arr))));
+    }
+
+}

+ 15 - 0
biz-ghs/clear/models/OrderCgClear.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace bizGhs\clear\models;
+
+use bizGhs\base\models\Base;
+
+class OrderCgClear extends Base
+{
+
+    public static function tableName()
+    {
+        return 'xhOrderCgClear';
+    }
+
+}

+ 5 - 0
biz-ghs/order/classes/OrderClearClass.php

@@ -12,6 +12,7 @@ use common\components\noticeUtil;
 use common\components\orderSn;
 use common\components\util;
 use bizGhs\base\classes\BaseClass;
+use bizGhs\clear\classes\OrderCgClearClass;
 use bizHd\purchase\classes\PurchaseClass;
 use bizHd\purchase\classes\PurchaseClearClass;
 
@@ -82,6 +83,7 @@ class OrderClearClass extends BaseClass
                 //只要新建账单,老账单就取消掉
                 $item->status = PurchaseClearClass::STATUS_EXPIRE;
                 $item->save();
+                OrderCgClearClass::markExpireByClearId($item->id);
             }
         }
 
@@ -225,6 +227,8 @@ class OrderClearClass extends BaseClass
             util::fail('结账金额大于总欠款金额');
         }
         $return = PurchaseClearClass::add($clearData, true);
+        $pairs = OrderCgClearClass::buildPairsFromOrders($list);
+        OrderCgClearClass::bindRelations($return, $pairs);
         $complete = $post['complete'] ?? 0;
         if ($complete == 1) {
             self::confirmClear($return, $payWay);
@@ -316,6 +320,7 @@ class OrderClearClass extends BaseClass
             ];
             ShopMoneyChangeClass::addData($change);
         }
+        OrderCgClearClass::markDoneByClearId($clearId);
     }
 
 }

+ 270 - 0
biz-hd/purchase/classes/OrderCgClearClass.php

@@ -0,0 +1,270 @@
+<?php
+
+namespace bizHd\purchase\classes;
+
+use bizGhs\clear\classes\ClearClass;
+use bizGhs\order\classes\OrderClass;
+use bizHd\base\classes\BaseClass;
+use common\components\dict;
+
+class OrderCgClearClass extends BaseClass
+{
+
+    public static $baseFile = '\bizHd\purchase\models\OrderCgClear';
+
+    const STATUS_AWAIT_PAY = 1;
+    const STATUS_HAS_PAY = 2;
+    const STATUS_EXPIRE = 3;
+
+    public static function buildPairsFromOrders($orderList)
+    {
+        if (empty($orderList)) {
+            return [];
+        }
+        $purchaseIds = [];
+        foreach ($orderList as $order) {
+            $purchaseId = is_array($order) ? ($order['purchaseId'] ?? 0) : ($order->purchaseId ?? 0);
+            if (!empty($purchaseId)) {
+                $purchaseIds[] = $purchaseId;
+            }
+        }
+        $cgMap = [];
+        if (!empty($purchaseIds)) {
+            $cgList = PurchaseClass::getByIds(array_unique($purchaseIds));
+            if (!empty($cgList)) {
+                $cgMap = array_column($cgList, null, 'id');
+            }
+        }
+        $pairs = [];
+        foreach ($orderList as $order) {
+            $orderId = is_array($order) ? ($order['id'] ?? 0) : ($order->id ?? 0);
+            $orderSn = is_array($order) ? ($order['orderSn'] ?? '') : ($order->orderSn ?? '');
+            $amount = self::resolveDebtAmount($order);
+            $cgId = intval(is_array($order) ? ($order['purchaseId'] ?? 0) : ($order->purchaseId ?? 0));
+            $cgSn = '';
+            if ($cgId > 0 && isset($cgMap[$cgId])) {
+                $cgSn = $cgMap[$cgId]['orderSn'] ?? '';
+            }
+            $pairs[] = [
+                'orderId' => intval($orderId),
+                'orderSn' => $orderSn,
+                'cgId' => $cgId,
+                'cgSn' => $cgSn,
+                'amount' => $amount,
+            ];
+        }
+        return $pairs;
+    }
+
+    public static function buildPairsFromPurchases($purchaseList)
+    {
+        if (empty($purchaseList)) {
+            return [];
+        }
+        $saleIds = [];
+        foreach ($purchaseList as $purchase) {
+            $saleId = is_array($purchase) ? ($purchase['saleId'] ?? 0) : ($purchase->saleId ?? 0);
+            if (!empty($saleId)) {
+                $saleIds[] = $saleId;
+            }
+        }
+        $orderMap = [];
+        if (!empty($saleIds)) {
+            $orderList = OrderClass::getByIds(array_unique($saleIds));
+            if (!empty($orderList)) {
+                $orderMap = array_column($orderList, null, 'id');
+            }
+        }
+        $pairs = [];
+        foreach ($purchaseList as $purchase) {
+            $cgId = is_array($purchase) ? ($purchase['id'] ?? 0) : ($purchase->id ?? 0);
+            $cgSn = is_array($purchase) ? ($purchase['orderSn'] ?? '') : ($purchase->orderSn ?? '');
+            $amount = self::resolveDebtAmount($purchase);
+            $orderId = intval(is_array($purchase) ? ($purchase['saleId'] ?? 0) : ($purchase->saleId ?? 0));
+            $orderSn = '';
+            if ($orderId > 0 && isset($orderMap[$orderId])) {
+                $orderSn = $orderMap[$orderId]['orderSn'] ?? '';
+            }
+            $pairs[] = [
+                'orderId' => $orderId,
+                'orderSn' => $orderSn,
+                'cgId' => intval($cgId),
+                'cgSn' => $cgSn,
+                'amount' => $amount,
+            ];
+        }
+        return $pairs;
+    }
+
+    public static function bindRelations($clear, $pairs, $status = self::STATUS_AWAIT_PAY)
+    {
+        $clearId = is_object($clear) ? ($clear->id ?? 0) : ($clear['id'] ?? 0);
+        $clearSn = is_object($clear) ? ($clear->orderSn ?? '') : ($clear['orderSn'] ?? '');
+        if (empty($clearId) || empty($pairs)) {
+            return;
+        }
+        foreach ($pairs as $pair) {
+            $orderId = intval($pair['orderId'] ?? 0);
+            $cgId = intval($pair['cgId'] ?? 0);
+            if ($orderId <= 0 && $cgId <= 0) {
+                continue;
+            }
+            $exists = self::getByCondition([
+                'clearId' => $clearId,
+                'orderId' => $orderId,
+                'cgId' => $cgId,
+            ], true);
+            $newAmount = self::normalizeAmount($pair['amount'] ?? 0);
+            if (!empty($exists)) {
+                self::patchAmountIfEmpty($exists, $newAmount);
+                continue;
+            }
+            self::add([
+                'clearId' => $clearId,
+                'clearSn' => $clearSn,
+                'orderId' => $orderId,
+                'orderSn' => $pair['orderSn'] ?? '',
+                'cgId' => $cgId,
+                'cgSn' => $pair['cgSn'] ?? '',
+                'amount' => $newAmount,
+                'status' => $status,
+            ]);
+        }
+    }
+
+    public static function markDoneByClearId($clearId)
+    {
+        self::markStatusByClearId($clearId, self::STATUS_HAS_PAY);
+    }
+
+    public static function markExpireByClearId($clearId)
+    {
+        self::markStatusByClearId($clearId, self::STATUS_EXPIRE);
+    }
+
+    public static function markStatusByClearId($clearId, $status)
+    {
+        $clearId = intval($clearId);
+        $status = intval($status);
+        if ($clearId <= 0) {
+            return;
+        }
+        $list = self::getAllByCondition(['clearId' => $clearId], null, '*', null, true);
+        if (empty($list)) {
+            return;
+        }
+        foreach ($list as $item) {
+            $item->status = $status;
+            $item->save();
+        }
+    }
+
+    public static function getOrderIdsByClearId($clearId, $fallbackSaleIds = '')
+    {
+        $clearId = intval($clearId);
+        if ($clearId <= 0) {
+            return self::parseIds($fallbackSaleIds);
+        }
+        $list = self::getAllByCondition(['clearId' => $clearId, 'orderId>' => 0], null, 'orderId', null);
+        $ids = array_values(array_unique(array_filter(array_map('intval', array_column($list, 'orderId')))));
+        if (!empty($ids)) {
+            return $ids;
+        }
+        return self::parseIds($fallbackSaleIds);
+    }
+
+    public static function getCgIdsByClearId($clearId, $fallbackPurchaseIds = '')
+    {
+        $clearId = intval($clearId);
+        if ($clearId <= 0) {
+            return self::parseIds($fallbackPurchaseIds);
+        }
+        $list = self::getAllByCondition(['clearId' => $clearId, 'cgId>' => 0], null, 'cgId', null);
+        $ids = array_values(array_unique(array_filter(array_map('intval', array_column($list, 'cgId')))));
+        if (!empty($ids)) {
+            return $ids;
+        }
+        return self::parseIds($fallbackPurchaseIds);
+    }
+
+    public static function getPendingClearIdByOrderId($orderId, $customId)
+    {
+        $orderId = intval($orderId);
+        $customId = intval($customId);
+        if ($orderId <= 0) {
+            return 0;
+        }
+        $list = self::getAllByCondition([
+            'orderId' => $orderId,
+            'status' => self::STATUS_AWAIT_PAY,
+        ], null, 'clearId', null);
+        if (empty($list)) {
+            return 0;
+        }
+        $clearIds = array_unique(array_map('intval', array_column($list, 'clearId')));
+        $now = date("Y-m-d H:i:s");
+        foreach ($clearIds as $clearId) {
+            $clear = ClearClass::getById($clearId, true);
+            if (empty($clear)) {
+                continue;
+            }
+            if (($clear->status ?? 0) != PurchaseClearClass::STATUS_AWAIT_PAY) {
+                continue;
+            }
+            if ($customId > 0 && ($clear->customId ?? 0) != $customId) {
+                continue;
+            }
+            $deadline = $clear->deadline ?? '';
+            if (!empty($deadline) && $deadline < $now) {
+                continue;
+            }
+            return intval($clearId);
+        }
+        return 0;
+    }
+
+    protected static function resolveDebtAmount($entity)
+    {
+        return self::normalizeAmount(self::readField($entity, 'actPrice'));
+    }
+
+    protected static function readField($entity, $key)
+    {
+        return is_array($entity) ? ($entity[$key] ?? 0) : ($entity->$key ?? 0);
+    }
+
+    protected static function normalizeAmount($amount)
+    {
+        if ($amount === '' || $amount === null) {
+            return '0.00';
+        }
+        return bcadd((string)$amount, '0', 2);
+    }
+
+    protected static function patchAmountIfEmpty($row, $amount)
+    {
+        $amount = self::normalizeAmount($amount);
+        if (bccomp($amount, '0', 2) <= 0) {
+            return false;
+        }
+        $current = self::normalizeAmount(is_object($row) ? ($row->amount ?? 0) : ($row['amount'] ?? 0));
+        if (bccomp($current, '0', 2) > 0) {
+            return false;
+        }
+        if (is_object($row)) {
+            $row->amount = $amount;
+            return $row->save(false);
+        }
+        return false;
+    }
+
+    protected static function parseIds($idStr)
+    {
+        if ($idStr === '' || $idStr === null) {
+            return [];
+        }
+        $arr = explode(',', (string)$idStr);
+        return array_values(array_unique(array_filter(array_map('intval', $arr))));
+    }
+
+}

+ 5 - 1
biz-hd/purchase/classes/PurchaseClearClass.php

@@ -92,7 +92,10 @@ class PurchaseClearClass extends BaseClass
             'ghsShopName' => $ghsShopName,
         ];
         $data = array_merge($data, $params);
-        return self::add($data);
+        $clear = self::add($data);
+        $pairs = OrderCgClearClass::buildPairsFromPurchases($purchaseList);
+        OrderCgClearClass::bindRelations($clear, $pairs);
+        return $clear;
     }
 
     //第三方支付回调 ssh 2021.4.28
@@ -217,6 +220,7 @@ class PurchaseClearClass extends BaseClass
 
         //客户付供货商款减少
         CustomClass::clearDebtAmountReduce($custom, $ghs, $order->actPrice, $order);
+        OrderCgClearClass::markDoneByClearId($clearId);
     }
 
 }

+ 15 - 0
biz-hd/purchase/models/OrderCgClear.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace bizHd\purchase\models;
+
+use bizHd\base\models\Base;
+
+class OrderCgClear extends Base
+{
+
+    public static function tableName()
+    {
+        return 'xhOrderCgClear';
+    }
+
+}

+ 254 - 3
console/controllers/ClearController.php

@@ -2,15 +2,54 @@
 
 namespace console\controllers;
 
-use bizGhs\order\classes\OrderClass;
+use bizGhs\clear\classes\OrderCgClearClass;
+use bizGhs\clear\models\Clear;
 use bizGhs\order\classes\OrderClearClass;
+use bizGhs\order\classes\OrderClass;
 use bizHd\purchase\classes\PurchaseClass;
-use yii\console\Controller;
+use bizHd\purchase\classes\PurchaseClearClass;
+use common\components\dict;
 use Yii;
+use yii\console\Controller;
+use yii\console\ExitCode;
 
 class ClearController extends Controller
 {
 
+    /** @var int 是否仅预览,1=是 */
+    public $dryRun = 0;
+
+    /** @var int 每批扫描 xhClear 条数(宜小,减轻锁与内存) */
+    public $batchSize = 50;
+
+    /** @var int 每批之间休眠毫秒数,降低对线上库压力 */
+    public $sleepMs = 100;
+
+    /** @var int 指定结账单 ID,0=全部 */
+    public $clearId = 0;
+
+    /** @var int 是否重置进度从最大 id 重跑,1=是 */
+    public $reset = 0;
+
+    /** @var int 本次最多处理 xhClear 条数,0=不限制 */
+    public $limit = 0;
+
+    /** @var int 每处理多少条输出一次进度,0=仅每批汇总 */
+    public $logEvery = 500;
+
+    public function options($actionID)
+    {
+        return array_merge(parent::options($actionID), [
+            'dryRun',
+            'batchSize',
+            'sleepMs',
+            'clearId',
+            'reset',
+            'limit',
+            'logEvery',
+        ]);
+    }
+
     //更新 ssh 2023503
     public function actionUpdate()
     {
@@ -38,4 +77,216 @@ class ClearController extends Controller
 
     }
 
-}
+    /**
+     * 历史 xhClear 回填 xhOrderCgClear
+     *
+     * 大数据量友好:按主键 id 分批倒序扫描,无长事务;批间休眠;关系表 clearId 批量去重。
+     *
+     * 用法:
+     *   php yii clear/backfill-order-cg-clear
+     *   php yii clear/backfill-order-cg-clear --batchSize=50 --sleepMs=200
+     *   php yii clear/backfill-order-cg-clear --dryRun=1 --limit=1000
+     *   php yii clear/backfill-order-cg-clear --clearId=123
+     *   php yii clear/backfill-order-cg-clear --reset=1
+     */
+    public function actionBackfillOrderCgClear()
+    {
+        ini_set('memory_limit', '512M');
+        set_time_limit(0);
+
+        if ($this->batchSize < 1) {
+            $this->stderr("batchSize 必须大于 0\n");
+            return ExitCode::UNSPECIFIED_ERROR;
+        }
+        if ($this->sleepMs < 0) {
+            $this->stderr("sleepMs 不能小于 0\n");
+            return ExitCode::UNSPECIFIED_ERROR;
+        }
+
+        $hd2Gys = intval(dict::getDict('clearStyle', 'hd2Gys'));
+        $gys2Hd = intval(dict::getDict('clearStyle', 'gys2Hd'));
+
+        if ($this->reset && $this->clearId <= 0) {
+            $this->clearBackfillProgress();
+            $this->stdout("已重置回填进度\n");
+        }
+
+        $cursorId = $this->clearId > 0 ? null : $this->loadBackfillProgress();
+        $total = 0;
+        $inserted = 0;
+        $skipped = 0;
+        $batchNo = 0;
+
+        $this->stdout(sprintf(
+            "开始回填 xhOrderCgClear | id DESC 分批 | dryRun=%d batchSize=%d sleepMs=%d limit=%d | 续跑 id<%s | 进度=%s\n",
+            (int)$this->dryRun,
+            $this->batchSize,
+            $this->sleepMs,
+            (int)$this->limit,
+            $cursorId === null ? 'max' : (string)$cursorId,
+            $this->getBackfillProgressFile()
+        ));
+
+        while (true) {
+            if ($this->limit > 0 && $total >= $this->limit) {
+                break;
+            }
+            $fetchLimit = $this->batchSize;
+            if ($this->limit > 0) {
+                $fetchLimit = min($fetchLimit, $this->limit - $total);
+                if ($fetchLimit < 1) {
+                    break;
+                }
+            }
+
+            $list = $this->fetchClearBatch($hd2Gys, $gys2Hd, $cursorId, $fetchLimit);
+            if (empty($list)) {
+                break;
+            }
+            $batchNo++;
+            $batchMinId = null;
+            $clearIds = array_map('intval', array_column($list, 'id'));
+            $existingClearIds = OrderCgClearClass::getExistingClearIds($clearIds);
+            $emptyAmountClearIds = OrderCgClearClass::getClearIdsWithEmptyAmount($clearIds);
+            $existingMap = array_fill_keys($existingClearIds, true);
+            $emptyAmountMap = array_fill_keys($emptyAmountClearIds, true);
+
+            foreach ($list as $clear) {
+                $total++;
+                $currentClearId = intval($clear['id'] ?? 0);
+                $batchMinId = $currentClearId;
+                $cursorId = $currentClearId;
+
+                $saleIds = trim($clear['saleIds'] ?? '');
+                $purchaseIds = trim($clear['purchaseIds'] ?? '');
+                if ($saleIds === '' && $purchaseIds === '') {
+                    $skipped++;
+                    continue;
+                }
+                if (isset($existingMap[$currentClearId]) && !isset($emptyAmountMap[$currentClearId])) {
+                    $skipped++;
+                    continue;
+                }
+                if ($this->dryRun) {
+                    if ($this->logEvery > 0 && $total % $this->logEvery === 0) {
+                        $this->stdout("dryRun 已扫描 {$total} 条,当前 clearId={$currentClearId}\n");
+                    }
+                    continue;
+                }
+                $count = OrderCgClearClass::backfillFromClear($clear, true);
+                if ($count > 0) {
+                    $inserted += $count;
+                } else {
+                    $skipped++;
+                }
+                if ($this->logEvery > 0 && $total % $this->logEvery === 0) {
+                    $this->stdout("进度:扫描 {$total},写入关系 {$inserted} 行,跳过 {$skipped},当前 clearId={$currentClearId}\n");
+                }
+            }
+
+            if (!$this->dryRun && $this->clearId <= 0 && $batchMinId !== null) {
+                $this->saveBackfillProgress($batchMinId);
+            }
+
+            $this->stdout(sprintf(
+                "第 %d 批完成:本批 %d 条,累计扫描 %d,写入关系 %d 行,跳过 %d,checkpoint id=%d\n",
+                $batchNo,
+                count($list),
+                $total,
+                $inserted,
+                $skipped,
+                (int)$batchMinId
+            ));
+
+            if ($this->clearId > 0 || count($list) < $fetchLimit) {
+                break;
+            }
+            $this->sleepBetweenBatch();
+        }
+
+        if (!$this->dryRun && $this->clearId <= 0 && $cursorId !== null) {
+            $this->stdout("进度已保存,下次从 id < {$cursorId} 继续\n");
+        }
+        $this->stdout("全部完成:扫描 {$total} 条 xhClear,写入 {$inserted} 行关系,跳过 {$skipped} 条\n");
+        return ExitCode::OK;
+    }
+
+    /**
+     * 按主键倒序轻量拉取一批 xhClear(只查回填所需字段,LIMIT 走主键索引)
+     */
+    private function fetchClearBatch($hd2Gys, $gys2Hd, $cursorId, $limit)
+    {
+        if ($this->clearId > 0) {
+            $row = Yii::$app->db->createCommand(
+                'SELECT id, orderSn, saleIds, purchaseIds, status, clearStyle FROM ' . Clear::tableName()
+                . ' WHERE id = :id AND clearStyle IN (:s1, :s2) LIMIT 1'
+            )->bindValues([
+                ':id' => intval($this->clearId),
+                ':s1' => $hd2Gys,
+                ':s2' => $gys2Hd,
+            ])->queryOne();
+            return $row ? [$row] : [];
+        }
+
+        $sql = 'SELECT id, orderSn, saleIds, purchaseIds, status, clearStyle FROM ' . Clear::tableName()
+            . ' WHERE clearStyle IN (:s1, :s2)';
+        $params = [
+            ':s1' => $hd2Gys,
+            ':s2' => $gys2Hd,
+        ];
+        if ($cursorId !== null) {
+            $sql .= ' AND id < :cursorId';
+            $params[':cursorId'] = intval($cursorId);
+        }
+        $sql .= ' ORDER BY id DESC LIMIT ' . intval($limit);
+        return Yii::$app->db->createCommand($sql)->bindValues($params)->queryAll();
+    }
+
+    private function sleepBetweenBatch()
+    {
+        if ($this->sleepMs > 0) {
+            usleep($this->sleepMs * 1000);
+        }
+    }
+
+    private function getBackfillProgressFile()
+    {
+        return Yii::getAlias('@console/runtime/order_cg_clear_backfill.last_id');
+    }
+
+    private function loadBackfillProgress()
+    {
+        $file = $this->getBackfillProgressFile();
+        if (!is_file($file)) {
+            return null;
+        }
+        $id = intval(trim((string)file_get_contents($file)));
+        return $id > 0 ? $id : null;
+    }
+
+    private function saveBackfillProgress($clearId)
+    {
+        if ($this->dryRun || $this->clearId > 0) {
+            return;
+        }
+        $clearId = intval($clearId);
+        if ($clearId <= 0) {
+            return;
+        }
+        $file = $this->getBackfillProgressFile();
+        $dir = dirname($file);
+        if (!is_dir($dir)) {
+            mkdir($dir, 0777, true);
+        }
+        file_put_contents($file, (string)$clearId);
+    }
+
+    private function clearBackfillProgress()
+    {
+        $file = $this->getBackfillProgressFile();
+        if (is_file($file)) {
+            unlink($file);
+        }
+    }
+
+}